* [PATCH net-next v3 0/3] Introduce HSR/PRP HW offload support for PRU-ICSSM Ethernet driver
@ 2026-08-04 12:14 Parvathi Pudi
2026-08-04 12:14 ` [PATCH net-next v3 1/3] net: ti: icssm-prueth: Add HSR and PRP HW offload mode support for AM57xx, AM437x and AM335x Parvathi Pudi
` (2 more replies)
0 siblings, 3 replies; 7+ messages in thread
From: Parvathi Pudi @ 2026-08-04 12:14 UTC (permalink / raw)
To: andrew+netdev, davem, edumazet, kuba, pabeni, danishanwar,
parvathi, rogerq, pmohan, afd, basharath, arnd
Cc: linux-kernel, netdev, linux-arm-kernel, pratheesh, j-rameshbabu,
vigneshr, praneeth, srk, rogerq, m-malladi, krishna, mohan
Hi,
This series introduces HSR and PRP protocol HW offload support for ICSSM-Prueth driver.
HW offload support for HSR/PRP is implemented using dedicated HSR/PRP firmware running
on 2 PRU cores(PRU-ICSS) as a "DAN" available in AM57xx, AM437x and AM335x.
The following features are offloaded to HW in case of HSR and PRP:
1. L2 forwarding of a HSR frame via traditional store and forward or via cut-through (only for HSR)
2. Transmit frame duplication is offloaded to HW
3. Tag removal on the receive is offloaded to HW
4. Redundant duplicate packet discard on the receive is also offloaded to HW
In HW offload mode, redundant tag insertion in the transmit path will be still done by HSR driver
and firmware updates the LAN information available in the tag on the fly when PRU is transmitting
frame in that respective LAN.
HSR Test Setup:
--------------
___________ ______________ ___________
| | Link AB | | Link BC | |
__| AM57* |_________|AM57/AM43/AM33|_________| AM57* |___
| | Station A | | Station B | | Station C | |
| |___________| |______________| |___________| |
| |
|_________________________________________________________________|
Link CA
Steps to switch to HSR forward offload mode:
-------------------------------------------------
Example assuming eth1, eth2 ports of ICSSM on AM57x, AM437x and AM335x EVM's
1) Bring down both slave interfaces
ip link set eth1 down
ip link set eth2 down
2) Set matching MAC addresses on both slave interfaces
ip link set eth1 address <mac-addr>
ip link set eth2 address <mac-addr>
3) Enable HSR offload for both interfaces
ethtool -K eth1 hsr-fwd-offload on
ethtool -K eth1 hsr-dup-offload on
ethtool -K eth1 hsr-tag-rm-offload on
ethtool -K eth2 hsr-fwd-offload on
ethtool -K eth2 hsr-dup-offload on
ethtool -K eth2 hsr-tag-rm-offload on
4) Create HSR interface and add slave interfaces to it
ip link add name hsr0 type hsr slave1 eth1 slave2 eth2 \
supervision 45 version 1
5) Add IP address to the HSR interface
ip addr add <IP_ADDR>/24 dev hsr0
6) Bring up the HSR interface
ip link set hsr0 up
7) Bring up the both slave ports
ip link set eth1 up
ip link set eth2 up
Switching back to default mode:
--------------------------------
1) Bring down both slave interfaces
ip link set eth1 down
ip link set eth2 down
2) Delete HSR interface
ip link delete hsr0
3) Disable HSR port-to-port offloading mode, packet duplication
ethtool -K eth1 hsr-fwd-offload off
ethtool -K eth1 hsr-dup-offload off
ethtool -K eth1 hsr-tag-rm-offload off
ethtool -K eth2 hsr-fwd-offload off
ethtool -K eth2 hsr-dup-offload off
ethtool -K eth2 hsr-tag-rm-offload off
Testing the port-to-port frame forward offload feature:
-------------------------------------------------------
1) Connect the LAN cables as shown in the test setup.
2) Configure Station A and Station C in HSR non-offload mode.
3) Configure Station B is HSR offload mode.
4) Since HSR is a redundancy protocol, disconnect cable "Link CA",
to ensure frames from Station A reach Station C only through
Station B.
5) Run iperf3 Server on Station C and client on station A.
7) Check the CPU usage on Station B.
CPU usage report on Station B using mpstat when running UDP iperf3:
-------------------------------------------------------------------
AM57xx
------
1) Non-Offload case
-------------------
CPU %usr %nice %sys %iowait %irq %soft %steal %guest %idle
all 0.00 0.00 0.00 0.00 0.00 10.41 0.00 0.00 89.59
0 0.00 0.00 0.00 0.00 0.00 20.88 0.00 0.00 79.12
1 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00
2) Offload case
---------------
CPU %usr %nice %sys %iowait %irq %soft %steal %guest %idle
all 0.00 0.00 0.10 0.00 0.00 0.73 0.00 0.00 99.17
0 0.00 0.00 0.20 0.00 0.00 1.46 0.00 0.00 98.34
1 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00
AM437x
------
1) Non-Offload case
-------------------
CPU %usr %nice %sys %iowait %irq %soft %steal %guest %idle
all 0.30 0.00 0.80 0.00 0.00 35.19 0.00 0.00 63.72
0 0.30 0.00 0.80 0.00 0.00 35.19 0.00 0.00 63.72
2) Offload case
---------------
CPU %usr %nice %sys %iowait %irq %soft %steal %guest %idle
all 0.10 0.00 0.31 0.10 0.00 1.74 0.00 0.00 97.75
0 0.10 0.00 0.31 0.10 0.00 1.74 0.00 0.00 97.75
AM335x
------
1) Non Offload case
-------------------
CPU %usr %nice %sys %iowait %irq %soft %steal %guest %idle
all 0.30 0.00 1.10 0.00 0.00 90.32 0.00 0.00 8.28
0 0.30 0.00 1.10 0.00 0.00 90.32 0.00 0.00 8.28
2) Offload case
---------------
CPU %usr %nice %sys %iowait %irq %soft %steal %guest %idle
all 0.43 0.00 3.61 0.00 0.00 13.28 0.00 0.00 82.68
0 0.43 0.00 3.61 0.00 0.00 13.28 0.00 0.00 82.68
PRP Test Setup:
---------------
_________________ LAN-A __________________
| |eth1-----------eth1| |
| AM57/AM437/AM335| | AM57/AM437/AM335 |
| station A |eth2-----------eth2| station B |
|_________________| LAN-B |__________________|
Steps to switch to PRP offload mode:
------------------------------------
Example assuming eth1, eth2 ports of ICSSM on AM57x, AM437x and AM335x EVM's
1) Bring down both slave interfaces
ip link set eth1 down
ip link set eth2 down
2) Set matching MAC addresses on both slave interfaces
ip link set eth1 address <mac-addr>
ip link set eth2 address <mac-addr>
3) Enable PRP offload for both interfaces
ethtool -K eth1 hsr-dup-offload on
ethtool -K eth1 hsr-tag-rm-offload on
ethtool -K eth2 hsr-dup-offload on
ethtool -K eth2 hsr-tag-rm-offload on
4) Create PRP interface and add slave interfaces to it
ip link add name prp0 type hsr slave1 eth1 slave2 eth2 \
supervision 45 proto 1
5) Add IP address to the PRP interface
ip addr add <IP_ADDR>/24 dev prp0
6) Bring up the PRP interface
ip link set prp0 up
7) Bring up the both slave ports
ip link set eth1 up
ip link set eth2 up
Switching back to default mode:
--------------------------------
1) Bring down both slave interfaces
ip link set eth1 down
ip link set eth2 down
2) Delete PRP interface
ip link delete prp0
3) Disable PRP offloading mode
ethtool -K eth1 hsr-dup-offload off
ethtool -K eth1 hsr-tag-rm-offload off
ethtool -K eth2 hsr-dup-offload off
ethtool -K eth2 hsr-tag-rm-offload off
Testing the PRP offload feature:
--------------------------------
1) Connect eth1 of Station A to eth1 of Station B (LAN-A).
Connect eth2 of Station A to eth2 of Station B (LAN-B).
2) Configure Station A in PRP non-offload mode.
3) Configure Station B in PRP offload mode.
4) Run iperf3 Server on Station B and client on Station A.
5) Check the CPU usage on Station B.
6) Disconnect LAN-B cable to verify Station A frames still reach
Station B over LAN-A with no traffic interruption.
7) Reconnect LAN-B and disconnect LAN-A, verify the same.
CPU usage report on Station B using mpstat when running UDP iperf3:
-------------------------------------------------------------------
AM57x
-----
1) Non Offload case
-------------------
CPU %usr %nice %sys %iowait %irq %soft %steal %guest %idle
all 2.04 0.00 18.85 0.00 0.00 27.83 0.00 0.00 51.27
0 1.80 0.00 21.56 0.00 0.00 54.89 0.00 0.00 21.76
1 2.29 0.00 16.14 0.00 0.00 0.80 0.00 0.00 80.78
2) Offload case
---------------
CPU %usr %nice %sys %iowait %irq %soft %steal %guest %idle
all 2.79 0.00 18.36 0.00 0.00 18.16 0.00 0.00 60.68
0 3.89 0.00 22.16 0.00 0.00 36.13 0.00 0.00 37.82
1 1.69 0.00 14.56 0.00 0.00 0.20 0.00 0.00 83.55
AM437x
------
1) Non Offload case
-------------------
CPU %usr %nice %sys %iowait %irq %soft %steal %guest %idle
all 5.68 0.00 43.27 0.00 0.00 43.57 0.00 0.00 7.48
0 5.68 0.00 43.27 0.00 0.00 43.57 0.00 0.00 7.48
2) Offload case
---------------
CPU %usr %nice %sys %iowait %irq %soft %steal %guest %idle
all 6.39 0.00 42.86 0.00 0.00 32.57 0.00 0.00 18.18
0 6.39 0.00 42.86 0.00 0.00 32.57 0.00 0.00 18.18
AM335x
------
1) Non Offload case
-------------------
CPU %usr %nice %sys %iowait %irq %soft %steal %guest %idle
all 2.29 0.00 14.04 0.00 0.00 75.50 0.00 0.00 8.17
0 2.29 0.00 14.04 0.00 0.00 75.50 0.00 0.00 8.17
2) Offload case
---------------
CPU %usr %nice %sys %iowait %irq %soft %steal %guest %idle
all 5.70 0.00 48.50 0.00 0.00 29.00 0.00 0.00 16.80
0 5.70 0.00 48.50 0.00 0.00 29.00 0.00 0.00 16.80
Note:
hsr-tag-rm-offload and hsr-dup-offload are tightly coupled in the firmware implementation.
They both need to be enabled / disabled together and hsr-tag-ins-offload is unsupported.
This is the v3 of the patch series [v1]. This version of the patchset addresses the
comments made on [v2] of the series.
Changes from v2 to v3 :
*) Addressed the 8 Sashiko AI review comments that required code changes in this series.
*) Identified 4 Sashiko AI review comments as false positives and replied accordingly.
*) The remaining 7 comments correspond to pre-existing issues and will be addressed separately.
*) Rebased the series onto the latest net-next.
Changes from v1 to v2 :
*) Fixed all applicable issues, except for the false positives flagged by the AI review
on sashiko.dev.
*) Addressed Jakub Kicinski comments on patch 3 of the series.
*) Rebased the series on latest net-next.
[v1] https://lore.kernel.org/all/20260611123636.376577-1-parvathi@couthit.com/
[v2] https://lore.kernel.org/all/20260630124958.894360-1-parvathi@couthit.com/
Thanks and Regards,
Parvathi.
Roger Quadros (3):
net: ti: icssm-prueth: Add HSR and PRP HW offload mode support for
AM57xx, AM437x and AM335x
net: ti: icssm-prueth: Add priority based RX IRQ handlers
net: ti: icssm-prueth: Support duplicate HW offload feature for HSR
and PRP
drivers/net/ethernet/ti/Makefile | 2 +-
.../ethernet/ti/icssm/icssm_lre_firmware.h | 141 ++++
drivers/net/ethernet/ti/icssm/icssm_prueth.c | 764 ++++++++++++++++--
drivers/net/ethernet/ti/icssm/icssm_prueth.h | 87 +-
.../ethernet/ti/icssm/icssm_prueth_common.c | 278 +++++++
.../net/ethernet/ti/icssm/icssm_prueth_lre.c | 224 +++++
.../net/ethernet/ti/icssm/icssm_prueth_lre.h | 19 +
.../ethernet/ti/icssm/icssm_prueth_switch.c | 318 +++++++-
.../ethernet/ti/icssm/icssm_prueth_switch.h | 2 +
drivers/net/ethernet/ti/icssm/icssm_switch.h | 41 +-
10 files changed, 1787 insertions(+), 89 deletions(-)
create mode 100644 drivers/net/ethernet/ti/icssm/icssm_lre_firmware.h
create mode 100644 drivers/net/ethernet/ti/icssm/icssm_prueth_common.c
create mode 100644 drivers/net/ethernet/ti/icssm/icssm_prueth_lre.c
create mode 100644 drivers/net/ethernet/ti/icssm/icssm_prueth_lre.h
--
2.43.0
^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH net-next v3 1/3] net: ti: icssm-prueth: Add HSR and PRP HW offload mode support for AM57xx, AM437x and AM335x
2026-08-04 12:14 [PATCH net-next v3 0/3] Introduce HSR/PRP HW offload support for PRU-ICSSM Ethernet driver Parvathi Pudi
@ 2026-08-04 12:14 ` Parvathi Pudi
2026-08-11 8:39 ` Paolo Abeni
2026-08-04 12:14 ` [PATCH net-next v3 2/3] net: ti: icssm-prueth: Add priority based RX IRQ handlers Parvathi Pudi
2026-08-04 12:14 ` [PATCH net-next v3 3/3] net: ti: icssm-prueth: Support duplicate HW offload feature for HSR and PRP Parvathi Pudi
2 siblings, 1 reply; 7+ messages in thread
From: Parvathi Pudi @ 2026-08-04 12:14 UTC (permalink / raw)
To: andrew+netdev, davem, edumazet, kuba, pabeni, danishanwar,
parvathi, rogerq, pmohan, afd, basharath, arnd
Cc: linux-kernel, netdev, linux-arm-kernel, pratheesh, j-rameshbabu,
vigneshr, praneeth, srk, rogerq, m-malladi, krishna, mohan
From: Roger Quadros <rogerq@ti.com>
The PRU-ICSS subsystem on AM335x, AM437x and AM57xx SoCs supports dedicated
firmware implementing the IEC 62439-3 redundancy protocols: HSR and PRP.
Extend the ICSSM PRUETH driver to enable these operating modes in addition
to the existing dual-EMAC and RSTP switch configurations.
In both HSR and PRP modes, the two PRU Ethernet ports operate as LRE (Link
Redundancy Entity) slave ports, while the host port acts as the master.
In case of HW offload mode, Frame duplicate detection/discard for both HSR
and PRP and L2 forwarding in case of HSR are handled entirely by firmware
within the PRU cores.
For HSR, frames received on one PRU port are forwarded to the host and to
the peer PRU port (store-and-forward or cut-through), providing a redundant
ring path. For PRP, any one of the PRU port forwards received frames to the
host after firmware discards the duplicates.
The PRU-ICSS subsystem loads the Dual EMAC firmware by default. To enable
HSR or PRP functionality, the firmware must be changed accordingly. The
required reconfiguration steps are detailed below.
To switch from dual-EMAC to HSR (example: eth2 and eth3 as slave raw
ports):
$ ip link set eth2 down && ip link set eth3 down
$ ip link set eth2 address <mac-addr>
$ ip link set eth3 address <mac-addr>
$ ethtool -K eth2 hsr-tag-rm-offload on
$ ethtool -K eth2 hsr-fwd-offload on
$ ethtool -K eth2 hsr-dup-offload on
$ ethtool -K eth3 hsr-tag-rm-offload on
$ ethtool -K eth3 hsr-fwd-offload on
$ ethtool -K eth3 hsr-dup-offload on
$ ip link add name hsr0 type hsr slave1 eth2 slave2 eth3 supervison 45
version 1
$ ip link set eth2 up
$ ip link set eth3 up
To switch from dual-EMAC to PRP (example: eth2 and eth3 as slave raw
ports):
$ ip link set eth2 down && ip link set eth3 down
$ ip link set eth2 address <mac-addr>
$ ip link set eth3 address <mac-addr>
$ ethtool -K eth2 hsr-tag-rm-offload on
$ ethtool -K eth2 hsr-dup-offload on
$ ethtool -K eth3 hsr-tag-rm-offload on
$ ethtool -K eth3 hsr-dup-offload on
$ ip link add name prp0 type hsr slave1 eth2 slave2 eth3 supervision 45
proto 1
$ ip link set eth2 up
$ ip link set eth3 up
To revert back to dual-EMAC:
$ ip link set eth2 down && ip link set eth3 down
$ ip link delete hsr0 or prp0
$ ethtool -K eth2 hsr-tag-rm-offload off
$ ethtool -K eth2 hsr-fwd-offload off
$ ethtool -K eth2 hsr-dup-offload off
$ ethtool -K eth3 hsr-tag-rm-offload off
$ ethtool -K eth3 hsr-fwd-offload off
$ ethtool -K eth3 hsr-dup-offload off
Signed-off-by: Roger Quadros <rogerq@ti.com>
Signed-off-by: Andrew F. Davis <afd@ti.com>
Signed-off-by: Parvathi Pudi <parvathi@couthit.com>
---
drivers/net/ethernet/ti/Makefile | 2 +-
.../ethernet/ti/icssm/icssm_lre_firmware.h | 141 +++++++
drivers/net/ethernet/ti/icssm/icssm_prueth.c | 366 ++++++++++++++++--
drivers/net/ethernet/ti/icssm/icssm_prueth.h | 32 +-
.../net/ethernet/ti/icssm/icssm_prueth_lre.c | 211 ++++++++++
.../net/ethernet/ti/icssm/icssm_prueth_lre.h | 19 +
6 files changed, 747 insertions(+), 24 deletions(-)
create mode 100644 drivers/net/ethernet/ti/icssm/icssm_lre_firmware.h
create mode 100644 drivers/net/ethernet/ti/icssm/icssm_prueth_lre.c
create mode 100644 drivers/net/ethernet/ti/icssm/icssm_prueth_lre.h
diff --git a/drivers/net/ethernet/ti/Makefile b/drivers/net/ethernet/ti/Makefile
index f4276c9a7762..e4a10d60e1a6 100644
--- a/drivers/net/ethernet/ti/Makefile
+++ b/drivers/net/ethernet/ti/Makefile
@@ -4,7 +4,7 @@
#
obj-$(CONFIG_TI_PRUETH) += icssm-prueth.o
-icssm-prueth-y := icssm/icssm_prueth.o icssm/icssm_prueth_switch.o icssm/icssm_switchdev.o
+icssm-prueth-y := icssm/icssm_prueth.o icssm/icssm_prueth_switch.o icssm/icssm_switchdev.o icssm/icssm_prueth_lre.o
ti-cpsw-common-y += cpsw-common.o davinci_cpdma.o
ti-cpsw-priv-y += cpsw_priv.o cpsw_ethtool.o
diff --git a/drivers/net/ethernet/ti/icssm/icssm_lre_firmware.h b/drivers/net/ethernet/ti/icssm/icssm_lre_firmware.h
new file mode 100644
index 000000000000..b5ab0ec87c5f
--- /dev/null
+++ b/drivers/net/ethernet/ti/icssm/icssm_lre_firmware.h
@@ -0,0 +1,141 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/* Copyright (C) 2017-2020 Texas Instruments Incorporated - http://www.ti.com */
+#ifndef __ICSS_LRE_FIRMWARE_H
+#define __ICSS_LRE_FIRMWARE_H
+
+#define ICSS_LRE_HSR_MODE_OFFSET 0x1E76
+#define ICSS_LRE_MODEH 0x01
+
+/* PRU0 DMEM */
+#define ICSS_LRE_DBG_START 0x1E00
+
+#define ICSS_LRE_DUPLICATE_HOST_TABLE 0x0200
+
+/* PRU1 DMEM */
+#define ICSS_LRE_DUPLICATE_PORT_TABLE_PRU0 0x0200
+#define ICSS_LRE_DUPLICATE_PORT_TABLE_PRU1 0x0E00
+
+/* Size and setup (N and M) of duplicate host table */
+#define ICSS_LRE_DUPLICATE_HOST_TABLE_SIZE 0x1C08
+/* Size and setup (N and M) of duplicate port table (HSR Only) */
+#define ICSS_LRE_DUPLICATE_PORT_TABLE_SIZE 0x1C1C
+/* Time after which an entry is removed from the duplicate
+ * table (10 ms resolution)
+ */
+#define ICSS_LRE_DUPLI_FORGET_TIME 0x1C24
+/* Time interval to check the port duplicate table */
+#define ICSS_LRE_DUPLI_PORT_CHECK_RESO 0x1C2C
+/* Time interval to check the host duplicate table */
+#define ICSS_LRE_DUPLI_HOST_CHECK_RESO 0x1C30
+/* NodeTable | Host | Port */
+#define ICSS_LRE_HOST_TIMER_CHECK_FLAGS 0x1C38
+/* Arbitration flag for the host duplicate table */
+#define ICSS_LRE_HOST_DUPLICATE_ARBITRATION 0x1C3C
+/* Supervision address in LRE */
+#define ICSS_LRE_SUP_ADDR 0x1C4C
+#define ICSS_LRE_SUP_ADDR_LOW 0x1C50
+
+/* Time in TimeTicks (1/100s) */
+#define ICSS_LRE_DUPLICATE_FORGET_TIME_400_MS 40
+#define ICSS_LRE_NODE_FORGET_TIME_60000_MS 6000
+#define ICSS_LRE_MAX_FORGET_TIME 0xFFDF
+
+#define ICSS_LRE_DUPLICATE_PORT_TABLE_DMEM_SIZE 0x0C00
+#define ICSS_LRE_DUPLICATE_HOST_TABLE_DMEM_SIZE 0x1800
+#define ICSS_LRE_STATS_DMEM_SIZE 0x0080
+#define ICSS_LRE_DEBUG_COUNTER_DMEM_SIZE 0x0050
+
+#define ICSS_LRE_DUPLICATE_HOST_TABLE_SIZE_INIT 0x800004 /* N = 128, M = 4 */
+#define ICSS_LRE_DUPLICATE_PORT_TABLE_SIZE_INIT 0x400004 /* N = 64, M = 4 */
+#define ICSS_LRE_MASTER_SLAVE_BUSY_BITS_CLEAR 0x0
+#define ICSS_LRE_TABLE_CHECK_RESOLUTION_10_MS 0xA
+#define ICSS_LRE_SUP_ADDRESS_INIT_OCTETS_HIGH 0x4E1501
+#define ICSS_LRE_SUP_ADDRESS_INIT_OCTETS_LOW 0x1
+
+/* SHARED RAM */
+
+/* 8 bytes of VLAN PCP to RX QUEUE MAPPING */
+#define ICSS_LRE_QUEUE_2_PCP_MAP_OFFSET 0x124
+#define ICSS_LRE_START 0x140
+
+/* Count of HSR/PRP tagged frames successfully transmitted on port A/B */
+#define ICSS_LRE_CNT_TX_A (ICSS_LRE_START + 4)
+#define ICSS_LRE_DUPLICATE_DISCARD (ICSS_LRE_START + 104)
+#define ICSS_LRE_TRANSPARENT_RECEPTION (ICSS_LRE_START + 108)
+#define ICSS_LRE_CNT_NODES (ICSS_LRE_START + 52)
+
+/* SRAM */
+#define ICSS_LRE_IEC62439_CONST_DUPLICATE_ACCEPT 0x01
+#define ICSS_LRE_IEC62439_CONST_DUPLICATE_DISCARD 0x02
+#define ICSS_LRE_IEC62439_CONST_TRANSP_RECEPTION_REMOVE_RCT 0x01
+#define ICSS_LRE_IEC62439_CONST_TRANSP_RECEPTION_PASS_RCT 0x02
+
+/* Enable/disable interrupts for high/low priority instead of per port.
+ * 0 = disabled (default), 1 = enabled
+ */
+#define ICSS_LRE_PRIORITY_INTRS_STATUS_OFFSET 0x1FAA
+/* Enable/disable timestamping of packets. 0 = disabled (default) 1 = enabled */
+#define ICSS_LRE_TIMESTAMP_PKTS_STATUS_OFFSET 0x1FAB
+#define ICSS_LRE_TIMESTAMP_ARRAY_OFFSET 0xC200
+
+/* HOST_TIMER_CHECK_FLAGS bits */
+#define ICSS_LRE_HOST_TIMER_NODE_TABLE_CHECK_BIT BIT(0)
+#define ICSS_LRE_HOST_TIMER_NODE_TABLE_CLEAR_BIT BIT(4)
+#define ICSS_LRE_HOST_TIMER_HOST_TABLE_CHECK_BIT BIT(8)
+#define ICSS_LRE_HOST_TIMER_P1_TABLE_CHECK_BIT BIT(16)
+#define ICSS_LRE_HOST_TIMER_P2_TABLE_CHECK_BIT BIT(24)
+#define ICSS_LRE_HOST_TIMER_PORT_TABLE_CHECK_BITS \
+ (ICSS_LRE_HOST_TIMER_P1_TABLE_CHECK_BIT | \
+ ICSS_LRE_HOST_TIMER_P2_TABLE_CHECK_BIT)
+
+/* PRU1 DMEM */
+/* Node table offsets are different for AM3/4 vs AM57/K2G, set by firmware */
+#define ICSS_LRE_V1_0_HASH_MASK 0x3F
+#define ICSS_LRE_V1_0_INDEX_ARRAY_NT 0x60
+#define ICSS_LRE_V1_0_BIN_ARRAY 0x1A00
+#define ICSS_LRE_V1_0_NODE_TABLE_NEW 0x1FC0
+#define ICSS_LRE_V1_0_INDEX_ARRAY_LOC PRUETH_MEM_DRAM0
+#define ICSS_LRE_V1_0_BIN_ARRAY_LOC PRUETH_MEM_DRAM0
+#define ICSS_LRE_V1_0_NODE_TABLE_LOC PRUETH_MEM_SHARED_RAM
+#define ICSS_LRE_V1_0_INDEX_TBL_MAX_ENTRIES 64
+#define ICSS_LRE_V1_0_BIN_TBL_MAX_ENTRIES 128
+#define ICSS_LRE_V1_0_NODE_TBL_MAX_ENTRIES 128
+
+#define ICSS_LRE_V2_1_HASH_MASK 0xFF
+#define ICSS_LRE_V2_1_INDEX_ARRAY_NT 0x3000
+#define ICSS_LRE_V2_1_BIN_ARRAY \
+ (ICSS_LRE_V2_1_INDEX_ARRAY_NT + \
+ (ICSS_LRE_V2_1_INDEX_TBL_MAX_ENTRIES * 6))
+#define ICSS_LRE_V2_1_NODE_TABLE_NEW \
+ (ICSS_LRE_V2_1_BIN_ARRAY + \
+ (ICSS_LRE_V2_1_BIN_TBL_MAX_ENTRIES * 8))
+#define ICSS_LRE_V2_1_INDEX_ARRAY_LOC PRUETH_MEM_SHARED_RAM
+#define ICSS_LRE_V2_1_BIN_ARRAY_LOC PRUETH_MEM_SHARED_RAM
+#define ICSS_LRE_V2_1_NODE_TABLE_LOC PRUETH_MEM_SHARED_RAM
+#define ICSS_LRE_V2_1_INDEX_TBL_MAX_ENTRIES 256
+#define ICSS_LRE_V2_1_BIN_TBL_MAX_ENTRIES 256
+#define ICSS_LRE_V2_1_NODE_TBL_MAX_ENTRIES 256
+
+#define ICSS_LRE_NODE_FREE 0x10
+#define ICSS_LRE_NODE_TAKEN 0x01
+#define ICSS_LRE_NT_REM_NODE_TYPE_MASK 0x1F
+#define ICSS_LRE_NT_REM_NODE_TYPE_SHIFT 0x00
+
+#define ICSS_LRE_NT_REM_NODE_TYPE_SANA 0x01
+#define ICSS_LRE_NT_REM_NODE_TYPE_SANB 0x02
+#define ICSS_LRE_NT_REM_NODE_TYPE_SANAB 0x03
+#define ICSS_LRE_NT_REM_NODE_TYPE_DAN 0x04
+#define ICSS_LRE_NT_REM_NODE_TYPE_REDBOX 0x08
+#define ICSS_LRE_NT_REM_NODE_TYPE_VDAN 0x10
+
+#define ICSS_LRE_NT_REM_NODE_HSR_BIT 0x20 /* if set node is HSR */
+
+#define ICSS_LRE_NT_REM_NODE_DUP_MASK 0xC0
+#define ICSS_LRE_NT_REM_NODE_DUP_SHIFT 0x06
+
+/* Node entry duplicate type: DupAccept */
+#define ICSS_LRE_NT_REM_NODE_DUP_ACCEPT 0x40
+/* Node entry duplicate type: DupDiscard */
+#define ICSS_LRE_NT_REM_NODE_DUP_DISCARD 0x80
+
+#endif /* __ICSS_LRE_FIRMWARE_H */
diff --git a/drivers/net/ethernet/ti/icssm/icssm_prueth.c b/drivers/net/ethernet/ti/icssm/icssm_prueth.c
index b7e94244355a..138248f4a1ee 100644
--- a/drivers/net/ethernet/ti/icssm/icssm_prueth.c
+++ b/drivers/net/ethernet/ti/icssm/icssm_prueth.c
@@ -30,6 +30,7 @@
#include "icssm_prueth.h"
#include "icssm_prueth_switch.h"
+#include "icssm_prueth_lre.h"
#include "icssm_vlan_mcast_filter_mmap.h"
#include "../icssg/icssg_mii_rt.h"
#include "../icssg/icss_iep.h"
@@ -40,6 +41,30 @@
#define TX_CLK_DELAY_100M 0x6
#define HR_TIMER_TX_DELAY_US 100
+#define NETIF_PRUETH_LRE_OFFLOAD_FEATURES (NETIF_F_HW_HSR_FWD | \
+ NETIF_F_HW_HSR_TAG_RM)
+
+static const struct prueth_fw_offsets fw_offsets_v2_1;
+static void icssm_prueth_set_fw_offsets(struct prueth *prueth)
+{
+ /* Set Multicast filter control and table offsets */
+ if (PRUETH_IS_EMAC(prueth) || PRUETH_IS_SWITCH(prueth)) {
+ prueth->fw_offsets.mc_ctrl_offset =
+ ICSS_EMAC_FW_MULTICAST_FILTER_CTRL_OFFSET;
+ prueth->fw_offsets.mc_filter_mask =
+ ICSS_EMAC_FW_MULTICAST_FILTER_MASK_OFFSET;
+ prueth->fw_offsets.mc_filter_tbl =
+ ICSS_EMAC_FW_MULTICAST_FILTER_TABLE;
+ } else {
+ prueth->fw_offsets.mc_ctrl_offset =
+ ICSS_LRE_FW_MULTICAST_TABLE_SEARCH_OP_CONTROL_BIT;
+ prueth->fw_offsets.mc_filter_mask =
+ ICSS_LRE_FW_MULTICAST_FILTER_MASK;
+ prueth->fw_offsets.mc_filter_tbl =
+ ICSS_LRE_FW_MULTICAST_FILTER_TABLE;
+ }
+}
+
static void icssm_prueth_write_reg(struct prueth *prueth,
enum prueth_mem region,
unsigned int reg, u32 val)
@@ -309,12 +334,15 @@ static void icssm_prueth_hostinit(struct prueth *prueth)
icssm_prueth_mii_init(prueth);
}
-/* This function initialize the driver in EMAC mode
+/* Initialize the driver in EMAC, HSR or PRP mode
* based on eth_type
*/
static void icssm_prueth_init_ethernet_mode(struct prueth *prueth)
{
+ icssm_prueth_set_fw_offsets(prueth);
icssm_prueth_hostinit(prueth);
+ if (prueth_is_lre(prueth))
+ icssm_prueth_lre_config(prueth);
}
static void icssm_prueth_port_enable(struct prueth_emac *emac, bool enable)
@@ -564,7 +592,7 @@ static int icssm_prueth_tx_enqueue(struct prueth_emac *emac,
}
pkt_block_size = DIV_ROUND_UP(pktlen, ICSS_BLOCK_SIZE);
- if (pkt_block_size > free_blocks) /* out of queue space */
+ if (pkt_block_size >= free_blocks) /* out of queue space */
return -ENOBUFS;
/* calculate end BD address post write */
@@ -609,6 +637,9 @@ static int icssm_prueth_tx_enqueue(struct prueth_emac *emac,
/* update first buffer descriptor */
wr_buf_desc = (pktlen << PRUETH_BD_LENGTH_SHIFT) &
PRUETH_BD_LENGTH_MASK;
+ if (PRUETH_IS_HSR(prueth))
+ wr_buf_desc |= BIT(PRUETH_BD_HSR_FRAME_SHIFT);
+
sram = prueth->mem[PRUETH_MEM_SHARED_RAM].va;
if (!PRUETH_IS_EMAC(prueth))
writel(wr_buf_desc, sram + readw(&queue_desc->wr_ptr));
@@ -627,6 +658,12 @@ static int icssm_prueth_tx_enqueue(struct prueth_emac *emac,
void icssm_parse_packet_info(struct prueth *prueth, u32 buffer_descriptor,
struct prueth_packet_info *pkt_info)
{
+ if (prueth_is_lre(prueth))
+ pkt_info->start_offset = !!(buffer_descriptor &
+ PRUETH_BD_START_FLAG_MASK);
+ else
+ pkt_info->start_offset = false;
+
pkt_info->port = (buffer_descriptor & PRUETH_BD_PORT_MASK) >>
PRUETH_BD_PORT_SHIFT;
pkt_info->length = (buffer_descriptor & PRUETH_BD_LENGTH_MASK) >>
@@ -661,10 +698,14 @@ int icssm_emac_rx_packet(struct prueth_emac *emac, u16 *bd_rd_ptr,
unsigned int actual_pkt_len;
bool buffer_wrapped = false;
void *src_addr, *dst_addr;
+ u16 start_offset = 0;
struct sk_buff *skb;
int pkt_block_size;
void *ocmc_ram;
+ if (PRUETH_IS_HSR(emac->prueth))
+ start_offset = (pkt_info->start_offset ?
+ ICSSM_LRE_TAG_SIZE : 0);
/* the PRU firmware deals mostly in pointers already
* offset into ram, we would like to deal in indexes
* within the queue we are working with for code
@@ -687,7 +728,8 @@ int icssm_emac_rx_packet(struct prueth_emac *emac, u16 *bd_rd_ptr,
/* calculate new pointer in ram */
*bd_rd_ptr = rxqueue->buffer_desc_offset + (update_block * BD_SIZE);
- actual_pkt_len = pkt_info->length;
+ /* Exclude the HSR tag bytes already stripped by firmware, if any. */
+ actual_pkt_len = pkt_info->length - start_offset;
/* Allocate a socket buffer for this packet */
skb = netdev_alloc_skb_ip_align(ndev, actual_pkt_len);
@@ -707,6 +749,7 @@ int icssm_emac_rx_packet(struct prueth_emac *emac, u16 *bd_rd_ptr,
*/
src_addr = ocmc_ram + rxqueue->buffer_offset +
(read_block * ICSS_BLOCK_SIZE);
+ src_addr += start_offset;
/* Copy the data from PRU buffers(OCMC) to socket buffer(DRAM) */
if (buffer_wrapped) { /* wrapped around buffer */
@@ -720,6 +763,9 @@ int icssm_emac_rx_packet(struct prueth_emac *emac, u16 *bd_rd_ptr,
if (pkt_info->length < bytes)
bytes = pkt_info->length;
+ /* If applicable, account for the HSR tag removed */
+ bytes -= start_offset;
+
/* copy non-wrapped part */
memcpy(dst_addr, src_addr, bytes);
@@ -741,6 +787,12 @@ int icssm_emac_rx_packet(struct prueth_emac *emac, u16 *bd_rd_ptr,
icssm_prueth_sw_learn_fdb(emac, skb->data + ETH_ALEN);
}
+ /* For PRP, the RCT trailer is at the frame tail, exclude it from
+ * the length to avoid passing it up the stack.
+ */
+ if (PRUETH_IS_PRP(emac->prueth) && pkt_info->start_offset)
+ actual_pkt_len -= ICSSM_LRE_TAG_SIZE;
+
skb_put(skb, actual_pkt_len);
/* send packet up the stack */
@@ -804,13 +856,12 @@ static int icssm_emac_rx_packets(struct prueth_emac *emac, int budget)
rd_buf_desc = readl(shared_ram + bd_rd_ptr);
icssm_parse_packet_info(prueth, rd_buf_desc, &pkt_info);
- if (pkt_info.length <= 0) {
- /* a packet length of zero will cause us to
- * never move the read pointer ahead, locking
- * the driver, so we manually have to move it
- * to the write pointer, discarding all
- * remaining packets in this queue. This should
- * never happen.
+ if (pkt_info.length < EMAC_MIN_PKTLEN) {
+ /* if the packet is too small we skip it but we
+ * still need to move the read pointer ahead
+ * and assume something is wrong with the read
+ * pointer as the firmware should be filtering
+ * these packets
*/
update_rd_ptr = bd_wr_ptr;
emac->stats.rx_length_errors++;
@@ -982,14 +1033,20 @@ static int icssm_emac_ndo_open(struct net_device *ndev)
icssm_prueth_init_ethernet_mode(prueth);
/* reset and start PRU firmware */
- if (PRUETH_IS_SWITCH(prueth)) {
+ if (!PRUETH_IS_EMAC(prueth)) {
+ /* Switch, HSR and PRP protocols share same queue structure */
ret = icssm_prueth_sw_emac_config(emac);
if (ret)
- return ret;
+ goto free_hrtimer;
- ret = icssm_prueth_sw_init_fdb_table(prueth);
- if (ret)
- return ret;
+ if (PRUETH_IS_SWITCH(prueth)) {
+ ret = icssm_prueth_sw_init_fdb_table(prueth);
+ if (ret)
+ goto free_hrtimer;
+ } else {
+ /* LRE mode: set up duplicate-table check flags */
+ icssm_prueth_lre_config_check_flags(prueth);
+ }
} else {
icssm_prueth_emac_config(emac);
}
@@ -1046,6 +1103,9 @@ static int icssm_emac_ndo_open(struct net_device *ndev)
icss_iep_exit(prueth->iep);
free_mem:
icssm_prueth_free_memory(emac->prueth);
+free_hrtimer:
+ if (prueth_is_lre(prueth) && !prueth->emac_configured)
+ icssm_prueth_lre_cleanup(prueth);
return ret;
}
@@ -1079,6 +1139,9 @@ static int icssm_emac_ndo_stop(struct net_device *ndev)
else
rproc_shutdown(emac->pru);
+ if (prueth_is_lre(prueth) && !prueth->emac_configured)
+ icssm_prueth_lre_cleanup(prueth);
+
/* free rx interrupts */
free_irq(emac->rx_irq, ndev);
@@ -1122,7 +1185,8 @@ static int icssm_prueth_change_mode(struct prueth *prueth,
}
}
- if (mode == PRUSS_ETHTYPE_EMAC || mode == PRUSS_ETHTYPE_SWITCH) {
+ if (mode == PRUSS_ETHTYPE_EMAC || mode == PRUSS_ETHTYPE_SWITCH ||
+ mode == PRUSS_ETHTYPE_HSR || mode == PRUSS_ETHTYPE_PRP) {
prueth->eth_type = mode;
} else {
dev_err(prueth->dev, "unknown mode\n");
@@ -1266,11 +1330,16 @@ static void icssm_emac_mc_filter_ctrl(struct prueth_emac *emac, bool enable)
{
struct prueth *prueth = emac->prueth;
void __iomem *mc_filter_ctrl;
+ u32 mc_ctrl_offset;
void __iomem *ram;
u32 reg;
ram = prueth->mem[emac->dram].va;
- mc_filter_ctrl = ram + ICSS_EMAC_FW_MULTICAST_FILTER_CTRL_OFFSET;
+ if (prueth_is_lre(prueth))
+ ram = prueth->mem[PRUETH_MEM_DRAM1].va;
+
+ mc_ctrl_offset = prueth->fw_offsets.mc_ctrl_offset;
+ mc_filter_ctrl = ram + mc_ctrl_offset;
if (enable)
reg = ICSS_EMAC_FW_MULTICAST_FILTER_CTRL_ENABLED;
@@ -1289,7 +1358,10 @@ static void icssm_emac_mc_filter_reset(struct prueth_emac *emac)
void __iomem *ram;
ram = prueth->mem[emac->dram].va;
- mc_filter_tbl_base = ICSS_EMAC_FW_MULTICAST_FILTER_TABLE;
+ if (prueth_is_lre(prueth))
+ ram = prueth->mem[PRUETH_MEM_DRAM1].va;
+
+ mc_filter_tbl_base = prueth->fw_offsets.mc_filter_tbl;
mc_filter_tbl = ram + mc_filter_tbl_base;
memset_io(mc_filter_tbl, 0, ICSS_EMAC_FW_MULTICAST_TABLE_SIZE_BYTES);
@@ -1302,11 +1374,16 @@ static void icssm_emac_mc_filter_hashmask
{
struct prueth *prueth = emac->prueth;
void __iomem *mc_filter_mask;
+ u32 mc_filter_mask_base;
void __iomem *ram;
ram = prueth->mem[emac->dram].va;
+ if (prueth_is_lre(prueth))
+ ram = prueth->mem[PRUETH_MEM_DRAM1].va;
+
+ mc_filter_mask_base = prueth->fw_offsets.mc_filter_mask;
- mc_filter_mask = ram + ICSS_EMAC_FW_MULTICAST_FILTER_MASK_OFFSET;
+ mc_filter_mask = ram + mc_filter_mask_base;
memcpy_toio(mc_filter_mask, mask,
ICSS_EMAC_FW_MULTICAST_FILTER_MASK_SIZE_BYTES);
}
@@ -1316,11 +1393,16 @@ static void icssm_emac_mc_filter_bin_update(struct prueth_emac *emac, u8 hash,
{
struct prueth *prueth = emac->prueth;
void __iomem *mc_filter_tbl;
+ u32 mc_filter_tbl_base;
void __iomem *ram;
ram = prueth->mem[emac->dram].va;
+ if (prueth_is_lre(prueth))
+ ram = prueth->mem[PRUETH_MEM_DRAM1].va;
+
+ mc_filter_tbl_base = prueth->fw_offsets.mc_filter_tbl;
- mc_filter_tbl = ram + ICSS_EMAC_FW_MULTICAST_FILTER_TABLE;
+ mc_filter_tbl = ram + mc_filter_tbl_base;
writeb(val, mc_filter_tbl + hash);
}
@@ -1360,6 +1442,8 @@ static void icssm_emac_ndo_set_rx_mode(struct net_device *ndev)
{
struct prueth_emac *emac = netdev_priv(ndev);
bool promisc = ndev->flags & IFF_PROMISC;
+ /* Spinlock for multicast filter table */
+ spinlock_t *mc_filter_tbl_lock;
struct netdev_hw_addr *ha;
struct prueth *prueth;
unsigned long flags;
@@ -1371,8 +1455,13 @@ static void icssm_emac_ndo_set_rx_mode(struct net_device *ndev)
sram = prueth->mem[PRUETH_MEM_SHARED_RAM].va;
reg = readl(sram + EMAC_PROMISCUOUS_MODE_OFFSET);
+ if (prueth_is_lre(prueth))
+ mc_filter_tbl_lock = &prueth->addr_lock;
+ else
+ mc_filter_tbl_lock = &emac->addr_lock;
+
/* It is a shared table. So lock the access */
- spin_lock_irqsave(&emac->addr_lock, flags);
+ spin_lock_irqsave(mc_filter_tbl_lock, flags);
/* Disable and reset multicast filter, allows allmulti */
icssm_emac_mc_filter_ctrl(emac, false);
@@ -1429,15 +1518,95 @@ static void icssm_emac_ndo_set_rx_mode(struct net_device *ndev)
}
unlock:
- spin_unlock_irqrestore(&emac->addr_lock, flags);
+ spin_unlock_irqrestore(mc_filter_tbl_lock, flags);
+}
+
+static netdev_features_t icssm_emac_ndo_fix_features(struct net_device *ndev,
+ netdev_features_t features)
+{
+ /* hsr tag removal offload and hsr fwd offload are tightly coupled in
+ * firmware implementation. Both these features need to be enabled /
+ * disabled together.
+ */
+ if (!(ndev->features & NETIF_PRUETH_LRE_OFFLOAD_FEATURES))
+ if ((features & NETIF_F_HW_HSR_FWD) ||
+ (features & NETIF_F_HW_HSR_TAG_RM))
+ features |= NETIF_PRUETH_LRE_OFFLOAD_FEATURES;
+
+ if ((ndev->features & NETIF_F_HW_HSR_FWD) ||
+ (ndev->features & NETIF_F_HW_HSR_TAG_RM))
+ if (!(features & NETIF_F_HW_HSR_FWD) ||
+ !(features & NETIF_F_HW_HSR_TAG_RM))
+ features &= ~NETIF_PRUETH_LRE_OFFLOAD_FEATURES;
+
+ return features;
+}
+
+/**
+ * icssm_emac_ndo_set_features - Configure HSR/PRP offload features
+ * @ndev: network device
+ * @features: Requested feature set
+ *
+ * Called by ethtool -K to configure HSR/PRP offload features. The request
+ * is rejected if this interface or its paired interface is running.
+ *
+ * Return: 0 on success, -EINVAL or -EBUSY on error.
+ */
+static int icssm_emac_ndo_set_features(struct net_device *ndev,
+ netdev_features_t features)
+{
+ struct prueth_emac *emac, *other_emac;
+ netdev_features_t have, wanted;
+ struct prueth *prueth;
+ bool change_request;
+ int ret = -EBUSY;
+
+ emac = netdev_priv(ndev);
+ prueth = emac->prueth;
+ /* MAC instance index starts from 0. So index by port_id - 1 */
+ other_emac = emac->prueth->emac[(emac->port_id == PRUETH_PORT_MII0) ?
+ PRUETH_PORT_MII1 - 1 : PRUETH_PORT_MII0 - 1];
+ wanted = features & NETIF_PRUETH_LRE_OFFLOAD_FEATURES;
+ have = ndev->features & NETIF_PRUETH_LRE_OFFLOAD_FEATURES;
+ change_request = ((wanted ^ have) != 0);
+
+ if (!prueth->fw_data->support_lre)
+ return 0;
+
+ if (PRUETH_IS_SWITCH(prueth) && change_request) {
+ /* LRE offload cannot be enabled in switch mode, remove the
+ * bridge first to revert to EMAC mode.
+ */
+ netdev_err(ndev,
+ "Switch to HSR/PRP not allowed\n");
+ return -EINVAL;
+ }
+
+ if (netif_running(ndev) && change_request) {
+ netdev_err(ndev,
+ "Can't change feature when device runs\n");
+ return ret;
+ }
+
+ if (other_emac && netif_running(other_emac->ndev) && change_request) {
+ netdev_err(ndev,
+ "Can't change feature when other device runs\n");
+ return ret;
+ }
+
+ return 0;
}
static const struct net_device_ops emac_netdev_ops = {
.ndo_open = icssm_emac_ndo_open,
.ndo_stop = icssm_emac_ndo_stop,
.ndo_start_xmit = icssm_emac_ndo_start_xmit,
+ .ndo_set_mac_address = eth_mac_addr,
+ .ndo_validate_addr = eth_validate_addr,
.ndo_get_stats64 = icssm_emac_ndo_get_stats64,
.ndo_set_rx_mode = icssm_emac_ndo_set_rx_mode,
+ .ndo_set_features = icssm_emac_ndo_set_features,
+ .ndo_fix_features = icssm_emac_ndo_fix_features,
};
/* get emac_port corresponding to eth_node name */
@@ -1589,6 +1758,9 @@ static int icssm_prueth_netdev_init(struct prueth *prueth,
ndev->hw_features |= NETIF_F_HW_L2FW_DOFFLOAD;
}
+ if (prueth->support_lre)
+ ndev->hw_features |= NETIF_PRUETH_LRE_OFFLOAD_FEATURES;
+
ndev->dev.of_node = eth_node;
ndev->netdev_ops = &emac_netdev_ops;
@@ -1741,6 +1913,105 @@ static int icssm_prueth_ndev_port_unlink(struct net_device *ndev)
return ret;
}
+static int icssm_prueth_hsr_port_link(struct net_device *ndev,
+ struct net_device *hsr_ndev)
+{
+ struct prueth_emac *emac = netdev_priv(ndev);
+ struct prueth *prueth = emac->prueth;
+ enum pruss_ethtype mode;
+ enum hsr_version ver;
+ unsigned long flags;
+ u8 all_slaves;
+ int ret = 0;
+
+ if (PRUETH_IS_SWITCH(prueth))
+ return -EOPNOTSUPP;
+
+ hsr_get_version(hsr_ndev, &ver);
+
+ if (ver == HSR_V1)
+ mode = PRUSS_ETHTYPE_HSR;
+ else if (ver == PRP_V1)
+ mode = PRUSS_ETHTYPE_PRP;
+ else
+ return -EOPNOTSUPP;
+
+ all_slaves = BIT(PRUETH_PORT_MII0) | BIT(PRUETH_PORT_MII1);
+
+ spin_lock_irqsave(&prueth->addr_lock, flags);
+
+ if (!prueth->hsr_members) {
+ prueth->hsr_dev = hsr_ndev;
+ } else {
+ /* Adding the port to a second bridge is not supported */
+ if (prueth->hsr_dev != hsr_ndev) {
+ spin_unlock_irqrestore(&prueth->addr_lock, flags);
+ return -EOPNOTSUPP;
+ }
+ }
+
+ prueth->hsr_members |= BIT(emac->port_id);
+
+ spin_unlock_irqrestore(&prueth->addr_lock, flags);
+
+ if (!prueth_is_lre(prueth) && prueth->hsr_members == all_slaves) {
+ ret = icssm_prueth_change_mode(prueth, mode);
+ if (ret < 0) {
+ dev_err(prueth->dev, "Failed to enable %s mode\n",
+ (mode == PRUSS_ETHTYPE_HSR) ?
+ "HSR" : "PRP");
+ goto free_hsr;
+ } else {
+ dev_info(prueth->dev,
+ "TI PRU ethernet now in %s mode\n",
+ (mode == PRUSS_ETHTYPE_HSR) ?
+ "HSR" : "PRP");
+ }
+ }
+
+ return 0;
+
+free_hsr:
+ spin_lock_irqsave(&prueth->addr_lock, flags);
+
+ prueth->hsr_dev = NULL;
+ prueth->hsr_members &= ~all_slaves;
+
+ spin_unlock_irqrestore(&prueth->addr_lock, flags);
+ return ret;
+}
+
+static int icssm_prueth_hsr_port_unlink(struct net_device *ndev)
+{
+ struct prueth_emac *emac = netdev_priv(ndev);
+ struct prueth *prueth = emac->prueth;
+ unsigned long flags;
+ int ret = 0;
+
+ spin_lock_irqsave(&prueth->addr_lock, flags);
+
+ prueth->hsr_members &= ~BIT(emac->port_id);
+
+ spin_unlock_irqrestore(&prueth->addr_lock, flags);
+
+ if (prueth_is_lre(prueth) && !prueth->hsr_members) {
+ ret = icssm_prueth_change_mode(prueth, PRUSS_ETHTYPE_EMAC);
+ if (ret < 0) {
+ dev_err(prueth->dev, "Failed to enable dual EMAC mode\n");
+ return ret;
+ }
+ }
+
+ spin_lock_irqsave(&prueth->addr_lock, flags);
+
+ if (!prueth->hsr_members)
+ prueth->hsr_dev = NULL;
+
+ spin_unlock_irqrestore(&prueth->addr_lock, flags);
+
+ return 0;
+}
+
static int icssm_prueth_ndev_event(struct notifier_block *unused,
unsigned long event, void *ptr)
{
@@ -1754,6 +2025,17 @@ static int icssm_prueth_ndev_event(struct notifier_block *unused,
switch (event) {
case NETDEV_CHANGEUPPER:
info = ptr;
+ if (is_hsr_master(info->upper_dev)) {
+ if (info->linking) {
+ if (ndev->features &
+ NETIF_PRUETH_LRE_OFFLOAD_FEATURES)
+ ret = icssm_prueth_hsr_port_link
+ (ndev, info->upper_dev);
+ } else {
+ ret = icssm_prueth_hsr_port_unlink(ndev);
+ }
+ }
+
if (netif_is_bridge_master(info->upper_dev)) {
if (info->linking)
ret = icssm_prueth_ndev_port_link
@@ -1796,6 +2078,7 @@ static int icssm_prueth_probe(struct platform_device *pdev)
struct device *dev = &pdev->dev;
struct device_node *np;
struct prueth *prueth;
+ bool has_lre = false;
struct pruss *pruss;
int i, ret;
@@ -1810,6 +2093,7 @@ static int icssm_prueth_probe(struct platform_device *pdev)
platform_set_drvdata(pdev, prueth);
prueth->dev = dev;
prueth->fw_data = device_get_match_data(dev);
+ prueth->fw_offsets = fw_offsets_v2_1;
eth_ports_node = of_get_child_by_name(np, "ethernet-ports");
if (!eth_ports_node)
@@ -1955,6 +2239,17 @@ static int icssm_prueth_probe(struct platform_device *pdev)
prueth->mem[PRUETH_MEM_OCMC].va,
prueth->mem[PRUETH_MEM_OCMC].size);
+ if (IS_ENABLED(CONFIG_HSR) && prueth->fw_data->support_lre)
+ has_lre = true;
+
+ /* LRE requires both ethernet nodes to be present in
+ * DT, otherwise clear the support flag
+ */
+ if (has_lre && (!eth0_node || !eth1_node))
+ has_lre = false;
+
+ prueth->support_lre = has_lre;
+ spin_lock_init(&prueth->addr_lock);
/* setup netdev interfaces */
if (eth0_node) {
ret = icssm_prueth_netdev_init(prueth, eth0_node);
@@ -2176,15 +2471,24 @@ static struct prueth_private_data am335x_prueth_pdata = {
.fw_pru[PRUSS_PRU0] = {
.fw_name[PRUSS_ETHTYPE_EMAC] =
"ti-pruss/am335x-pru0-prueth-fw.elf",
+ .fw_name[PRUSS_ETHTYPE_HSR] =
+ "ti-pruss/am335x-pru0-pruhsr-fw.elf",
+ .fw_name[PRUSS_ETHTYPE_PRP] =
+ "ti-pruss/am335x-pru0-pruprp-fw.elf",
.fw_name[PRUSS_ETHTYPE_SWITCH] =
"ti-pruss/am335x-pru0-prusw-fw.elf",
},
.fw_pru[PRUSS_PRU1] = {
.fw_name[PRUSS_ETHTYPE_EMAC] =
"ti-pruss/am335x-pru1-prueth-fw.elf",
+ .fw_name[PRUSS_ETHTYPE_HSR] =
+ "ti-pruss/am335x-pru1-pruhsr-fw.elf",
+ .fw_name[PRUSS_ETHTYPE_PRP] =
+ "ti-pruss/am335x-pru1-pruprp-fw.elf",
.fw_name[PRUSS_ETHTYPE_SWITCH] =
"ti-pruss/am335x-pru1-prusw-fw.elf",
},
+ .support_lre = true,
.support_switch = true,
};
@@ -2194,15 +2498,24 @@ static struct prueth_private_data am437x_prueth_pdata = {
.fw_pru[PRUSS_PRU0] = {
.fw_name[PRUSS_ETHTYPE_EMAC] =
"ti-pruss/am437x-pru0-prueth-fw.elf",
+ .fw_name[PRUSS_ETHTYPE_HSR] =
+ "ti-pruss/am437x-pru0-pruhsr-fw.elf",
+ .fw_name[PRUSS_ETHTYPE_PRP] =
+ "ti-pruss/am437x-pru0-pruprp-fw.elf",
.fw_name[PRUSS_ETHTYPE_SWITCH] =
"ti-pruss/am437x-pru0-prusw-fw.elf",
},
.fw_pru[PRUSS_PRU1] = {
.fw_name[PRUSS_ETHTYPE_EMAC] =
"ti-pruss/am437x-pru1-prueth-fw.elf",
+ .fw_name[PRUSS_ETHTYPE_HSR] =
+ "ti-pruss/am437x-pru1-pruhsr-fw.elf",
+ .fw_name[PRUSS_ETHTYPE_PRP] =
+ "ti-pruss/am437x-pru1-pruprp-fw.elf",
.fw_name[PRUSS_ETHTYPE_SWITCH] =
"ti-pruss/am437x-pru1-prusw-fw.elf",
},
+ .support_lre = true,
.support_switch = true,
};
@@ -2212,16 +2525,25 @@ static struct prueth_private_data am57xx_prueth_pdata = {
.fw_pru[PRUSS_PRU0] = {
.fw_name[PRUSS_ETHTYPE_EMAC] =
"ti-pruss/am57xx-pru0-prueth-fw.elf",
+ .fw_name[PRUSS_ETHTYPE_HSR] =
+ "ti-pruss/am57xx-pru0-pruhsr-fw.elf",
+ .fw_name[PRUSS_ETHTYPE_PRP] =
+ "ti-pruss/am57xx-pru0-pruprp-fw.elf",
.fw_name[PRUSS_ETHTYPE_SWITCH] =
"ti-pruss/am57xx-pru0-prusw-fw.elf",
},
.fw_pru[PRUSS_PRU1] = {
.fw_name[PRUSS_ETHTYPE_EMAC] =
"ti-pruss/am57xx-pru1-prueth-fw.elf",
+ .fw_name[PRUSS_ETHTYPE_HSR] =
+ "ti-pruss/am57xx-pru1-pruhsr-fw.elf",
+ .fw_name[PRUSS_ETHTYPE_PRP] =
+ "ti-pruss/am57xx-pru1-pruprp-fw.elf",
.fw_name[PRUSS_ETHTYPE_SWITCH] =
"ti-pruss/am57xx-pru1-prusw-fw.elf",
},
+ .support_lre = true,
.support_switch = true,
};
diff --git a/drivers/net/ethernet/ti/icssm/icssm_prueth.h b/drivers/net/ethernet/ti/icssm/icssm_prueth.h
index d5b49b462c24..a5d5bcd08bcd 100644
--- a/drivers/net/ethernet/ti/icssm/icssm_prueth.h
+++ b/drivers/net/ethernet/ti/icssm/icssm_prueth.h
@@ -16,13 +16,17 @@
#include "icssm_switch.h"
#include "icssm_prueth_ptp.h"
#include "icssm_prueth_fdb_tbl.h"
+#include "icssm_lre_firmware.h"
/* ICSSM size of redundancy tag */
#define ICSSM_LRE_TAG_SIZE 6
+#define PRUETH_TIMER_MS (10)
+
/* PRUSS local memory map */
#define ICSS_LOCAL_SHARED_RAM 0x00010000
#define EMAC_MAX_PKTLEN (ETH_HLEN + VLAN_HLEN + ETH_DATA_LEN)
+#define EMAC_MIN_PKTLEN ETH_ZLEN
/* Below macro is for 1528 Byte Frame support, to Allow even with
* Redundancy tag
*/
@@ -42,6 +46,8 @@ enum pruss_ethtype {
#define PRUETH_IS_EMAC(p) ((p)->eth_type == PRUSS_ETHTYPE_EMAC)
#define PRUETH_IS_SWITCH(p) ((p)->eth_type == PRUSS_ETHTYPE_SWITCH)
+#define PRUETH_IS_HSR(p) ((p)->eth_type == PRUSS_ETHTYPE_HSR)
+#define PRUETH_IS_PRP(p) ((p)->eth_type == PRUSS_ETHTYPE_PRP)
/**
* struct prueth_queue_desc - Queue descriptor
@@ -86,6 +92,7 @@ struct prueth_queue_info {
/**
* struct prueth_packet_info - Info about a packet in buffer
+ * @start_offset: true if frame carries an HSR/PRP start offset
* @shadow: this packet is stored in the collision queue
* @port: port packet is on
* @length: length of packet
@@ -96,6 +103,7 @@ struct prueth_queue_info {
* @timestamp: Specifies if timestamp is appended to the packet
*/
struct prueth_packet_info {
+ bool start_offset;
bool shadow;
unsigned int port;
unsigned int length;
@@ -171,6 +179,13 @@ enum prueth_mem {
PRUETH_MEM_MAX,
};
+/* Firmware offsets/size information */
+struct prueth_fw_offsets {
+ u32 mc_ctrl_offset;
+ u32 mc_filter_mask;
+ u32 mc_filter_tbl;
+};
+
enum pruss_device {
PRUSS_AM57XX = 0,
PRUSS_AM43XX,
@@ -183,11 +198,13 @@ enum pruss_device {
* @driver_data: PRU Ethernet device name
* @fw_pru: firmware names to be used for PRUSS ethernet usecases
* @support_switch: boolean to indicate if switch is enabled
+ * @support_lre: boolean to indicate if LRE is enabled
*/
struct prueth_private_data {
enum pruss_device driver_data;
const struct prueth_firmware fw_pru[PRUSS_NUM_PRUS];
bool support_switch;
+ bool support_lre;
};
struct prueth_emac_stats {
@@ -248,13 +265,20 @@ struct prueth {
struct icss_iep *iep;
const struct prueth_private_data *fw_data;
- struct prueth_fw_offsets *fw_offsets;
+ struct prueth_fw_offsets fw_offsets;
struct device_node *eth_node[PRUETH_NUM_MACS];
struct prueth_emac *emac[PRUETH_NUM_MACS];
struct net_device *registered_netdevs[PRUETH_NUM_MACS];
+ bool support_lre;
+ unsigned int tbl_check_mask;
+ struct hrtimer tbl_check_timer;
+ /* serialize access to LRE VLAN/MC filter table */
+ spinlock_t addr_lock;
+
struct net_device *hw_bridge_dev;
+ struct net_device *hsr_dev;
struct fdb_tbl *fdb_tbl;
struct notifier_block prueth_netdevice_nb;
@@ -264,6 +288,7 @@ struct prueth {
unsigned int eth_type;
size_t ocmc_ram_size;
u8 emac_configured;
+ u8 hsr_members;
u8 br_members;
};
@@ -277,4 +302,9 @@ int icssm_emac_rx_packet(struct prueth_emac *emac, u16 *bd_rd_ptr,
void icssm_emac_mc_filter_bin_allow(struct prueth_emac *emac, u8 hash);
void icssm_emac_mc_filter_bin_disallow(struct prueth_emac *emac, u8 hash);
u8 icssm_emac_get_mc_hash(u8 *mac, u8 *mask);
+
+static inline bool prueth_is_lre(struct prueth *prueth)
+{
+ return PRUETH_IS_HSR(prueth) || PRUETH_IS_PRP(prueth);
+}
#endif /* __NET_TI_PRUETH_H */
diff --git a/drivers/net/ethernet/ti/icssm/icssm_prueth_lre.c b/drivers/net/ethernet/ti/icssm/icssm_prueth_lre.c
new file mode 100644
index 000000000000..6276dd1e8bb1
--- /dev/null
+++ b/drivers/net/ethernet/ti/icssm/icssm_prueth_lre.c
@@ -0,0 +1,211 @@
+// SPDX-License-Identifier: GPL-2.0
+
+/* Texas Instruments PRUETH hsr/prp Link Redundancy Entity (LRE) Driver.
+ *
+ * Copyright (C) 2020 Texas Instruments Incorporated - http://www.ti.com
+ */
+
+#include <linux/kernel.h>
+#include <linux/string.h>
+
+#include "icssm_lre_firmware.h"
+#include "icssm_prueth_lre.h"
+#include "icssm_prueth.h"
+#include "icssm_prueth_switch.h"
+
+void icssm_prueth_lre_config_check_flags(struct prueth *prueth)
+{
+ void __iomem *dram1 = prueth->mem[PRUETH_MEM_DRAM1].va;
+
+ /* HSR/PRP: initialize check table when first port is up */
+ if (prueth->emac_configured)
+ return;
+
+ prueth->tbl_check_mask = ICSS_LRE_HOST_TIMER_HOST_TABLE_CHECK_BIT;
+ if (PRUETH_IS_HSR(prueth))
+ prueth->tbl_check_mask |=
+ ICSS_LRE_HOST_TIMER_PORT_TABLE_CHECK_BITS;
+ writel(prueth->tbl_check_mask, dram1 + ICSS_LRE_HOST_TIMER_CHECK_FLAGS);
+}
+
+/* A group of PCPs are mapped to a Queue. This is the size of firmware
+ * array in shared memory
+ */
+#define PCP_GROUP_TO_QUEUE_MAP_SIZE 4
+
+/* PRU firmware default PCP to priority Queue map for ingress & egress
+ *
+ * At ingress to Host
+ * ==================
+ * byte 0 => PRU 1, PCP 0-3 => Q3
+ * byte 1 => PRU 1, PCP 4-7 => Q2
+ * byte 2 => PRU 0, PCP 0-3 => Q1
+ * byte 3 => PRU 0, PCP 4-7 => Q0
+ *
+ * At egress to wire/network on PRU-0 and PRU-1
+ * ============================================
+ * byte 0 => Host, PCP 0-3 => Q3
+ * byte 1 => Host, PCP 4-7 => Q2
+ *
+ * PRU-0
+ * -----
+ * byte 2 => PRU-1, PCP 0-3 => Q1
+ * byte 3 => PRU-1, PCP 4-7 => Q0
+ *
+ * PRU-1
+ * -----
+ * byte 2 => PRU-0, PCP 0-3 => Q1
+ * byte 3 => PRU-0, PCP 4-7 => Q0
+ *
+ * queue names below are named 1 based. i.e PRUETH_QUEUE1 is Q0,
+ * PRUETH_QUEUE2 is Q1 and so forth. Firmware convention is that
+ * a lower queue number has higher priority than a higher queue
+ * number.
+ */
+static u8 fw_pcp_default_priority_queue_map[PCP_GROUP_TO_QUEUE_MAP_SIZE] = {
+ /* port 2 or PRU 1 */
+ PRUETH_QUEUE4, PRUETH_QUEUE3,
+ /* port 1 or PRU 0 */
+ PRUETH_QUEUE2, PRUETH_QUEUE1,
+};
+
+static void icssm_prueth_lre_pcp_queue_map_config(struct prueth *prueth)
+{
+ void __iomem *sram = prueth->mem[PRUETH_MEM_SHARED_RAM].va;
+
+ memcpy_toio(sram + ICSS_LRE_QUEUE_2_PCP_MAP_OFFSET,
+ &fw_pcp_default_priority_queue_map[0],
+ PCP_GROUP_TO_QUEUE_MAP_SIZE);
+}
+
+static void icssm_prueth_lre_host_table_init(struct prueth *prueth)
+{
+ void __iomem *dram0 = prueth->mem[PRUETH_MEM_DRAM0].va;
+ void __iomem *dram1 = prueth->mem[PRUETH_MEM_DRAM1].va;
+
+ memset_io(dram0 + ICSS_LRE_DUPLICATE_HOST_TABLE, 0,
+ ICSS_LRE_DUPLICATE_HOST_TABLE_DMEM_SIZE);
+
+ writel(ICSS_LRE_DUPLICATE_HOST_TABLE_SIZE_INIT,
+ dram1 + ICSS_LRE_DUPLICATE_HOST_TABLE_SIZE);
+
+ writel(ICSS_LRE_TABLE_CHECK_RESOLUTION_10_MS,
+ dram1 + ICSS_LRE_DUPLI_HOST_CHECK_RESO);
+
+ writel(ICSS_LRE_MASTER_SLAVE_BUSY_BITS_CLEAR,
+ dram1 + ICSS_LRE_HOST_DUPLICATE_ARBITRATION);
+}
+
+static void icssm_prueth_lre_port_table_init(struct prueth *prueth)
+{
+ void __iomem *dram1 = prueth->mem[PRUETH_MEM_DRAM1].va;
+
+ if (PRUETH_IS_HSR(prueth)) {
+ memset_io(dram1 + ICSS_LRE_DUPLICATE_PORT_TABLE_PRU0, 0,
+ ICSS_LRE_DUPLICATE_PORT_TABLE_DMEM_SIZE);
+ memset_io(dram1 + ICSS_LRE_DUPLICATE_PORT_TABLE_PRU1, 0,
+ ICSS_LRE_DUPLICATE_PORT_TABLE_DMEM_SIZE);
+
+ writel(ICSS_LRE_DUPLICATE_PORT_TABLE_SIZE_INIT,
+ dram1 + ICSS_LRE_DUPLICATE_PORT_TABLE_SIZE);
+ } else {
+ writel(0, dram1 + ICSS_LRE_DUPLICATE_PORT_TABLE_SIZE);
+ }
+
+ writel(ICSS_LRE_TABLE_CHECK_RESOLUTION_10_MS,
+ dram1 + ICSS_LRE_DUPLI_PORT_CHECK_RESO);
+}
+
+static void icssm_prueth_lre_init(struct prueth *prueth)
+{
+ void __iomem *sram = prueth->mem[PRUETH_MEM_SHARED_RAM].va;
+
+ memset_io(sram + ICSS_LRE_START, 0, ICSS_LRE_STATS_DMEM_SIZE);
+
+ writel(ICSS_LRE_IEC62439_CONST_DUPLICATE_DISCARD,
+ sram + ICSS_LRE_DUPLICATE_DISCARD);
+ writel(ICSS_LRE_IEC62439_CONST_TRANSP_RECEPTION_REMOVE_RCT,
+ sram + ICSS_LRE_TRANSPARENT_RECEPTION);
+}
+
+static void icssm_prueth_lre_dbg_init(struct prueth *prueth)
+{
+ void __iomem *dram0 = prueth->mem[PRUETH_MEM_DRAM0].va;
+
+ memset_io(dram0 + ICSS_LRE_DBG_START, 0,
+ ICSS_LRE_DEBUG_COUNTER_DMEM_SIZE);
+}
+
+static void icssm_prueth_lre_protocol_init(struct prueth *prueth)
+{
+ void __iomem *dram0 = prueth->mem[PRUETH_MEM_DRAM0].va;
+ void __iomem *dram1 = prueth->mem[PRUETH_MEM_DRAM1].va;
+
+ if (PRUETH_IS_HSR(prueth))
+ writew(ICSS_LRE_MODEH, dram0 + ICSS_LRE_HSR_MODE_OFFSET);
+
+ writel(ICSS_LRE_DUPLICATE_FORGET_TIME_400_MS,
+ dram1 + ICSS_LRE_DUPLI_FORGET_TIME);
+ writel(ICSS_LRE_SUP_ADDRESS_INIT_OCTETS_HIGH,
+ dram1 + ICSS_LRE_SUP_ADDR);
+ writel(ICSS_LRE_SUP_ADDRESS_INIT_OCTETS_LOW,
+ dram1 + ICSS_LRE_SUP_ADDR_LOW);
+}
+
+static enum hrtimer_restart icssm_prueth_lre_timer(struct hrtimer *timer)
+{
+ struct prueth *prueth;
+ unsigned int timeout;
+ void __iomem *dram;
+
+ prueth = container_of(timer, struct prueth, tbl_check_timer);
+ dram = prueth->mem[PRUETH_MEM_DRAM1].va;
+ timeout = PRUETH_TIMER_MS;
+
+ hrtimer_forward_now(timer, ms_to_ktime(timeout));
+ if (prueth->emac_configured !=
+ (BIT(PRUETH_PORT_MII0) | BIT(PRUETH_PORT_MII1)))
+ return HRTIMER_RESTART;
+
+ /* Set the flags for duplicate tables so the firmware checks and
+ * updates them every 10 milliseconds.
+ */
+ writel(prueth->tbl_check_mask, dram + ICSS_LRE_HOST_TIMER_CHECK_FLAGS);
+
+ return HRTIMER_RESTART;
+}
+
+static void icssm_prueth_lre_init_timer(struct prueth *prueth)
+{
+ hrtimer_setup(&prueth->tbl_check_timer, &icssm_prueth_lre_timer,
+ CLOCK_MONOTONIC, HRTIMER_MODE_REL);
+}
+
+static void icssm_prueth_lre_start_timer(struct prueth *prueth)
+{
+ unsigned int timeout = PRUETH_TIMER_MS;
+
+ if (hrtimer_active(&prueth->tbl_check_timer))
+ return;
+
+ hrtimer_start(&prueth->tbl_check_timer, ms_to_ktime(timeout),
+ HRTIMER_MODE_REL);
+}
+
+void icssm_prueth_lre_config(struct prueth *prueth)
+{
+ icssm_prueth_lre_init_timer(prueth);
+ icssm_prueth_lre_start_timer(prueth);
+ icssm_prueth_lre_pcp_queue_map_config(prueth);
+ icssm_prueth_lre_host_table_init(prueth);
+ icssm_prueth_lre_port_table_init(prueth);
+ icssm_prueth_lre_init(prueth);
+ icssm_prueth_lre_dbg_init(prueth);
+ icssm_prueth_lre_protocol_init(prueth);
+}
+
+void icssm_prueth_lre_cleanup(struct prueth *prueth)
+{
+ if (hrtimer_active(&prueth->tbl_check_timer))
+ hrtimer_cancel(&prueth->tbl_check_timer);
+}
diff --git a/drivers/net/ethernet/ti/icssm/icssm_prueth_lre.h b/drivers/net/ethernet/ti/icssm/icssm_prueth_lre.h
new file mode 100644
index 000000000000..0fe4d1ae5823
--- /dev/null
+++ b/drivers/net/ethernet/ti/icssm/icssm_prueth_lre.h
@@ -0,0 +1,19 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/* Copyright (C) 2020 Texas Instruments Incorporated - http://www.ti.com
+ */
+
+#ifndef __NET_TI_PRUETH_LRE_H
+#define __NET_TI_PRUETH_LRE_H
+
+#include <linux/etherdevice.h>
+#include <linux/interrupt.h>
+#include <linux/if_vlan.h>
+
+#include "icssm_prueth.h"
+#include "icssm_lre_firmware.h"
+
+void icssm_prueth_lre_config(struct prueth *prueth);
+void icssm_prueth_lre_cleanup(struct prueth *prueth);
+void icssm_prueth_lre_config_check_flags(struct prueth *prueth);
+
+#endif /* __NET_TI_PRUETH_LRE_H */
--
2.43.0
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH net-next v3 2/3] net: ti: icssm-prueth: Add priority based RX IRQ handlers
2026-08-04 12:14 [PATCH net-next v3 0/3] Introduce HSR/PRP HW offload support for PRU-ICSSM Ethernet driver Parvathi Pudi
2026-08-04 12:14 ` [PATCH net-next v3 1/3] net: ti: icssm-prueth: Add HSR and PRP HW offload mode support for AM57xx, AM437x and AM335x Parvathi Pudi
@ 2026-08-04 12:14 ` Parvathi Pudi
2026-08-11 8:39 ` Paolo Abeni
2026-08-04 12:14 ` [PATCH net-next v3 3/3] net: ti: icssm-prueth: Support duplicate HW offload feature for HSR and PRP Parvathi Pudi
2 siblings, 1 reply; 7+ messages in thread
From: Parvathi Pudi @ 2026-08-04 12:14 UTC (permalink / raw)
To: andrew+netdev, davem, edumazet, kuba, pabeni, danishanwar,
parvathi, rogerq, pmohan, afd, basharath, arnd
Cc: linux-kernel, netdev, linux-arm-kernel, pratheesh, j-rameshbabu,
vigneshr, praneeth, srk, rogerq, m-malladi, krishna, mohan
From: Roger Quadros <rogerq@ti.com>
This patch adds support for priority based interrupt handling for the STP/
RSTP Switch, HSR and PRP protocols along with extra logic to address first
come first served to avoid port dominance.
In RSTP switch, HSR, and PRP modes the host port can receive frames from
any one of PRU ports. Servicing RX interrupts in arrival order does not
guarantee the frames are delivered to the stack in wire-arrival order due
to port dominance.
In order to achieve that, each PRU records an IEP (Industrial Ethernet
Peripheral) arrival HW timestamp into the receive buffer and pass this
information to driver. The driver will read the RX HW timestamp from frame
and process the frame which has arrived first among the two ports, giving
the stack wire-arrival ordering.
Dual-EMAC mode continues to use per-port interrupts.
Signed-off-by: Roger Quadros <rogerq@ti.com>
Signed-off-by: Andrew F. Davis <afd@ti.com>
Signed-off-by: Basharath Hussain Khaja <basharath@couthit.com>
Signed-off-by: Parvathi Pudi <parvathi@couthit.com>
---
drivers/net/ethernet/ti/Makefile | 2 +-
drivers/net/ethernet/ti/icssm/icssm_prueth.c | 171 +++++++++--
drivers/net/ethernet/ti/icssm/icssm_prueth.h | 44 ++-
.../ethernet/ti/icssm/icssm_prueth_common.c | 281 ++++++++++++++++++
.../net/ethernet/ti/icssm/icssm_prueth_lre.c | 13 +
.../ethernet/ti/icssm/icssm_prueth_switch.c | 8 +
.../ethernet/ti/icssm/icssm_prueth_switch.h | 1 +
drivers/net/ethernet/ti/icssm/icssm_switch.h | 6 +
8 files changed, 492 insertions(+), 34 deletions(-)
create mode 100644 drivers/net/ethernet/ti/icssm/icssm_prueth_common.c
diff --git a/drivers/net/ethernet/ti/Makefile b/drivers/net/ethernet/ti/Makefile
index e4a10d60e1a6..b6651fe73afd 100644
--- a/drivers/net/ethernet/ti/Makefile
+++ b/drivers/net/ethernet/ti/Makefile
@@ -4,7 +4,7 @@
#
obj-$(CONFIG_TI_PRUETH) += icssm-prueth.o
-icssm-prueth-y := icssm/icssm_prueth.o icssm/icssm_prueth_switch.o icssm/icssm_switchdev.o icssm/icssm_prueth_lre.o
+icssm-prueth-y := icssm/icssm_prueth.o icssm/icssm_prueth_switch.o icssm/icssm_switchdev.o icssm/icssm_prueth_lre.o icssm/icssm_prueth_common.o
ti-cpsw-common-y += cpsw-common.o davinci_cpdma.o
ti-cpsw-priv-y += cpsw_priv.o cpsw_ethtool.o
diff --git a/drivers/net/ethernet/ti/icssm/icssm_prueth.c b/drivers/net/ethernet/ti/icssm/icssm_prueth.c
index 138248f4a1ee..7031b78d2cc2 100644
--- a/drivers/net/ethernet/ti/icssm/icssm_prueth.c
+++ b/drivers/net/ethernet/ti/icssm/icssm_prueth.c
@@ -44,7 +44,21 @@
#define NETIF_PRUETH_LRE_OFFLOAD_FEATURES (NETIF_F_HW_HSR_FWD | \
NETIF_F_HW_HSR_TAG_RM)
-static const struct prueth_fw_offsets fw_offsets_v2_1;
+/* ICSSM (v2.1) - supports 64-bit IEP counter.
+ * Firmware stores packet timestamps using lower 32 bits
+ * which wraps at 0xffffffff.
+ */
+static const struct prueth_fw_offsets fw_offsets_v2_1 = {
+ .iep_wrap = 0xffffffff,
+};
+
+/* ICSSM (v1.0) - supports 32-bit IEP counter, which resets the
+ * counter every one second (nanosecond resolution).
+ */
+static const struct prueth_fw_offsets fw_offsets_v1_0 = {
+ .iep_wrap = NSEC_PER_SEC,
+};
+
static void icssm_prueth_set_fw_offsets(struct prueth *prueth)
{
/* Set Multicast filter control and table offsets */
@@ -343,6 +357,8 @@ static void icssm_prueth_init_ethernet_mode(struct prueth *prueth)
icssm_prueth_hostinit(prueth);
if (prueth_is_lre(prueth))
icssm_prueth_lre_config(prueth);
+ else if (PRUETH_IS_SWITCH(prueth))
+ icssm_prueth_sw_config_packet_timestamping(prueth);
}
static void icssm_prueth_port_enable(struct prueth_emac *emac, bool enable)
@@ -802,8 +818,8 @@ int icssm_emac_rx_packet(struct prueth_emac *emac, u16 *bd_rd_ptr,
local_bh_enable();
/* update stats */
- emac->stats.rx_bytes += actual_pkt_len;
- emac->stats.rx_packets++;
+ atomic64_add(actual_pkt_len, &emac->stats.rx_bytes);
+ atomic64_inc(&emac->stats.rx_packets);
return 0;
}
@@ -842,7 +858,7 @@ static int icssm_emac_rx_packets(struct prueth_emac *emac, int budget)
rxqueue = &queue_infos[PRUETH_PORT_HOST][i];
overflow_cnt = readb(&queue_desc->overflow_cnt);
if (overflow_cnt > 0) {
- emac->stats.rx_over_errors += overflow_cnt;
+ atomic64_add(overflow_cnt, &emac->stats.rx_over_errors);
/* reset to zero */
writeb(0, &queue_desc->overflow_cnt);
}
@@ -864,7 +880,7 @@ static int icssm_emac_rx_packets(struct prueth_emac *emac, int budget)
* these packets
*/
update_rd_ptr = bd_wr_ptr;
- emac->stats.rx_length_errors++;
+ atomic64_inc(&emac->stats.rx_length_errors);
} else if (pkt_info.length > EMAC_MAX_FRM_SUPPORT) {
/* if the packet is too large we skip it but we
* still need to move the read pointer ahead
@@ -873,7 +889,7 @@ static int icssm_emac_rx_packets(struct prueth_emac *emac, int budget)
* these packets
*/
update_rd_ptr = bd_wr_ptr;
- emac->stats.rx_length_errors++;
+ atomic64_inc(&emac->stats.rx_length_errors);
} else {
update_rd_ptr = bd_rd_ptr;
ret = icssm_emac_rx_packet(emac, &update_rd_ptr,
@@ -1071,11 +1087,25 @@ static int icssm_emac_ndo_open(struct net_device *ndev)
goto iep_exit;
}
- ret = icssm_emac_request_irqs(emac);
- if (ret)
- goto rproc_shutdown;
+ if (PRUETH_IS_EMAC(prueth)) {
+ napi_enable(&emac->napi);
+ } else {
+ if (!prueth->emac_configured &&
+ (PRUETH_IS_SWITCH(prueth) || prueth_is_lre(prueth))) {
+ napi_enable(&prueth->napi_hpq);
+ napi_enable(&prueth->napi_lpq);
+ }
+ }
- napi_enable(&emac->napi);
+ /* In switch and LRE modes the shared HPQ/LPQ IRQs are used,
+ * register them here and reuse for both modes.
+ */
+ if (PRUETH_IS_EMAC(prueth))
+ ret = icssm_emac_request_irqs(emac);
+ else
+ ret = icssm_prueth_common_request_irqs(emac);
+ if (ret)
+ goto disable_napi;
/* start PHY */
phy_start(emac->phydev);
@@ -1092,7 +1122,17 @@ static int icssm_emac_ndo_open(struct net_device *ndev)
return 0;
-rproc_shutdown:
+disable_napi:
+ if (PRUETH_IS_EMAC(prueth)) {
+ napi_disable(&emac->napi);
+ } else {
+ if (!prueth->emac_configured &&
+ (PRUETH_IS_SWITCH(prueth) || prueth_is_lre(prueth))) {
+ napi_disable(&prueth->napi_lpq);
+ napi_disable(&prueth->napi_hpq);
+ }
+ }
+
if (!PRUETH_IS_EMAC(prueth))
icssm_prueth_sw_shutdown_prus(emac, ndev);
else
@@ -1127,12 +1167,26 @@ static int icssm_emac_ndo_stop(struct net_device *ndev)
/* disable the mac port */
icssm_prueth_port_enable(emac, false);
+ netif_stop_queue(ndev);
+
/* stop PHY */
phy_stop(emac->phydev);
- napi_disable(&emac->napi);
hrtimer_cancel(&emac->tx_hrtimer);
+ if (PRUETH_IS_EMAC(prueth)) {
+ napi_disable(&emac->napi);
+ free_irq(emac->rx_irq, ndev);
+ } else {
+ if (!prueth->emac_configured &&
+ (PRUETH_IS_SWITCH(prueth) || prueth_is_lre(prueth))) {
+ napi_disable(&prueth->napi_lpq);
+ napi_disable(&prueth->napi_hpq);
+ }
+ /* Free IRQs on last port before halting PRU */
+ icssm_prueth_common_free_irqs(emac);
+ }
+
/* stop the PRU */
if (!PRUETH_IS_EMAC(prueth))
icssm_prueth_sw_shutdown_prus(emac, ndev);
@@ -1142,9 +1196,6 @@ static int icssm_emac_ndo_stop(struct net_device *ndev)
if (prueth_is_lre(prueth) && !prueth->emac_configured)
icssm_prueth_lre_cleanup(prueth);
- /* free rx interrupts */
- free_irq(emac->rx_irq, ndev);
-
/* free memory related to sw */
icssm_prueth_free_memory(emac->prueth);
@@ -1281,8 +1332,8 @@ static enum netdev_tx icssm_emac_ndo_start_xmit(struct sk_buff *skb,
goto fail_tx;
}
- emac->stats.tx_packets++;
- emac->stats.tx_bytes += skb->len;
+ atomic64_inc(&emac->stats.tx_packets);
+ atomic64_add(skb->len, &emac->stats.tx_bytes);
dev_kfree_skb_any(skb);
return NETDEV_TX_OK;
@@ -1296,7 +1347,7 @@ static enum netdev_tx icssm_emac_ndo_start_xmit(struct sk_buff *skb,
ret = NETDEV_TX_BUSY;
} else {
/* error */
- emac->stats.tx_dropped++;
+ atomic64_inc(&emac->stats.tx_dropped);
ret = NET_XMIT_DROP;
}
@@ -1316,13 +1367,13 @@ static void icssm_emac_ndo_get_stats64(struct net_device *ndev,
{
struct prueth_emac *emac = netdev_priv(ndev);
- stats->rx_packets = emac->stats.rx_packets;
- stats->rx_bytes = emac->stats.rx_bytes;
- stats->tx_packets = emac->stats.tx_packets;
- stats->tx_bytes = emac->stats.tx_bytes;
- stats->tx_dropped = emac->stats.tx_dropped;
- stats->rx_over_errors = emac->stats.rx_over_errors;
- stats->rx_length_errors = emac->stats.rx_length_errors;
+ stats->rx_packets = atomic64_read(&emac->stats.rx_packets);
+ stats->rx_bytes = atomic64_read(&emac->stats.rx_bytes);
+ stats->tx_packets = atomic64_read(&emac->stats.tx_packets);
+ stats->tx_bytes = atomic64_read(&emac->stats.tx_bytes);
+ stats->tx_dropped = atomic64_read(&emac->stats.tx_dropped);
+ stats->rx_over_errors = atomic64_read(&emac->stats.rx_over_errors);
+ stats->rx_length_errors = atomic64_read(&emac->stats.rx_length_errors);
}
/* enable/disable MC filter */
@@ -1766,9 +1817,25 @@ static int icssm_prueth_netdev_init(struct prueth *prueth,
netif_napi_add(ndev, &emac->napi, icssm_emac_napi_poll);
+ if ((prueth->support_lre || fw_data->support_switch) &&
+ emac->port_id == PRUETH_PORT_MII0) {
+ netif_napi_add(ndev, &prueth->napi_hpq,
+ icssm_prueth_lre_napi_poll_hpq);
+ netif_napi_add(ndev, &prueth->napi_lpq,
+ icssm_prueth_lre_napi_poll_lpq);
+ }
+
hrtimer_setup(&emac->tx_hrtimer, &icssm_emac_tx_timer_callback,
CLOCK_MONOTONIC, HRTIMER_MODE_REL_PINNED);
+ if ((prueth->support_lre || fw_data->support_switch) &&
+ emac->port_id == PRUETH_PORT_MII0) {
+ prueth->hp->ndev = ndev;
+ prueth->hp->priority = 0;
+ prueth->lp->ndev = ndev;
+ prueth->lp->priority = 1;
+ }
+
return 0;
free:
emac->ndev = NULL;
@@ -1780,6 +1847,7 @@ static int icssm_prueth_netdev_init(struct prueth *prueth,
static void icssm_prueth_netdev_exit(struct prueth *prueth,
struct device_node *eth_node)
{
+ const struct prueth_private_data *fw_data = prueth->fw_data;
struct prueth_emac *emac;
enum prueth_mac mac;
@@ -1794,6 +1862,13 @@ static void icssm_prueth_netdev_exit(struct prueth *prueth,
phy_disconnect(emac->phydev);
netif_napi_del(&emac->napi);
+
+ if ((prueth->support_lre || fw_data->support_switch) &&
+ emac->port_id == PRUETH_PORT_MII0) {
+ netif_napi_del(&prueth->napi_hpq);
+ netif_napi_del(&prueth->napi_lpq);
+ }
+
prueth->emac[mac] = NULL;
}
@@ -2093,7 +2168,13 @@ static int icssm_prueth_probe(struct platform_device *pdev)
platform_set_drvdata(pdev, prueth);
prueth->dev = dev;
prueth->fw_data = device_get_match_data(dev);
- prueth->fw_offsets = fw_offsets_v2_1;
+
+ if (prueth->fw_data->fw_rev == FW_REV_V1_0)
+ prueth->fw_offsets = fw_offsets_v1_0;
+ else if (prueth->fw_data->fw_rev == FW_REV_V2_1)
+ prueth->fw_offsets = fw_offsets_v2_1;
+ else
+ return -EINVAL;
eth_ports_node = of_get_child_by_name(np, "ethernet-ports");
if (!eth_ports_node)
@@ -2248,6 +2329,41 @@ static int icssm_prueth_probe(struct platform_device *pdev)
if (has_lre && (!eth0_node || !eth1_node))
has_lre = false;
+ /* Switch and LRE share HPQ/LPQ IRQs across both ports,
+ * allocate the shared priority structures once here
+ */
+ if (prueth->fw_data->support_switch || has_lre) {
+ prueth->hp = devm_kzalloc(dev,
+ sizeof(struct prueth_ndev_priority),
+ GFP_KERNEL);
+ if (!prueth->hp) {
+ ret = -ENOMEM;
+ goto free_pool;
+ }
+ prueth->lp = devm_kzalloc(dev,
+ sizeof(struct prueth_ndev_priority),
+ GFP_KERNEL);
+ if (!prueth->lp) {
+ ret = -ENOMEM;
+ goto free_pool;
+ }
+
+ prueth->rx_lpq_irq = of_irq_get_byname(np, "rx_lp");
+ if (prueth->rx_lpq_irq < 0) {
+ ret = prueth->rx_lpq_irq;
+ if (ret != -EPROBE_DEFER)
+ dev_err(prueth->dev, "could not get rx_lp irq\n");
+ goto free_pool;
+ }
+ prueth->rx_hpq_irq = of_irq_get_byname(np, "rx_hp");
+ if (prueth->rx_hpq_irq < 0) {
+ ret = prueth->rx_hpq_irq;
+ if (ret != -EPROBE_DEFER)
+ dev_err(prueth->dev, "could not get rx_hp irq\n");
+ goto free_pool;
+ }
+ }
+
prueth->support_lre = has_lre;
spin_lock_init(&prueth->addr_lock);
/* setup netdev interfaces */
@@ -2488,6 +2604,7 @@ static struct prueth_private_data am335x_prueth_pdata = {
.fw_name[PRUSS_ETHTYPE_SWITCH] =
"ti-pruss/am335x-pru1-prusw-fw.elf",
},
+ .fw_rev = FW_REV_V1_0,
.support_lre = true,
.support_switch = true,
};
@@ -2515,6 +2632,7 @@ static struct prueth_private_data am437x_prueth_pdata = {
.fw_name[PRUSS_ETHTYPE_SWITCH] =
"ti-pruss/am437x-pru1-prusw-fw.elf",
},
+ .fw_rev = FW_REV_V1_0,
.support_lre = true,
.support_switch = true,
};
@@ -2543,6 +2661,7 @@ static struct prueth_private_data am57xx_prueth_pdata = {
"ti-pruss/am57xx-pru1-prusw-fw.elf",
},
+ .fw_rev = FW_REV_V2_1,
.support_lre = true,
.support_switch = true,
};
diff --git a/drivers/net/ethernet/ti/icssm/icssm_prueth.h b/drivers/net/ethernet/ti/icssm/icssm_prueth.h
index a5d5bcd08bcd..9af19617967e 100644
--- a/drivers/net/ethernet/ti/icssm/icssm_prueth.h
+++ b/drivers/net/ethernet/ti/icssm/icssm_prueth.h
@@ -179,11 +179,22 @@ enum prueth_mem {
PRUETH_MEM_MAX,
};
+/* PRU firmware revision */
+enum fw_revision {
+ FW_REV_INVALID = 0,
+ FW_REV_V1_0,
+ FW_REV_V2_1
+};
+
/* Firmware offsets/size information */
struct prueth_fw_offsets {
u32 mc_ctrl_offset;
u32 mc_filter_mask;
u32 mc_filter_tbl;
+ /* IEP wrap is used in the rx packet ordering logic and
+ * is different for ICSSM v1.0 vs 2.1
+ */
+ u32 iep_wrap;
};
enum pruss_device {
@@ -197,24 +208,26 @@ enum pruss_device {
* struct prueth_private_data - PRU Ethernet private data
* @driver_data: PRU Ethernet device name
* @fw_pru: firmware names to be used for PRUSS ethernet usecases
+ * @fw_rev: Firmware revision identifier
* @support_switch: boolean to indicate if switch is enabled
* @support_lre: boolean to indicate if LRE is enabled
*/
struct prueth_private_data {
enum pruss_device driver_data;
const struct prueth_firmware fw_pru[PRUSS_NUM_PRUS];
+ enum fw_revision fw_rev;
bool support_switch;
bool support_lre;
};
struct prueth_emac_stats {
- u64 tx_packets;
- u64 tx_dropped;
- u64 tx_bytes;
- u64 rx_packets;
- u64 rx_bytes;
- u64 rx_length_errors;
- u64 rx_over_errors;
+ atomic64_t tx_packets;
+ atomic64_t tx_dropped;
+ atomic64_t tx_bytes;
+ atomic64_t rx_packets;
+ atomic64_t rx_bytes;
+ atomic64_t rx_length_errors;
+ atomic64_t rx_over_errors;
};
/* data for each emac port */
@@ -255,6 +268,11 @@ struct prueth_emac {
int offload_fwd_mark;
};
+struct prueth_ndev_priority {
+ struct net_device *ndev;
+ int priority;
+};
+
struct prueth {
struct device *dev;
struct pruss *pruss;
@@ -270,6 +288,12 @@ struct prueth {
struct device_node *eth_node[PRUETH_NUM_MACS];
struct prueth_emac *emac[PRUETH_NUM_MACS];
struct net_device *registered_netdevs[PRUETH_NUM_MACS];
+ struct prueth_ndev_priority *hp, *lp;
+ /* NAPI for lp and hp queue scans */
+ struct napi_struct napi_lpq;
+ struct napi_struct napi_hpq;
+ int rx_lpq_irq;
+ int rx_hpq_irq;
bool support_lre;
unsigned int tbl_check_mask;
@@ -303,6 +327,12 @@ void icssm_emac_mc_filter_bin_allow(struct prueth_emac *emac, u8 hash);
void icssm_emac_mc_filter_bin_disallow(struct prueth_emac *emac, u8 hash);
u8 icssm_emac_get_mc_hash(u8 *mac, u8 *mask);
+int icssm_prueth_lre_napi_poll_lpq(struct napi_struct *napi, int budget);
+int icssm_prueth_lre_napi_poll_hpq(struct napi_struct *napi, int budget);
+
+int icssm_prueth_common_request_irqs(struct prueth_emac *emac);
+void icssm_prueth_common_free_irqs(struct prueth_emac *emac);
+
static inline bool prueth_is_lre(struct prueth *prueth)
{
return PRUETH_IS_HSR(prueth) || PRUETH_IS_PRP(prueth);
diff --git a/drivers/net/ethernet/ti/icssm/icssm_prueth_common.c b/drivers/net/ethernet/ti/icssm/icssm_prueth_common.c
new file mode 100644
index 000000000000..43140add8172
--- /dev/null
+++ b/drivers/net/ethernet/ti/icssm/icssm_prueth_common.c
@@ -0,0 +1,281 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Texas Instruments ICSSM Ethernet Driver
+ *
+ * Copyright (C) 2018-2022 Texas Instruments Incorporated - https://www.ti.com/
+ *
+ */
+
+#include <linux/kernel.h>
+#include <linux/string.h>
+#include <linux/if_vlan.h>
+
+#include "icssm_prueth.h"
+#include "icssm_prueth_switch.h"
+
+static int icssm_prueth_common_emac_rx_packets(struct prueth_emac *emac,
+ int quota, u8 qid1, u8 qid2)
+{
+ u16 bd_rd_ptr, bd_wr_ptr, update_rd_ptr, bd_rd_ptr_o, bd_wr_ptr_o;
+ const struct prueth_queue_info *rxqueue, *rxqueue_o, *rxqueue_p;
+ struct prueth_queue_desc __iomem *queue_desc, *queue_desc_o;
+ struct prueth_packet_info pkt_info, pkt_info_o, *pkt_info_p;
+ u32 rd_buf_desc, rd_buf_desc_o, pkt_ts, pkt_ts_o, iep_wrap;
+ int ret, used = 0, port, port0_q_empty, port1_q_empty;
+ struct prueth_emac *emac_p, *other_emac;
+ void __iomem *shared_ram, *ocmc_ram;
+ u8 overflow_cnt, overflow_cnt_o;
+ u16 *bd_rd_ptr_p, *bd_wr_ptr_p;
+ struct prueth *prueth;
+
+ prueth = emac->prueth;
+ ocmc_ram = prueth->mem[PRUETH_MEM_OCMC].va;
+ shared_ram = prueth->mem[PRUETH_MEM_SHARED_RAM].va;
+ other_emac = prueth->emac[(emac->port_id == PRUETH_PORT_MII0) ?
+ PRUETH_PORT_MII1 - 1 : PRUETH_PORT_MII0 - 1];
+
+ iep_wrap = prueth->fw_offsets.iep_wrap;
+ /* search host queues for packets */
+ queue_desc = emac->rx_queue_descs + qid1;
+ queue_desc_o = other_emac->rx_queue_descs + qid2;
+
+ rxqueue = &sw_queue_infos[PRUETH_PORT_HOST][qid1];
+ rxqueue_o = &sw_queue_infos[PRUETH_PORT_HOST][qid2];
+
+ /* skip Rx if budget is 0 */
+ if (!quota)
+ return 0;
+
+ overflow_cnt = readb(&queue_desc->overflow_cnt);
+ overflow_cnt_o = readb(&queue_desc_o->overflow_cnt);
+
+ if (overflow_cnt > 0) {
+ atomic64_add(overflow_cnt, &emac->stats.rx_over_errors);
+ writeb(0, &queue_desc->overflow_cnt);
+ }
+ if (overflow_cnt_o > 0) {
+ atomic64_add(overflow_cnt_o, &other_emac->stats.rx_over_errors);
+ writeb(0, &queue_desc_o->overflow_cnt);
+ }
+
+ bd_rd_ptr = readw(&queue_desc->rd_ptr);
+ bd_wr_ptr = readw(&queue_desc->wr_ptr);
+
+ bd_rd_ptr_o = readw(&queue_desc_o->rd_ptr);
+ bd_wr_ptr_o = readw(&queue_desc_o->wr_ptr);
+
+ port0_q_empty = (bd_rd_ptr == bd_wr_ptr);
+ port1_q_empty = (bd_rd_ptr_o == bd_wr_ptr_o);
+
+ while (!port0_q_empty || !port1_q_empty) {
+ rd_buf_desc = readl(shared_ram + bd_rd_ptr);
+ rd_buf_desc_o = readl(shared_ram + bd_rd_ptr_o);
+
+ icssm_parse_packet_info(prueth, rd_buf_desc, &pkt_info);
+ icssm_parse_packet_info(prueth, rd_buf_desc_o, &pkt_info_o);
+
+ pkt_ts = readl(ocmc_ram + ICSS_LRE_TIMESTAMP_ARRAY_OFFSET +
+ bd_rd_ptr - SRAM_START_OFFSET);
+ pkt_ts_o = readl(ocmc_ram + ICSS_LRE_TIMESTAMP_ARRAY_OFFSET +
+ bd_rd_ptr_o - SRAM_START_OFFSET);
+
+ if (!port0_q_empty && !port1_q_empty) {
+ /* Both ports have a pending frame, pick the
+ * earlier one by comparing timestamps and
+ * account for wraparound.
+ */
+ if (pkt_ts > pkt_ts_o)
+ port = (pkt_ts - pkt_ts_o) > (iep_wrap / 2) ?
+ 0 : 1;
+ else
+ port = (pkt_ts_o - pkt_ts) > (iep_wrap / 2) ?
+ 1 : 0;
+
+ } else if (!port0_q_empty) {
+ /* Packet(s) in port0 queue only */
+ port = 0;
+ } else {
+ /* Packet(s) in port1 queue only */
+ port = 1;
+ }
+
+ /* Select correct data structures for queue/packet selected */
+ if (port == 0) {
+ pkt_info_p = &pkt_info;
+ bd_wr_ptr_p = &bd_wr_ptr;
+ bd_rd_ptr_p = &bd_rd_ptr;
+ emac_p = emac;
+ rxqueue_p = rxqueue;
+ } else {
+ pkt_info_p = &pkt_info_o;
+ bd_wr_ptr_p = &bd_wr_ptr_o;
+ bd_rd_ptr_p = &bd_rd_ptr_o;
+ emac_p = other_emac;
+ rxqueue_p = rxqueue_o;
+ }
+
+ if ((*pkt_info_p).length < EMAC_MIN_PKTLEN) {
+ /* Undersized frame: firmware should have filtered
+ * these before they reach the host queue. Advance
+ * the read pointer to skip it.
+ */
+ update_rd_ptr = *bd_wr_ptr_p;
+ atomic64_inc(&emac_p->stats.rx_length_errors);
+ } else if ((*pkt_info_p).length > EMAC_MAX_FRM_SUPPORT) {
+ /* Oversized frame: firmware should have filtered
+ * these before they reach the host queue. Advance
+ * the read pointer to skip it.
+ */
+ update_rd_ptr = *bd_wr_ptr_p;
+ atomic64_inc(&emac_p->stats.rx_length_errors);
+ } else {
+ update_rd_ptr = *bd_rd_ptr_p;
+ ret = icssm_emac_rx_packet(emac_p, &update_rd_ptr,
+ pkt_info_p, rxqueue_p);
+ if (ret)
+ return used;
+
+ used++;
+ }
+
+ /* Zero the BD after consuming it, a misaligned rd_ptr
+ * would otherwise mistake stale data for a valid incoming
+ * frame.
+ */
+ if (port == 0) {
+ writel(0, shared_ram + bd_rd_ptr);
+ writew(update_rd_ptr, &queue_desc->rd_ptr);
+ bd_rd_ptr = update_rd_ptr;
+ } else {
+ writel(0, shared_ram + bd_rd_ptr_o);
+ writew(update_rd_ptr, &queue_desc_o->rd_ptr);
+ bd_rd_ptr_o = update_rd_ptr;
+ }
+
+ port0_q_empty = (bd_rd_ptr == bd_wr_ptr) ? 1 : 0;
+ port1_q_empty = (bd_rd_ptr_o == bd_wr_ptr_o) ? 1 : 0;
+
+ if (used >= quota)
+ return used;
+ }
+
+ return used;
+}
+
+int icssm_prueth_lre_napi_poll_lpq(struct napi_struct *napi, int budget)
+{
+ struct prueth_emac *emac;
+ struct net_device *ndev;
+ struct prueth *prueth;
+ int num_rx_packets;
+ u8 qid1, qid2;
+
+ prueth = container_of(napi, struct prueth, napi_lpq);
+ ndev = prueth->lp->ndev;
+ emac = netdev_priv(ndev);
+ qid1 = PRUETH_QUEUE2;
+ qid2 = PRUETH_QUEUE4;
+
+ num_rx_packets = icssm_prueth_common_emac_rx_packets(emac, budget,
+ qid1, qid2);
+ if (num_rx_packets < budget && napi_complete_done(napi, num_rx_packets))
+ enable_irq(prueth->rx_lpq_irq);
+
+ return num_rx_packets;
+}
+
+int icssm_prueth_lre_napi_poll_hpq(struct napi_struct *napi, int budget)
+{
+ struct prueth_emac *emac;
+ struct net_device *ndev;
+ struct prueth *prueth;
+ int num_rx_packets;
+ u8 qid1, qid2;
+
+ prueth = container_of(napi, struct prueth, napi_hpq);
+ ndev = prueth->hp->ndev;
+ emac = netdev_priv(ndev);
+ qid1 = PRUETH_QUEUE1;
+ qid2 = PRUETH_QUEUE3;
+
+ num_rx_packets = icssm_prueth_common_emac_rx_packets(emac, budget,
+ qid1, qid2);
+ if (num_rx_packets < budget && napi_complete_done(napi, num_rx_packets))
+ enable_irq(prueth->rx_hpq_irq);
+
+ return num_rx_packets;
+}
+
+static irqreturn_t icssm_prueth_common_emac_rx_hardirq(int irq, void *dev_id)
+{
+ struct prueth_ndev_priority *ndev_prio;
+ struct prueth_emac *emac;
+ struct net_device *ndev;
+ struct prueth *prueth;
+
+ ndev_prio = (struct prueth_ndev_priority *)dev_id;
+ ndev = ndev_prio->ndev;
+ emac = netdev_priv(ndev);
+ prueth = emac->prueth;
+
+ /* disable Rx system event */
+ if (ndev_prio->priority == 1) {
+ disable_irq_nosync(prueth->rx_lpq_irq);
+ napi_schedule(&prueth->napi_lpq);
+ } else {
+ disable_irq_nosync(prueth->rx_hpq_irq);
+ napi_schedule(&prueth->napi_hpq);
+ }
+
+ return IRQ_HANDLED;
+}
+
+int icssm_prueth_common_request_irqs(struct prueth_emac *emac)
+{
+ struct prueth *prueth = emac->prueth;
+ int ret;
+
+ /* Request irq when first port is initialized */
+ if (prueth->emac_configured)
+ return 0;
+
+ ret = request_irq(prueth->rx_hpq_irq,
+ icssm_prueth_common_emac_rx_hardirq,
+ IRQF_TRIGGER_HIGH, "eth_hp_int", prueth->hp);
+ if (ret) {
+ netdev_err(emac->ndev, "unable to request RX HPQ IRQ\n");
+ return ret;
+ }
+
+ ret = request_irq(prueth->rx_lpq_irq,
+ icssm_prueth_common_emac_rx_hardirq,
+ IRQF_TRIGGER_HIGH, "eth_lp_int", prueth->lp);
+ if (ret) {
+ netdev_err(emac->ndev, "unable to request RX LPQ IRQ\n");
+ goto free_rx_hpq_irq;
+ }
+
+ return 0;
+
+free_rx_hpq_irq:
+ free_irq(prueth->rx_hpq_irq, prueth->hp);
+
+ return ret;
+}
+
+/**
+ * icssm_prueth_common_free_irqs - free irq
+ *
+ * @emac: EMAC data structure
+ *
+ */
+void icssm_prueth_common_free_irqs(struct prueth_emac *emac)
+{
+ struct prueth *prueth = emac->prueth;
+
+ /* HSR/PRP: free irqs when last port is down */
+ if (prueth->emac_configured)
+ return;
+
+ free_irq(prueth->rx_lpq_irq, prueth->lp);
+ free_irq(prueth->rx_hpq_irq, prueth->hp);
+}
diff --git a/drivers/net/ethernet/ti/icssm/icssm_prueth_lre.c b/drivers/net/ethernet/ti/icssm/icssm_prueth_lre.c
index 6276dd1e8bb1..239542101943 100644
--- a/drivers/net/ethernet/ti/icssm/icssm_prueth_lre.c
+++ b/drivers/net/ethernet/ti/icssm/icssm_prueth_lre.c
@@ -152,6 +152,14 @@ static void icssm_prueth_lre_protocol_init(struct prueth *prueth)
dram1 + ICSS_LRE_SUP_ADDR_LOW);
}
+static void icssm_prueth_lre_config_packet_timestamping(struct prueth *prueth)
+{
+ void __iomem *sram = prueth->mem[PRUETH_MEM_SHARED_RAM].va;
+
+ writeb(1, sram + ICSS_LRE_PRIORITY_INTRS_STATUS_OFFSET);
+ writeb(1, sram + ICSS_LRE_TIMESTAMP_PKTS_STATUS_OFFSET);
+}
+
static enum hrtimer_restart icssm_prueth_lre_timer(struct hrtimer *timer)
{
struct prueth *prueth;
@@ -202,6 +210,11 @@ void icssm_prueth_lre_config(struct prueth *prueth)
icssm_prueth_lre_init(prueth);
icssm_prueth_lre_dbg_init(prueth);
icssm_prueth_lre_protocol_init(prueth);
+ /* Enable per-packet timestamping so the driver can order
+ * received frames by arrival time across the two slave ports.
+ */
+ icssm_prueth_lre_config_packet_timestamping(prueth);
+
}
void icssm_prueth_lre_cleanup(struct prueth *prueth)
diff --git a/drivers/net/ethernet/ti/icssm/icssm_prueth_switch.c b/drivers/net/ethernet/ti/icssm/icssm_prueth_switch.c
index 66866ea37913..9d67fc7c23ac 100644
--- a/drivers/net/ethernet/ti/icssm/icssm_prueth_switch.c
+++ b/drivers/net/ethernet/ti/icssm/icssm_prueth_switch.c
@@ -886,6 +886,14 @@ void icssm_prueth_sw_hostconfig(struct prueth *prueth)
sizeof(queue_descs[PRUETH_PORT_QUEUE_HOST]));
}
+void icssm_prueth_sw_config_packet_timestamping(struct prueth *prueth)
+{
+ void __iomem *sram = prueth->mem[PRUETH_MEM_SHARED_RAM].va;
+
+ writeb(1, sram + SWITCH_PRIORITY_INTRS_STATUS_OFFSET);
+ writeb(1, sram + SWITCH_TIMESTAMP_PKTS_STATUS_OFFSET);
+}
+
static int icssm_prueth_sw_port_config(struct prueth *prueth,
enum prueth_port port_id)
{
diff --git a/drivers/net/ethernet/ti/icssm/icssm_prueth_switch.h b/drivers/net/ethernet/ti/icssm/icssm_prueth_switch.h
index e6111bba166e..fe7197f9a6da 100644
--- a/drivers/net/ethernet/ti/icssm/icssm_prueth_switch.h
+++ b/drivers/net/ethernet/ti/icssm/icssm_prueth_switch.h
@@ -29,6 +29,7 @@ void icssm_prueth_sw_fdb_del(struct prueth_emac *emac,
int icssm_prueth_sw_learn_fdb(struct prueth_emac *emac, u8 *src_mac);
int icssm_prueth_sw_purge_fdb(struct prueth_emac *emac);
void icssm_prueth_sw_hostconfig(struct prueth *prueth);
+void icssm_prueth_sw_config_packet_timestamping(struct prueth *prueth);
int icssm_prueth_sw_emac_config(struct prueth_emac *emac);
int icssm_prueth_sw_boot_prus(struct prueth *prueth, struct net_device *ndev);
int icssm_prueth_sw_shutdown_prus(struct prueth_emac *emac,
diff --git a/drivers/net/ethernet/ti/icssm/icssm_switch.h b/drivers/net/ethernet/ti/icssm/icssm_switch.h
index 5ba9ce14da44..13b1b93ef4c2 100644
--- a/drivers/net/ethernet/ti/icssm/icssm_switch.h
+++ b/drivers/net/ethernet/ti/icssm/icssm_switch.h
@@ -331,6 +331,12 @@
#define P0_COL_BUFFER_OFFSET 0xEE00
#define P0_Q1_BUFFER_OFFSET 0x0000
+/* Enable/disable interrupts for high/low priority instead of per port.
+ * 0 = disabled (default), 1 = enabled
+ */
+#define SWITCH_PRIORITY_INTRS_STATUS_OFFSET 0x1FAA
+#define SWITCH_TIMESTAMP_PKTS_STATUS_OFFSET 0x1FAB
+
#define V2_1_FDB_TBL_LOC PRUETH_MEM_SHARED_RAM
#define V2_1_FDB_TBL_OFFSET 0x2000
--
2.43.0
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH net-next v3 3/3] net: ti: icssm-prueth: Support duplicate HW offload feature for HSR and PRP
2026-08-04 12:14 [PATCH net-next v3 0/3] Introduce HSR/PRP HW offload support for PRU-ICSSM Ethernet driver Parvathi Pudi
2026-08-04 12:14 ` [PATCH net-next v3 1/3] net: ti: icssm-prueth: Add HSR and PRP HW offload mode support for AM57xx, AM437x and AM335x Parvathi Pudi
2026-08-04 12:14 ` [PATCH net-next v3 2/3] net: ti: icssm-prueth: Add priority based RX IRQ handlers Parvathi Pudi
@ 2026-08-04 12:14 ` Parvathi Pudi
2026-08-11 8:39 ` Paolo Abeni
2 siblings, 1 reply; 7+ messages in thread
From: Parvathi Pudi @ 2026-08-04 12:14 UTC (permalink / raw)
To: andrew+netdev, davem, edumazet, kuba, pabeni, danishanwar,
parvathi, rogerq, pmohan, afd, basharath, arnd
Cc: linux-kernel, netdev, linux-arm-kernel, pratheesh, j-rameshbabu,
vigneshr, praneeth, srk, rogerq, m-malladi, krishna, mohan
From: Roger Quadros <rogerq@ti.com>
In HSR and PRP modes each outgoing frame must be sent on both PRU slave
ports.
Previously the driver was writing the frame into each port's transmit queue
independently after updating the tags resulting in performing two OCMC
buffer copy operations.
Frame duplicate offloading is implemented with a common shared queue
between the two ports. The driver writes the frame once into OCMC RAM,
each port reads from the shared queue and replicates the transmission to
both PRU ports, synchronising between PRU ports are maintained within
firmware with appropriate handling.
For HSR the driver inspects the encapsulated ethertype in the HSR tag.
PTP frames (ETH_P_1588) are sent on the directed port only to avoid double
duplication and all other HSR frames are duplicated to both ports.
VLAN-tagged HSR frames are handled by advancing past the 4-byte VLAN header
before reading the HSR tag.
For PRP the driver checks the 6-byte RCT trailer for the ETH_P_PRP suffix
to identify redundancy-tagged frames. Frames without an RCT are sent on the
originating port only.
Signed-off-by: Roger Quadros <rogerq@ti.com>
Signed-off-by: Andrew F. Davis <afd@ti.com>
Signed-off-by: Parvathi Pudi <parvathi@couthit.com>
---
drivers/net/ethernet/ti/icssm/icssm_prueth.c | 245 ++++++++++++--
drivers/net/ethernet/ti/icssm/icssm_prueth.h | 11 +-
.../ethernet/ti/icssm/icssm_prueth_common.c | 7 +-
.../ethernet/ti/icssm/icssm_prueth_switch.c | 310 +++++++++++++++++-
.../ethernet/ti/icssm/icssm_prueth_switch.h | 1 +
drivers/net/ethernet/ti/icssm/icssm_switch.h | 35 +-
6 files changed, 563 insertions(+), 46 deletions(-)
diff --git a/drivers/net/ethernet/ti/icssm/icssm_prueth.c b/drivers/net/ethernet/ti/icssm/icssm_prueth.c
index 7031b78d2cc2..43862946d4f1 100644
--- a/drivers/net/ethernet/ti/icssm/icssm_prueth.c
+++ b/drivers/net/ethernet/ti/icssm/icssm_prueth.c
@@ -36,12 +36,14 @@
#include "../icssg/icss_iep.h"
#define OCMC_RAM_SIZE (SZ_64K)
+#define PRUETH_ETHER_TYPE_OFFSET 12
#define TX_START_DELAY 0x40
#define TX_CLK_DELAY_100M 0x6
#define HR_TIMER_TX_DELAY_US 100
#define NETIF_PRUETH_LRE_OFFLOAD_FEATURES (NETIF_F_HW_HSR_FWD | \
+ NETIF_F_HW_HSR_DUP | \
NETIF_F_HW_HSR_TAG_RM)
/* ICSSM (v2.1) - supports 64-bit IEP counter.
@@ -79,6 +81,32 @@ static void icssm_prueth_set_fw_offsets(struct prueth *prueth)
}
}
+/* Queue Descriptors initialization for HSR PRP */
+const struct prueth_queue_desc hsr_prp_txopt_queue_descs[][NUM_QUEUES] = {
+ [PRUETH_PORT_QUEUE_HOST] = {
+ { .rd_ptr = P0_Q1_BD_OFFSET, .wr_ptr = P0_Q1_BD_OFFSET, },
+ { .rd_ptr = P0_Q2_BD_OFFSET, .wr_ptr = P0_Q2_BD_OFFSET, },
+ { .rd_ptr = P0_Q3_BD_OFFSET, .wr_ptr = P0_Q3_BD_OFFSET, },
+ { .rd_ptr = P0_Q4_BD_OFFSET, .wr_ptr = P0_Q4_BD_OFFSET, },
+ },
+ [PRUETH_PORT_QUEUE_MII0] = {
+ { .rd_ptr = P0_Q3_BD_OFFSET, .wr_ptr = P0_Q3_BD_OFFSET, },
+ { .rd_ptr = P0_Q4_BD_OFFSET, .wr_ptr = P0_Q4_BD_OFFSET, },
+ { .rd_ptr = P1_Q3_TXOPT_BD_OFFSET,
+ .wr_ptr = P1_Q3_TXOPT_BD_OFFSET, },
+ { .rd_ptr = P2_Q1_TXOPT_BD_OFFSET,
+ .wr_ptr = P2_Q1_TXOPT_BD_OFFSET, },
+ },
+ [PRUETH_PORT_QUEUE_MII1] = {
+ { .rd_ptr = P0_Q1_BD_OFFSET, .wr_ptr = P0_Q1_BD_OFFSET, },
+ { .rd_ptr = P0_Q2_BD_OFFSET, .wr_ptr = P0_Q2_BD_OFFSET, },
+ { .rd_ptr = P1_Q3_TXOPT_BD_OFFSET,
+ .wr_ptr = P1_Q3_TXOPT_BD_OFFSET, },
+ { .rd_ptr = P2_Q1_TXOPT_BD_OFFSET,
+ .wr_ptr = P2_Q1_TXOPT_BD_OFFSET, },
+ }
+};
+
static void icssm_prueth_write_reg(struct prueth *prueth,
enum prueth_mem region,
unsigned int reg, u32 val)
@@ -97,6 +125,17 @@ static void icssm_prueth_write_reg(struct prueth *prueth,
static enum pruss_mem pruss_mem_ids[] = { PRUSS_MEM_DRAM0, PRUSS_MEM_DRAM1,
PRUSS_MEM_SHRD_RAM2 };
+struct prp_txopt_rct {
+ __be16 sequence_nr;
+ __be16 lan_id_and_lsdu_size;
+ __be16 prp_suffix;
+};
+
+struct hsr_txopt_ethhdr {
+ struct ethhdr ethhdr;
+ struct hsr_tag hsr_tag;
+};
+
static const struct prueth_queue_info queue_infos[][NUM_QUEUES] = {
[PRUETH_PORT_QUEUE_HOST] = {
[PRUETH_QUEUE1] = {
@@ -551,15 +590,24 @@ static int icssm_prueth_tx_enqueue(struct prueth_emac *emac,
struct sk_buff *skb,
enum prueth_queue_id queue_id)
{
+ struct prueth_queue_desc __iomem *queue_desc_other_port = NULL;
struct prueth_queue_desc __iomem *queue_desc;
const struct prueth_queue_info *txqueue;
- struct net_device *ndev = emac->ndev;
struct prueth *prueth = emac->prueth;
+ struct hsr_txopt_ethhdr *hsr_ethhdr;
unsigned int buffer_desc_count;
+ struct prueth_emac *other_emac;
int free_blocks, update_block;
+ struct vlan_ethhdr *vlan_hdr;
bool buffer_wrapped = false;
int write_block, read_block;
+ int free_blocks_other_port;
+ int read_block_other_port;
void *src_addr, *dst_addr;
+ u16 bd_rd_ptr_other_port;
+ struct ethhdr *ethhdr;
+ bool is_vlan = false;
+ bool link_up = false;
int pkt_block_size;
void __iomem *sram;
void __iomem *dram;
@@ -567,16 +615,19 @@ static int icssm_prueth_tx_enqueue(struct prueth_emac *emac,
u16 update_wr_ptr;
u32 wr_buf_desc;
void *ocmc_ram;
+ __be16 proto;
+ u8 *hdr;
+
+ other_emac = emac->prueth->emac[(emac->port_id == PRUETH_PORT_MII0) ?
+ PRUETH_PORT_MII1 - 1 : PRUETH_PORT_MII0 - 1];
+
+ if (prueth_is_lre(prueth) && (emac->link || other_emac->link))
+ link_up = true;
if (!PRUETH_IS_EMAC(prueth))
dram = prueth->mem[PRUETH_MEM_DRAM1].va;
else
dram = emac->prueth->mem[emac->dram].va;
- if (eth_skb_pad(skb)) {
- if (netif_msg_tx_err(emac) && net_ratelimit())
- netdev_err(ndev, "packet pad failed\n");
- return -ENOMEM;
- }
/* which port to tx: MII0 or MII1 */
txport = emac->tx_port_queue;
@@ -584,7 +635,10 @@ static int icssm_prueth_tx_enqueue(struct prueth_emac *emac,
pktlen = skb->len;
/* Get the tx queue */
queue_desc = emac->tx_queue_descs + queue_id;
- if (!PRUETH_IS_EMAC(prueth))
+ /* Tx queue context */
+ if (prueth_is_lre(prueth))
+ txqueue = &lre_queue_infos[txport][queue_id];
+ else if (PRUETH_IS_SWITCH(prueth))
txqueue = &sw_queue_infos[txport][queue_id];
else
txqueue = &queue_infos[txport][queue_id];
@@ -607,6 +661,29 @@ static int icssm_prueth_tx_enqueue(struct prueth_emac *emac,
free_blocks = buffer_desc_count;
}
+ /* Fetch queue state for the second LRE port */
+ if (prueth_is_lre(prueth) && link_up) {
+ queue_desc_other_port = emac->tx_queue_descs_other_port +
+ queue_id;
+ bd_rd_ptr_other_port = readw(&queue_desc_other_port->rd_ptr);
+
+ read_block_other_port = (bd_rd_ptr_other_port -
+ txqueue->buffer_desc_offset) / BD_SIZE;
+
+ if (write_block > read_block_other_port) {
+ free_blocks_other_port = buffer_desc_count -
+ write_block;
+ free_blocks_other_port += read_block_other_port;
+ } else if (write_block < read_block_other_port) {
+ free_blocks_other_port = read_block_other_port -
+ write_block;
+ } else {
+ free_blocks_other_port = buffer_desc_count;
+ }
+
+ if (free_blocks_other_port < free_blocks)
+ free_blocks = free_blocks_other_port;
+ }
pkt_block_size = DIV_ROUND_UP(pktlen, ICSS_BLOCK_SIZE);
if (pkt_block_size >= free_blocks) /* out of queue space */
return -ENOBUFS;
@@ -656,6 +733,57 @@ static int icssm_prueth_tx_enqueue(struct prueth_emac *emac,
if (PRUETH_IS_HSR(prueth))
wr_buf_desc |= BIT(PRUETH_BD_HSR_FRAME_SHIFT);
+ if (prueth_is_lre(prueth)) {
+ ethhdr = (struct ethhdr *)skb_mac_header(skb);
+ proto = ethhdr->h_proto;
+
+ if (proto == htons(ETH_P_8021Q)) {
+ vlan_hdr = (struct vlan_ethhdr *)ethhdr;
+ proto = vlan_hdr->h_vlan_encapsulated_proto;
+ is_vlan = true;
+ }
+
+ /* Extract HSR sequence number and LAN ID
+ * from the tag for the Buffer Descriptor
+ */
+ if (proto == htons(ETH_P_HSR)) {
+ hdr = skb_mac_header(skb);
+
+ if (is_vlan) {
+ hsr_ethhdr =
+ (struct hsr_txopt_ethhdr *)(hdr +
+ VLAN_HLEN);
+ } else {
+ hsr_ethhdr = (struct hsr_txopt_ethhdr *)hdr;
+ }
+
+ /* PTP frames (ETH_P_1588) carry no LAN ID
+ * in the HSR tag
+ */
+ if (hsr_ethhdr->hsr_tag.encap_proto !=
+ htons(ETH_P_1588)) {
+ wr_buf_desc |= PRUETH_BD_LAN_INFO_MASK;
+ } else {
+ wr_buf_desc |= (txport <<
+ PRUETH_BD_LAN_A_SHIFT);
+ }
+ wr_buf_desc |= PRUETH_BD_RED_PKT_MASK;
+ } else {
+ /* Read PRP RCT to extract sequence number and LAN ID */
+ struct prp_txopt_rct *rct =
+ (struct prp_txopt_rct *)(skb_tail_pointer(skb) -
+ ICSSM_LRE_TAG_SIZE);
+
+ if (rct->prp_suffix == htons(ETH_P_PRP)) {
+ wr_buf_desc |= PRUETH_BD_LAN_INFO_MASK;
+ wr_buf_desc |= PRUETH_BD_RED_PKT_MASK;
+ } else {
+ wr_buf_desc |= (txport <<
+ PRUETH_BD_LAN_A_SHIFT);
+ }
+ }
+ }
+
sram = prueth->mem[PRUETH_MEM_SHARED_RAM].va;
if (!PRUETH_IS_EMAC(prueth))
writel(wr_buf_desc, sram + readw(&queue_desc->wr_ptr));
@@ -668,6 +796,10 @@ static int icssm_prueth_tx_enqueue(struct prueth_emac *emac,
update_wr_ptr = txqueue->buffer_desc_offset + (update_block * BD_SIZE);
writew(update_wr_ptr, &queue_desc->wr_ptr);
+ /* update the write pointer in queue descriptor of other port */
+ if (prueth_is_lre(prueth) && link_up)
+ writew(update_wr_ptr, &queue_desc_other_port->wr_ptr);
+
return 0;
}
@@ -680,8 +812,10 @@ void icssm_parse_packet_info(struct prueth *prueth, u32 buffer_descriptor,
else
pkt_info->start_offset = false;
- pkt_info->port = (buffer_descriptor & PRUETH_BD_PORT_MASK) >>
- PRUETH_BD_PORT_SHIFT;
+ /* Flag from BD to indicate packet is valid for HOST or not. */
+ pkt_info->host_recv_flag = !!(buffer_descriptor &
+ PRUETH_BD_HOST_RECV_MASK);
+
pkt_info->length = (buffer_descriptor & PRUETH_BD_LENGTH_MASK) >>
PRUETH_BD_LENGTH_SHIFT;
pkt_info->broadcast = !!(buffer_descriptor & PRUETH_BD_BROADCAST_MASK);
@@ -713,11 +847,13 @@ int icssm_emac_rx_packet(struct prueth_emac *emac, u16 *bd_rd_ptr,
int read_block, update_block;
unsigned int actual_pkt_len;
bool buffer_wrapped = false;
+ int adjust_for_hsr_tag = 0;
void *src_addr, *dst_addr;
u16 start_offset = 0;
struct sk_buff *skb;
int pkt_block_size;
void *ocmc_ram;
+ u16 type;
if (PRUETH_IS_HSR(emac->prueth))
start_offset = (pkt_info->start_offset ?
@@ -744,9 +880,19 @@ int icssm_emac_rx_packet(struct prueth_emac *emac, u16 *bd_rd_ptr,
/* calculate new pointer in ram */
*bd_rd_ptr = rxqueue->buffer_desc_offset + (update_block * BD_SIZE);
+ if (PRUETH_IS_HSR(emac->prueth)) {
+ if (!pkt_info->host_recv_flag)
+ return 0;
+ }
+
/* Exclude the HSR tag bytes already stripped by firmware, if any. */
actual_pkt_len = pkt_info->length - start_offset;
+ if (PRUETH_IS_HSR(emac->prueth)) {
+ if (!start_offset && !pkt_info->timestamp)
+ actual_pkt_len -= ICSSM_LRE_TAG_SIZE;
+ }
+
/* Allocate a socket buffer for this packet */
skb = netdev_alloc_skb_ip_align(ndev, actual_pkt_len);
if (!skb) {
@@ -767,6 +913,29 @@ int icssm_emac_rx_packet(struct prueth_emac *emac, u16 *bd_rd_ptr,
(read_block * ICSS_BLOCK_SIZE);
src_addr += start_offset;
+ /* Copy destination and source MAC address */
+ memcpy(dst_addr, src_addr, PRUETH_ETHER_TYPE_OFFSET);
+ src_addr += PRUETH_ETHER_TYPE_OFFSET;
+ dst_addr += PRUETH_ETHER_TYPE_OFFSET;
+
+ adjust_for_hsr_tag += PRUETH_ETHER_TYPE_OFFSET;
+
+ /* Check for VLAN tag */
+ type = get_unaligned_be16(src_addr);
+
+ if (type == ETH_P_8021Q) {
+ memcpy(dst_addr, src_addr, VLAN_HLEN);
+ src_addr += VLAN_HLEN;
+ dst_addr += VLAN_HLEN;
+ adjust_for_hsr_tag += VLAN_HLEN;
+ }
+
+ /* HSR tag removal handling */
+ if (PRUETH_IS_HSR(emac->prueth)) {
+ if (!start_offset && !pkt_info->timestamp)
+ src_addr += ICSSM_LRE_TAG_SIZE;
+ }
+
/* Copy the data from PRU buffers(OCMC) to socket buffer(DRAM) */
if (buffer_wrapped) { /* wrapped around buffer */
int bytes = (buffer_desc_count - read_block) * ICSS_BLOCK_SIZE;
@@ -782,19 +951,25 @@ int icssm_emac_rx_packet(struct prueth_emac *emac, u16 *bd_rd_ptr,
/* If applicable, account for the HSR tag removed */
bytes -= start_offset;
+ if (PRUETH_IS_HSR(emac->prueth)) {
+ if (!start_offset && !pkt_info->timestamp)
+ bytes -= ICSSM_LRE_TAG_SIZE;
+ }
+
/* copy non-wrapped part */
- memcpy(dst_addr, src_addr, bytes);
+ memcpy(dst_addr, src_addr, bytes - adjust_for_hsr_tag);
/* copy wrapped part */
- dst_addr += bytes;
+ dst_addr += (bytes - adjust_for_hsr_tag);
remaining = actual_pkt_len - bytes;
src_addr = ocmc_ram + rxqueue->buffer_offset;
memcpy(dst_addr, src_addr, remaining);
src_addr += remaining;
} else {
- memcpy(dst_addr, src_addr, actual_pkt_len);
- src_addr += actual_pkt_len;
+ memcpy(dst_addr, src_addr, actual_pkt_len -
+ adjust_for_hsr_tag);
+ src_addr += actual_pkt_len - adjust_for_hsr_tag;
}
if (PRUETH_IS_SWITCH(emac->prueth)) {
@@ -1320,18 +1495,32 @@ static enum netdev_tx icssm_emac_ndo_start_xmit(struct sk_buff *skb,
struct net_device *ndev)
{
struct prueth_emac *emac = netdev_priv(ndev);
+ /* Spinlock for Tx Queues */
+ spinlock_t *lock_queue;
+ unsigned long flags;
int ret;
u16 qid;
qid = icssm_prueth_get_tx_queue_id(emac->prueth, skb);
- ret = icssm_prueth_tx_enqueue(emac, skb, qid);
- if (ret) {
- if (ret != -ENOBUFS && netif_msg_tx_err(emac) &&
- net_ratelimit())
- netdev_err(ndev, "packet queue failed: %d\n", ret);
+ /* Select the TX queue spin lock for this queue ID */
+ if (prueth_is_lre(emac->prueth))
+ lock_queue = &emac->prueth->lre_host_queue_lock[qid - 2];
+ else
+ lock_queue = &emac->host_queue_lock[qid - 2];
+
+ if (eth_skb_pad(skb)) {
+ if (netif_msg_tx_err(emac) && net_ratelimit())
+ netdev_err(ndev, "packet pad failed\n");
+ ret = -ENOMEM;
goto fail_tx;
}
+ spin_lock_irqsave(lock_queue, flags);
+ ret = icssm_prueth_tx_enqueue(emac, skb, qid);
+ spin_unlock_irqrestore(lock_queue, flags);
+ if (ret)
+ goto fail_tx;
+
atomic64_inc(&emac->stats.tx_packets);
atomic64_add(skb->len, &emac->stats.tx_bytes);
dev_kfree_skb_any(skb);
@@ -1575,19 +1764,22 @@ static void icssm_emac_ndo_set_rx_mode(struct net_device *ndev)
static netdev_features_t icssm_emac_ndo_fix_features(struct net_device *ndev,
netdev_features_t features)
{
- /* hsr tag removal offload and hsr fwd offload are tightly coupled in
- * firmware implementation. Both these features need to be enabled /
- * disabled together.
+ /* hsr tag removal offload, hsr fwd offload and hsr dup offload are
+ * tightly coupled in firmware implementation. These features
+ * must always be enabled/disabled together.
*/
if (!(ndev->features & NETIF_PRUETH_LRE_OFFLOAD_FEATURES))
if ((features & NETIF_F_HW_HSR_FWD) ||
- (features & NETIF_F_HW_HSR_TAG_RM))
+ (features & NETIF_F_HW_HSR_TAG_RM) ||
+ (features & NETIF_F_HW_HSR_DUP))
features |= NETIF_PRUETH_LRE_OFFLOAD_FEATURES;
if ((ndev->features & NETIF_F_HW_HSR_FWD) ||
- (ndev->features & NETIF_F_HW_HSR_TAG_RM))
+ (ndev->features & NETIF_F_HW_HSR_TAG_RM) ||
+ (ndev->features & NETIF_F_HW_HSR_DUP))
if (!(features & NETIF_F_HW_HSR_FWD) ||
- !(features & NETIF_F_HW_HSR_TAG_RM))
+ !(features & NETIF_F_HW_HSR_TAG_RM) ||
+ !(features & NETIF_F_HW_HSR_DUP))
features &= ~NETIF_PRUETH_LRE_OFFLOAD_FEATURES;
return features;
@@ -1774,6 +1966,9 @@ static int icssm_prueth_netdev_init(struct prueth *prueth,
spin_lock_init(&emac->lock);
spin_lock_init(&emac->addr_lock);
+ spin_lock_init(&emac->host_queue_lock[0]);
+ spin_lock_init(&emac->host_queue_lock[1]);
+
/* get mac address from DT and set private and netdev addr */
ret = of_get_ethdev_address(eth_node, ndev);
if (!is_valid_ether_addr(ndev->dev_addr)) {
@@ -2366,6 +2561,8 @@ static int icssm_prueth_probe(struct platform_device *pdev)
prueth->support_lre = has_lre;
spin_lock_init(&prueth->addr_lock);
+ spin_lock_init(&prueth->lre_host_queue_lock[0]);
+ spin_lock_init(&prueth->lre_host_queue_lock[1]);
/* setup netdev interfaces */
if (eth0_node) {
ret = icssm_prueth_netdev_init(prueth, eth0_node);
diff --git a/drivers/net/ethernet/ti/icssm/icssm_prueth.h b/drivers/net/ethernet/ti/icssm/icssm_prueth.h
index 9af19617967e..2447fb8b99e7 100644
--- a/drivers/net/ethernet/ti/icssm/icssm_prueth.h
+++ b/drivers/net/ethernet/ti/icssm/icssm_prueth.h
@@ -94,7 +94,7 @@ struct prueth_queue_info {
* struct prueth_packet_info - Info about a packet in buffer
* @start_offset: true if frame carries an HSR/PRP start offset
* @shadow: this packet is stored in the collision queue
- * @port: port packet is on
+ * @host_recv_flag: this frame should be received by host
* @length: length of packet
* @broadcast: this packet is a broadcast packet
* @error: this packet has an error
@@ -105,7 +105,7 @@ struct prueth_queue_info {
struct prueth_packet_info {
bool start_offset;
bool shadow;
- unsigned int port;
+ bool host_recv_flag;
unsigned int length;
bool broadcast;
bool error;
@@ -240,6 +240,8 @@ struct prueth_emac {
struct phy_device *phydev;
struct prueth_queue_desc __iomem *rx_queue_descs;
struct prueth_queue_desc __iomem *tx_queue_descs;
+ /* LRE duplicates each TX frame to both ports */
+ struct prueth_queue_desc __iomem *tx_queue_descs_other_port;
int link;
int speed;
@@ -263,6 +265,7 @@ struct prueth_emac {
spinlock_t lock;
spinlock_t addr_lock; /* serialize access to VLAN/MC filter table */
+ spinlock_t host_queue_lock[NUM_QUEUES / 2];
struct hrtimer tx_hrtimer;
struct prueth_emac_stats stats;
int offload_fwd_mark;
@@ -314,9 +317,13 @@ struct prueth {
u8 emac_configured;
u8 hsr_members;
u8 br_members;
+
+ /* Per-queue TX lock - LRE uses only the two high-priority queues */
+ spinlock_t lre_host_queue_lock[NUM_QUEUES / 2];
};
extern const struct prueth_queue_desc queue_descs[][NUM_QUEUES];
+extern const struct prueth_queue_desc hsr_prp_txopt_queue_descs[][NUM_QUEUES];
void icssm_parse_packet_info(struct prueth *prueth, u32 buffer_descriptor,
struct prueth_packet_info *pkt_info);
diff --git a/drivers/net/ethernet/ti/icssm/icssm_prueth_common.c b/drivers/net/ethernet/ti/icssm/icssm_prueth_common.c
index 43140add8172..a99cdbb79bbf 100644
--- a/drivers/net/ethernet/ti/icssm/icssm_prueth_common.c
+++ b/drivers/net/ethernet/ti/icssm/icssm_prueth_common.c
@@ -137,16 +137,13 @@ static int icssm_prueth_common_emac_rx_packets(struct prueth_emac *emac,
used++;
}
- /* Zero the BD after consuming it, a misaligned rd_ptr
- * would otherwise mistake stale data for a valid incoming
- * frame.
+ /* Leave the BD intact after reading. Firmware reuses it to
+ * forward the frame to the second LRE port.
*/
if (port == 0) {
- writel(0, shared_ram + bd_rd_ptr);
writew(update_rd_ptr, &queue_desc->rd_ptr);
bd_rd_ptr = update_rd_ptr;
} else {
- writel(0, shared_ram + bd_rd_ptr_o);
writew(update_rd_ptr, &queue_desc_o->rd_ptr);
bd_rd_ptr_o = update_rd_ptr;
}
diff --git a/drivers/net/ethernet/ti/icssm/icssm_prueth_switch.c b/drivers/net/ethernet/ti/icssm/icssm_prueth_switch.c
index 9d67fc7c23ac..bda2fac13d74 100644
--- a/drivers/net/ethernet/ti/icssm/icssm_prueth_switch.c
+++ b/drivers/net/ethernet/ti/icssm/icssm_prueth_switch.c
@@ -199,6 +199,189 @@ static const struct prueth_queue_info rx_queue_infos[][NUM_QUEUES] = {
},
};
+/* Tx Queue context for HSR and PRP */
+const struct prueth_queue_info lre_queue_infos[][NUM_QUEUES] = {
+ [PRUETH_PORT_QUEUE_HOST] = {
+ [PRUETH_QUEUE1] = {
+ P0_Q1_BUFFER_OFFSET,
+ P0_QUEUE_DESC_OFFSET,
+ P0_Q1_BD_OFFSET,
+ P0_Q1_BD_OFFSET + ((HOST_QUEUE_1_SIZE - 1) * BD_SIZE),
+ },
+ [PRUETH_QUEUE2] = {
+ P0_Q2_BUFFER_OFFSET,
+ P0_QUEUE_DESC_OFFSET + 8,
+ P0_Q2_BD_OFFSET,
+ P0_Q2_BD_OFFSET + ((HOST_QUEUE_2_SIZE - 1) * BD_SIZE),
+ },
+ [PRUETH_QUEUE3] = {
+ P0_Q3_BUFFER_OFFSET,
+ P0_QUEUE_DESC_OFFSET + 16,
+ P0_Q3_BD_OFFSET,
+ P0_Q3_BD_OFFSET + ((HOST_QUEUE_3_SIZE - 1) * BD_SIZE),
+ },
+ [PRUETH_QUEUE4] = {
+ P0_Q4_BUFFER_OFFSET,
+ P0_QUEUE_DESC_OFFSET + 24,
+ P0_Q4_BD_OFFSET,
+ P0_Q4_BD_OFFSET + ((HOST_QUEUE_4_SIZE - 1) * BD_SIZE),
+ },
+ },
+ [PRUETH_PORT_QUEUE_MII0] = {
+ [PRUETH_QUEUE1] = {
+ P0_Q3_BUFFER_OFFSET,
+ P0_Q3_BUFFER_OFFSET +
+ ((HOST_QUEUE_3_SIZE - 1) * ICSS_BLOCK_SIZE),
+ P0_Q3_BD_OFFSET,
+ P0_Q3_BD_OFFSET + ((HOST_QUEUE_3_SIZE - 1) * BD_SIZE),
+ },
+ [PRUETH_QUEUE2] = {
+ P0_Q4_BUFFER_OFFSET,
+ P0_Q4_BUFFER_OFFSET +
+ ((HOST_QUEUE_4_SIZE - 1) * ICSS_BLOCK_SIZE),
+ P0_Q4_BD_OFFSET,
+ P0_Q4_BD_OFFSET + ((HOST_QUEUE_4_SIZE - 1) * BD_SIZE),
+ },
+ [PRUETH_QUEUE3] = {
+ P1_Q3_TXOPT_BUFFER_OFFSET,
+ P1_Q3_TXOPT_BUFFER_OFFSET +
+ ((QUEUE_3_TXOPT_SIZE - 1) * ICSS_BLOCK_SIZE),
+ P1_Q3_TXOPT_BD_OFFSET,
+ P1_Q3_TXOPT_BD_OFFSET +
+ ((QUEUE_3_TXOPT_SIZE - 1) * BD_SIZE),
+ },
+ [PRUETH_QUEUE4] = {
+ P2_Q1_TXOPT_BUFFER_OFFSET,
+ P2_Q1_TXOPT_BUFFER_OFFSET +
+ ((QUEUE_4_TXOPT_SIZE - 1) * ICSS_BLOCK_SIZE),
+ P2_Q1_TXOPT_BD_OFFSET,
+ P2_Q1_TXOPT_BD_OFFSET +
+ ((QUEUE_4_TXOPT_SIZE - 1) * BD_SIZE),
+ },
+ },
+ [PRUETH_PORT_QUEUE_MII1] = {
+ [PRUETH_QUEUE1] = {
+ P0_Q1_BUFFER_OFFSET,
+ P0_Q1_BUFFER_OFFSET +
+ ((HOST_QUEUE_1_SIZE - 1) * ICSS_BLOCK_SIZE),
+ P0_Q1_BD_OFFSET,
+ P0_Q1_BD_OFFSET +
+ ((HOST_QUEUE_1_SIZE - 1) * BD_SIZE),
+ },
+ [PRUETH_QUEUE2] = {
+ P0_Q2_BUFFER_OFFSET,
+ P0_Q2_BUFFER_OFFSET +
+ ((HOST_QUEUE_2_SIZE - 1) * ICSS_BLOCK_SIZE),
+ P0_Q2_BD_OFFSET,
+ P0_Q2_BD_OFFSET +
+ ((HOST_QUEUE_2_SIZE - 1) * BD_SIZE),
+ },
+ [PRUETH_QUEUE3] = {
+ P1_Q3_TXOPT_BUFFER_OFFSET,
+ P1_Q3_TXOPT_BUFFER_OFFSET +
+ ((QUEUE_3_TXOPT_SIZE - 1) * ICSS_BLOCK_SIZE),
+ P1_Q3_TXOPT_BD_OFFSET,
+ P1_Q3_TXOPT_BD_OFFSET +
+ ((QUEUE_3_TXOPT_SIZE - 1) * BD_SIZE),
+ },
+ [PRUETH_QUEUE4] = {
+ P2_Q1_TXOPT_BUFFER_OFFSET,
+ P2_Q1_TXOPT_BUFFER_OFFSET +
+ ((QUEUE_4_TXOPT_SIZE - 1) * ICSS_BLOCK_SIZE),
+ P2_Q1_TXOPT_BD_OFFSET,
+ P2_Q1_TXOPT_BD_OFFSET +
+ ((QUEUE_4_TXOPT_SIZE - 1) * BD_SIZE),
+ },
+
+ },
+};
+
+/* Rx Queue Context for HSR and PRP */
+static const struct prueth_queue_info lre_rx_queue_infos[][NUM_QUEUES] = {
+ [PRUETH_PORT_QUEUE_HOST] = {
+ [PRUETH_QUEUE1] = {
+ P0_Q1_BUFFER_OFFSET,
+ HOST_QUEUE_DESC_OFFSET,
+ P0_Q1_BD_OFFSET,
+ P0_Q1_BD_OFFSET + ((HOST_QUEUE_1_SIZE - 1) * BD_SIZE),
+ },
+ [PRUETH_QUEUE2] = {
+ P0_Q2_BUFFER_OFFSET,
+ HOST_QUEUE_DESC_OFFSET + 8,
+ P0_Q2_BD_OFFSET,
+ P0_Q2_BD_OFFSET + ((HOST_QUEUE_2_SIZE - 1) * BD_SIZE),
+ },
+ [PRUETH_QUEUE3] = {
+ P0_Q3_BUFFER_OFFSET,
+ HOST_QUEUE_DESC_OFFSET + 16,
+ P0_Q3_BD_OFFSET,
+ P0_Q3_BD_OFFSET + ((HOST_QUEUE_3_SIZE - 1) * BD_SIZE),
+ },
+ [PRUETH_QUEUE4] = {
+ P0_Q4_BUFFER_OFFSET,
+ HOST_QUEUE_DESC_OFFSET + 24,
+ P0_Q4_BD_OFFSET,
+ P0_Q4_BD_OFFSET + ((HOST_QUEUE_4_SIZE - 1) * BD_SIZE),
+ },
+ },
+ [PRUETH_PORT_QUEUE_MII0] = {
+ [PRUETH_QUEUE1] = {
+ P0_Q3_BUFFER_OFFSET,
+ P1_QUEUE_DESC_OFFSET,
+ P0_Q3_BD_OFFSET,
+ P0_Q3_BD_OFFSET + ((HOST_QUEUE_3_SIZE - 1) * BD_SIZE),
+ },
+ [PRUETH_QUEUE2] = {
+ P0_Q4_BUFFER_OFFSET,
+ P1_QUEUE_DESC_OFFSET + 8,
+ P0_Q4_BD_OFFSET,
+ P0_Q4_BD_OFFSET + ((HOST_QUEUE_4_SIZE - 1) * BD_SIZE),
+ },
+ [PRUETH_QUEUE3] = {
+ P1_Q3_TXOPT_BUFFER_OFFSET,
+ P1_QUEUE_DESC_OFFSET + 16,
+ P1_Q3_TXOPT_BD_OFFSET,
+ P1_Q3_TXOPT_BD_OFFSET +
+ ((QUEUE_3_TXOPT_SIZE - 1) * BD_SIZE),
+ },
+ [PRUETH_QUEUE4] = {
+ P2_Q1_TXOPT_BUFFER_OFFSET,
+ P1_QUEUE_DESC_OFFSET + 24,
+ P2_Q1_TXOPT_BD_OFFSET,
+ P2_Q1_TXOPT_BD_OFFSET +
+ ((QUEUE_4_TXOPT_SIZE - 1) * BD_SIZE),
+ },
+ },
+ [PRUETH_PORT_QUEUE_MII1] = {
+ [PRUETH_QUEUE1] = {
+ P0_Q1_BUFFER_OFFSET,
+ P2_QUEUE_DESC_OFFSET,
+ P0_Q1_BD_OFFSET,
+ P0_Q1_BD_OFFSET + ((HOST_QUEUE_1_SIZE - 1) * BD_SIZE),
+ },
+ [PRUETH_QUEUE2] = {
+ P0_Q2_BUFFER_OFFSET,
+ P2_QUEUE_DESC_OFFSET + 8,
+ P0_Q2_BD_OFFSET,
+ P0_Q2_BD_OFFSET + ((HOST_QUEUE_2_SIZE - 1) * BD_SIZE),
+ },
+ [PRUETH_QUEUE3] = {
+ P1_Q3_TXOPT_BUFFER_OFFSET,
+ P2_QUEUE_DESC_OFFSET + 16,
+ P1_Q3_TXOPT_BD_OFFSET,
+ P1_Q3_TXOPT_BD_OFFSET +
+ ((QUEUE_3_TXOPT_SIZE - 1) * BD_SIZE),
+ },
+ [PRUETH_QUEUE4] = {
+ P2_Q1_TXOPT_BUFFER_OFFSET,
+ P2_QUEUE_DESC_OFFSET + 24,
+ P2_Q1_TXOPT_BD_OFFSET,
+ P2_Q1_TXOPT_BD_OFFSET +
+ ((QUEUE_4_TXOPT_SIZE - 1) * BD_SIZE),
+ },
+ },
+};
+
void icssm_prueth_sw_free_fdb_table(struct prueth *prueth)
{
if (prueth->emac_configured)
@@ -856,8 +1039,12 @@ void icssm_prueth_sw_hostconfig(struct prueth *prueth)
/* queue information table */
dram = dram1_base + P0_Q1_RX_CONTEXT_OFFSET;
- memcpy_toio(dram, sw_queue_infos[PRUETH_PORT_QUEUE_HOST],
- sizeof(sw_queue_infos[PRUETH_PORT_QUEUE_HOST]));
+ if (prueth_is_lre(prueth))
+ memcpy_toio(dram, lre_queue_infos[PRUETH_PORT_QUEUE_HOST],
+ sizeof(lre_queue_infos[PRUETH_PORT_QUEUE_HOST]));
+ else
+ memcpy_toio(dram, sw_queue_infos[PRUETH_PORT_QUEUE_HOST],
+ sizeof(sw_queue_infos[PRUETH_PORT_QUEUE_HOST]));
/* buffer descriptor offset table*/
dram = dram1_base + QUEUE_DESCRIPTOR_OFFSET_ADDR;
@@ -882,8 +1069,15 @@ void icssm_prueth_sw_hostconfig(struct prueth *prueth)
/* queue table */
dram = dram1_base + P0_QUEUE_DESC_OFFSET;
- memcpy_toio(dram, queue_descs[PRUETH_PORT_QUEUE_HOST],
- sizeof(queue_descs[PRUETH_PORT_QUEUE_HOST]));
+ if (prueth_is_lre(prueth))
+ memcpy_toio(dram,
+ hsr_prp_txopt_queue_descs[PRUETH_PORT_QUEUE_HOST],
+ sizeof(hsr_prp_txopt_queue_descs
+ [PRUETH_PORT_QUEUE_HOST]));
+ else
+ memcpy_toio(dram, queue_descs[PRUETH_PORT_QUEUE_HOST],
+ sizeof(queue_descs[PRUETH_PORT_QUEUE_HOST]));
+
}
void icssm_prueth_sw_config_packet_timestamping(struct prueth *prueth)
@@ -983,6 +1177,109 @@ static int icssm_prueth_sw_port_config(struct prueth *prueth,
return 0;
}
+/* Configure TX/RX queue contexts and buffer descriptor tables for LRE port */
+static int icssm_prueth_lre_port_config(struct prueth *prueth,
+ enum prueth_port port_id)
+{
+ unsigned int tx_context_ofs_addr, rx_context_ofs, queue_desc_ofs;
+ void __iomem *dram, *dram_base, *dram_mac;
+ struct prueth_emac *emac;
+
+ emac = prueth->emac[port_id - 1];
+ switch (port_id) {
+ case PRUETH_PORT_MII0:
+ tx_context_ofs_addr = TX_CONTEXT_P1_Q1_OFFSET_ADDR;
+ rx_context_ofs = P1_Q1_RX_CONTEXT_OFFSET;
+ queue_desc_ofs = P1_QUEUE_DESC_OFFSET;
+ /* for switch PORT MII0 mac addr is in DRAM0. */
+ dram_mac = prueth->mem[PRUETH_MEM_DRAM0].va;
+ break;
+ case PRUETH_PORT_MII1:
+ tx_context_ofs_addr = TX_CONTEXT_P2_Q1_OFFSET_ADDR;
+ rx_context_ofs = P2_Q1_RX_CONTEXT_OFFSET;
+ queue_desc_ofs = P2_QUEUE_DESC_OFFSET;
+
+ /* for switch PORT MII1 mac addr is in DRAM1. */
+ dram_mac = prueth->mem[PRUETH_MEM_DRAM1].va;
+ break;
+ default:
+ netdev_err(emac->ndev, "invalid port\n");
+ return -EINVAL;
+ }
+
+ /* setup mac address */
+ memcpy_toio(dram_mac + PORT_MAC_ADDR, emac->mac_addr, ETH_ALEN);
+
+ /* Remaining switch port configs are in DRAM1 */
+ dram_base = prueth->mem[PRUETH_MEM_DRAM1].va;
+
+ /* queue information table */
+ memcpy_toio(dram_base + tx_context_ofs_addr,
+ lre_queue_infos[port_id],
+ sizeof(lre_queue_infos[port_id]));
+
+ memcpy_toio(dram_base + rx_context_ofs,
+ lre_rx_queue_infos[port_id],
+ sizeof(lre_rx_queue_infos[port_id]));
+
+ /* buffer descriptor offset table*/
+ dram = dram_base + QUEUE_DESCRIPTOR_OFFSET_ADDR +
+ (port_id * NUM_QUEUES * sizeof(u16));
+ writew(lre_queue_infos[port_id][PRUETH_QUEUE1].buffer_desc_offset,
+ dram);
+ writew(lre_queue_infos[port_id][PRUETH_QUEUE2].buffer_desc_offset,
+ dram + 2);
+ writew(lre_queue_infos[port_id][PRUETH_QUEUE3].buffer_desc_offset,
+ dram + 4);
+ writew(lre_queue_infos[port_id][PRUETH_QUEUE4].buffer_desc_offset,
+ dram + 6);
+
+ /* buffer offset table */
+ dram = dram_base + QUEUE_OFFSET_ADDR +
+ port_id * NUM_QUEUES * sizeof(u16);
+ writew(lre_queue_infos[port_id][PRUETH_QUEUE1].buffer_offset, dram);
+ writew(lre_queue_infos[port_id][PRUETH_QUEUE2].buffer_offset,
+ dram + 2);
+ writew(lre_queue_infos[port_id][PRUETH_QUEUE3].buffer_offset,
+ dram + 4);
+ writew(lre_queue_infos[port_id][PRUETH_QUEUE4].buffer_offset,
+ dram + 6);
+
+ /* queue size lookup table */
+ dram = dram_base + QUEUE_SIZE_ADDR +
+ port_id * NUM_QUEUES * sizeof(u16);
+ writew(HOST_QUEUE_1_SIZE, dram);
+ writew(HOST_QUEUE_2_SIZE, dram + 2);
+ writew(QUEUE_3_TXOPT_SIZE, dram + 4);
+ writew(QUEUE_4_TXOPT_SIZE, dram + 6);
+
+ /* queue table */
+ memcpy_toio(dram_base + queue_desc_ofs,
+ &hsr_prp_txopt_queue_descs[port_id][0],
+ 4 * sizeof(hsr_prp_txopt_queue_descs[port_id][0]));
+
+ /* In HSR/PRP mode both slave ports share the host receive queue
+ * descriptor region (P0_QUEUE_DESC_OFFSET). The firmware arbitrates
+ * ownership; the driver always reads from the same host-side descriptor
+ * base regardless of which physical port the frame arrived on.
+ */
+ emac->rx_queue_descs = dram_base + P0_QUEUE_DESC_OFFSET;
+ emac->tx_queue_descs = dram_base +
+ lre_rx_queue_infos[port_id][PRUETH_QUEUE1].queue_desc_offset;
+
+ if (port_id == PRUETH_PORT_MII0) {
+ emac->tx_queue_descs_other_port = dram_base +
+ lre_rx_queue_infos
+ [port_id + 1][PRUETH_QUEUE1].queue_desc_offset;
+ } else if (port_id == PRUETH_PORT_MII1) {
+ emac->tx_queue_descs_other_port = dram_base +
+ lre_rx_queue_infos
+ [port_id - 1][PRUETH_QUEUE1].queue_desc_offset;
+ }
+
+ return 0;
+}
+
int icssm_prueth_sw_emac_config(struct prueth_emac *emac)
{
struct prueth *prueth = emac->prueth;
@@ -997,7 +1294,10 @@ int icssm_prueth_sw_emac_config(struct prueth_emac *emac)
if (prueth->emac_configured & BIT(emac->port_id))
return 0;
- ret = icssm_prueth_sw_port_config(prueth, emac->port_id);
+ if (prueth_is_lre(prueth))
+ ret = icssm_prueth_lre_port_config(prueth, emac->port_id);
+ else
+ ret = icssm_prueth_sw_port_config(prueth, emac->port_id);
if (ret)
return ret;
diff --git a/drivers/net/ethernet/ti/icssm/icssm_prueth_switch.h b/drivers/net/ethernet/ti/icssm/icssm_prueth_switch.h
index fe7197f9a6da..a009827b01d7 100644
--- a/drivers/net/ethernet/ti/icssm/icssm_prueth_switch.h
+++ b/drivers/net/ethernet/ti/icssm/icssm_prueth_switch.h
@@ -17,6 +17,7 @@ u8 icssm_prueth_sw_get_stp_state(struct prueth *prueth,
enum prueth_port port);
extern const struct prueth_queue_info sw_queue_infos[][4];
+extern const struct prueth_queue_info lre_queue_infos[][4];
void icssm_prueth_sw_fdb_tbl_init(struct prueth *prueth);
int icssm_prueth_sw_init_fdb_table(struct prueth *prueth);
diff --git a/drivers/net/ethernet/ti/icssm/icssm_switch.h b/drivers/net/ethernet/ti/icssm/icssm_switch.h
index 13b1b93ef4c2..b0d623b4979b 100644
--- a/drivers/net/ethernet/ti/icssm/icssm_switch.h
+++ b/drivers/net/ethernet/ti/icssm/icssm_switch.h
@@ -24,6 +24,9 @@
#define QUEUE_3_SIZE 97 /* Protocol specific */
#define QUEUE_4_SIZE 97 /* NRT (IP,ARP, ICMP) */
+#define QUEUE_3_TXOPT_SIZE 194 /* Protocol specific - High Priority */
+#define QUEUE_4_TXOPT_SIZE 194 /* NRT(IP,ARP, ICMP) - Low Priority*/
+
/* Host queue size (number of BDs). Each BD points to data buffer of 32 bytes.
* HOST PORT QUEUES can buffer up to 4 full sized frames per queue
*/
@@ -49,20 +52,18 @@
* For RED, NodeTable lookup was successful.
* 7 Flood Packet should be flooded (destination MAC
* address found in FDB). For switch only.
- * 8..12 Block_length number of valid bytes in this specific block.
- * Will be <=32 bytes on last block of packet
+ * 8 RED_INFO Set if the frame carries an HSR or PRP
+ * redundancy tag
+ * 10 HostRecv Set if the frame is destined for the host port
* 13 More "More" bit indicating that there are more blocks
* 14 Shadow indicates that "index" is pointing into shadow
* buffer
* 15 TimeStamp indicates that this packet has time stamp in
* separate buffer - only needed if PTP runs on
* host
- * 16..17 Port different meaning for ingress and egress,
- * Ingress: Port = 0 indicates phy port 1 and
- * Port = 1 indicates phy port 2.
- * Egress: 0 sends on phy port 1 and 1 sends on
- * phy port 2. Port = 2 goes over MAC table
- * look-up
+ * 16..17 LAN Destination LAN for transmission:
+ * bit 16 = LAN A, bit 17 = LAN B, set both to
+ * duplicate to both LANs.
* 18..28 Length 11 bit of total packet length which is put into
* first BD only so that host access only one BD
* 29 VlanTag indicates that packet has Length/Type field of
@@ -86,14 +87,21 @@
#define PRUETH_BD_SW_FLOOD_MASK BIT(7)
#define PRUETH_BD_SW_FLOOD_SHIFT 7
+#define PRUETH_BD_RED_PKT_MASK BIT(8)
+#define PRUETH_BD_RED_PKT 8
+
+#define PRUETH_BD_HOST_RECV_MASK BIT(10)
+#define PRUETH_BD_HOST_RECV_SHIFT 10
+
#define PRUETH_BD_SHADOW_MASK BIT(14)
#define PRUETH_BD_SHADOW_SHIFT 14
#define PRUETH_BD_TIMESTAMP_MASK BIT(15)
#define PRUETH_BD_TIMESTAMP_SHIFT 15
-#define PRUETH_BD_PORT_MASK GENMASK(17, 16)
-#define PRUETH_BD_PORT_SHIFT 16
+#define PRUETH_BD_LAN_INFO_MASK GENMASK(17, 16)
+#define PRUETH_BD_LAN_A_SHIFT 16
+#define PRUETH_BD_LAN_B_SHIFT 17
#define PRUETH_BD_LENGTH_MASK GENMASK(28, 18)
#define PRUETH_BD_LENGTH_SHIFT 18
@@ -298,6 +306,9 @@
#define P0_Q4_BD_OFFSET (P0_Q3_BD_OFFSET + HOST_QUEUE_3_SIZE * BD_SIZE)
#define P0_Q3_BD_OFFSET (P0_Q2_BD_OFFSET + HOST_QUEUE_2_SIZE * BD_SIZE)
#define P0_Q2_BD_OFFSET (P0_Q1_BD_OFFSET + HOST_QUEUE_1_SIZE * BD_SIZE)
+#define P1_Q3_TXOPT_BD_OFFSET (P0_Q4_BD_OFFSET + HOST_QUEUE_4_SIZE * BD_SIZE)
+#define P2_Q1_TXOPT_BD_OFFSET (P1_Q3_TXOPT_BD_OFFSET + \
+ QUEUE_3_TXOPT_SIZE * BD_SIZE)
#define P0_Q1_BD_OFFSET P0_BUFFER_DESC_OFFSET
#define P0_BUFFER_DESC_OFFSET SRAM_START_OFFSET
@@ -328,6 +339,10 @@
ICSS_BLOCK_SIZE)
#define P0_Q2_BUFFER_OFFSET (P0_Q1_BUFFER_OFFSET + HOST_QUEUE_1_SIZE * \
ICSS_BLOCK_SIZE)
+#define P1_Q3_TXOPT_BUFFER_OFFSET (P0_Q4_BUFFER_OFFSET + \
+ HOST_QUEUE_4_SIZE * ICSS_BLOCK_SIZE)
+#define P2_Q1_TXOPT_BUFFER_OFFSET (P1_Q3_TXOPT_BUFFER_OFFSET + \
+ QUEUE_3_TXOPT_SIZE * ICSS_BLOCK_SIZE)
#define P0_COL_BUFFER_OFFSET 0xEE00
#define P0_Q1_BUFFER_OFFSET 0x0000
--
2.43.0
^ permalink raw reply related [flat|nested] 7+ messages in thread
* Re: [PATCH net-next v3 1/3] net: ti: icssm-prueth: Add HSR and PRP HW offload mode support for AM57xx, AM437x and AM335x
2026-08-04 12:14 ` [PATCH net-next v3 1/3] net: ti: icssm-prueth: Add HSR and PRP HW offload mode support for AM57xx, AM437x and AM335x Parvathi Pudi
@ 2026-08-11 8:39 ` Paolo Abeni
0 siblings, 0 replies; 7+ messages in thread
From: Paolo Abeni @ 2026-08-11 8:39 UTC (permalink / raw)
To: parvathi
Cc: andrew+netdev, davem, edumazet, kuba, pabeni, danishanwar, rogerq,
pmohan, afd, basharath, arnd, linux-kernel, netdev,
linux-arm-kernel, pratheesh, j-rameshbabu, vigneshr, praneeth,
srk, rogerq, m-malladi, krishna, mohan
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
Full review at: https://netdev-ai.bots.linux.dev/sashiko/
---
--- Patch [1]: [PATCH net-next v3 1/3] net: ti: icssm-prueth: Add HSR and PRP HW offload mode support for AM57xx, AM437x and AM335x ---
commit f653927696e40849a8dce31458a3da04f6773137
Author: Roger Quadros <rogerq@ti.com>
net: ti: icssm-prueth: Add HSR and PRP HW offload mode support for AM57xx, AM437x and AM335x
This adds HSR and PRP offload modes to the ICSSM PRUETH driver, on top of
the existing dual-EMAC and RSTP switch modes. The two PRU ports become LRE
slave ports and dedicated PRU firmware handles duplicate discard and, for
HSR, L2 forwarding. Mode selection is driven by the HSR/PRP offload feature
bits plus the hsr upper device link notification.
A few questions on the new code below.
> diff --git a/drivers/net/ethernet/ti/icssm/icssm_prueth.c b/drivers/net/ethernet/ti/icssm/icssm_prueth.c
> index b7e94244355a3..138248f4a1eed 100644
> --- a/drivers/net/ethernet/ti/icssm/icssm_prueth.c
> +++ b/drivers/net/ethernet/ti/icssm/icssm_prueth.c
[ ... ]
> @@ -40,6 +41,30 @@
> #define TX_CLK_DELAY_100M 0x6
> #define HR_TIMER_TX_DELAY_US 100
>
> +#define NETIF_PRUETH_LRE_OFFLOAD_FEATURES (NETIF_F_HW_HSR_FWD | \
> + NETIF_F_HW_HSR_TAG_RM)
> +
> +static const struct prueth_fw_offsets fw_offsets_v2_1;
> +static void icssm_prueth_set_fw_offsets(struct prueth *prueth)
> +{
> + /* Set Multicast filter control and table offsets */
> + if (PRUETH_IS_EMAC(prueth) || PRUETH_IS_SWITCH(prueth)) {
> + prueth->fw_offsets.mc_ctrl_offset =
> + ICSS_EMAC_FW_MULTICAST_FILTER_CTRL_OFFSET;
> + prueth->fw_offsets.mc_filter_mask =
> + ICSS_EMAC_FW_MULTICAST_FILTER_MASK_OFFSET;
> + prueth->fw_offsets.mc_filter_tbl =
> + ICSS_EMAC_FW_MULTICAST_FILTER_TABLE;
> + } else {
> + prueth->fw_offsets.mc_ctrl_offset =
> + ICSS_LRE_FW_MULTICAST_TABLE_SEARCH_OP_CONTROL_BIT;
> + prueth->fw_offsets.mc_filter_mask =
> + ICSS_LRE_FW_MULTICAST_FILTER_MASK;
> + prueth->fw_offsets.mc_filter_tbl =
> + ICSS_LRE_FW_MULTICAST_FILTER_TABLE;
> + }
> +}
> +
> static void icssm_prueth_write_reg(struct prueth *prueth,
> enum prueth_mem region,
> unsigned int reg, u32 val)
> @@ -309,12 +334,15 @@ static void icssm_prueth_hostinit(struct prueth *prueth)
> icssm_prueth_mii_init(prueth);
> }
>
> -/* This function initialize the driver in EMAC mode
> +/* Initialize the driver in EMAC, HSR or PRP mode
> * based on eth_type
> */
> static void icssm_prueth_init_ethernet_mode(struct prueth *prueth)
> {
> + icssm_prueth_set_fw_offsets(prueth);
> icssm_prueth_hostinit(prueth);
> + if (prueth_is_lre(prueth))
> + icssm_prueth_lre_config(prueth);
> }
> @@ -564,7 +592,7 @@ static int icssm_prueth_tx_enqueue(struct prueth_emac *emac,
> }
>
> pkt_block_size = DIV_ROUND_UP(pktlen, ICSS_BLOCK_SIZE);
> - if (pkt_block_size > free_blocks) /* out of queue space */
> + if (pkt_block_size >= free_blocks) /* out of queue space */
> return -ENOBUFS;
[Severity: Low]
[Finding: main-1-1]
[Sources: opus-5]
This changes the queue full test in icssm_prueth_tx_enqueue() for every
mode, including the existing dual-EMAC and RSTP switch paths, and it is not
mentioned in the commit message. Was this meant to be part of the HSR/PRP
work?
When pkt_block_size equals free_blocks, update_block wraps exactly onto
read_block, so the new wr_ptr equals rd_ptr, which both the driver and the
firmware read as "queue empty".
If that is the reason for the change, would it be better as its own patch
with a Fixes: tag pointing at the commit that added
icssm_prueth_tx_enqueue()?
[ ... ]
> @@ -1122,7 +1185,8 @@ static int icssm_prueth_change_mode(struct prueth *prueth,
> }
> }
>
> - if (mode == PRUSS_ETHTYPE_EMAC || mode == PRUSS_ETHTYPE_SWITCH) {
> + if (mode == PRUSS_ETHTYPE_EMAC || mode == PRUSS_ETHTYPE_SWITCH ||
> + mode == PRUSS_ETHTYPE_HSR || mode == PRUSS_ETHTYPE_PRP) {
> prueth->eth_type = mode;
> } else {
> dev_err(prueth->dev, "unknown mode\n");
[Severity: High]
[Finding: main-4-12]
[Sources: opus-5]
This isn't a bug introduced by this patch, but the patch adds two new entry
points into icssm_prueth_change_mode() (HSR and PRP) together with firmware
names that are usually absent from a stock /lib/firmware, so the existing
failure path becomes much easier to reach.
icssm_prueth_change_mode() calls ndo_stop()/ndo_open() directly, so
__LINK_STATE_START is never cleared. icssm_emac_ndo_open() boots the PRU
firmware before napi_enable() and the IRQ requests, so a missing
ti-pruss/am57xx-pru0-pruhsr-fw.elf returns an error with both interfaces
left netif_running(), their rx IRQ already freed, their NAPI disabled, and
prueth->eth_type already switched to the new mode.
A later "ip link set eth2 down" re-enters icssm_emac_ndo_stop(), which now
takes the non-EMAC branch:
if (!prueth->emac_configured &&
(PRUETH_IS_SWITCH(prueth) || prueth_is_lre(prueth))) {
napi_disable(&prueth->napi_lpq);
napi_disable(&prueth->napi_hpq);
}
on NAPI instances that were never enabled. netif_napi_add_weight_locked()
leaves NAPI_STATE_SCHED and NAPI_STATE_NPSVC set, and napi_disable_locked()
then spins:
while (val & (NAPIF_STATE_SCHED | NAPIF_STATE_NPSVC)) {
usleep_range(20, 200);
val = READ_ONCE(n->state);
}
with nothing left to clear those bits, while rtnl is held. In the same path
icssm_prueth_common_free_irqs() calls free_irq() on interrupts that were
never requested.
Should icssm_prueth_change_mode() restore eth_type and the interface state
when ndo_open() fails?
[ ... ]
> @@ -1316,11 +1393,16 @@ static void icssm_emac_mc_filter_bin_update(struct prueth_emac *emac, u8 hash,
> {
> struct prueth *prueth = emac->prueth;
> void __iomem *mc_filter_tbl;
> + u32 mc_filter_tbl_base;
> void __iomem *ram;
>
> ram = prueth->mem[emac->dram].va;
> + if (prueth_is_lre(prueth))
> + ram = prueth->mem[PRUETH_MEM_DRAM1].va;
> +
> + mc_filter_tbl_base = prueth->fw_offsets.mc_filter_tbl;
>
> - mc_filter_tbl = ram + ICSS_EMAC_FW_MULTICAST_FILTER_TABLE;
> + mc_filter_tbl = ram + mc_filter_tbl_base;
> writeb(val, mc_filter_tbl + hash);
> }
[Severity: Medium]
[Finding: main-2-7]
[Sources: opus-5]
Can prueth->fw_offsets.mc_filter_tbl still be zero here?
The offsets are only filled in by icssm_prueth_set_fw_offsets(), reached
from icssm_prueth_init_ethernet_mode(), which icssm_emac_ndo_open() calls
under "if (!prueth->emac_configured)". Until the first open,
icssm_prueth_probe() has only done:
prueth->fw_offsets = fw_offsets_v2_1;
and that object initializes .iep_wrap only, so mc_ctrl_offset,
mc_filter_mask and mc_filter_tbl are all 0.
icssm_emac_mc_filter_bin_allow() is reachable in that window from the
switchdev blocking notifier, icssm_prueth_sw_blocking_event() ->
switchdev_handle_port_obj_add() -> icssm_prueth_switchdev_obj_add(), which
handles SWITCHDEV_OBJ_ID_HOST_MDB with no netif_running() or mode check:
hash = icssm_emac_get_mc_hash(mdb->addr, emac->mc_filter_mask);
icssm_emac_mc_filter_bin_allow(emac, hash);
With a zero base the writeb() lands at PRU DMEM offset 0 + hash rather than
the multicast filter table, and the MDB entry is lost, since
icssm_emac_ndo_set_rx_mode() only reprograms from the netdev and bridge mc
lists. Before this patch the base was the compile time constant
ICSS_EMAC_FW_MULTICAST_FILTER_TABLE, so the write always hit the real
table.
[ ... ]
> @@ -1429,15 +1518,95 @@ static void icssm_emac_ndo_set_rx_mode(struct net_device *ndev)
[ ... ]
> +/**
> + * icssm_emac_ndo_set_features - Configure HSR/PRP offload features
> + * @ndev: network device
> + * @features: Requested feature set
> + *
> + * Called by ethtool -K to configure HSR/PRP offload features. The request
> + * is rejected if this interface or its paired interface is running.
> + *
> + * Return: 0 on success, -EINVAL or -EBUSY on error.
> + */
> +static int icssm_emac_ndo_set_features(struct net_device *ndev,
> + netdev_features_t features)
> +{
> + struct prueth_emac *emac, *other_emac;
> + netdev_features_t have, wanted;
> + struct prueth *prueth;
> + bool change_request;
> + int ret = -EBUSY;
> +
> + emac = netdev_priv(ndev);
> + prueth = emac->prueth;
> + /* MAC instance index starts from 0. So index by port_id - 1 */
> + other_emac = emac->prueth->emac[(emac->port_id == PRUETH_PORT_MII0) ?
> + PRUETH_PORT_MII1 - 1 : PRUETH_PORT_MII0 - 1];
> + wanted = features & NETIF_PRUETH_LRE_OFFLOAD_FEATURES;
> + have = ndev->features & NETIF_PRUETH_LRE_OFFLOAD_FEATURES;
> + change_request = ((wanted ^ have) != 0);
> +
> + if (!prueth->fw_data->support_lre)
> + return 0;
> +
> + if (PRUETH_IS_SWITCH(prueth) && change_request) {
> + /* LRE offload cannot be enabled in switch mode, remove the
> + * bridge first to revert to EMAC mode.
> + */
> + netdev_err(ndev,
> + "Switch to HSR/PRP not allowed\n");
> + return -EINVAL;
> + }
> +
> + if (netif_running(ndev) && change_request) {
> + netdev_err(ndev,
> + "Can't change feature when device runs\n");
> + return ret;
> + }
> +
> + if (other_emac && netif_running(other_emac->ndev) && change_request) {
> + netdev_err(ndev,
> + "Can't change feature when other device runs\n");
> + return ret;
> + }
> +
> + return 0;
> }
[Severity: Medium]
[Finding: main-3-9]
[Sources: opus-5]
icssm_emac_ndo_set_features() only validates the request and never touches
the datapath. The firmware switch happens exclusively in
icssm_prueth_ndev_event(), and only if the LRE bits are already set when the
NETDEV_CHANGEUPPER linking event fires:
if (is_hsr_master(info->upper_dev)) {
if (info->linking) {
if (ndev->features &
NETIF_PRUETH_LRE_OFFLOAD_FEATURES)
ret = icssm_prueth_hsr_port_link
(ndev, info->upper_dev);
What happens if the hsr device is created before the offload is enabled,
i.e. the reverse of the order in the commit message? With both ports down,
none of the three guards above fire, so set_features returns 0,
prueth->eth_type stays PRUSS_ETHTYPE_EMAC and "ip link set eth2 up" boots
the dual EMAC firmware, while both netdevs now advertise
NETIF_F_HW_HSR_FWD and NETIF_F_HW_HSR_TAG_RM.
The HSR core reads the slave features per frame, for example in
hsr_forward_do():
if ((port->dev->features & NETIF_F_HW_HSR_DUP) && sent)
continue;
so the stack stops duplicating and tag-handling while the EMAC firmware does
neither, with nothing reported to the user. Clearing the features while the
ports are down but still hsr slaves gives the opposite mismatch: LRE
firmware loaded with the offload bits cleared.
Should set_features reject the change while the port is an hsr slave, or
drive the mode switch itself?
> @@ -1741,6 +1913,105 @@ static int icssm_prueth_ndev_port_unlink(struct net_device *ndev)
> return ret;
> }
>
> +static int icssm_prueth_hsr_port_link(struct net_device *ndev,
> + struct net_device *hsr_ndev)
> +{
> + struct prueth_emac *emac = netdev_priv(ndev);
> + struct prueth *prueth = emac->prueth;
> + enum pruss_ethtype mode;
> + enum hsr_version ver;
> + unsigned long flags;
> + u8 all_slaves;
> + int ret = 0;
> +
> + if (PRUETH_IS_SWITCH(prueth))
> + return -EOPNOTSUPP;
> +
> + hsr_get_version(hsr_ndev, &ver);
> +
> + if (ver == HSR_V1)
> + mode = PRUSS_ETHTYPE_HSR;
> + else if (ver == PRP_V1)
> + mode = PRUSS_ETHTYPE_PRP;
> + else
> + return -EOPNOTSUPP;
> +
> + all_slaves = BIT(PRUETH_PORT_MII0) | BIT(PRUETH_PORT_MII1);
> +
> + spin_lock_irqsave(&prueth->addr_lock, flags);
> +
> + if (!prueth->hsr_members) {
> + prueth->hsr_dev = hsr_ndev;
> + } else {
> + /* Adding the port to a second bridge is not supported */
> + if (prueth->hsr_dev != hsr_ndev) {
> + spin_unlock_irqrestore(&prueth->addr_lock, flags);
> + return -EOPNOTSUPP;
> + }
> + }
> +
> + prueth->hsr_members |= BIT(emac->port_id);
> +
> + spin_unlock_irqrestore(&prueth->addr_lock, flags);
> +
> + if (!prueth_is_lre(prueth) && prueth->hsr_members == all_slaves) {
> + ret = icssm_prueth_change_mode(prueth, mode);
> + if (ret < 0) {
> + dev_err(prueth->dev, "Failed to enable %s mode\n",
> + (mode == PRUSS_ETHTYPE_HSR) ?
> + "HSR" : "PRP");
> + goto free_hsr;
> + } else {
> + dev_info(prueth->dev,
> + "TI PRU ethernet now in %s mode\n",
> + (mode == PRUSS_ETHTYPE_HSR) ?
> + "HSR" : "PRP");
> + }
> + }
> +
> + return 0;
> +
> +free_hsr:
> + spin_lock_irqsave(&prueth->addr_lock, flags);
> +
> + prueth->hsr_dev = NULL;
> + prueth->hsr_members &= ~all_slaves;
> +
> + spin_unlock_irqrestore(&prueth->addr_lock, flags);
> + return ret;
> +}
[Severity: Medium]
[Finding: main-3-10]
[Sources: opus-5]
icssm_prueth_change_mode() commits prueth->eth_type = mode before reopening
the ports and returns the ndo_open() error without restoring it. So when
this free_hsr path runs, hsr_dev and both port bits of hsr_members are
cleared, including the peer port that is still linked, while eth_type stays
PRUSS_ETHTYPE_HSR or PRUSS_ETHTYPE_PRP.
The driver then keeps taking the non-EMAC paths with no hsr device attached:
icssm_prueth_sw_emac_config(), icssm_prueth_lre_config_check_flags(), the
DRAM1 multicast filter offsets and the HSR bit in the TX buffer descriptor.
Because the switch is gated on
if (!prueth_is_lre(prueth) && prueth->hsr_members == all_slaves) {
a later link request for the other protocol computes mode but skips
icssm_prueth_change_mode() entirely, so creating prp0 after a failed hsr0
attempt leaves the HSR firmware running while the PRUETH_IS_PRP() rx/tx
paths are active. Should eth_type be restored here?
> +
> +static int icssm_prueth_hsr_port_unlink(struct net_device *ndev)
> +{
> + struct prueth_emac *emac = netdev_priv(ndev);
> + struct prueth *prueth = emac->prueth;
> + unsigned long flags;
> + int ret = 0;
> +
> + spin_lock_irqsave(&prueth->addr_lock, flags);
> +
> + prueth->hsr_members &= ~BIT(emac->port_id);
> +
> + spin_unlock_irqrestore(&prueth->addr_lock, flags);
> +
> + if (prueth_is_lre(prueth) && !prueth->hsr_members) {
> + ret = icssm_prueth_change_mode(prueth, PRUSS_ETHTYPE_EMAC);
> + if (ret < 0) {
> + dev_err(prueth->dev, "Failed to enable dual EMAC mode\n");
> + return ret;
> + }
> + }
> +
> + spin_lock_irqsave(&prueth->addr_lock, flags);
> +
> + if (!prueth->hsr_members)
> + prueth->hsr_dev = NULL;
> +
> + spin_unlock_irqrestore(&prueth->addr_lock, flags);
> +
> + return 0;
> +}
[Severity: Medium]
[Finding: main-3-10]
[Sources: opus-5]
On this early return, the trailing block that clears prueth->hsr_dev is
skipped, so the driver keeps a pointer to the hsr master that is being
unlinked and destroyed, stored without dev_hold(), and eth_type stays HSR or
PRP with no members left. Is the early return intended here?
> @@ -1754,6 +2025,17 @@ static int icssm_prueth_ndev_event(struct notifier_block *unused,
> switch (event) {
> case NETDEV_CHANGEUPPER:
> info = ptr;
> + if (is_hsr_master(info->upper_dev)) {
> + if (info->linking) {
> + if (ndev->features &
> + NETIF_PRUETH_LRE_OFFLOAD_FEATURES)
> + ret = icssm_prueth_hsr_port_link
> + (ndev, info->upper_dev);
> + } else {
> + ret = icssm_prueth_hsr_port_unlink(ndev);
> + }
> + }
> +
[ ... ]
> @@ -1810,6 +2093,7 @@ static int icssm_prueth_probe(struct platform_device *pdev)
> platform_set_drvdata(pdev, prueth);
> prueth->dev = dev;
> prueth->fw_data = device_get_match_data(dev);
> + prueth->fw_offsets = fw_offsets_v2_1;
[ ... ]
> @@ -1955,6 +2239,17 @@ static int icssm_prueth_probe(struct platform_device *pdev)
> prueth->mem[PRUETH_MEM_OCMC].va,
> prueth->mem[PRUETH_MEM_OCMC].size);
>
> + if (IS_ENABLED(CONFIG_HSR) && prueth->fw_data->support_lre)
> + has_lre = true;
[Severity: Low]
[Finding: main-2-5]
[Sources: opus-5]
IS_ENABLED(CONFIG_HSR) is also true for CONFIG_HSR=m, and the driver now
calls is_hsr_master() and hsr_get_version() unconditionally from
icssm_prueth_ndev_event() and icssm_prueth_hsr_port_link().
include/linux/if_hsr.h only provides inline stubs for those two helpers when
CONFIG_HSR is disabled entirely:
#if IS_ENABLED(CONFIG_HSR)
extern bool is_hsr_master(struct net_device *dev);
extern int hsr_get_version(struct net_device *dev, enum hsr_version *ver);
so CONFIG_TI_PRUETH=y with CONFIG_HSR=m compiles but fails to link vmlinux
with undefined references to is_hsr_master and hsr_get_version.
config TI_PRUETH in drivers/net/ethernet/ti/Kconfig has no HSR dependency,
while TI_ICSSG_PRUETH in the same file carries:
depends on HSR || !HSR
Should TI_PRUETH get the same dependency?
[ ... ]
Cross-instance finding from sashiko-gemini (93d76c56ba422a009191856aaee1fc66bed8b1079668a5e6078c97f738cb4d2c):
[Severity: Medium]
[Finding: 93d76c56ba422a009191856aaee1fc66bed8b1079668a5e6078c97f738cb4d2c]
[Sources: sashiko-gemini]
The `NETIF_F_HW_HSR_DUP` flag is missing from `NETIF_PRUETH_LRE_OFFLOAD_FEATURES`, preventing hardware duplicate discard offloading.
--
This is an AI-generated review.
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH net-next v3 2/3] net: ti: icssm-prueth: Add priority based RX IRQ handlers
2026-08-04 12:14 ` [PATCH net-next v3 2/3] net: ti: icssm-prueth: Add priority based RX IRQ handlers Parvathi Pudi
@ 2026-08-11 8:39 ` Paolo Abeni
0 siblings, 0 replies; 7+ messages in thread
From: Paolo Abeni @ 2026-08-11 8:39 UTC (permalink / raw)
To: parvathi
Cc: andrew+netdev, davem, edumazet, kuba, pabeni, danishanwar, rogerq,
pmohan, afd, basharath, arnd, linux-kernel, netdev,
linux-arm-kernel, pratheesh, j-rameshbabu, vigneshr, praneeth,
srk, rogerq, m-malladi, krishna, mohan
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
Full review at: https://netdev-ai.bots.linux.dev/sashiko/
---
--- Patch [2]: [PATCH net-next v3 2/3] net: ti: icssm-prueth: Add priority based RX IRQ handlers ---
commit f88c5c0ded57f1071a4ce5dab475dff188d16fb0
Author: Roger Quadros <rogerq@ti.com>
net: ti: icssm-prueth: Add priority based RX IRQ handlers
This adds shared high/low priority RX interrupt handlers and two shared NAPI
instances for switch, HSR and PRP modes, so the host can drain both PRU port
queues in wire-arrival order using the per-frame IEP timestamp. Dual-EMAC
mode keeps its per-port interrupt and NAPI. A new file,
icssm_prueth_common.c, holds the shared poll, IRQ request and IRQ free
helpers.
> diff --git a/drivers/net/ethernet/ti/icssm/icssm_prueth.c b/drivers/net/ethernet/ti/icssm/icssm_prueth.c
> index 138248f4a1eed..7031b78d2cc2f 100644
> --- a/drivers/net/ethernet/ti/icssm/icssm_prueth.c
> +++ b/drivers/net/ethernet/ti/icssm/icssm_prueth.c
[ ... ]
> @@ -842,7 +858,7 @@ static int icssm_emac_rx_packets(struct prueth_emac *emac, int budget)
> rxqueue = &queue_infos[PRUETH_PORT_HOST][i];
> overflow_cnt = readb(&queue_desc->overflow_cnt);
> if (overflow_cnt > 0) {
> - emac->stats.rx_over_errors += overflow_cnt;
> + atomic64_add(overflow_cnt, &emac->stats.rx_over_errors);
> /* reset to zero */
> writeb(0, &queue_desc->overflow_cnt);
> }
[Severity: Low]
[Finding: fable-5-2-6]
[Sources: fable-5]
Is the switch branch in this function still reachable after this change?
icssm_emac_rx_packets() still carries the comment
/* Start and end queue is made common for EMAC, RSTP */
and the selection
if (PRUETH_IS_SWITCH(emac->prueth))
rxqueue = &sw_queue_infos[PRUETH_PORT_HOST][i];
but the only scheduler of emac->napi is the per-port RX hardirq, and after
this patch icssm_emac_ndo_open() requests emac->rx_irq and enables emac->napi
only when PRUETH_IS_EMAC(prueth). Would it be clearer to drop the switch
branch and update the comment?
> @@ -1071,11 +1087,25 @@ static int icssm_emac_ndo_open(struct net_device *ndev)
> goto iep_exit;
> }
>
> - ret = icssm_emac_request_irqs(emac);
> - if (ret)
> - goto rproc_shutdown;
> + if (PRUETH_IS_EMAC(prueth)) {
> + napi_enable(&emac->napi);
> + } else {
> + if (!prueth->emac_configured &&
> + (PRUETH_IS_SWITCH(prueth) || prueth_is_lre(prueth))) {
> + napi_enable(&prueth->napi_hpq);
> + napi_enable(&prueth->napi_lpq);
> + }
> + }
>
> - napi_enable(&emac->napi);
> + /* In switch and LRE modes the shared HPQ/LPQ IRQs are used,
> + * register them here and reuse for both modes.
> + */
> + if (PRUETH_IS_EMAC(prueth))
> + ret = icssm_emac_request_irqs(emac);
> + else
> + ret = icssm_prueth_common_request_irqs(emac);
> + if (ret)
> + goto disable_napi;
>
> /* start PHY */
> phy_start(emac->phydev);
[ ... ]
> @@ -1127,12 +1167,26 @@ static int icssm_emac_ndo_stop(struct net_device *ndev)
> /* disable the mac port */
> icssm_prueth_port_enable(emac, false);
>
> + netif_stop_queue(ndev);
> +
> /* stop PHY */
> phy_stop(emac->phydev);
>
> - napi_disable(&emac->napi);
> hrtimer_cancel(&emac->tx_hrtimer);
[Severity: Low]
[Finding: main-5-23]
[Sources: opus-5]
Can the tx hrtimer undo this netif_stop_queue()?
The queue is stopped before phy_stop() (which can sleep) and before
hrtimer_cancel(&emac->tx_hrtimer), and the callback does:
drivers/net/ethernet/ti/icssm/icssm_prueth.c:icssm_emac_tx_timer_callback() {
...
if (netif_queue_stopped(emac->ndev))
netif_wake_queue(emac->ndev);
...
}
So a timer armed by the previous HR_TIMER_TX_DELAY_US back-pressure event and
expiring in that window re-wakes the queue.
On the icssm_prueth_change_mode() path ndo_stop() is called directly from the
netdev notifier rather than through dev_close(), so __LINK_STATE_START stays
set and the qdisc is still active, and a re-woken queue can enter
ndo_start_xmit() for a port whose firmware and queues are being
reconfigured. Would cancelling the hrtimer first, or using
netif_tx_disable(), be preferable?
>
> + if (PRUETH_IS_EMAC(prueth)) {
> + napi_disable(&emac->napi);
> + free_irq(emac->rx_irq, ndev);
> + } else {
> + if (!prueth->emac_configured &&
> + (PRUETH_IS_SWITCH(prueth) || prueth_is_lre(prueth))) {
> + napi_disable(&prueth->napi_lpq);
> + napi_disable(&prueth->napi_hpq);
> + }
> + /* Free IRQs on last port before halting PRU */
> + icssm_prueth_common_free_irqs(emac);
> + }
> +
[Severity: High]
[Finding: fable-5-5-26]
[Sources: fable-5, opus-5, sonnet-5]
When the first of the two ports is closed, is anything left to stop RX for
that port?
This branch skips napi_disable() while the peer is still configured,
icssm_prueth_common_free_irqs() returns early for the same reason, and
icssm_prueth_sw_shutdown_prus() also early-returns:
drivers/net/ethernet/ti/icssm/icssm_prueth_switch.c:icssm_prueth_sw_shutdown_prus() {
...
if (prueth->emac_configured)
return 0;
...
}
Both PRUs therefore keep enqueuing frames, and the shared poll has no
per-port liveness check, so it keeps draining both host queues and calls
ret = icssm_emac_rx_packet(emac_p, &update_rd_ptr,
pkt_info_p, rxqueue_p);
for the port that was just stopped, which ends in netif_receive_skb() with
skb->dev set to the closed interface, plus stats and FDB updates for it.
Both shared NAPIs and prueth->hp->ndev / prueth->lp->ndev are bound to the
MII0 netdev, and icssm_prueth_remove() unregisters registered_netdevs[0]
first. Its dev_close() takes the early-return path while MII1 is still
configured, so can the shared IRQ and NAPI still deliver skbs into an
already unregistered net_device and re-schedule NAPI on it?
Before this patch each port tore down its own IRQ and NAPI unconditionally in
its own ndo_stop.
> /* stop the PRU */
> if (!PRUETH_IS_EMAC(prueth))
> icssm_prueth_sw_shutdown_prus(emac, ndev);
[ ... ]
> @@ -1766,9 +1817,25 @@ static int icssm_prueth_netdev_init(struct prueth *prueth,
>
> netif_napi_add(ndev, &emac->napi, icssm_emac_napi_poll);
>
> + if ((prueth->support_lre || fw_data->support_switch) &&
> + emac->port_id == PRUETH_PORT_MII0) {
> + netif_napi_add(ndev, &prueth->napi_hpq,
> + icssm_prueth_lre_napi_poll_hpq);
> + netif_napi_add(ndev, &prueth->napi_lpq,
> + icssm_prueth_lre_napi_poll_lpq);
> + }
> +
[Severity: Low]
[Finding: fable-5-2-8]
[Sources: fable-5, sonnet-5]
This isn't a bug, but the names are scoped to LRE while the polls are also
used by switch (RSTP) mode, which the same patch configures via
icssm_prueth_sw_config_packet_timestamping(). The same applies to the comment
in icssm_prueth_common_free_irqs():
/* HSR/PRP: free irqs when last port is down */
although icssm_emac_ndo_stop() calls it for every !PRUETH_IS_EMAC(prueth)
mode, switch included. Would icssm_prueth_common_napi_poll_hpq/_lpq and a
comment mentioning switch as well be more accurate?
> hrtimer_setup(&emac->tx_hrtimer, &icssm_emac_tx_timer_callback,
> CLOCK_MONOTONIC, HRTIMER_MODE_REL_PINNED);
>
> + if ((prueth->support_lre || fw_data->support_switch) &&
> + emac->port_id == PRUETH_PORT_MII0) {
> + prueth->hp->ndev = ndev;
> + prueth->hp->priority = 0;
> + prueth->lp->ndev = ndev;
> + prueth->lp->priority = 1;
> + }
> +
> return 0;
> free:
> emac->ndev = NULL;
[ ... ]
> @@ -2093,7 +2168,13 @@ static int icssm_prueth_probe(struct platform_device *pdev)
> platform_set_drvdata(pdev, prueth);
> prueth->dev = dev;
> prueth->fw_data = device_get_match_data(dev);
> - prueth->fw_offsets = fw_offsets_v2_1;
> +
> + if (prueth->fw_data->fw_rev == FW_REV_V1_0)
> + prueth->fw_offsets = fw_offsets_v1_0;
> + else if (prueth->fw_data->fw_rev == FW_REV_V2_1)
> + prueth->fw_offsets = fw_offsets_v2_1;
> + else
> + return -EINVAL;
>
> eth_ports_node = of_get_child_by_name(np, "ethernet-ports");
> if (!eth_ports_node)
[ ... ]
> diff --git a/drivers/net/ethernet/ti/icssm/icssm_prueth.h b/drivers/net/ethernet/ti/icssm/icssm_prueth.h
> index a5d5bcd08bcdb..9af19617967ee 100644
> --- a/drivers/net/ethernet/ti/icssm/icssm_prueth.h
> +++ b/drivers/net/ethernet/ti/icssm/icssm_prueth.h
[ ... ]
> @@ -197,24 +208,26 @@ enum pruss_device {
> struct prueth_emac_stats {
> - u64 tx_packets;
> - u64 tx_dropped;
> - u64 tx_bytes;
> - u64 rx_packets;
> - u64 rx_bytes;
> - u64 rx_length_errors;
> - u64 rx_over_errors;
> + atomic64_t tx_packets;
> + atomic64_t tx_dropped;
> + atomic64_t tx_bytes;
> + atomic64_t rx_packets;
> + atomic64_t rx_bytes;
> + atomic64_t rx_length_errors;
> + atomic64_t rx_over_errors;
> };
[Severity: Medium]
[Finding: fable-5-1-2]
[Sources: fable-5, opus-5]
Could the commit message mention the changes beyond the RX IRQ handlers?
The changelog says:
Dual-EMAC mode continues to use per-port interrupts.
but the patch also converts every counter in struct prueth_emac_stats from
u64 to atomic64_t and rewrites all seven update sites plus
icssm_emac_ndo_get_stats64(), which adds an atomic read-modify-write per
packet and per byte to the dual-EMAC TX and RX hot paths on 32-bit ARM.
Two further undescribed changes: the new enum fw_revision plus fw_rev in the
three platform data entries makes icssm_prueth_probe() return -EINVAL when
fw_rev is unset, a new probe failure mode for any platform data added later
without it, and icssm_emac_ndo_stop() gains netif_stop_queue().
The atomic conversion looks required by the new design, since napi_hpq and
napi_lpq can run concurrently on different CPUs and both update the same
emac counters. Documentation/networking/statistics.rst points at
u64_stats_sync for per-device packet and byte counters rather than
atomic64_t; would that be preferable here, and could the conversion be split
into its own patch?
[ ... ]
> diff --git a/drivers/net/ethernet/ti/icssm/icssm_prueth_common.c b/drivers/net/ethernet/ti/icssm/icssm_prueth_common.c
> new file mode 100644
> index 0000000000000..43140add81725
> --- /dev/null
> +++ b/drivers/net/ethernet/ti/icssm/icssm_prueth_common.c
> @@ -0,0 +1,281 @@
[ ... ]
> +static int icssm_prueth_common_emac_rx_packets(struct prueth_emac *emac,
> + int quota, u8 qid1, u8 qid2)
> +{
[ ... ]
> + prueth = emac->prueth;
> + ocmc_ram = prueth->mem[PRUETH_MEM_OCMC].va;
> + shared_ram = prueth->mem[PRUETH_MEM_SHARED_RAM].va;
> + other_emac = prueth->emac[(emac->port_id == PRUETH_PORT_MII0) ?
> + PRUETH_PORT_MII1 - 1 : PRUETH_PORT_MII0 - 1];
> +
> + iep_wrap = prueth->fw_offsets.iep_wrap;
> + /* search host queues for packets */
> + queue_desc = emac->rx_queue_descs + qid1;
> + queue_desc_o = other_emac->rx_queue_descs + qid2;
> +
> + rxqueue = &sw_queue_infos[PRUETH_PORT_HOST][qid1];
> + rxqueue_o = &sw_queue_infos[PRUETH_PORT_HOST][qid2];
> +
> + /* skip Rx if budget is 0 */
> + if (!quota)
> + return 0;
> +
> + overflow_cnt = readb(&queue_desc->overflow_cnt);
> + overflow_cnt_o = readb(&queue_desc_o->overflow_cnt);
[Severity: High]
[Finding: fable-5-3-13]
[Sources: fable-5, opus-5]
Can other_emac->rx_queue_descs be NULL or stale here?
rx_queue_descs is assigned only in the opening port's own ndo_open path, via
icssm_prueth_sw_emac_config() -> icssm_prueth_sw_port_config():
emac->rx_queue_descs = dram1_base + P0_QUEUE_DESC_OFFSET;
The shared HP and LP IRQs are requested and both shared NAPIs enabled on the
first port open, while prueth->emac_configured is still 0, and
icssm_prueth_sw_boot_prus() boots both PRUs, so the first HP/LP interrupt can
run this poll while the peer port has never been opened in this mode.
icssm_prueth_change_mode() only stops and reopens ports for which
netif_running() was true:
portstatus[i] = netif_running(ndev);
if (!portstatus[i])
continue;
so this sequence enters switch mode with eth1 never configured:
ip link set eth0 down
ip link set eth1 down
ip link set eth0 master br0
ip link set eth1 master br0
ip link set eth0 up
If eth1 has never been opened since probe, rx_queue_descs is the zeroed value
from devm_alloc_etherdev() and readb(&queue_desc_o->overflow_cnt) reads a
small fixed address. If eth1 was last opened in dual-EMAC mode, it still
holds the EMAC value sram + HOST_QUEUE_DESC_OFFSET rather than
dram1 + P0_QUEUE_DESC_OFFSET, so the rd/wr pointers read from it are used as
offsets into shared RAM and OCMC below and a bogus rd_ptr is written back
with writew(). The same recipe applies to HSR/PRP when only one slave is
brought up.
Should the poll skip a port that has no descriptors configured for the
current mode? This looks unchanged at the end of the series, where
icssm_prueth_lre_port_config() also sets rx_queue_descs per port.
> +
> + if (overflow_cnt > 0) {
> + atomic64_add(overflow_cnt, &emac->stats.rx_over_errors);
> + writeb(0, &queue_desc->overflow_cnt);
> + }
> + if (overflow_cnt_o > 0) {
> + atomic64_add(overflow_cnt_o, &other_emac->stats.rx_over_errors);
> + writeb(0, &queue_desc_o->overflow_cnt);
> + }
> +
> + bd_rd_ptr = readw(&queue_desc->rd_ptr);
> + bd_wr_ptr = readw(&queue_desc->wr_ptr);
> +
> + bd_rd_ptr_o = readw(&queue_desc_o->rd_ptr);
> + bd_wr_ptr_o = readw(&queue_desc_o->wr_ptr);
> +
> + port0_q_empty = (bd_rd_ptr == bd_wr_ptr);
> + port1_q_empty = (bd_rd_ptr_o == bd_wr_ptr_o);
> +
> + while (!port0_q_empty || !port1_q_empty) {
> + rd_buf_desc = readl(shared_ram + bd_rd_ptr);
> + rd_buf_desc_o = readl(shared_ram + bd_rd_ptr_o);
> +
> + icssm_parse_packet_info(prueth, rd_buf_desc, &pkt_info);
> + icssm_parse_packet_info(prueth, rd_buf_desc_o, &pkt_info_o);
> +
> + pkt_ts = readl(ocmc_ram + ICSS_LRE_TIMESTAMP_ARRAY_OFFSET +
> + bd_rd_ptr - SRAM_START_OFFSET);
> + pkt_ts_o = readl(ocmc_ram + ICSS_LRE_TIMESTAMP_ARRAY_OFFSET +
> + bd_rd_ptr_o - SRAM_START_OFFSET);
[ ... ]
> + if (used >= quota)
> + return used;
> + }
> +
> + return used;
> +}
> +
> +int icssm_prueth_lre_napi_poll_lpq(struct napi_struct *napi, int budget)
> +{
[ ... ]
> + num_rx_packets = icssm_prueth_common_emac_rx_packets(emac, budget,
> + qid1, qid2);
> + if (num_rx_packets < budget && napi_complete_done(napi, num_rx_packets))
> + enable_irq(prueth->rx_lpq_irq);
> +
> + return num_rx_packets;
> +}
[Severity: Medium]
[Finding: fable-5-5-25]
[Sources: fable-5, opus-5]
Can this enable_irq() run after the matching free_irq()?
enable_irq() happens after napi_complete_done() has already cleared
NAPIF_STATE_SCHED, and napi_disable() waits only on that bit, so in
icssm_emac_ndo_stop():
napi_disable(&prueth->napi_lpq);
napi_disable(&prueth->napi_hpq);
icssm_prueth_common_free_irqs(emac); /* -> free_irq() */
can complete while the poll has not yet reached enable_irq(). free_irq()
shuts the line down leaving depth 1, so the late enable_irq() re-enables a
line with no handler, giving an unbalanced enable warning or an unhandled
interrupt storm, or a skewed disable depth against the next request_irq().
The open error path has the same inversion: icssm_emac_ndo_open() enables
napi_hpq and napi_lpq before requesting the IRQs, and when the LPQ
request_irq() fails icssm_prueth_common_request_irqs() does
free_rx_hpq_irq:
free_irq(prueth->rx_hpq_irq, prueth->hp);
while napi_hpq is enabled and possibly mid-poll; napi_disable() only runs
later at the disable_napi: label. Would requesting the IRQs before enabling
the NAPIs, and disabling the NAPIs before freeing them, close this?
The same enable_irq()-after-napi_complete_done() pattern already exists in
the dual-EMAC icssm_emac_napi_poll(), so the idiom is not new, but the open
path ordering is introduced here.
[ ... ]
> +/**
> + * icssm_prueth_common_free_irqs - free irq
> + *
> + * @emac: EMAC data structure
> + *
> + */
> +void icssm_prueth_common_free_irqs(struct prueth_emac *emac)
> +{
> + struct prueth *prueth = emac->prueth;
> +
> + /* HSR/PRP: free irqs when last port is down */
> + if (prueth->emac_configured)
> + return;
> +
> + free_irq(prueth->rx_lpq_irq, prueth->lp);
> + free_irq(prueth->rx_hpq_irq, prueth->hp);
> +}
[ ... ]
> diff --git a/drivers/net/ethernet/ti/icssm/icssm_prueth_switch.c b/drivers/net/ethernet/ti/icssm/icssm_prueth_switch.c
> index 66866ea37913f..9d67fc7c23acc 100644
> --- a/drivers/net/ethernet/ti/icssm/icssm_prueth_switch.c
> +++ b/drivers/net/ethernet/ti/icssm/icssm_prueth_switch.c
> @@ -886,6 +886,14 @@ void icssm_prueth_sw_hostconfig(struct prueth *prueth)
> sizeof(queue_descs[PRUETH_PORT_QUEUE_HOST]));
> }
>
> +void icssm_prueth_sw_config_packet_timestamping(struct prueth *prueth)
> +{
> + void __iomem *sram = prueth->mem[PRUETH_MEM_SHARED_RAM].va;
> +
> + writeb(1, sram + SWITCH_PRIORITY_INTRS_STATUS_OFFSET);
> + writeb(1, sram + SWITCH_TIMESTAMP_PKTS_STATUS_OFFSET);
> +}
> +
[ ... ]
> diff --git a/drivers/net/ethernet/ti/icssm/icssm_switch.h b/drivers/net/ethernet/ti/icssm/icssm_switch.h
> index 5ba9ce14da445..13b1b93ef4c27 100644
> --- a/drivers/net/ethernet/ti/icssm/icssm_switch.h
> +++ b/drivers/net/ethernet/ti/icssm/icssm_switch.h
> @@ -331,6 +331,12 @@
> #define P0_COL_BUFFER_OFFSET 0xEE00
> #define P0_Q1_BUFFER_OFFSET 0x0000
>
> +/* Enable/disable interrupts for high/low priority instead of per port.
> + * 0 = disabled (default), 1 = enabled
> + */
> +#define SWITCH_PRIORITY_INTRS_STATUS_OFFSET 0x1FAA
> +#define SWITCH_TIMESTAMP_PKTS_STATUS_OFFSET 0x1FAB
[Severity: Low]
[Finding: main-2-4]
[Sources: opus-5, sonnet-5]
Are these two defines in the right section of the header?
They land in the block the header labels
/* Memory Usage of L3 OCMC RAM */
right after P0_COL_BUFFER_OFFSET and P0_Q1_BUFFER_OFFSET, but the only
consumer, icssm_prueth_sw_config_packet_timestamping(), writes them into
prueth->mem[PRUETH_MEM_SHARED_RAM].va. Their values also match the shared RAM
constants ICSS_LRE_PRIORITY_INTRS_STATUS_OFFSET and
ICSS_LRE_TIMESTAMP_PKTS_STATUS_OFFSET in icssm_lre_firmware.h, so the writes
themselves look fine and only the placement is off.
The duplication is asymmetric too: switch-named mirrors are added for the two
status bytes, yet icssm_prueth_common_emac_rx_packets() reads the arrival
timestamps for switch-mode frames through the LRE-only
ICSS_LRE_TIMESTAMP_ARRAY_OFFSET, and the switch memory map has no entry
saying the switch firmware publishes that array at the same offset. Does the
switch firmware place the timestamp array there, and could the switch memory
map document it?
> #define V2_1_FDB_TBL_LOC PRUETH_MEM_SHARED_RAM
> #define V2_1_FDB_TBL_OFFSET 0x2000
--
This is an AI-generated review.
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH net-next v3 3/3] net: ti: icssm-prueth: Support duplicate HW offload feature for HSR and PRP
2026-08-04 12:14 ` [PATCH net-next v3 3/3] net: ti: icssm-prueth: Support duplicate HW offload feature for HSR and PRP Parvathi Pudi
@ 2026-08-11 8:39 ` Paolo Abeni
0 siblings, 0 replies; 7+ messages in thread
From: Paolo Abeni @ 2026-08-11 8:39 UTC (permalink / raw)
To: parvathi
Cc: andrew+netdev, davem, edumazet, kuba, pabeni, danishanwar, rogerq,
pmohan, afd, basharath, arnd, linux-kernel, netdev,
linux-arm-kernel, pratheesh, j-rameshbabu, vigneshr, praneeth,
srk, rogerq, m-malladi, krishna, mohan
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
Full review at: https://netdev-ai.bots.linux.dev/sashiko/
---
--- Patch [3]: [PATCH net-next v3 3/3] net: ti: icssm-prueth: Support duplicate HW offload feature for HSR and PRP ---
commit e6e3942b25c7905f76512f3ac3b7e21bb0915c5e
Author: Roger Quadros <rogerq@ti.com>
net: ti: icssm-prueth: Support duplicate HW offload feature for HSR and PRP
This adds NETIF_F_HW_HSR_DUP support by moving the two per-port LRE
transmit queues onto one shared BD ring, so a frame is copied into OCMC RAM
once and both PRU ports transmit it. The TX path inspects the HSR tag or
the PRP RCT trailer to decide whether to set the LAN A/LAN B duplication
bits, and the receive path switches from the BD Port field to a new HostRecv
bit.
A few questions about the new code below.
> diff --git a/drivers/net/ethernet/ti/icssm/icssm_prueth.c b/drivers/net/ethernet/ti/icssm/icssm_prueth.c
> index 7031b78d2cc2f..43862946d4f14 100644
> --- a/drivers/net/ethernet/ti/icssm/icssm_prueth.c
> +++ b/drivers/net/ethernet/ti/icssm/icssm_prueth.c
[ ... ]
> @@ -567,16 +615,19 @@ static int icssm_prueth_tx_enqueue(struct prueth_emac *emac,
> u16 update_wr_ptr;
> u32 wr_buf_desc;
> void *ocmc_ram;
> + __be16 proto;
> + u8 *hdr;
> +
> + other_emac = emac->prueth->emac[(emac->port_id == PRUETH_PORT_MII0) ?
> + PRUETH_PORT_MII1 - 1 : PRUETH_PORT_MII0 - 1];
> +
> + if (prueth_is_lre(prueth) && (emac->link || other_emac->link))
> + link_up = true;
[Severity: Medium]
[Finding: main-5-19]
[Sources: opus-5]
Is emac->link safe to read here without emac->lock?
icssm_emac_adjust_link() mutates it under the lock:
spin_lock_irqsave(&emac->lock, flags);
...
} else if (emac->link) {
new_state = true;
emac->link = 0;
}
The transmit path holds only prueth->lre_host_queue_lock[qid - 2], so the
lockset intersection with adjust_link is empty.
Since link_up decides whether the second write pointer of the shared queue
is advanced, can a sample that races with adjust_link leave the two write
pointers for one ring permanently out of step?
> if (!PRUETH_IS_EMAC(prueth))
> dram = prueth->mem[PRUETH_MEM_DRAM1].va;
> else
> dram = emac->prueth->mem[emac->dram].va;
> - if (eth_skb_pad(skb)) {
> - if (netif_msg_tx_err(emac) && net_ratelimit())
> - netdev_err(ndev, "packet pad failed\n");
> - return -ENOMEM;
> - }
>
> /* which port to tx: MII0 or MII1 */
> txport = emac->tx_port_queue;
[ ... ]
> @@ -607,6 +661,29 @@ static int icssm_prueth_tx_enqueue(struct prueth_emac *emac,
> free_blocks = buffer_desc_count;
> }
>
> + /* Fetch queue state for the second LRE port */
> + if (prueth_is_lre(prueth) && link_up) {
> + queue_desc_other_port = emac->tx_queue_descs_other_port +
> + queue_id;
> + bd_rd_ptr_other_port = readw(&queue_desc_other_port->rd_ptr);
> +
> + read_block_other_port = (bd_rd_ptr_other_port -
> + txqueue->buffer_desc_offset) / BD_SIZE;
[Severity: High]
[Finding: main-3-13]
[Sources: opus-5]
Should this be conditional on the peer port actually having been
configured, i.e. prueth->emac_configured & BIT(other_port)?
link_up is true whenever either port's phy link is up, so with only one LRE
slave netdev opened this reads a descriptor that was never initialised.
icssm_prueth_hostinit() has memset_io()'d DRAM1:
if (prueth->eth_node[PRUETH_MAC1])
icssm_prueth_clearmem(prueth, PRUETH_MEM_DRAM1);
so bd_rd_ptr_other_port reads back 0 and, with both operands promoted to
int:
read_block_other_port = (0 - P1_Q3_TXOPT_BD_OFFSET) / BD_SIZE
= (0 - 4128) / 4 = -1032
free_blocks_other_port = 194 - 0 + (-1032) = -838
> +
> + if (write_block > read_block_other_port) {
> + free_blocks_other_port = buffer_desc_count -
> + write_block;
> + free_blocks_other_port += read_block_other_port;
> + } else if (write_block < read_block_other_port) {
> + free_blocks_other_port = read_block_other_port -
> + write_block;
> + } else {
> + free_blocks_other_port = buffer_desc_count;
> + }
> +
> + if (free_blocks_other_port < free_blocks)
> + free_blocks = free_blocks_other_port;
> + }
> pkt_block_size = DIV_ROUND_UP(pktlen, ICSS_BLOCK_SIZE);
> if (pkt_block_size >= free_blocks) /* out of queue space */
> return -ENOBUFS;
With free_blocks negative, does pkt_block_size >= free_blocks not stay true
forever, so every xmit returns -ENOBUFS and icssm_emac_ndo_start_xmit()
loops on netif_stop_queue() plus the 100us hrtimer retry, stalling TX on
the port that does have a link?
> @@ -656,6 +733,57 @@ static int icssm_prueth_tx_enqueue(struct prueth_emac *emac,
> if (PRUETH_IS_HSR(prueth))
> wr_buf_desc |= BIT(PRUETH_BD_HSR_FRAME_SHIFT);
>
> + if (prueth_is_lre(prueth)) {
> + ethhdr = (struct ethhdr *)skb_mac_header(skb);
> + proto = ethhdr->h_proto;
> +
> + if (proto == htons(ETH_P_8021Q)) {
> + vlan_hdr = (struct vlan_ethhdr *)ethhdr;
> + proto = vlan_hdr->h_vlan_encapsulated_proto;
> + is_vlan = true;
> + }
> +
> + /* Extract HSR sequence number and LAN ID
> + * from the tag for the Buffer Descriptor
> + */
> + if (proto == htons(ETH_P_HSR)) {
> + hdr = skb_mac_header(skb);
> +
> + if (is_vlan) {
> + hsr_ethhdr =
> + (struct hsr_txopt_ethhdr *)(hdr +
> + VLAN_HLEN);
> + } else {
> + hsr_ethhdr = (struct hsr_txopt_ethhdr *)hdr;
> + }
> +
> + /* PTP frames (ETH_P_1588) carry no LAN ID
> + * in the HSR tag
> + */
> + if (hsr_ethhdr->hsr_tag.encap_proto !=
> + htons(ETH_P_1588)) {
> + wr_buf_desc |= PRUETH_BD_LAN_INFO_MASK;
> + } else {
> + wr_buf_desc |= (txport <<
> + PRUETH_BD_LAN_A_SHIFT);
> + }
> + wr_buf_desc |= PRUETH_BD_RED_PKT_MASK;
> + } else {
> + /* Read PRP RCT to extract sequence number and LAN ID */
> + struct prp_txopt_rct *rct =
> + (struct prp_txopt_rct *)(skb_tail_pointer(skb) -
> + ICSSM_LRE_TAG_SIZE);
> +
> + if (rct->prp_suffix == htons(ETH_P_PRP)) {
> + wr_buf_desc |= PRUETH_BD_LAN_INFO_MASK;
> + wr_buf_desc |= PRUETH_BD_RED_PKT_MASK;
> + } else {
> + wr_buf_desc |= (txport <<
> + PRUETH_BD_LAN_A_SHIFT);
> + }
> + }
> + }
[Severity: Medium]
[Finding: main-1-4]
[Sources: opus-5]
Should the RCT branch be gated on PRUETH_IS_PRP(prueth)?
As written the else arm also runs in HSR mode, for every frame whose
(VLAN-inner) ethertype is not ETH_P_HSR, for example an untagged PTP, ARP or
IPv6 DAD frame sent directly on the slave netdev. The last six bytes of
such a frame are then reinterpreted as struct prp_txopt_rct, and if the
final two payload bytes happen to be 0x88FB the frame gets
wr_buf_desc |= PRUETH_BD_LAN_INFO_MASK;
wr_buf_desc |= PRUETH_BD_RED_PKT_MASK;
and is duplicated onto both LANs. With a raw socket those two bytes are
fully caller controlled. The commit message says "Frames without an RCT
are sent on the originating port only" - is that still what happens here in
HSR mode?
Also, the two comments say the sequence number and LAN ID are extracted
from the HSR tag and from the RCT, but neither sequence_nr nor
lan_id_and_lsdu_size is ever read. Could the comments be reworded?
> @@ -668,6 +796,10 @@ static int icssm_prueth_tx_enqueue(struct prueth_emac *emac,
> update_wr_ptr = txqueue->buffer_desc_offset + (update_block * BD_SIZE);
> writew(update_wr_ptr, &queue_desc->wr_ptr);
>
> + /* update the write pointer in queue descriptor of other port */
> + if (prueth_is_lre(prueth) && link_up)
> + writew(update_wr_ptr, &queue_desc_other_port->wr_ptr);
> +
> return 0;
> }
[Severity: High]
[Finding: main-1-5]
[Sources: opus-5]
The BD write and the OCMC payload copy above are unconditional, but the peer
write pointer is only mirrored when link_up is true. Can the two
descriptors that describe the same ring diverge permanently?
icssm_emac_ndo_open() does not stop the TX queue and emac->link stays 0
until the first icssm_emac_adjust_link() callback, so a frame sent right
after "ip link set ethX up" (with the peer also down) takes this path:
writel(wr_buf_desc, sram + readw(&queue_desc->wr_ptr));
...
writew(update_wr_ptr, &queue_desc->wr_ptr);
/* peer wr_ptr not advanced */
On the next transmit with link_up true, update_wr_ptr is computed from the
local descriptor and written into the peer descriptor, so does the peer PRU
jump over BDs it never saw as pending and transmit whatever ring content is
there now?
Separately, because the condition is an OR of both links, a peer whose link
is down still clamps free_blocks via free_blocks_other_port. Should the
peer's rd_ptr only be consulted when that peer is actually draining the
ring?
> @@ -680,8 +812,10 @@ void icssm_parse_packet_info(struct prueth *prueth, u32 buffer_descriptor,
> else
> pkt_info->start_offset = false;
>
> - pkt_info->port = (buffer_descriptor & PRUETH_BD_PORT_MASK) >>
> - PRUETH_BD_PORT_SHIFT;
> + /* Flag from BD to indicate packet is valid for HOST or not. */
> + pkt_info->host_recv_flag = !!(buffer_descriptor &
> + PRUETH_BD_HOST_RECV_MASK);
> +
[Severity: Medium]
[Finding: main-1-0]
[Sources: opus-5]
The commit message describes transmit-side duplicate offload only ("The
driver writes the frame once into OCMC RAM", HSR tag and RCT inspection),
but the patch also changes the receive path for every eth_type:
- icssm_parse_packet_info() stops decoding the BD Port field and decodes
the new HostRecv bit for all modes
- icssm_emac_rx_packet() silently returns 0 for HSR frames without the
HostRecv bit, with no drop counter, while the caller still counts the
descriptor as used++
- software HSR tag removal excises six bytes from the middle of the frame
- the OCMC to skb copy is restructured into MAC header, optional VLAN and
body stages for EMAC, switch, HSR and PRP alike
Could the changelog mention these receive-side changes?
> pkt_info->length = (buffer_descriptor & PRUETH_BD_LENGTH_MASK) >>
> PRUETH_BD_LENGTH_SHIFT;
> pkt_info->broadcast = !!(buffer_descriptor & PRUETH_BD_BROADCAST_MASK);
[ ... ]
> @@ -744,9 +880,19 @@ int icssm_emac_rx_packet(struct prueth_emac *emac, u16 *bd_rd_ptr,
> /* calculate new pointer in ram */
> *bd_rd_ptr = rxqueue->buffer_desc_offset + (update_block * BD_SIZE);
>
> + if (PRUETH_IS_HSR(emac->prueth)) {
> + if (!pkt_info->host_recv_flag)
> + return 0;
> + }
> +
[Severity: Low]
[Finding: main-2-6]
[Sources: opus-5]
The new bit is documented in icssm_switch.h without any protocol
qualification:
* 10 HostRecv Set if the frame is destined for the host port
and icssm_parse_packet_info() fills host_recv_flag for every eth_type, but
the only consumer is inside if (PRUETH_IS_HSR(...)). In PRP mode the flag
is parsed and then ignored, so BDs the firmware marks as not for the host
are still copied into an skb and passed to netif_receive_skb(). Is that
intentional, and if so should the header say the bit is HSR only?
> /* Exclude the HSR tag bytes already stripped by firmware, if any. */
> actual_pkt_len = pkt_info->length - start_offset;
>
> + if (PRUETH_IS_HSR(emac->prueth)) {
> + if (!start_offset && !pkt_info->timestamp)
> + actual_pkt_len -= ICSSM_LRE_TAG_SIZE;
> + }
> +
[Severity: Medium]
[Finding: main-1-3]
[Sources: opus-5]
This removes six bytes from every HSR-mode frame whose BD has neither the
start-offset bit nor the timestamp bit, without ever looking at the frame's
ethertype. Can that corrupt untagged frames?
The same new code already reads the ethertype a few lines below for the VLAN
test:
type = get_unaligned_be16(src_addr);
and the transmit side of this patch does discriminate on
proto == htons(ETH_P_HSR), and sends non-HSR protocols untagged on the
directed port. So untagged frames do appear on the ring.
For such a frame, actual_pkt_len is reduced by six, src_addr is advanced by
six past ordinary payload, and the wrapped-copy path applies the same
adjustment to bytes. Would gating the strip on the frame actually carrying
an HSR tag avoid delivering the frame six bytes short with a hole after the
MAC (and optional VLAN) header?
> /* Allocate a socket buffer for this packet */
> skb = netdev_alloc_skb_ip_align(ndev, actual_pkt_len);
> if (!skb) {
[ ... ]
> @@ -767,6 +913,29 @@ int icssm_emac_rx_packet(struct prueth_emac *emac, u16 *bd_rd_ptr,
> (read_block * ICSS_BLOCK_SIZE);
> src_addr += start_offset;
>
> + /* Copy destination and source MAC address */
> + memcpy(dst_addr, src_addr, PRUETH_ETHER_TYPE_OFFSET);
> + src_addr += PRUETH_ETHER_TYPE_OFFSET;
> + dst_addr += PRUETH_ETHER_TYPE_OFFSET;
> +
> + adjust_for_hsr_tag += PRUETH_ETHER_TYPE_OFFSET;
> +
> + /* Check for VLAN tag */
> + type = get_unaligned_be16(src_addr);
> +
> + if (type == ETH_P_8021Q) {
> + memcpy(dst_addr, src_addr, VLAN_HLEN);
> + src_addr += VLAN_HLEN;
> + dst_addr += VLAN_HLEN;
> + adjust_for_hsr_tag += VLAN_HLEN;
> + }
> +
> + /* HSR tag removal handling */
> + if (PRUETH_IS_HSR(emac->prueth)) {
> + if (!start_offset && !pkt_info->timestamp)
> + src_addr += ICSSM_LRE_TAG_SIZE;
> + }
> +
[ ... ]
> diff --git a/drivers/net/ethernet/ti/icssm/icssm_prueth.h b/drivers/net/ethernet/ti/icssm/icssm_prueth.h
> index 9af19617967ee..2447fb8b99e79 100644
> --- a/drivers/net/ethernet/ti/icssm/icssm_prueth.h
> +++ b/drivers/net/ethernet/ti/icssm/icssm_prueth.h
[ ... ]
> @@ -314,9 +317,13 @@ struct prueth {
> u8 emac_configured;
> u8 hsr_members;
> u8 br_members;
> +
> + /* Per-queue TX lock - LRE uses only the two high-priority queues */
> + spinlock_t lre_host_queue_lock[NUM_QUEUES / 2];
> };
[Severity: Low]
[Finding: main-2-9]
[Sources: opus-5]
This isn't a bug, but the comment says "the two high-priority queues" while
the queues the LRE transmit path can reach are PRUETH_QUEUE3 and
PRUETH_QUEUE4:
emac_pcp_tx_priority_queue_map[0..3] == {QUEUE4, QUEUE4, QUEUE3, QUEUE3}
and this same patch labels them in icssm_switch.h as
#define QUEUE_3_TXOPT_SIZE 194 /* Protocol specific - High Priority */
#define QUEUE_4_TXOPT_SIZE 194 /* NRT(IP,ARP, ICMP) - Low Priority*/
which is one high and one low priority queue.
> diff --git a/drivers/net/ethernet/ti/icssm/icssm_prueth_common.c b/drivers/net/ethernet/ti/icssm/icssm_prueth_common.c
> index 43140add81725..a99cdbb79bbf0 100644
> --- a/drivers/net/ethernet/ti/icssm/icssm_prueth_common.c
> +++ b/drivers/net/ethernet/ti/icssm/icssm_prueth_common.c
> @@ -137,16 +137,13 @@ static int icssm_prueth_common_emac_rx_packets(struct prueth_emac *emac,
> used++;
> }
>
> - /* Zero the BD after consuming it, a misaligned rd_ptr
> - * would otherwise mistake stale data for a valid incoming
> - * frame.
> + /* Leave the BD intact after reading. Firmware reuses it to
> + * forward the frame to the second LRE port.
> */
> if (port == 0) {
> - writel(0, shared_ram + bd_rd_ptr);
> writew(update_rd_ptr, &queue_desc->rd_ptr);
> bd_rd_ptr = update_rd_ptr;
> } else {
> - writel(0, shared_ram + bd_rd_ptr_o);
> writew(update_rd_ptr, &queue_desc_o->rd_ptr);
> bd_rd_ptr_o = update_rd_ptr;
> }
[Severity: Medium]
[Finding: main-1-2]
[Sources: opus-5]
Should this removal be gated on prueth_is_lre()?
The justification is LRE specific, but this is the common host receive
routine: icssm_prueth_lre_napi_poll_hpq()/lpq() are registered and enabled
whenever PRUETH_IS_SWITCH(prueth) || prueth_is_lre(prueth), so RSTP switch
mode loses the invalidation too.
The immediately preceding patch in this series added the zeroing with the
opposite reasoning, and the single-port path still does it:
drivers/net/ethernet/ti/icssm/icssm_prueth.c:icssm_emac_rx_packets() {
...
/* after reading the buffer descriptor we clear it
* to prevent improperly moved read pointer errors
* from simply looking like old packets.
*/
writel(0, shared_ram + bd_rd_ptr);
...
}
In switch mode, after any rd_ptr skew, including the two length-error paths
in this same loop that force update_rd_ptr = *bd_wr_ptr_p, can a stale
non-zero BD word be re-parsed as a valid length and flags descriptor and up
to pkt_info->length bytes of old OCMC content be pushed to
netif_receive_skb()?
> diff --git a/drivers/net/ethernet/ti/icssm/icssm_prueth_switch.c b/drivers/net/ethernet/ti/icssm/icssm_prueth_switch.c
> index 9d67fc7c23acc..bda2fac13d749 100644
> --- a/drivers/net/ethernet/ti/icssm/icssm_prueth_switch.c
> +++ b/drivers/net/ethernet/ti/icssm/icssm_prueth_switch.c
[ ... ]
> @@ -983,6 +1177,109 @@ static int icssm_prueth_sw_port_config(struct prueth *prueth,
> return 0;
> }
>
> +/* Configure TX/RX queue contexts and buffer descriptor tables for LRE port */
> +static int icssm_prueth_lre_port_config(struct prueth *prueth,
> + enum prueth_port port_id)
> +{
[ ... ]
> + /* queue size lookup table */
> + dram = dram_base + QUEUE_SIZE_ADDR +
> + port_id * NUM_QUEUES * sizeof(u16);
> + writew(HOST_QUEUE_1_SIZE, dram);
> + writew(HOST_QUEUE_2_SIZE, dram + 2);
> + writew(QUEUE_3_TXOPT_SIZE, dram + 4);
> + writew(QUEUE_4_TXOPT_SIZE, dram + 6);
[Severity: Low]
[Finding: main-2-10]
[Sources: opus-5]
For PRUETH_PORT_QUEUE_MII0 the entries QUEUE1 and QUEUE2 of
lre_queue_infos describe host queues 3 and 4:
[PRUETH_QUEUE1] = {
P0_Q3_BUFFER_OFFSET,
...
P0_Q3_BD_OFFSET + ((HOST_QUEUE_3_SIZE - 1) * BD_SIZE),
},
so the firmware is told HOST_QUEUE_1_SIZE / HOST_QUEUE_2_SIZE for rings
whose buffer_desc_end the driver derives from HOST_QUEUE_3_SIZE /
HOST_QUEUE_4_SIZE. All four constants are 194 today, so nothing breaks,
but should the sizes be derived from the same constants the queue info
table uses?
> +
> + /* queue table */
> + memcpy_toio(dram_base + queue_desc_ofs,
> + &hsr_prp_txopt_queue_descs[port_id][0],
> + 4 * sizeof(hsr_prp_txopt_queue_descs[port_id][0]));
[Severity: High]
[Finding: main-3-13]
[Sources: opus-5]
Is it safe to reset these descriptors while the peer port is live?
hsr_prp_txopt_queue_descs[MII0][2..3] and [MII1][2..3] both point at
P1_Q3_TXOPT_BD_OFFSET / P2_Q1_TXOPT_BD_OFFSET, so the two per-port
descriptor sets describe the same BD ring and the same OCMC buffer. This
memcpy_toio() rewinds rd_ptr and wr_ptr of that shared ring to its base.
The path is reachable from ndo_open of the second slave, and again on
"ip link set ethX down; ip link set ethX up" while the other slave is up,
because icssm_prueth_sw_shutdown_prus() returns early while
emac_configured is non-zero and icssm_emac_ndo_stop() only clears the
port's bit, so both PRUs keep running.
Also, this runs with no lock held, while the peer's
icssm_emac_ndo_start_xmit() holds only prueth->lre_host_queue_lock[qid - 2]
when it reads queue_desc_other_port->rd_ptr and writes
queue_desc_other_port->wr_ptr. Should the configuration path take the same
lock?
> +
> + /* In HSR/PRP mode both slave ports share the host receive queue
> + * descriptor region (P0_QUEUE_DESC_OFFSET). The firmware arbitrates
> + * ownership; the driver always reads from the same host-side descriptor
> + * base regardless of which physical port the frame arrived on.
> + */
> + emac->rx_queue_descs = dram_base + P0_QUEUE_DESC_OFFSET;
> + emac->tx_queue_descs = dram_base +
> + lre_rx_queue_infos[port_id][PRUETH_QUEUE1].queue_desc_offset;
> +
> + if (port_id == PRUETH_PORT_MII0) {
> + emac->tx_queue_descs_other_port = dram_base +
> + lre_rx_queue_infos
> + [port_id + 1][PRUETH_QUEUE1].queue_desc_offset;
> + } else if (port_id == PRUETH_PORT_MII1) {
> + emac->tx_queue_descs_other_port = dram_base +
> + lre_rx_queue_infos
> + [port_id - 1][PRUETH_QUEUE1].queue_desc_offset;
> + }
> +
> + return 0;
> +}
[ ... ]
> diff --git a/drivers/net/ethernet/ti/icssm/icssm_switch.h b/drivers/net/ethernet/ti/icssm/icssm_switch.h
> index 13b1b93ef4c27..b0d623b4979b0 100644
> --- a/drivers/net/ethernet/ti/icssm/icssm_switch.h
> +++ b/drivers/net/ethernet/ti/icssm/icssm_switch.h
[ ... ]
> @@ -49,20 +52,18 @@
> * For RED, NodeTable lookup was successful.
> * 7 Flood Packet should be flooded (destination MAC
> * address found in FDB). For switch only.
> - * 8..12 Block_length number of valid bytes in this specific block.
> - * Will be <=32 bytes on last block of packet
> + * 8 RED_INFO Set if the frame carries an HSR or PRP
> + * redundancy tag
> + * 10 HostRecv Set if the frame is destined for the host port
> * 13 More "More" bit indicating that there are more blocks
> * 14 Shadow indicates that "index" is pointing into shadow
> * buffer
> * 15 TimeStamp indicates that this packet has time stamp in
> * separate buffer - only needed if PTP runs on
> * host
> - * 16..17 Port different meaning for ingress and egress,
> - * Ingress: Port = 0 indicates phy port 1 and
> - * Port = 1 indicates phy port 2.
> - * Egress: 0 sends on phy port 1 and 1 sends on
> - * phy port 2. Port = 2 goes over MAC table
> - * look-up
> + * 16..17 LAN Destination LAN for transmission:
> + * bit 16 = LAN A, bit 17 = LAN B, set both to
> + * duplicate to both LANs.
[Severity: Low]
[Finding: main-2-7]
[Sources: opus-5]
This header still serves the EMAC and switch firmware, but the bit table
now only describes the LRE interpretation. The Block_length description
for bits 8..12 and the Port description for bits 16..17, including
"Port = 2 goes over MAC table look-up", are gone, bits 9, 11 and 12 are
left undocumented, and PRUETH_BD_PORT_MASK/SHIFT are removed.
icssm_prueth_tx_enqueue() only programs the LAN bits inside
if (prueth_is_lre(prueth)), so could the comment note that the RED_INFO,
HostRecv and LAN meanings apply to the LRE firmware only, and keep the
EMAC/switch meanings alongside them?
Thanks for looking at these.
--
This is an AI-generated review.
^ permalink raw reply [flat|nested] 7+ messages in thread
end of thread, other threads:[~2026-08-11 8:40 UTC | newest]
Thread overview: 7+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-04 12:14 [PATCH net-next v3 0/3] Introduce HSR/PRP HW offload support for PRU-ICSSM Ethernet driver Parvathi Pudi
2026-08-04 12:14 ` [PATCH net-next v3 1/3] net: ti: icssm-prueth: Add HSR and PRP HW offload mode support for AM57xx, AM437x and AM335x Parvathi Pudi
2026-08-11 8:39 ` Paolo Abeni
2026-08-04 12:14 ` [PATCH net-next v3 2/3] net: ti: icssm-prueth: Add priority based RX IRQ handlers Parvathi Pudi
2026-08-11 8:39 ` Paolo Abeni
2026-08-04 12:14 ` [PATCH net-next v3 3/3] net: ti: icssm-prueth: Support duplicate HW offload feature for HSR and PRP Parvathi Pudi
2026-08-11 8:39 ` Paolo Abeni
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox