Linux-ARM-Kernel Archive on lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH net-next v5 0/3] Introduce HSR/PRP HW offload support for PRU-ICSSM Ethernet driver
@ 2026-10-05 15:41 Parvathi Pudi
  2026-10-05 15:41 ` [PATCH net-next v5 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-10-05 15:41 UTC (permalink / raw)
  To: andrew+netdev, davem, edumazet, kuba, pabeni, danishanwar,
	parvathi, rogerq, pmohan, afd, vadim.fedorenko, haokexin,
	basharath, arnd
  Cc: linux-kernel, netdev, linux-arm-kernel, horms, 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-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 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-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 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-fwd-offload, hsr-tag-rm-offload and hsr-dup-offload are tightly coupled in the
  firmware implementation. All need to be enabled / disabled together and
  hsr-tag-ins-offload is unsupported.

This is the v5 of the patch series [v1]. This version of the patchset addresses the
comments made on [v4] of the series.

Changes from v4 to v5 :

*) Addressed 3 shashiko AI review comments which required code changes.
*) There are four comments which are false positives:
   - Shared NAPI is initialized and used only when both MII0 and MII1 are present. If MII0
     is absent, the device remains in EMAC mode and uses the per-port NAPI.
   - The shared RX IRQ handler is registered only in SWITCH/HSR/PRP modes and with MII0 absent,
     EMAC mode uses the per-port IRQ handler instead.
   - HSR packet TX duplication (hsr-dup-offload) and tag removal (hsr-tag-rm-offload) are
     unconditional by design because HSR mode is entered only when the HSR offload features are
     enabled. Otherwise, the device stays in EMAC mode and the HSR core handles these operations
     in software.
*) There is one pre-existing comment related to the VLAN tag check in the TX path which will be
   addressed in a separate series.
*) Rebased the series onto the latest net-next.

Changes from v3 to v4 :

*) Addressed 27 sashiko AI review comments that required code changes in this series.
*) There are three pre-existing sashiko comments that are not introduced by this series:
   - FDB learning can sleep in softirq context
   - mode changes can bypass normal network state synchronization
   - allocation failure can leave the RX descriptor unadvanced, potentially causing
     repeated interrupts.
   These will be addressed in a separate series.
*) There are four comments which are false positives:
   - NETIF_F_HW_HSR_DUP is for TX duplication, not RX duplicate discard, and is added in patch 3.
   - The __packed comment is not applicable since C packing rules already handle this structure, as
     noted in feedback on an earlier series (https://lore.kernel.org/all/20250708175301.599c82b8@kernel.org/).
   - The LRE timer check depends on interface admin state, not link state, and both interfaces
     are expected to remain up for HSR/PRP.
   - atomic64_t is used to safely update statistics from the RX/TX paths, and the associated overhead
     does not cause a performance regression in this context.
*) Rebased the series onto the latest net-next.

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/
[v3] https://lore.kernel.org/all/20260804121644.465118-1-parvathi@couthit.com/
[v4] https://lore.kernel.org/all/20260921134343.200426-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/Kconfig               |   1 +
 drivers/net/ethernet/ti/Makefile              |   2 +-
 .../ethernet/ti/icssm/icssm_lre_firmware.h    | 141 ++++
 drivers/net/ethernet/ti/icssm/icssm_prueth.c  | 782 ++++++++++++++++--
 drivers/net/ethernet/ti/icssm/icssm_prueth.h  |  89 +-
 .../ethernet/ti/icssm/icssm_prueth_common.c   | 310 +++++++
 .../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   | 333 +++++++-
 .../ethernet/ti/icssm/icssm_prueth_switch.h   |   2 +
 drivers/net/ethernet/ti/icssm/icssm_switch.h  |  40 +-
 11 files changed, 1829 insertions(+), 114 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 v5 1/3] net: ti: icssm-prueth: Add HSR and PRP HW offload mode support for AM57xx, AM437x and AM335x
  2026-10-05 15:41 [PATCH net-next v5 0/3] Introduce HSR/PRP HW offload support for PRU-ICSSM Ethernet driver Parvathi Pudi
@ 2026-10-05 15:41 ` Parvathi Pudi
  2026-10-09  3:43   ` netdev-bot+sashiko
  2026-10-05 15:41 ` [PATCH net-next v5 2/3] net: ti: icssm-prueth: Add priority based RX IRQ handlers Parvathi Pudi
  2026-10-05 15:41 ` [PATCH net-next v5 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-10-05 15:41 UTC (permalink / raw)
  To: andrew+netdev, davem, edumazet, kuba, pabeni, danishanwar,
	parvathi, rogerq, pmohan, afd, vadim.fedorenko, haokexin,
	basharath, arnd
  Cc: linux-kernel, netdev, linux-arm-kernel, horms, 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 eth3 hsr-tag-rm-offload on
$ ethtool -K eth3 hsr-fwd-offload on
$ ip link add name hsr0 type hsr slave1 eth2 slave2 eth3 supervision 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-fwd-offload on
$ ethtool -K eth3 hsr-tag-rm-offload on
$ ethtool -K eth3 hsr-fwd-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 eth3 hsr-tag-rm-offload off
$ ethtool -K eth3 hsr-fwd-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/Kconfig               |   1 +
 drivers/net/ethernet/ti/Makefile              |   2 +-
 .../ethernet/ti/icssm/icssm_lre_firmware.h    | 141 +++++++
 drivers/net/ethernet/ti/icssm/icssm_prueth.c  | 381 +++++++++++++++++-
 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 +
 7 files changed, 764 insertions(+), 23 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/Kconfig b/drivers/net/ethernet/ti/Kconfig
index c60b04921c62..afed42572fd1 100644
--- a/drivers/net/ethernet/ti/Kconfig
+++ b/drivers/net/ethernet/ti/Kconfig
@@ -237,6 +237,7 @@ config TI_PRUETH
 	depends on NET_SWITCHDEV
 	select TI_ICSS_IEP
 	depends on PTP_1588_CLOCK_OPTIONAL
+	depends on HSR || !HSR
 	help
 	  Some TI SoCs has Programmable Realtime Unit (PRU) cores which can
 	  support Single or Dual Ethernet ports with the help of firmware code
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..453c9b259d77 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,29 @@
 #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 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 +333,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)
@@ -609,6 +636,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 +657,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 +697,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 +727,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 +748,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 +762,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 +786,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 +855,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 +1032,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 +1102,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 +1138,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 +1184,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 +1329,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 +1357,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 +1373,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 +1392,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 +1441,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 +1454,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 +1517,109 @@ 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 net_device *upper;
+	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;
+	}
+
+	upper = netdev_master_upper_dev_get(ndev);
+	if (upper && is_hsr_master(upper) && change_request) {
+		/* Firmware mode is switched to HSR/PRP based on the offload
+		 * features set at the time this port joins the hsr device, so
+		 * changing the features afterwards would desync ndev->features
+		 * from the firmware mode without the hsr core noticing.
+		 * Require unlinking from the hsr device first.
+		 */
+		netdev_err(ndev,
+			   "Can't change offload features while port is an HSR/PRP slave\n");
+		return -EBUSY;
+	}
+
+	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 +1771,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 +1926,109 @@ 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, prev_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) {
+		prev_mode = prueth->eth_type;
+		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:
+	prueth->eth_type = prev_mode;
+
+	spin_lock_irqsave(&prueth->addr_lock, flags);
+
+	prueth->hsr_members &= ~BIT(emac->port_id);
+
+	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;
+	enum pruss_ethtype prev_mode;
+	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) {
+		prev_mode = prueth->eth_type;
+		ret = icssm_prueth_change_mode(prueth, PRUSS_ETHTYPE_EMAC);
+		if (ret < 0) {
+			dev_err(prueth->dev, "Failed to enable dual EMAC mode\n");
+			prueth->eth_type = prev_mode;
+		}
+	}
+
+	spin_lock_irqsave(&prueth->addr_lock, flags);
+
+	if (!prueth->hsr_members)
+		prueth->hsr_dev = NULL;
+
+	spin_unlock_irqrestore(&prueth->addr_lock, flags);
+
+	return ret;
+}
+
 static int icssm_prueth_ndev_event(struct notifier_block *unused,
 				   unsigned long event, void *ptr)
 {
@@ -1754,6 +2042,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 +2095,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 +2110,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);
+	icssm_prueth_set_fw_offsets(prueth);
 
 	eth_ports_node = of_get_child_by_name(np, "ethernet-ports");
 	if (!eth_ports_node)
@@ -1955,6 +2256,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 +2488,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 +2515,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 +2542,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 v5 2/3] net: ti: icssm-prueth: Add priority based RX IRQ handlers
  2026-10-05 15:41 [PATCH net-next v5 0/3] Introduce HSR/PRP HW offload support for PRU-ICSSM Ethernet driver Parvathi Pudi
  2026-10-05 15:41 ` [PATCH net-next v5 1/3] net: ti: icssm-prueth: Add HSR and PRP HW offload mode support for AM57xx, AM437x and AM335x Parvathi Pudi
@ 2026-10-05 15:41 ` Parvathi Pudi
  2026-10-09  3:43   ` netdev-bot+sashiko
  2026-10-05 15:41 ` [PATCH net-next v5 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-10-05 15:41 UTC (permalink / raw)
  To: andrew+netdev, davem, edumazet, kuba, pabeni, danishanwar,
	parvathi, rogerq, pmohan, afd, vadim.fedorenko, haokexin,
	basharath, arnd
  Cc: linux-kernel, netdev, linux-arm-kernel, horms, 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.

Also convert struct prueth_emac_stats counters from u64 to atomic64_t,
since napi_hpq and napi_lpq can now run concurrently on different CPUs
and update the same counters.

The IEP wrap value used in the timestamp comparison differs between
firmware revisions, so this patch adds an fw_rev field to the platform
data and an enum fw_revision to identify it. icssm_prueth_probe() now
fails with -EINVAL if a platform data entry does not set fw_rev.

icssm_emac_ndo_stop() now stops TX before cancelling the TX timer.
The timer and link-up handlers restart the TX queue only when the port
is started.

Since icssm_prueth_change_mode() calls ndo_stop() while the device is
still running, TX should not restart while the shared RX/TX resources
are being changed or cleaned up.

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  | 183 +++++++++--
 drivers/net/ethernet/ti/icssm/icssm_prueth.h  |  44 ++-
 .../ethernet/ti/icssm/icssm_prueth_common.c   | 309 ++++++++++++++++++
 .../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, 526 insertions(+), 40 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 453c9b259d77..42c83946f17a 100644
--- a/drivers/net/ethernet/ti/icssm/icssm_prueth.c
+++ b/drivers/net/ethernet/ti/icssm/icssm_prueth.c
@@ -44,6 +44,21 @@
 #define NETIF_PRUETH_LRE_OFFLOAD_FEATURES       (NETIF_F_HW_HSR_FWD | \
 						 NETIF_F_HW_HSR_TAG_RM)
 
+/* 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 */
@@ -342,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)
@@ -464,7 +481,8 @@ static void icssm_emac_adjust_link(struct net_device *ndev)
 
 	if (emac->link) {
 	       /* reactivate the transmit queue if it is stopped */
-		if (netif_running(ndev) && netif_queue_stopped(ndev))
+		if (netif_running(ndev) && netif_queue_stopped(ndev) &&
+		    (prueth->emac_configured & BIT(emac->port_id)))
 			netif_wake_queue(ndev);
 	} else {
 		if (!netif_queue_stopped(ndev))
@@ -801,8 +819,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;
 }
@@ -824,7 +842,6 @@ static int icssm_emac_rx_packets(struct prueth_emac *emac, int budget)
 
 	shared_ram = emac->prueth->mem[PRUETH_MEM_SHARED_RAM].va;
 
-	/* Start and end queue is made common for EMAC, RSTP */
 	start_queue = emac->rx_queue_start;
 	end_queue = emac->rx_queue_end;
 
@@ -835,13 +852,10 @@ static int icssm_emac_rx_packets(struct prueth_emac *emac, int budget)
 	/* search host queues for packets */
 	for (i = start_queue; i <= end_queue; i++) {
 		queue_desc = emac->rx_queue_descs + i;
-		if (PRUETH_IS_SWITCH(emac->prueth))
-			rxqueue = &sw_queue_infos[PRUETH_PORT_HOST][i];
-		else
-			rxqueue = &queue_infos[PRUETH_PORT_HOST][i];
+		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);
 		}
@@ -863,7 +877,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
@@ -872,7 +886,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,
@@ -1070,19 +1084,33 @@ 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;
 
+	prueth->emac_configured |= BIT(emac->port_id);
 	/* start PHY */
 	phy_start(emac->phydev);
 
 	/* enable the port and vlan */
 	icssm_prueth_port_enable(emac, true);
 
-	prueth->emac_configured |= BIT(emac->port_id);
 	if (PRUETH_IS_SWITCH(prueth))
 		icssm_prueth_sw_set_stp_state(prueth, emac->port_id,
 					      BR_STATE_LEARNING);
@@ -1091,7 +1119,10 @@ 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);
+
 	if (!PRUETH_IS_EMAC(prueth))
 		icssm_prueth_sw_shutdown_prus(emac, ndev);
 	else
@@ -1126,11 +1157,28 @@ static int icssm_emac_ndo_stop(struct net_device *ndev)
 	/* disable the mac port */
 	icssm_prueth_port_enable(emac, false);
 
+	/* Stop TX first. netif_tx_disable() also waits for an xmit that is
+	 * already running. Then cancel any tx_hrtimer that xmit may have
+	 * armed.
+	 */
+	netif_tx_disable(ndev);
+	hrtimer_cancel(&emac->tx_hrtimer);
+
 	/* 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))
@@ -1141,9 +1189,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);
 
@@ -1280,8 +1325,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;
@@ -1295,7 +1340,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;
 	}
 
@@ -1315,13 +1360,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 */
@@ -1662,6 +1707,11 @@ static enum hrtimer_restart icssm_emac_tx_timer_callback(struct hrtimer *timer)
 {
 	struct prueth_emac *emac =
 			container_of(timer, struct prueth_emac, tx_hrtimer);
+	struct prueth *prueth = emac->prueth;
+
+	/* Don't restart TX on a port that ndo_stop() is tearing down */
+	if (!(READ_ONCE(prueth->emac_configured) & BIT(emac->port_id)))
+		return HRTIMER_NORESTART;
 
 	if (netif_queue_stopped(emac->ndev))
 		netif_wake_queue(emac->ndev);
@@ -1779,9 +1829,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_common_napi_poll_hpq);
+		netif_napi_add(ndev, &prueth->napi_lpq,
+			       icssm_prueth_common_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;
@@ -1793,6 +1859,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;
 
@@ -1807,6 +1874,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;
 }
 
@@ -2110,6 +2184,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);
+	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;
+
 	icssm_prueth_set_fw_offsets(prueth);
 
 	eth_ports_node = of_get_child_by_name(np, "ethernet-ports");
@@ -2265,6 +2346,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 */
@@ -2505,6 +2621,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,
 };
@@ -2532,6 +2649,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,
 };
@@ -2560,6 +2678,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..8cca5b87d4ff 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_common_napi_poll_lpq(struct napi_struct *napi, int budget);
+int icssm_prueth_common_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..1a5e5a84345e
--- /dev/null
+++ b/drivers/net/ethernet/ti/icssm/icssm_prueth_common.c
@@ -0,0 +1,309 @@
+// 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_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_queue_desc __iomem *queue_desc_o = NULL;
+	struct prueth_queue_desc __iomem *queue_desc = NULL;
+	struct prueth_emac *emac_p, *other_emac;
+	void __iomem *shared_ram, *ocmc_ram;
+	bool port0_configured, port1_configured;
+	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;
+	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;
+
+	/* A port's rx_queue_descs is only valid once that port has
+	 * completed its own ndo_open() for the currently active mode:
+	 * before that it is NULL (never opened) or stale (opened in a
+	 * different mode), since the shared HP/LP IRQs and PRUs are
+	 * started as soon as the first port opens, independent of the
+	 * peer port's state. Treat an unconfigured port's queue as
+	 * permanently empty instead of dereferencing rx_queue_descs.
+	 */
+	port0_configured = prueth->emac_configured & BIT(emac->port_id);
+	port1_configured = other_emac &&
+			   (prueth->emac_configured & BIT(other_emac->port_id));
+
+	if (port0_configured) {
+		queue_desc = emac->rx_queue_descs + qid1;
+
+		overflow_cnt = readb(&queue_desc->overflow_cnt);
+		if (overflow_cnt > 0) {
+			atomic64_add(overflow_cnt, &emac->stats.rx_over_errors);
+			writeb(0, &queue_desc->overflow_cnt);
+		}
+
+		bd_rd_ptr = readw(&queue_desc->rd_ptr);
+		bd_wr_ptr = readw(&queue_desc->wr_ptr);
+		port0_q_empty = (bd_rd_ptr == bd_wr_ptr);
+	} else {
+		bd_rd_ptr = 0;
+		bd_wr_ptr = 0;
+		port0_q_empty = 1;
+	}
+
+	if (port1_configured) {
+		queue_desc_o = other_emac->rx_queue_descs + qid2;
+
+		overflow_cnt_o = readb(&queue_desc_o->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_o = readw(&queue_desc_o->rd_ptr);
+		bd_wr_ptr_o = readw(&queue_desc_o->wr_ptr);
+		port1_q_empty = (bd_rd_ptr_o == bd_wr_ptr_o);
+	} else {
+		bd_rd_ptr_o = 0;
+		bd_wr_ptr_o = 0;
+		port1_q_empty = 1;
+	}
+
+	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
+			 * it, so the read pointer can't be trusted. Drop
+			 * everything pending in this queue by moving the
+			 * read pointer to the write pointer.
+			 */
+			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: same handling as above */
+			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_common_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_common_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");
+		goto free_napi_hpq_lpq;
+	}
+
+	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);
+
+free_napi_hpq_lpq:
+	napi_disable(&prueth->napi_lpq);
+	napi_disable(&prueth->napi_hpq);
+
+	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/Switch: 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..b4d6b8660539 100644
--- a/drivers/net/ethernet/ti/icssm/icssm_switch.h
+++ b/drivers/net/ethernet/ti/icssm/icssm_switch.h
@@ -301,6 +301,12 @@
 #define P0_Q1_BD_OFFSET		P0_BUFFER_DESC_OFFSET
 #define P0_BUFFER_DESC_OFFSET	SRAM_START_OFFSET
 
+/* 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
+
 /* Memory Usage of L3 OCMC RAM */
 
 /* L3 64KB Memory - mainly buffer Pool */
-- 
2.43.0



^ permalink raw reply related	[flat|nested] 7+ messages in thread

* [PATCH net-next v5 3/3] net: ti: icssm-prueth: Support duplicate HW offload feature for HSR and PRP
  2026-10-05 15:41 [PATCH net-next v5 0/3] Introduce HSR/PRP HW offload support for PRU-ICSSM Ethernet driver Parvathi Pudi
  2026-10-05 15:41 ` [PATCH net-next v5 1/3] net: ti: icssm-prueth: Add HSR and PRP HW offload mode support for AM57xx, AM437x and AM335x Parvathi Pudi
  2026-10-05 15:41 ` [PATCH net-next v5 2/3] net: ti: icssm-prueth: Add priority based RX IRQ handlers Parvathi Pudi
@ 2026-10-05 15:41 ` Parvathi Pudi
  2026-10-09  3:43   ` netdev-bot+sashiko
  2 siblings, 1 reply; 7+ messages in thread
From: Parvathi Pudi @ 2026-10-05 15:41 UTC (permalink / raw)
  To: andrew+netdev, davem, edumazet, kuba, pabeni, danishanwar,
	parvathi, rogerq, pmohan, afd, vadim.fedorenko, haokexin,
	basharath, arnd
  Cc: linux-kernel, netdev, linux-arm-kernel, horms, 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.

NETIF_F_HW_HSR_DUP is now supported and is tied to NETIF_F_HW_HSR_TAG_RM
and NETIF_F_HW_HSR_FWD in ndo_fix_features(), since the firmware handles
all three features together.

To enable HSR/PRP offload, enable hsr-dup-offload on both slave ports:

$ ethtool -K eth2 hsr-dup-offload on
$ ethtool -K eth3 hsr-dup-offload on

With duplicate offload enabled, the HSR core sends each frame only once
through the first slave port. The firmware then duplicates and sends the
frame through both ports.

On the receive side, icssm_parse_packet_info() reads the new HostRecv bit
from the firmware instead of the unused BD Port field. This bit tells which
HSR copy should be passed to the network stack.

icssm_emac_rx_packet() ignores the other duplicate without creating an skb,
since receiving duplicate HSR packets is expected and not an error. The
OCMC-to-skb copy is also split into MAC header, VLAN, and payload parts.
This makes it possible to remove the HSR tag from the middle of the packet.

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  | 254 +++++++++++---
 drivers/net/ethernet/ti/icssm/icssm_prueth.h  |  13 +-
 .../ethernet/ti/icssm/icssm_prueth_common.c   |  11 +-
 .../ethernet/ti/icssm/icssm_prueth_switch.c   | 325 +++++++++++++++++-
 .../ethernet/ti/icssm/icssm_prueth_switch.h   |   1 +
 drivers/net/ethernet/ti/icssm/icssm_switch.h  |  34 +-
 6 files changed, 563 insertions(+), 75 deletions(-)

diff --git a/drivers/net/ethernet/ti/icssm/icssm_prueth.c b/drivers/net/ethernet/ti/icssm/icssm_prueth.c
index 42c83946f17a..e5e888a1e6f4 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)
@@ -552,32 +580,37 @@ 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;
 	unsigned int buffer_desc_count;
 	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 hsr_tag *hsr_tag;
+	struct ethhdr *ethhdr;
+	bool is_vlan = false;
 	int pkt_block_size;
 	void __iomem *sram;
 	void __iomem *dram;
 	int txport, pktlen;
 	u16 update_wr_ptr;
 	u32 wr_buf_desc;
+	u16 prp_ethtype;
 	void *ocmc_ram;
+	__be16 proto;
+	u8 *hdr;
 
 	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;
@@ -585,7 +618,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];
@@ -608,6 +644,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)) {
+		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;
@@ -657,6 +716,51 @@ 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;
+		}
+
+		/* Check if the SKB has HSR tag */
+		if (PRUETH_IS_HSR(prueth) && proto == htons(ETH_P_HSR)) {
+			hdr = skb_mac_header(skb) + ETH_HLEN;
+			if (is_vlan)
+				hdr += VLAN_HLEN;
+
+			hsr_tag = (struct hsr_tag *)hdr;
+
+			/* PTP frames (ETH_P_1588) are directed frames
+			 * so skip the duplication
+			 */
+			if (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 if (PRUETH_IS_PRP(prueth)) {
+			/* Check if the SKB has PRP tag */
+			prp_ethtype = get_unaligned_be16(skb_tail_pointer(skb) -
+							 ETH_TLEN);
+
+			if (prp_ethtype == 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);
+			}
+		} 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));
@@ -669,6 +773,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))
+		writew(update_wr_ptr, &queue_desc_other_port->wr_ptr);
+
 	return 0;
 }
 
@@ -681,8 +789,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);
@@ -712,17 +822,19 @@ int icssm_emac_rx_packet(struct prueth_emac *emac, u16 *bd_rd_ptr,
 	struct net_device *ndev = emac->ndev;
 	unsigned int buffer_desc_count;
 	int read_block, update_block;
+	struct vlan_ethhdr *vlan_hdr;
 	unsigned int actual_pkt_len;
 	bool buffer_wrapped = false;
+	int adjust_for_hsr_tag = 0;
 	void *src_addr, *dst_addr;
-	u16 start_offset = 0;
+	bool has_hsr_tag = false;
+	bool has_vlan = false;
+	struct ethhdr *ethhdr;
 	struct sk_buff *skb;
 	int pkt_block_size;
 	void *ocmc_ram;
+	__be16 proto;
 
-	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
@@ -731,6 +843,8 @@ int icssm_emac_rx_packet(struct prueth_emac *emac, u16 *bd_rd_ptr,
 	buffer_desc_count = icssm_get_buff_desc_count(rxqueue);
 	read_block = (*bd_rd_ptr - rxqueue->buffer_desc_offset) / BD_SIZE;
 	pkt_block_size = DIV_ROUND_UP(pkt_info->length, ICSS_BLOCK_SIZE);
+	/* OCMC RAM is not cached and read order is not important */
+	ocmc_ram = (__force void *)emac->prueth->mem[PRUETH_MEM_OCMC].va;
 
 	/* calculate end BD address post read */
 	update_block = read_block + pkt_block_size;
@@ -742,11 +856,36 @@ int icssm_emac_rx_packet(struct prueth_emac *emac, u16 *bd_rd_ptr,
 			buffer_wrapped = true;
 	}
 
+	/* Get the start address of the first buffer from
+	 * the read buffer description
+	 */
+	src_addr = ocmc_ram + rxqueue->buffer_offset +
+		   (read_block * ICSS_BLOCK_SIZE);
+
 	/* calculate new pointer in ram */
 	*bd_rd_ptr = rxqueue->buffer_desc_offset + (update_block * BD_SIZE);
 
-	/* 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 (!pkt_info->host_recv_flag)
+			return 0;
+
+		ethhdr = (struct ethhdr *)src_addr;
+		proto = ethhdr->h_proto;
+
+		if (proto == htons(ETH_P_8021Q)) {
+			has_vlan = true;
+			vlan_hdr = (struct vlan_ethhdr *)ethhdr;
+			proto = vlan_hdr->h_vlan_encapsulated_proto;
+		}
+
+		if (proto == htons(ETH_P_HSR) && !pkt_info->timestamp)
+			has_hsr_tag = true;
+	}
+
+	actual_pkt_len = pkt_info->length;
+
+	if (has_hsr_tag)
+		actual_pkt_len -= ICSSM_LRE_TAG_SIZE;
 
 	/* Allocate a socket buffer for this packet */
 	skb = netdev_alloc_skb_ip_align(ndev, actual_pkt_len);
@@ -758,15 +897,23 @@ int icssm_emac_rx_packet(struct prueth_emac *emac, u16 *bd_rd_ptr,
 
 	dst_addr = skb->data;
 
-	/* OCMC RAM is not cached and read order is not important */
-	ocmc_ram = (__force void *)emac->prueth->mem[PRUETH_MEM_OCMC].va;
+	/* 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;
 
-	/* Get the start address of the first buffer from
-	 * the read buffer description
-	 */
-	src_addr = ocmc_ram + rxqueue->buffer_offset +
-		   (read_block * ICSS_BLOCK_SIZE);
-	src_addr += start_offset;
+	adjust_for_hsr_tag += PRUETH_ETHER_TYPE_OFFSET;
+
+	if (has_vlan) {
+		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 (has_hsr_tag)
+		src_addr += ICSSM_LRE_TAG_SIZE;
 
 	/* Copy the data from PRU buffers(OCMC) to socket buffer(DRAM) */
 	if (buffer_wrapped) { /* wrapped around buffer */
@@ -780,22 +927,23 @@ 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;
+		if (has_hsr_tag)
+			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)) {
@@ -1313,17 +1461,31 @@ 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);
+	/* 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");
+		atomic64_inc(&emac->stats.tx_dropped);
+		return NETDEV_TX_OK;
+	}
+
+	spin_lock_irqsave(lock_queue, flags);
 	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);
+	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);
@@ -1338,10 +1500,6 @@ static enum netdev_tx icssm_emac_ndo_start_xmit(struct sk_buff *skb,
 			      us_to_ktime(HR_TIMER_TX_DELAY_US),
 			      HRTIMER_MODE_REL_PINNED);
 		ret = NETDEV_TX_BUSY;
-	} else {
-		/* error */
-		atomic64_inc(&emac->stats.tx_dropped);
-		ret = NET_XMIT_DROP;
 	}
 
 	return ret;
@@ -1568,19 +1726,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;
@@ -1786,6 +1947,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)) {
@@ -2383,6 +2547,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 8cca5b87d4ff..9ed4176b66fa 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,15 @@ struct prueth {
 	u8 emac_configured;
 	u8 hsr_members;
 	u8 br_members;
+
+	/* Per-queue TX lock - LRE uses only two priority queues
+	 * one for high priority and the other for low priority.
+	 */
+	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 1a5e5a84345e..de61304e2011 100644
--- a/drivers/net/ethernet/ti/icssm/icssm_prueth_common.c
+++ b/drivers/net/ethernet/ti/icssm/icssm_prueth_common.c
@@ -161,16 +161,17 @@ 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);
+			if (PRUETH_IS_SWITCH(prueth))
+				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);
+			if (PRUETH_IS_SWITCH(prueth))
+				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..7bc65b2901cb 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_1_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_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),
+		},
+	},
+	[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)
@@ -898,7 +1092,7 @@ static int icssm_prueth_sw_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;
+	void __iomem *dram, *dram_base;
 	struct prueth_emac *emac;
 	void __iomem *dram1_base;
 
@@ -909,26 +1103,17 @@ static int icssm_prueth_sw_port_config(struct prueth *prueth,
 		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, 6);
-
 	/* Remaining switch port configs are in DRAM1 */
 	dram_base = prueth->mem[PRUETH_MEM_DRAM1].va;
 
@@ -983,21 +1168,129 @@ 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 void 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;
+	struct prueth_emac *emac;
+
+	if (port_id == 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;
+	} else if (port_id ==  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;
+	} else {
+		dev_err(prueth->dev, "invalid port %d\n", port_id);
+		return;
+	}
+
+	emac = prueth->emac[port_id - 1];
+	/* 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;
+	}
+}
+
 int icssm_prueth_sw_emac_config(struct prueth_emac *emac)
 {
 	struct prueth *prueth = emac->prueth;
 	u32 sharedramaddr, ocmcaddr;
-	int ret;
+	void __iomem *dram_mac;
+	int ret = 0;
 
 	/* PRU needs local shared RAM address for C28 */
 	sharedramaddr = ICSS_LOCAL_SHARED_RAM;
 	/* PRU needs real global OCMC address for C30*/
 	ocmcaddr = (u32)prueth->mem[PRUETH_MEM_OCMC].pa;
 
+	if (emac->port_id == PRUETH_PORT_MII0) {
+		dram_mac = prueth->mem[PRUETH_MEM_DRAM0].va;
+	} else if (emac->port_id == PRUETH_PORT_MII1) {
+		dram_mac = prueth->mem[PRUETH_MEM_DRAM1].va;
+	} else {
+		netdev_err(emac->ndev, "invalid port\n");
+		return -EINVAL;
+	}
+
+	/* setup mac address */
+	memcpy_toio(dram_mac + PORT_MAC_ADDR, emac->mac_addr, ETH_ALEN);
+
 	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) && !prueth->emac_configured) {
+		icssm_prueth_lre_port_config(prueth, PRUETH_PORT_MII0);
+		icssm_prueth_lre_port_config(prueth, PRUETH_PORT_MII1);
+	} else if (PRUETH_IS_SWITCH(prueth)) {
+		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 b4d6b8660539..ec5925c812c3 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
  */
@@ -51,18 +54,19 @@
  *				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.
+ *				For HSR only.
  * 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. For HSR and PRP only.
  * 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 +90,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 +309,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
 
@@ -334,6 +348,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 v5 1/3] net: ti: icssm-prueth: Add HSR and PRP HW offload mode support for AM57xx, AM437x and AM335x
  2026-10-05 15:41 ` [PATCH net-next v5 1/3] net: ti: icssm-prueth: Add HSR and PRP HW offload mode support for AM57xx, AM437x and AM335x Parvathi Pudi
@ 2026-10-09  3:43   ` netdev-bot+sashiko
  0 siblings, 0 replies; 7+ messages in thread
From: netdev-bot+sashiko @ 2026-10-09  3:43 UTC (permalink / raw)
  To: parvathi
  Cc: andrew+netdev, davem, edumazet, kuba, pabeni, danishanwar, rogerq,
	pmohan, afd, vadim.fedorenko, haokexin, basharath, arnd,
	linux-kernel, netdev, linux-arm-kernel, horms, pratheesh,
	j-rameshbabu, vigneshr, praneeth, srk, rogerq, m-malladi, krishna,
	mohan

Thank you for your contribution! Sashiko AI review found 12 potential
issue(s) to consider.

Critical: 0 · High: 1 (1) · Medium: 7 · Low: 4

If you believe none of the issues are legitimate, please reply to each
issue of Medium severity or higher and explain why the AI is wrong.
If any of them are legitimate and you plan to respin, please let patchwork
know by including "pw-bot: cr" as a separate line at the end of your reply
(one such reply per series is enough).

- [High] When icssm_prueth_change_mode() fails partway,
  icssm_prueth_hsr_port_link() (free_hsr) and…
- [Medium] icssm_prueth_hsr_port_link()
  (drivers/net/ethernet/ti/icssm/icssm_prueth.c) records an HSR/PRP…
- [Medium] In LRE mode,
  icssm_emac_mc_filter_ctrl/reset/hashmask/bin_update all redirect `ram`…
- [Medium] icssm_prueth_ndev_event() returns NOTIFY_DONE unless
  icssm_prueth_sw_port_dev_check(ndev) passes.
- [Medium] icssm_prueth_lre_protocol_init() always writes
  ICSS_LRE_SUP_ADDRESS_INIT_OCTETS_HIGH (0x4E1501) and _LOW (0x1) to…
- [Medium] icssm_emac_ndo_fix_features() depends on the current
  ndev->features, not just on the requested set.
- [Medium] icssm_prueth_lre_timer() calls hrtimer_forward_now() and
  returns early unless `prueth->emac_configured == BIT(MII0)|BIT(MII1)`.
- [Medium] Mode changes are not synchronised with multicast-filter
  programming.
- [Low] The patch changes behaviour in every mode without saying so in
  the commit message.
- [Low] Two in-tree descriptions of the PCP-to-queue map size disagree.
- [Low] In icssm_prueth_hsr_port_link(), the comment on the hsr_dev
  mismatch check says "Adding the port to a second bridge is not…
- [Low] icssm_prueth_lre_timer() runs in hardirq context and reads
  prueth->emac_configured with a plain load to decide whether to…

Pre-existing issues:
- [High] icssm_prueth_change_mode() calls ndo_stop()/ndo_open() directly
  and never calls dev_close().

--

net: ti: icssm-prueth: Add HSR and PRP HW offload mode support for AM57xx, AM437x and AM335x

This patch extends the ICSSM PRUETH driver to support the HSR and PRP
offload firmware on AM335x, AM437x and AM57xx. The firmware is switched
when both ports join an hsr/prp master with hsr-fwd and hsr-tag-rm
offload enabled. The patch also adds LRE table initialisation, a
periodic duplicate-table check hrtimer, and a shared multicast filter
table for LRE mode.

> diff --git a/drivers/net/ethernet/ti/icssm/icssm_prueth.c b/drivers/net/ethernet/ti/icssm/icssm_prueth.c
> index b7e94244355a3..453c9b259d77a 100644
> --- a/drivers/net/ethernet/ti/icssm/icssm_prueth.c
> +++ b/drivers/net/ethernet/ti/icssm/icssm_prueth.c

[ ... ]

> @@ -804,13 +855,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++;

[Severity: Low]
This changes behaviour in every mode, not only HSR/PRP, and the commit
message doesn't mention it. In EMAC and switch mode too, any descriptor
that reports 1 to 59 bytes now takes this branch.

The new comment says "if the packet is too small we skip it". However,
update_rd_ptr = bd_wr_ptr still throws away everything left in the host
queue, which is what the old comment said. Could the comment say that the
remaining packets are dropped?

Also, emac_netdev_ops gains:

	.ndo_set_mac_address = eth_mac_addr,
	.ndo_validate_addr = eth_validate_addr,

This allows MAC address changes in dual-EMAC and switch modes as well.
Should these two changes be mentioned in the commit message, or split
into separate patches?

[ ... ]

> @@ -1122,7 +1184,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]
This isn't a bug introduced by this patch, but icssm_prueth_change_mode()
calls ndo_stop()/ndo_open() directly instead of dev_close()/dev_open().
The new HSR/PRP notifier callers make its failure path much easier to
reach.

ndo_open() can fail here, for example when ti-pruss/*-pruhsr-fw.elf or
*-pruprp-fw.elf is not installed and icssm_prueth_sw_boot_prus() fails.
change_mode() then just returns:

		ret = ndev->netdev_ops->ndo_open(ndev);
		if (ret < 0) {
			netdev_err(ndev, "failed to start: %d", ret);
			return ret;
		}

The netdev stays IFF_UP and netif_running(), even though the earlier
ndo_stop() already tore down its IRQs, NAPI, rproc and IEP.
icssm_prueth_hsr_port_link() then restores eth_type to EMAC.

A later "ip link set ethX down", or unregister_netdev() from
icssm_prueth_remove(), calls ndo_stop() a second time. Would that call
napi_disable(&emac->napi) on a NAPI that is already disabled, and block
forever with RTNL held?

It also looks like free_irq() would run on an IRQ that was already freed,
and rproc_shutdown()/icss_iep_exit() would run twice.

[ ... ]

> @@ -1289,7 +1357,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);

[Severity: Medium]
In LRE mode all four mc filter helpers now use the same DRAM1 table, so
both slave ports program one physical filter. However,
icssm_emac_ndo_set_rx_mode() still runs per netdev. It disables the
filter, clears the whole shared table here, and refills it from the mc
list and IFF_ALLMULTI flag of the calling ndev only.

Doesn't that mean the port that ran set_rx_mode last decides the hardware
filter? Some memberships exist on only one slave, for example ptp4l or
lldpd packet-socket memberships, or "ip maddr add dev eth2". Those, and
that slave's allmulti state, would be wiped when the other slave's
rx_mode runs.

Also, icssm_emac_ndo_set_rx_mode() programs promiscuous mode only inside
if (PRUETH_IS_EMAC(prueth)). A promiscuous slave in HSR/PRP mode would
still have the hashed multicast filter enabled in firmware.

The new prueth->addr_lock serialises the writes, but it doesn't merge
the state requested by the two ports.

> @@ -1302,11 +1373,16 @@ static void icssm_emac_mc_filter_hashmask

[ ... ]

> @@ -1371,8 +1454,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);

[Severity: Medium]
Does this lock choice stay valid if the mode changes at the same time?
The lock is chosen from an unlocked read of eth_type. Each mc filter
helper then reads prueth_is_lre() and prueth->fw_offsets again on its
own.

icssm_prueth_change_mode() writes prueth->eth_type, and
icssm_prueth_set_fw_offsets() (called from ndo_open) rewrites the three
offset fields. Neither takes either addr_lock, and the netdevs stay
IFF_UP throughout.

Could a concurrent set_rx_mode program the shared DRAM1 table under a
different lock than its peer? Could it combine the LRE DRAM1 selection
with stale EMAC offsets?

change_mode() also bypasses dev_open(), so it looks like the multicast
filter isn't reprogrammed after the new firmware starts.

[ ... ]

> @@ -1429,15 +1517,109 @@ 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.
> +	 */
> +	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;
> +}

[Severity: Medium]
Can this coupling turn the offload back off on a later feature update?
The result depends on the current ndev->features. ethnl_set_features()
stores only the bits the user requested in wanted_features, and
__netdev_update_features() recomputes from wanted_features every time.

For example, "ethtool -K eth2 hsr-tag-rm-offload on" on its own turns
both bits on, because ndev->features had neither. But wanted_features
holds only TAG_RM. On the next recalculation both bits are active and
FWD is missing from the request, so both get cleared.

When the port is enslaved, dev_disable_lro() in hsr_portdev_setup()
calls netdev_update_features(). That would run into this case, get
-EBUSY from icssm_emac_ndo_set_features(), and log "set_features()
failed". After the hsr device is removed, any unrelated feature update
would silently turn the offload off.

The same applies once hsr-dup-offload is added to
NETIF_PRUETH_LRE_OFFLOAD_FEATURES later in the series. The documented
sequence (tag-rm on, then fwd on) never requests dup, so the offload
would end up off.

[ ... ]

> @@ -1741,6 +1926,109 @@ 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)
> +{

[ ... ]

> +	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 */

[Severity: Low]
This isn't a bug, but the comment says "bridge" while the check compares
HSR/PRP master devices. It looks copied from
icssm_prueth_ndev_port_link().

> +		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) {
> +		prev_mode = prueth->eth_type;
> +		ret = icssm_prueth_change_mode(prueth, mode);

[Severity: Medium]
What happens when only one port of this prueth instance becomes an
offloaded hsr slave? The firmware is switched only when both local
ports are members. Otherwise this returns 0 and dual-EMAC firmware keeps
running.

Two cases seem to lead there.

In the first case, only one port has hsr-fwd/tag-rm offload enabled.
icssm_emac_ndo_fix_features() and icssm_emac_ndo_set_features() don't
require the sibling to match. icssm_prueth_ndev_event() records
membership only for the offloaded port. The hsr core still stops
software forwarding towards that port, based on that port's own feature
bit:

net/hsr/hsr_forward.c:hsr_drop_frame() {
    ...
	if (port->dev->features & NETIF_F_HW_HSR_FWD)
		return prp_is_lan_dup(frame->port_rcv->type, port);
    ...
}

Once hsr-dup-offload is advertised, hsr_forward_do() would also skip the
second copy for that port.

In the second case, the two hsr slaves come from different ICSS
instances (AM57xx has two PRU-ICSS with two ports each). Each prueth
records one membership bit and stays in EMAC mode. hsr_dev_finalize()
still sets fwd_offloaded:

net/hsr/hsr_device.c:hsr_dev_finalize() {
    ...
	if ((slave[0]->features & NETIF_F_HW_HSR_FWD) &&
	    (slave[1]->features & NETIF_F_HW_HSR_FWD))
		hsr->fwd_offloaded = true;
    ...
}

As a result, promiscuous mode is skipped and software forwarding is
suppressed.

In both cases, wouldn't neither software nor firmware forward (or
duplicate) transit frames, while the link reports success?

> +		if (ret < 0) {
> +			dev_err(prueth->dev, "Failed to enable %s mode\n",
> +				(mode == PRUSS_ETHTYPE_HSR) ?
> +				"HSR" : "PRP");
> +			goto free_hsr;
> +		} else {

[ ... ]

> +	return 0;
> +
> +free_hsr:
> +	prueth->eth_type = prev_mode;

[Severity: High]
Is restoring only eth_type enough here? icssm_prueth_change_mode()
returns on the first failed ndo_open() and doesn't unwind the ports it
already reopened.

Take this sequence:

- Port 0 reopens in LRE mode.
- icssm_prueth_lre_config() calls hrtimer_setup()/hrtimer_start() on
  tbl_check_timer.
- Both PRUs boot the LRE firmware and the shared hpq/lpq NAPIs are
  enabled.
- Port 1's ndo_open() fails, and eth_type goes back to EMAC.

A later ndo_stop() on port 0 chooses its teardown from the current
eth_type, so it would take the EMAC path:

- napi_disable(&emac->napi) on a NAPI that was never enabled in LRE
  mode, which waits forever on NAPI_STATE_SCHED.
- free_irq() on an rx_irq that was never requested.
- rproc_shutdown() of PRU0 only.

It would also skip this cleanup in icssm_emac_ndo_stop():

	if (prueth_is_lre(prueth) && !prueth->emac_configured)
		icssm_prueth_lre_cleanup(prueth);

so the 10 ms tbl_check_timer keeps re-arming. icssm_prueth_remove()
doesn't call hrtimer_cancel(&prueth->tbl_check_timer) unconditionally.
Can the callback then run after the devm-allocated prueth is freed?

Re-entering LRE mode would also call hrtimer_setup() on a timer that is
still queued. icssm_prueth_lre_config() calls it before the
hrtimer_active() check in icssm_prueth_lre_start_timer().

icssm_prueth_hsr_port_unlink() does the same enum-only rollback in the
other direction.

[ ... ]

> @@ -1754,6 +2042,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);
> +			}
> +		}

[Severity: Medium]
Can these HSR events be missed? Earlier in this function there is:

	if (!icssm_prueth_sw_port_dev_check(ndev))
		return NOTIFY_DONE;

icssm_prueth_sw_port_dev_check() requires NETIF_F_HW_L2FW_DOFFLOAD. That
bit is in hw_features on all three platforms, and "ethtool -K ethX
l2-fwd-offload off" clears it. icssm_emac_ndo_set_features() rejects
changes to the LRE bits only, so clearing it is allowed even while the
port is an hsr slave.

Suppose l2-fwd-offload is off and hsr-fwd/tag-rm are on for both ports.
hsr_dev_finalize() sets fwd_offloaded, but icssm_prueth_hsr_port_link()
is never called, so the PRUs keep running dual-EMAC firmware.

Now suppose l2-fwd-offload is cleared on one slave while HSR is active.
When hsr0 is deleted, that slave's unlink event is dropped. hsr_members
keeps its bit, the firmware stays in HSR/PRP mode with no hsr master, and
prueth->hsr_dev keeps a stale pointer. Every later hsr link would then
fail with -EOPNOTSUPP.

[ ... ]

> 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 0000000000000..6276dd1e8bb1b
> --- /dev/null
> +++ b/drivers/net/ethernet/ti/icssm/icssm_prueth_lre.c

[ ... ]

> +/* 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

[Severity: Low]
This doesn't match the description in icssm_lre_firmware.h:

/* 8 bytes of VLAN PCP to RX QUEUE MAPPING */
#define ICSS_LRE_QUEUE_2_PCP_MAP_OFFSET		0x124

icssm_prueth_lre_pcp_queue_map_config() copies only 4 bytes. Is the
header comment wrong, or do the other 4 bytes keep whatever the previous
firmware left there?

[ ... ]

> +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);
> +}

[Severity: Medium]
Does this supervision address match the one the hsr device uses? These
fixed constants program 01:15:4E:00:01:00. The hsr core sets the last
byte from the user's configuration:

net/hsr/hsr_device.c:hsr_dev_finalize() {
    ...
	hsr->sup_multicast_addr[ETH_ALEN - 1] = multicast_spec;
    ...
}

With "supervision 45" from the commit message example, the address on
the wire is 01:15:4E:00:01:2D. icssm_prueth_hsr_port_link() reads only
the protocol version from hsr_ndev and never reprograms
ICSS_LRE_SUP_ADDR.

With fwd offload enabled:

- the slaves aren't put in promiscuous mode,
- the hsr core doesn't dev_mc_add() the supervision address on them,
- the hashed multicast filter is enabled in LRE mode.

Would supervision frames sent to the configured address then be neither
recognised by the firmware nor accepted by the filter?

> +static enum hrtimer_restart icssm_prueth_lre_timer(struct hrtimer *timer)
> +{

[ ... ]

> +	hrtimer_forward_now(timer, ms_to_ktime(timeout));
> +	if (prueth->emac_configured !=
> +	    (BIT(PRUETH_PORT_MII0) | BIT(PRUETH_PORT_MII1)))
> +		return HRTIMER_RESTART;

[Severity: Medium]
Should the table check stop when only one port is down?
icssm_prueth_lre_config_check_flags() writes
ICSS_LRE_HOST_TIMER_CHECK_FLAGS only once, when the first port opens.
icssm_prueth_sw_shutdown_prus() keeps both PRUs running while any port is
configured.

So while one slave is administratively down, the firmware keeps handling
frames on the other port but no longer gets the periodic duplicate/port
table check requests.

If the firmware clears these flags after each check, as the 10 ms
refresh suggests, wouldn't duplicate-table aging
(ICSS_LRE_DUPLI_FORGET_TIME) stop in the single-link state that HSR/PRP
is designed to survive?

[Severity: Low]
This reads prueth->emac_configured in hardirq context with a plain load.
icssm_emac_ndo_open() and icssm_emac_ndo_stop() update the same field
with non-atomic |= and &= under RTNL. There is no lock and no
READ_ONCE()/WRITE_ONCE().

The timer can therefore see both ports as configured just before
ndo_stop() clears a bit, and write DRAM1 while the PRUs are being shut
down. Is that intended?

[ ... ]

-- 
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261005154654.576663-1-parvathi%40couthit.com


^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: [PATCH net-next v5 2/3] net: ti: icssm-prueth: Add priority based RX IRQ handlers
  2026-10-05 15:41 ` [PATCH net-next v5 2/3] net: ti: icssm-prueth: Add priority based RX IRQ handlers Parvathi Pudi
@ 2026-10-09  3:43   ` netdev-bot+sashiko
  0 siblings, 0 replies; 7+ messages in thread
From: netdev-bot+sashiko @ 2026-10-09  3:43 UTC (permalink / raw)
  To: parvathi
  Cc: andrew+netdev, davem, edumazet, kuba, pabeni, danishanwar, rogerq,
	pmohan, afd, vadim.fedorenko, haokexin, basharath, arnd,
	linux-kernel, netdev, linux-arm-kernel, horms, pratheesh,
	j-rameshbabu, vigneshr, praneeth, srk, rogerq, m-malladi, krishna,
	mohan

Thank you for your contribution! Sashiko AI review found 4 potential
issue(s) to consider.

Critical: 0 · High: 1 (1) · Medium: 2 · Low: 1

If you believe none of the issues are legitimate, please reply to each
issue of Medium severity or higher and explain why the AI is wrong.
If any of them are legitimate and you plan to respin, please let patchwork
know by including "pw-bot: cr" as a separate line at the end of your reply
(one such reply per series is enough).

- [High] The new 'do not restart TX during teardown' logic is a check-
  then-act sequence with no lock or ordering against ndo_stop().
- [Medium] icssm_prueth_common_emac_rx_packets() reads the two queues'
  write pointers at different times before the loop and never re-reads…
- [Medium] The shared HP/LP NAPI pollers use a plain, unsynchronised read
  of prueth->emac_configured as the gate for dereferencing a port's…
- [Low] This feature patch also carries a fix for an existing bug, with
  no Fixes: tag.

Pre-existing issues:
- [High] This problem wasn't introduced by this patch, but the patch
  extends it to the new shared resources.

--

net: ti: icssm-prueth: Add priority based RX IRQ handlers

This patch adds shared high/low priority RX IRQs and NAPI instances for
switch, HSR and PRP modes. Frames from the two PRU ports are delivered
in order of the IEP arrival timestamp that the firmware records. It also
converts the per-port stats to atomic64_t, adds a firmware revision field
that selects the IEP wrap value, and changes ndo_stop() to stop TX before
it cancels the TX hrtimer.

> diff --git a/drivers/net/ethernet/ti/icssm/icssm_prueth.c b/drivers/net/ethernet/ti/icssm/icssm_prueth.c
> index 453c9b259d77a..42c83946f17a7 100644
> --- a/drivers/net/ethernet/ti/icssm/icssm_prueth.c
> +++ b/drivers/net/ethernet/ti/icssm/icssm_prueth.c

[ ... ]

> @@ -1126,11 +1157,28 @@ static int icssm_emac_ndo_stop(struct net_device *ndev)
>  	/* disable the mac port */
>  	icssm_prueth_port_enable(emac, false);
>  
> +	/* Stop TX first. netif_tx_disable() also waits for an xmit that is
> +	 * already running. Then cancel any tx_hrtimer that xmit may have
> +	 * armed.
> +	 */
> +	netif_tx_disable(ndev);
> +	hrtimer_cancel(&emac->tx_hrtimer);

[Severity: Low]
This isn't a bug, but these TX teardown changes look like a fix for an
existing problem. They are netif_tx_disable() before hrtimer_cancel()
here, plus the new emac_configured checks in
icssm_emac_tx_timer_callback() and icssm_emac_adjust_link().

The problem comes from icssm_prueth_change_mode(), which calls
ndo_stop()/ndo_open() directly while the netdev stays running. Before this
patch, icssm_emac_ndo_stop() never stopped TX, and the timer callback woke
the queue unconditionally. TX could therefore restart while the PRUs were
shut down and shared RAM/OCMC was being re-initialised.

Could this be split into its own patch, with a Fixes: tag pointing at the
commit that introduced icssm_prueth_change_mode()? Right now it is mixed
in with the HPQ/LPQ rework, the atomic64 stats conversion and the fw_rev
probe requirement, so it can't be backported on its own.

> +
>  	/* 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);
> +	}

[Severity: High]
This is a pre-existing issue, but this patch extends it to the new shared
resources. The shared NAPIs and IRQs are torn down whenever
emac_configured drops to zero, with no check that this port actually
acquired them. What happens if ndo_stop() runs on a port whose ndo_open()
failed?

icssm_prueth_change_mode() calls ndo_open() directly and just returns on
error:

    ret = ndev->netdev_ops->ndo_open(ndev);
    if (ret < 0) {
        netdev_err(ndev, "failed to start: %d", ret);
        return ret;
    }

The netdev keeps __LINK_STATE_START, but its emac_configured bit is never
set and napi_hpq/napi_lpq stay disabled (SCHED|NPSVC). One way to get
here: the switch firmware is missing, so
icssm_prueth_sw_boot_prus()->rproc_boot() fails during the EMAC to switch
change that bridge creation triggers.

A later "ip link set ethX down" then does:

    dev_close()
      icssm_emac_ndo_stop()
        napi_disable(&prueth->napi_lpq)
          napi_disable_locked()   <- spins forever on SCHED|NPSVC

and it does this while holding rtnl_lock and the netdev lock.

Would icssm_prueth_common_free_irqs() then also call free_irq() on rx_lp
and rx_hp IRQs that were never requested, with rproc_shutdown() and
icss_iep_exit() running a second time?

The EMAC path has the same pattern with the unconditional
napi_disable(&emac->napi) and free_irq(emac->rx_irq, ndev), which the
baseline also had.

[ ... ]

> @@ -1662,6 +1707,11 @@ static enum hrtimer_restart icssm_emac_tx_timer_callback(struct hrtimer *timer)
>  {
>  	struct prueth_emac *emac =
>  			container_of(timer, struct prueth_emac, tx_hrtimer);
> +	struct prueth *prueth = emac->prueth;
> +
> +	/* Don't restart TX on a port that ndo_stop() is tearing down */
> +	if (!(READ_ONCE(prueth->emac_configured) & BIT(emac->port_id)))
> +		return HRTIMER_NORESTART;
>  
>  	if (netif_queue_stopped(emac->ndev))
>  		netif_wake_queue(emac->ndev);

[Severity: High]
Is this check enough to keep TX from restarting during teardown? The
check and the wake are not atomic with respect to icssm_emac_ndo_stop().
That function does a plain clear of the bit, then netif_tx_disable(),
then hrtimer_cancel():

CPU0 (tx_hrtimer callback)          CPU1 (icssm_emac_ndo_stop)
READ_ONCE(emac_configured) has bit
                                    emac_configured &= ~BIT(port_id)
                                    icssm_prueth_port_enable(emac, false)
                                    netif_tx_disable(ndev)
netif_queue_stopped() -> true
netif_wake_queue()
                                    hrtimer_cancel() returns

hrtimer_cancel() only waits for the callback to finish. Nothing stops the
queue again afterwards.

In the icssm_prueth_change_mode() path the qdisc is still active. So
ndo_start_xmit() could run against the PRU queues while the PRUs are shut
down and icssm_prueth_hostinit() clears shared RAM/OCMC.

icssm_emac_adjust_link() has the same check-then-wake pattern. It uses a
plain load under emac->lock, and ndo_stop() never takes that lock.

emac_configured itself is also accessed racily: a plain u8
read-modify-write in ndo_stop(), READ_ONCE() here, and a plain load in
icssm_emac_adjust_link().

Would clearing the bit with release semantics and calling
netif_tx_disable() again after hrtimer_cancel() close this? Serialising
the check-and-wake against teardown with a lock would be another option.

[ ... ]

> 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..1a5e5a84345e1
> --- /dev/null
> +++ b/drivers/net/ethernet/ti/icssm/icssm_prueth_common.c
> @@ -0,0 +1,309 @@

[ ... ]

> +	port0_configured = prueth->emac_configured & BIT(emac->port_id);
> +	port1_configured = other_emac &&
> +			   (prueth->emac_configured & BIT(other_emac->port_id));

[ ... ]

> +	if (port1_configured) {
> +		queue_desc_o = other_emac->rx_queue_descs + qid2;

[Severity: Medium]
Are these plain loads of emac_configured enough to make rx_queue_descs
safe to dereference?

When the second port opens while the first is already up,
icssm_prueth_sw_port_config() stores emac->rx_queue_descs. After that,
icssm_emac_ndo_open() sets the bit:

    prueth->emac_configured |= BIT(emac->port_id);

In this case icssm_prueth_sw_init_fdb_table(), icssm_prueth_sw_boot_prus()
and icssm_prueth_common_request_irqs() all return early. So no lock,
barrier or MMIO write orders the rx_queue_descs store before the bit
store.

On SMP (AM57xx), could the HPQ/LPQ poller on the other CPU see the bit
before the pointer? If the port has never been opened since probe, it
would then compute other_emac->rx_queue_descs + qid2 from NULL and call
readb()/readw() on the result.

On the teardown side, ndo_stop() for a non-last port clears its bit but
does not synchronise with the shared NAPIs. A poll that has already read
port1_configured as true can keep calling icssm_emac_rx_packet(other_emac,
...) and writing its rd_ptr, up to the budget, while ndo_stop() runs.

Are smp_store_release()/smp_load_acquire() on the mask (or a lock shared
with the pollers), plus napi_synchronize() when a non-last port stops,
needed here?

[ ... ]

> +		bd_rd_ptr_o = readw(&queue_desc_o->rd_ptr);
> +		bd_wr_ptr_o = readw(&queue_desc_o->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);

[ ... ]

> +		} else if (!port0_q_empty) {
> +			/* Packet(s) in port0 queue only */
> +			port = 0;
> +		} else {
> +			/* Packet(s) in port1 queue only */
> +			port = 1;
> +		}

[Severity: Medium]
Can this deliver frames out of wire order? bd_wr_ptr and bd_wr_ptr_o are
each read once before the loop, at different times, and never re-read.

Consider:

  readw(&queue_desc->wr_ptr)     port0 looks empty
  frame A completes on port0
  frame B completes on port1
  readw(&queue_desc_o->wr_ptr)   port1 has B

port0_q_empty stays true for the whole loop. The else branch picks port1,
and B goes to netif_receive_skb() without being compared against A's
timestamp. A is only seen on the next poll.

The same thing happens once port0's snapshot has been used up. In HSR/PRP,
consecutive frames of one flow can arrive first on different ports, so
this could show up as TCP reordering.

Would re-reading wr_ptr for a queue that looks empty, before taking a
frame from the peer queue, avoid this?

-- 
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261005154654.576663-1-parvathi%40couthit.com


^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: [PATCH net-next v5 3/3] net: ti: icssm-prueth: Support duplicate HW offload feature for HSR and PRP
  2026-10-05 15:41 ` [PATCH net-next v5 3/3] net: ti: icssm-prueth: Support duplicate HW offload feature for HSR and PRP Parvathi Pudi
@ 2026-10-09  3:43   ` netdev-bot+sashiko
  0 siblings, 0 replies; 7+ messages in thread
From: netdev-bot+sashiko @ 2026-10-09  3:43 UTC (permalink / raw)
  To: parvathi
  Cc: andrew+netdev, davem, edumazet, kuba, pabeni, danishanwar, rogerq,
	pmohan, afd, vadim.fedorenko, haokexin, basharath, arnd,
	linux-kernel, netdev, linux-arm-kernel, horms, pratheesh,
	j-rameshbabu, vigneshr, praneeth, srk, rogerq, m-malladi, krishna,
	mohan

Thank you for your contribution! Sashiko AI review found 8 potential
issue(s) to consider.

Critical: 0 · High: 0 · Medium: 7 · Low: 1

If you believe none of the issues are legitimate, please reply to each
issue of Medium severity or higher and explain why the AI is wrong.
If any of them are legitimate and you plan to respin, please let patchwork
know by including "pw-bot: cr" as a separate line at the end of your reply
(one such reply per series is enough).

- [Medium] The patch silently changes the driver/firmware contract for
  HSR RX and LRE TX.
- [Medium] HSR-tagged PTP frames sent through the HSR master lose their
  second transmission once hsr-dup-offload is enabled.
- [Medium] Coupling three features (HSR_FWD, HSR_TAG_RM, HSR_DUP) in
  icssm_emac_ndo_fix_features() breaks incremental enabling.
- [Medium] Enabling hsr-dup-offload on only the port that becomes HSR
  slave B silently loses redundancy.
- [Medium] icssm_prueth_tx_enqueue() cannot tell a full ring from an
  empty one.
- [Medium] The TX path now reads prueth->eth_type (through
  prueth_is_lre()) several times with no synchronisation.
- [Medium] In LRE mode, icssm_prueth_tx_enqueue() finds the L2 header
  with skb_mac_header(skb) instead of skb->data.
- [Low] icssm_prueth_tx_enqueue() identifies PRP frames only by checking
  that the last two bytes equal ETH_P_PRP…

--

net: ti: icssm-prueth: Support duplicate HW offload feature for HSR and PRP

This patch adds NETIF_F_HW_HSR_DUP support to the ICSSM PRU ethernet
driver. Each HSR/PRP frame is written once into a TX queue shared by both
PRU ports, and the firmware duplicates it. On receive, a new HostRecv BD
bit is used to drop the duplicate copy, and the driver strips the HSR tag
itself while copying out of OCMC.

> diff --git a/drivers/net/ethernet/ti/icssm/icssm_prueth.c b/drivers/net/ethernet/ti/icssm/icssm_prueth.c
> index 42c83946f17a7..e5e888a1e6f49 100644
> --- a/drivers/net/ethernet/ti/icssm/icssm_prueth.c
> +++ b/drivers/net/ethernet/ti/icssm/icssm_prueth.c

[ ... ]

> @@ -608,6 +644,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)) {
> +		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;

[Severity: Medium]
Can the shared ring wrap onto either port's reader here?

The check accepts pkt_block_size == free_blocks. In that case update_block
lands on the reader's block, so the new wr_ptr equals rd_ptr. The queue
convention is that rd_ptr == wr_ptr means empty, so the firmware would
never send the pending blocks. The next enqueue then sees write_block ==
read_block, takes the "all free" branch and overwrites them.

The own-port calculation had this off-by-one before this patch. The new
free_blocks_other_port calculation repeats it, and the ambiguous wr_ptr
is now written to both firmware consumers.

Should one block always be left unused, for example by rejecting
pkt_block_size >= free_blocks?

> @@ -657,6 +716,51 @@ 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;

[Severity: Medium]
Is skb_mac_header() always valid in the xmit path? The rest of
icssm_prueth_tx_enqueue() uses skb->data.

The AF_XDP copy-mode TX path does not seem to reset the mac header:

xsk_build_skb()
  __dev_direct_xmit()
    netdev_start_xmit()
      icssm_emac_ndo_start_xmit()
        icssm_prueth_tx_enqueue()

In that case skb->mac_header still holds its initial ~0 value, and
skb_mac_header() returns skb->head + 0xffff. Would this read h_proto, and
later the HSR tag, from memory about 64KB past the skb head? Those values
then set the LAN/RED BD bits.

Would reading the header from skb->data avoid this?

> +
> +		if (proto == htons(ETH_P_8021Q)) {
> +			vlan_hdr = (struct vlan_ethhdr *)ethhdr;
> +			proto = vlan_hdr->h_vlan_encapsulated_proto;
> +			is_vlan = true;
> +		}
> +
> +		/* Check if the SKB has HSR tag */
> +		if (PRUETH_IS_HSR(prueth) && proto == htons(ETH_P_HSR)) {
> +			hdr = skb_mac_header(skb) + ETH_HLEN;
> +			if (is_vlan)
> +				hdr += VLAN_HLEN;
> +
> +			hsr_tag = (struct hsr_tag *)hdr;
> +
> +			/* PTP frames (ETH_P_1588) are directed frames
> +			 * so skip the duplication
> +			 */
> +			if (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);
> +			}

[Severity: Medium]
With hsr-dup-offload enabled, do PTP frames sent through the HSR master
now go out on only one LAN?

hsr_forward_do() in net/hsr/hsr_forward.c sends the frame on the first
slave and then skips the second:

	if ((port->dev->features & NETIF_F_HW_HSR_DUP) && sent)
		continue;

The HSR core has no ETH_P_1588 exception. Here, though, every frame with
encap_proto == ETH_P_1588 gets only the originating LAN bit. So neither
the HSR core nor the firmware would send the second copy.

This check also cannot tell frames that came through hsr0 apart from
frames that userspace sent on purpose on a single slave.

> +			wr_buf_desc |= PRUETH_BD_RED_PKT_MASK;
> +		} else if (PRUETH_IS_PRP(prueth)) {
> +			/* Check if the SKB has PRP tag */
> +			prp_ethtype = get_unaligned_be16(skb_tail_pointer(skb) -
> +							 ETH_TLEN);
> +
> +			if (prp_ethtype == ETH_P_PRP) {
> +				wr_buf_desc |= PRUETH_BD_LAN_INFO_MASK;
> +				wr_buf_desc |= PRUETH_BD_RED_PKT_MASK;

[Severity: Low]
Should this also check the LSDU size field of the RCT? Only the last two
bytes are compared with ETH_P_PRP. The HSR core's PRP detection checks the
LSDU size as well.

Suppose a non-PRP frame is sent directly on a PRP slave and its payload
happens to end in 0x88fb. Would it be duplicated to both LANs and marked
as a redundancy frame, instead of going out on txport only?

[ ... ]

> @@ -669,6 +773,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))
> +		writew(update_wr_ptr, &queue_desc_other_port->wr_ptr);

[Severity: Medium]
Can queue_desc_other_port still be NULL at this point?

The TX path reads prueth->eth_type several times with no
synchronization:

- icssm_emac_ndo_start_xmit() reads it to pick the lock.
- icssm_prueth_tx_enqueue() reads it to choose the queue table, to set
  up queue_desc_other_port, to set the LAN bits, and once more here.

eth_type can change while a port is transmitting. In
icssm_prueth_hsr_port_unlink(), port 0 is reopened in EMAC mode. If port
1's ndo_open then fails, the old mode is restored without stopping port
0's TX:

	ret = icssm_prueth_change_mode(prueth, PRUSS_ETHTYPE_EMAC);
	if (ret < 0) {
		dev_err(prueth->dev, "Failed to enable dual EMAC mode\n");
		prueth->eth_type = prev_mode;
	}

Suppose an xmit on port 0 sees EMAC at the "Fetch queue state for the
second LRE port" check and HSR here. It would then dereference
queue_desc_other_port while it is still NULL. The free_hsr path in
icssm_prueth_hsr_port_link() has the same pattern in the other direction.

Mismatched reads could also take emac->host_queue_lock while writing to
the shared LRE ring. That lock does not serialize against the other port.

> @@ -681,8 +789,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);
> +

[ ... ]

> @@ -742,11 +856,36 @@ 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);
>  
> -	/* 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 (!pkt_info->host_recv_flag)
> +			return 0;
> +
> +		ethhdr = (struct ethhdr *)src_addr;
> +		proto = ethhdr->h_proto;
> +
> +		if (proto == htons(ETH_P_8021Q)) {
> +			has_vlan = true;
> +			vlan_hdr = (struct vlan_ethhdr *)ethhdr;
> +			proto = vlan_hdr->h_vlan_encapsulated_proto;
> +		}
> +
> +		if (proto == htons(ETH_P_HSR) && !pkt_info->timestamp)
> +			has_hsr_tag = true;
> +	}

[Severity: Medium]
Has the driver/firmware contract for HSR RX changed here? And does the
firmware the driver loads match the new contract?

The previous patch in this series trusted the firmware START_FLAG. It
skipped ICSSM_LRE_TAG_SIZE bytes because the tag was "already stripped by
firmware".

icssm_emac_rx_packet() now ignores start_offset in HSR mode. It reads
h_proto at buffer offset 0 and strips the HSR tag itself, except for
timestamped frames. The commit message does not mention that exception.

icssm_parse_packet_info() still fills start_offset from
PRUETH_BD_START_FLAG_MASK for HSR. The kernel-doc in struct
prueth_packet_info still says "true if frame carries an HSR/PRP start
offset".

If the firmware still shifts the frame, the ethertype is read at the
wrong offset. If it does not, the HSR RX path in the previous patch was
already broken, and the series does not bisect cleanly.

Every BD without the new HostRecv bit (bit 10) is now dropped silently.
On the TX side, the driver now relies on firmware support for the shared
queues and the LAN/RED BD bits. icssm_prueth_sw_boot_prus() still loads
the same firmware names, with no version or capability check.

With an older firmware image, would all HSR RX frames be dropped and the
shared TX queue layout be ignored?

[ ... ]

> @@ -1568,19 +1726,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)
>  {

[ ... ]

>  	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;

[Severity: Medium]
What happens if hsr-dup-offload is enabled on only one of the two ports?

This check only looks at the current device. With both ports down, DUP
can be enabled on just the port that later becomes HSR slave B.

When hsr0 is created, icssm_prueth_ndev_event() calls
icssm_prueth_hsr_port_link() only for ports that have
NETIF_PRUETH_LRE_OFFLOAD_FEATURES set. So hsr_members never equals
all_slaves, the firmware stays in dual EMAC mode, and linking still
succeeds.

hsr_forward_do() then sends on slave A, which has no DUP, and skips slave
B because B advertises NETIF_F_HW_HSR_DUP. In EMAC mode the frame sent on
A is not duplicated, so only LAN A receives it.

Should NETIF_F_HW_HSR_DUP be rejected or cleared when offload mode is not
actually active?

>  
>  	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;

[Severity: Medium]
Does coupling three features here break enabling them one at a time?

__netdev_update_features() starts from dev->wanted_features, and
ethnl_set_features() adds only the requested bits to wanted_features. The
bits that this function turns on automatically never get into
wanted_features.

For example, with both ports down:

  ethtool -K eth2 hsr-tag-rm-offload on
    wanted = TAG_RM
    first block sets all three: features = FWD | TAG_RM | DUP

  ethtool -K eth2 hsr-fwd-offload on
    wanted = TAG_RM | FWD
    first block skipped because ndev->features has LRE bits
    second block sees !(features & NETIF_F_HW_HSR_DUP), clears all three

So all three offloads end up off, the opposite of what was requested.
With only two coupled bits, the same sequence left both enabled.

Similarly, after "ethtool -K ethX hsr-dup-offload on", wanted_features
holds only DUP. Any later recompute, such as toggling an unrelated
hw_feature, would then clear all three.

-- 
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261005154654.576663-1-parvathi%40couthit.com


^ permalink raw reply	[flat|nested] 7+ messages in thread

end of thread, other threads:[~2026-10-09  3:43 UTC | newest]

Thread overview: 7+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-05 15:41 [PATCH net-next v5 0/3] Introduce HSR/PRP HW offload support for PRU-ICSSM Ethernet driver Parvathi Pudi
2026-10-05 15:41 ` [PATCH net-next v5 1/3] net: ti: icssm-prueth: Add HSR and PRP HW offload mode support for AM57xx, AM437x and AM335x Parvathi Pudi
2026-10-09  3:43   ` netdev-bot+sashiko
2026-10-05 15:41 ` [PATCH net-next v5 2/3] net: ti: icssm-prueth: Add priority based RX IRQ handlers Parvathi Pudi
2026-10-09  3:43   ` netdev-bot+sashiko
2026-10-05 15:41 ` [PATCH net-next v5 3/3] net: ti: icssm-prueth: Support duplicate HW offload feature for HSR and PRP Parvathi Pudi
2026-10-09  3:43   ` netdev-bot+sashiko

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox