Linux-ARM-Kernel Archive on lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH v3 1/6] phy: rockchip: samsung-hdptx: Fix rate recalculation for high bpc
From: Cristian Ciocaltea @ 2026-06-11 12:31 UTC (permalink / raw)
  To: Vinod Koul, Neil Armstrong, Heiko Stuebner, Algea Cao,
	Dmitry Baryshkov
  Cc: kernel, linux-phy, linux-arm-kernel, linux-rockchip, linux-kernel,
	Thomas Niederprüm, Simon Wright
In-Reply-To: <20260611-hdptx-clk-fixes-v3-0-67b1b0c00e16@collabora.com>

The PHY PLL can be programmed by an external component, e.g. the
bootloader, just before the recalc_rate() callback is invoked during
devm_clk_hw_register() in the probe path.

Therefore rk_hdptx_phy_clk_recalc_rate() finds the PLL enabled and
attempts to compute the clock rate, while making use of the bpc value
from the HDMI PHY configuration, which always defaults to 8 because
phy_configure() was not run at that point.  As a consequence, the
(re)calculated rate is incorrect when the actual bpc was higher than 8.

Do not rely on any of the hdmi_cfg members when computing the clock rate
and, instead, read the required input data (i.e. bpc), directly from the
hardware registers.

Fixes: 3481fc04d969 ("phy: rockchip: samsung-hdptx: Compute clk rate from PLL config")
Tested-by: Thomas Niederprüm <dubito@online.de>
Tested-by: Simon Wright <simon@symple.nz>
Signed-off-by: Cristian Ciocaltea <cristian.ciocaltea@collabora.com>
---
 drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c | 13 ++++---------
 1 file changed, 4 insertions(+), 9 deletions(-)

diff --git a/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c b/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c
index 2d973bc37f07..710603afff86 100644
--- a/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c
+++ b/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c
@@ -2168,7 +2168,7 @@ static u64 rk_hdptx_phy_clk_calc_rate_from_pll_cfg(struct rk_hdptx_phy *hdptx)
 	struct lcpll_config lcpll_hw;
 	struct ropll_config ropll_hw;
 	u64 fout, sdm;
-	u32 mode, val;
+	u32 mode, bpc, val;
 	int ret, i;
 
 	ret = regmap_read(hdptx->regmap, CMN_REG(0008), &mode);
@@ -2266,6 +2266,7 @@ static u64 rk_hdptx_phy_clk_calc_rate_from_pll_cfg(struct rk_hdptx_phy *hdptx)
 	if (ret)
 		return 0;
 	ropll_hw.pms_sdiv = ((val & PLL_PCG_POSTDIV_SEL_MASK) >> 4) + 1;
+	bpc = (FIELD_GET(PLL_PCG_CLK_SEL_MASK, val) << 1) + 8;
 
 	fout = PLL_REF_CLK * ropll_hw.pms_mdiv;
 	if (ropll_hw.sdm_en) {
@@ -2280,7 +2281,7 @@ static u64 rk_hdptx_phy_clk_calc_rate_from_pll_cfg(struct rk_hdptx_phy *hdptx)
 			fout = fout + sdm;
 	}
 
-	return div_u64(fout * 2, ropll_hw.pms_sdiv * 10);
+	return DIV_ROUND_CLOSEST_ULL(fout * 2 * 8, ropll_hw.pms_sdiv * 10 * bpc);
 }
 
 static unsigned long rk_hdptx_phy_clk_recalc_rate(struct clk_hw *hw,
@@ -2288,19 +2289,13 @@ static unsigned long rk_hdptx_phy_clk_recalc_rate(struct clk_hw *hw,
 {
 	struct rk_hdptx_phy *hdptx = to_rk_hdptx_phy(hw);
 	u32 status;
-	u64 rate;
 	int ret;
 
 	ret = regmap_read(hdptx->grf, GRF_HDPTX_CON0, &status);
 	if (ret || !(status & HDPTX_I_PLL_EN))
 		return 0;
 
-	rate = rk_hdptx_phy_clk_calc_rate_from_pll_cfg(hdptx);
-
-	if (hdptx->hdmi_cfg.mode == PHY_HDMI_MODE_FRL)
-		return rate;
-
-	return DIV_ROUND_CLOSEST_ULL(rate * 8, hdptx->hdmi_cfg.bpc);
+	return rk_hdptx_phy_clk_calc_rate_from_pll_cfg(hdptx);
 }
 
 static int rk_hdptx_phy_clk_determine_rate(struct clk_hw *hw,

-- 
2.54.0



^ permalink raw reply related

* [PATCH v3 2/6] phy: rockchip: samsung-hdptx: Handle uncommitted PHY config changes
From: Cristian Ciocaltea @ 2026-06-11 12:31 UTC (permalink / raw)
  To: Vinod Koul, Neil Armstrong, Heiko Stuebner, Algea Cao,
	Dmitry Baryshkov
  Cc: kernel, linux-phy, linux-arm-kernel, linux-rockchip, linux-kernel,
	Thomas Niederprüm, Simon Wright
In-Reply-To: <20260611-hdptx-clk-fixes-v3-0-67b1b0c00e16@collabora.com>

Any changes to the PHY link rate and/or color depth done via the HDMI
PHY configuration API are not immediately programmed into the hardware,
but are delayed until the PHY usage count gets incremented from 0 to 1,
that is when it is powered on or when the PLL clock exposed through
the CCF API is prepared, whichever comes first.

Since the clock might remain in prepared state after subsequent PHY
config changes, the programming can also be triggered via
clk_ops.set_rate().  However, from the clock consumer perspective (i.e.
VOP2 display controller), the (pixel) clock rate doesn't vary with bpc,
as that is handled internally by the PHY and reflected in the TDMS
character rate only.

As a consequence, changing the bpc while preserving the modeline may
lead to out-of-sync issues between CCF and HDMI PHY config state,
because the .set_rate() callback is not invoked when clock rate remains
constant.  This may also happen when the PHY PLL has been pre-programmed
by an external entity, e.g. the bootloader, which is actually a
regression introduced by the recent FRL patches.

Introduce a pll_config_dirty flag to keep track of uncommitted PHY
config changes and use it in clk_ops.determine_rate() to invalidate the
current clock rate (as known by CCF) and, consequently, ensure those
changes are programmed into hardware via clk_ops.set_rate().

Moreover, proceed with a similar fix in phy_ops.power_on() callback, to
handle the scenario where the CCF API is not used due to operating in
FRL mode, while the clock is still in a prepared state and thus
preventing rk_hdptx_phy_consumer_get() to apply the updated PHY
configuration.

Fixes: de5dba833118 ("phy: rockchip: samsung-hdptx: Add HDMI 2.1 FRL support")
Fixes: 9d0ec51d7c22 ("phy: rockchip: samsung-hdptx: Add high color depth management")
Tested-by: Thomas Niederprüm <dubito@online.de>
Tested-by: Simon Wright <simon@symple.nz>
Signed-off-by: Cristian Ciocaltea <cristian.ciocaltea@collabora.com>
---
 drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c | 84 +++++++++++++----------
 1 file changed, 48 insertions(+), 36 deletions(-)

diff --git a/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c b/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c
index 710603afff86..5295d5f6f287 100644
--- a/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c
+++ b/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c
@@ -413,6 +413,7 @@ struct rk_hdptx_phy {
 
 	/* clk provider */
 	struct clk_hw hw;
+	bool pll_config_dirty;
 	bool restrict_rate_change;
 
 	atomic_t usage_count;
@@ -1260,13 +1261,19 @@ static int rk_hdptx_tmds_ropll_cmn_config(struct rk_hdptx_phy *hdptx)
 
 static int rk_hdptx_pll_cmn_config(struct rk_hdptx_phy *hdptx)
 {
+	int ret;
+
 	if (hdptx->hdmi_cfg.rate <= HDMI20_MAX_RATE)
-		return rk_hdptx_tmds_ropll_cmn_config(hdptx);
+		ret = rk_hdptx_tmds_ropll_cmn_config(hdptx);
+	else if (hdptx->hdmi_cfg.rate == FRL_8G4L_RATE)
+		ret = rk_hdptx_frl_lcpll_ropll_cmn_config(hdptx);
+	else
+		ret = rk_hdptx_frl_lcpll_cmn_config(hdptx);
 
-	if (hdptx->hdmi_cfg.rate == FRL_8G4L_RATE)
-		return rk_hdptx_frl_lcpll_ropll_cmn_config(hdptx);
+	if (!ret)
+		hdptx->pll_config_dirty = false;
 
-	return rk_hdptx_frl_lcpll_cmn_config(hdptx);
+	return ret;
 }
 
 static int rk_hdptx_frl_lcpll_mode_config(struct rk_hdptx_phy *hdptx)
@@ -1347,25 +1354,22 @@ static int rk_hdptx_phy_consumer_get(struct rk_hdptx_phy *hdptx)
 		return 0;
 
 	ret = regmap_read(hdptx->grf, GRF_HDPTX_STATUS, &status);
-	if (ret)
-		goto dec_usage;
-
-	if (status & HDPTX_O_PLL_LOCK_DONE)
-		dev_warn(hdptx->dev, "PLL locked by unknown consumer!\n");
+	if (ret) {
+		atomic_dec(&hdptx->usage_count);
+		return ret;
+	}
 
 	if (mode == PHY_MODE_DP) {
 		rk_hdptx_dp_reset(hdptx);
 	} else {
-		ret = rk_hdptx_pll_cmn_config(hdptx);
-		if (ret)
-			goto dec_usage;
+		/*
+		 * Ignore PLL config errors at this point as pll_config_dirty
+		 * was not reset and, therefore, operation will be retried.
+		 */
+		rk_hdptx_pll_cmn_config(hdptx);
 	}
 
 	return 0;
-
-dec_usage:
-	atomic_dec(&hdptx->usage_count);
-	return ret;
 }
 
 static int rk_hdptx_phy_consumer_put(struct rk_hdptx_phy *hdptx, bool force)
@@ -1700,13 +1704,18 @@ static int rk_hdptx_phy_power_on(struct phy *phy)
 		if (ret)
 			rk_hdptx_phy_consumer_put(hdptx, true);
 	} else {
-		regmap_write(hdptx->grf, GRF_HDPTX_CON0,
-			     HDPTX_MODE_SEL << 16 | FIELD_PREP(HDPTX_MODE_SEL, 0x0));
+		if (hdptx->pll_config_dirty)
+			ret = rk_hdptx_pll_cmn_config(hdptx);
 
-		if (hdptx->hdmi_cfg.mode == PHY_HDMI_MODE_FRL)
-			ret = rk_hdptx_frl_lcpll_mode_config(hdptx);
-		else
-			ret = rk_hdptx_tmds_ropll_mode_config(hdptx);
+		if (!ret) {
+			regmap_write(hdptx->grf, GRF_HDPTX_CON0,
+				     HDPTX_MODE_SEL << 16 | FIELD_PREP(HDPTX_MODE_SEL, 0x0));
+
+			if (hdptx->hdmi_cfg.mode == PHY_HDMI_MODE_FRL)
+				ret = rk_hdptx_frl_lcpll_mode_config(hdptx);
+			else
+				ret = rk_hdptx_tmds_ropll_mode_config(hdptx);
+		}
 
 		if (ret)
 			rk_hdptx_phy_consumer_put(hdptx, true);
@@ -2081,7 +2090,10 @@ static int rk_hdptx_phy_configure(struct phy *phy, union phy_configure_opts *opt
 			dev_err(hdptx->dev, "invalid hdmi params for phy configure\n");
 		} else {
 			hdptx->restrict_rate_change = true;
-			dev_dbg(hdptx->dev, "%s rate=%llu bpc=%u\n", __func__,
+			hdptx->pll_config_dirty = true;
+
+			dev_dbg(hdptx->dev, "%s %s rate=%llu bpc=%u\n", __func__,
+				hdptx->hdmi_cfg.mode ? "FRL" : "TMDS",
 				hdptx->hdmi_cfg.rate, hdptx->hdmi_cfg.bpc);
 		}
 
@@ -2303,8 +2315,19 @@ static int rk_hdptx_phy_clk_determine_rate(struct clk_hw *hw,
 {
 	struct rk_hdptx_phy *hdptx = to_rk_hdptx_phy(hw);
 
-	if (hdptx->hdmi_cfg.mode == PHY_HDMI_MODE_FRL)
-		return hdptx->hdmi_cfg.rate;
+	/*
+	 * Invalidate current clock rate to ensure rk_hdptx_phy_clk_set_rate()
+	 * will be invoked to commit PLL configuration.
+	 */
+	if (hdptx->pll_config_dirty) {
+		req->rate = 0;
+		return 0;
+	}
+
+	if (hdptx->hdmi_cfg.mode == PHY_HDMI_MODE_FRL) {
+		req->rate = hdptx->hdmi_cfg.rate;
+		return 0;
+	}
 
 	/*
 	 * FIXME: Temporarily allow altering TMDS char rate via CCF.
@@ -2336,17 +2359,6 @@ static int rk_hdptx_phy_clk_set_rate(struct clk_hw *hw, unsigned long rate,
 				     unsigned long parent_rate)
 {
 	struct rk_hdptx_phy *hdptx = to_rk_hdptx_phy(hw);
-	unsigned long long link_rate = rate;
-
-	if (hdptx->hdmi_cfg.mode != PHY_HDMI_MODE_FRL)
-		link_rate = DIV_ROUND_CLOSEST_ULL(rate * hdptx->hdmi_cfg.bpc, 8);
-
-	/* Revert any unlikely link rate change since determine_rate() */
-	if (hdptx->hdmi_cfg.rate != link_rate) {
-		dev_warn(hdptx->dev, "Reverting unexpected rate change from %llu to %llu\n",
-			 link_rate, hdptx->hdmi_cfg.rate);
-		hdptx->hdmi_cfg.rate = link_rate;
-	}
 
 	/*
 	 * The link rate would be normally programmed in HW during

-- 
2.54.0



^ permalink raw reply related

* [PATCH v3 3/6] phy: rockchip: samsung-hdptx: Drop TMDS rate setup workaround
From: Cristian Ciocaltea @ 2026-06-11 12:31 UTC (permalink / raw)
  To: Vinod Koul, Neil Armstrong, Heiko Stuebner, Algea Cao,
	Dmitry Baryshkov
  Cc: kernel, linux-phy, linux-arm-kernel, linux-rockchip, linux-kernel,
	Thomas Niederprüm, Simon Wright
In-Reply-To: <20260611-hdptx-clk-fixes-v3-0-67b1b0c00e16@collabora.com>

Since commit ba9c2fe18c17 ("drm/rockchip: dw_hdmi_qp: Switch to
phy_configure()") the TMDS rate setup doesn't rely anymore on the
unconventional usage of the bus width, instead it is managed exclusively
through the HDMI PHY configuration API.

Drop the now obsolete workaround to retrieve the TMDS character rate via
phy_get_bus_width() during power_on().

While at it, get rid of the extra call to rk_hdptx_phy_consumer_put() by
moving the statement at the end of the function.

Tested-by: Thomas Niederprüm <dubito@online.de>
Tested-by: Simon Wright <simon@symple.nz>
Signed-off-by: Cristian Ciocaltea <cristian.ciocaltea@collabora.com>
---
 drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c | 27 +++++------------------
 1 file changed, 6 insertions(+), 21 deletions(-)

diff --git a/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c b/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c
index 5295d5f6f287..92c77e58518c 100644
--- a/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c
+++ b/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c
@@ -1660,22 +1660,6 @@ static int rk_hdptx_phy_power_on(struct phy *phy)
 	enum phy_mode mode = phy_get_mode(phy);
 	int ret, lane;
 
-	if (mode != PHY_MODE_DP) {
-		if (!hdptx->hdmi_cfg.rate && hdptx->hdmi_cfg.mode != PHY_HDMI_MODE_FRL) {
-			/*
-			 * FIXME: Temporary workaround to setup TMDS char rate
-			 * from the RK DW HDMI QP bridge driver.
-			 * Will be removed as soon the switch to the HDMI PHY
-			 * configuration API has been completed on both ends.
-			 */
-			hdptx->hdmi_cfg.rate = phy_get_bus_width(hdptx->phy) & 0xfffffff;
-			hdptx->hdmi_cfg.rate *= 100;
-		}
-
-		dev_dbg(hdptx->dev, "%s rate=%llu bpc=%u\n", __func__,
-			hdptx->hdmi_cfg.rate, hdptx->hdmi_cfg.bpc);
-	}
-
 	ret = rk_hdptx_phy_consumer_get(hdptx);
 	if (ret)
 		return ret;
@@ -1701,9 +1685,10 @@ static int rk_hdptx_phy_power_on(struct phy *phy)
 		rk_hdptx_dp_pll_init(hdptx);
 
 		ret = rk_hdptx_dp_aux_init(hdptx);
-		if (ret)
-			rk_hdptx_phy_consumer_put(hdptx, true);
 	} else {
+		dev_dbg(hdptx->dev, "%s rate=%llu bpc=%u\n", __func__,
+			hdptx->hdmi_cfg.rate, hdptx->hdmi_cfg.bpc);
+
 		if (hdptx->pll_config_dirty)
 			ret = rk_hdptx_pll_cmn_config(hdptx);
 
@@ -1716,11 +1701,11 @@ static int rk_hdptx_phy_power_on(struct phy *phy)
 			else
 				ret = rk_hdptx_tmds_ropll_mode_config(hdptx);
 		}
-
-		if (ret)
-			rk_hdptx_phy_consumer_put(hdptx, true);
 	}
 
+	if (ret)
+		rk_hdptx_phy_consumer_put(hdptx, true);
+
 	return ret;
 }
 

-- 
2.54.0



^ permalink raw reply related

* [PATCH v3 4/6] phy: rockchip: samsung-hdptx: Drop restrict_rate_change handling
From: Cristian Ciocaltea @ 2026-06-11 12:31 UTC (permalink / raw)
  To: Vinod Koul, Neil Armstrong, Heiko Stuebner, Algea Cao,
	Dmitry Baryshkov
  Cc: kernel, linux-phy, linux-arm-kernel, linux-rockchip, linux-kernel,
	Thomas Niederprüm, Simon Wright
In-Reply-To: <20260611-hdptx-clk-fixes-v3-0-67b1b0c00e16@collabora.com>

Since commit 6efbd0f46dd8 ("phy: rockchip: samsung-hdptx: Restrict
altering TMDS char rate via CCF"), adjusting the rate via the Common
Clock Framework API has been disallowed.

To avoid breaking existing users until switching to the PHY config API,
it introduced a temporary exception to the rule, controlled via the
'restrict_rate_change' flag.

As the API transition completed, remove the now deprecated exception
logic.

Tested-by: Thomas Niederprüm <dubito@online.de>
Tested-by: Simon Wright <simon@symple.nz>
Signed-off-by: Cristian Ciocaltea <cristian.ciocaltea@collabora.com>
---
 drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c | 42 +++++------------------
 1 file changed, 8 insertions(+), 34 deletions(-)

diff --git a/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c b/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c
index 92c77e58518c..529dd2d5d9bd 100644
--- a/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c
+++ b/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c
@@ -414,7 +414,6 @@ struct rk_hdptx_phy {
 	/* clk provider */
 	struct clk_hw hw;
 	bool pll_config_dirty;
-	bool restrict_rate_change;
 
 	atomic_t usage_count;
 
@@ -2074,7 +2073,6 @@ static int rk_hdptx_phy_configure(struct phy *phy, union phy_configure_opts *opt
 		if (ret) {
 			dev_err(hdptx->dev, "invalid hdmi params for phy configure\n");
 		} else {
-			hdptx->restrict_rate_change = true;
 			hdptx->pll_config_dirty = true;
 
 			dev_dbg(hdptx->dev, "%s %s rate=%llu bpc=%u\n", __func__,
@@ -2301,41 +2299,17 @@ static int rk_hdptx_phy_clk_determine_rate(struct clk_hw *hw,
 	struct rk_hdptx_phy *hdptx = to_rk_hdptx_phy(hw);
 
 	/*
-	 * Invalidate current clock rate to ensure rk_hdptx_phy_clk_set_rate()
-	 * will be invoked to commit PLL configuration.
+	 * For uncommitted PLL configuration, invalidate the current clock rate
+	 * to ensure rk_hdptx_phy_clk_set_rate() will be always invoked.
+	 * Otherwise, restrict the rate according to the PHY link setup.
 	 */
-	if (hdptx->pll_config_dirty) {
+	if (hdptx->pll_config_dirty)
 		req->rate = 0;
-		return 0;
-	}
-
-	if (hdptx->hdmi_cfg.mode == PHY_HDMI_MODE_FRL) {
+	else if (hdptx->hdmi_cfg.mode == PHY_HDMI_MODE_FRL)
 		req->rate = hdptx->hdmi_cfg.rate;
-		return 0;
-	}
-
-	/*
-	 * FIXME: Temporarily allow altering TMDS char rate via CCF.
-	 * To be dropped as soon as the RK DW HDMI QP bridge driver
-	 * switches to make use of phy_configure().
-	 */
-	if (!hdptx->restrict_rate_change && req->rate != hdptx->hdmi_cfg.rate) {
-		struct phy_configure_opts_hdmi hdmi = {
-			.tmds_char_rate = req->rate,
-		};
-
-		int ret = rk_hdptx_phy_verify_hdmi_config(hdptx, &hdmi, &hdptx->hdmi_cfg);
-
-		if (ret)
-			return ret;
-	}
-
-	/*
-	 * The TMDS char rate shall be adjusted via phy_configure() only,
-	 * hence ensure rk_hdptx_phy_clk_set_rate() won't be invoked with
-	 * a different rate argument.
-	 */
-	req->rate = DIV_ROUND_CLOSEST_ULL(hdptx->hdmi_cfg.rate * 8, hdptx->hdmi_cfg.bpc);
+	else
+		req->rate = DIV_ROUND_CLOSEST_ULL(hdptx->hdmi_cfg.rate * 8,
+						  hdptx->hdmi_cfg.bpc);
 
 	return 0;
 }

-- 
2.54.0



^ permalink raw reply related

* [PATCH v3 5/6] phy: rockchip: samsung-hdptx: Simplify GRF access with FIELD_PREP_WM16()
From: Cristian Ciocaltea @ 2026-06-11 12:31 UTC (permalink / raw)
  To: Vinod Koul, Neil Armstrong, Heiko Stuebner, Algea Cao,
	Dmitry Baryshkov
  Cc: kernel, linux-phy, linux-arm-kernel, linux-rockchip, linux-kernel,
	Thomas Niederprüm, Simon Wright
In-Reply-To: <20260611-hdptx-clk-fixes-v3-0-67b1b0c00e16@collabora.com>

The 16 most significant bits of the general-purpose register (GRF) are
used as a write-enable mask for the remaining 16 bits.

Make use of the recently introduced FIELD_PREP_WM16() macro to avoid
open-coding the bit shift operations and improve code readability.

Tested-by: Thomas Niederprüm <dubito@online.de>
Tested-by: Simon Wright <simon@symple.nz>
Signed-off-by: Cristian Ciocaltea <cristian.ciocaltea@collabora.com>
---
 drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c | 52 +++++++++++------------
 1 file changed, 25 insertions(+), 27 deletions(-)

diff --git a/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c b/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c
index 529dd2d5d9bd..5ed110e2adc7 100644
--- a/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c
+++ b/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c
@@ -1,7 +1,7 @@
 // SPDX-License-Identifier: GPL-2.0+
 /*
  * Copyright (c) 2021-2022 Rockchip Electronics Co., Ltd.
- * Copyright (c) 2024 Collabora Ltd.
+ * Copyright (c) 2024-2026 Collabora Ltd.
  *
  * Author: Algea Cao <algea.cao@rock-chips.com>
  * Author: Cristian Ciocaltea <cristian.ciocaltea@collabora.com>
@@ -10,6 +10,7 @@
 #include <linux/clk.h>
 #include <linux/clk-provider.h>
 #include <linux/delay.h>
+#include <linux/hw_bitfield.h>
 #include <linux/mfd/syscon.h>
 #include <linux/module.h>
 #include <linux/of.h>
@@ -949,7 +950,9 @@ static void rk_hdptx_pre_power_up(struct rk_hdptx_phy *hdptx)
 	reset_control_assert(hdptx->rsts[RST_CMN].rstc);
 	reset_control_assert(hdptx->rsts[RST_INIT].rstc);
 
-	val = (HDPTX_I_PLL_EN | HDPTX_I_BIAS_EN | HDPTX_I_BGR_EN) << 16;
+	val = (FIELD_PREP_WM16(HDPTX_I_PLL_EN, 0) |
+	       FIELD_PREP_WM16(HDPTX_I_BIAS_EN, 0) |
+	       FIELD_PREP_WM16(HDPTX_I_BGR_EN, 0));
 	regmap_write(hdptx->grf, GRF_HDPTX_CON0, val);
 }
 
@@ -960,8 +963,8 @@ static int rk_hdptx_post_enable_lane(struct rk_hdptx_phy *hdptx)
 
 	reset_control_deassert(hdptx->rsts[RST_LANE].rstc);
 
-	val = (HDPTX_I_BIAS_EN | HDPTX_I_BGR_EN) << 16 |
-	       HDPTX_I_BIAS_EN | HDPTX_I_BGR_EN;
+	val = (FIELD_PREP_WM16(HDPTX_I_BIAS_EN, 1) |
+	       FIELD_PREP_WM16(HDPTX_I_BGR_EN, 1));
 	regmap_write(hdptx->grf, GRF_HDPTX_CON0, val);
 
 	/* 3 lanes FRL mode */
@@ -990,16 +993,15 @@ static int rk_hdptx_post_enable_pll(struct rk_hdptx_phy *hdptx)
 	u32 val;
 	int ret;
 
-	val = (HDPTX_I_BIAS_EN | HDPTX_I_BGR_EN) << 16 |
-	       HDPTX_I_BIAS_EN | HDPTX_I_BGR_EN;
+	val = (FIELD_PREP_WM16(HDPTX_I_BIAS_EN, 1) |
+	       FIELD_PREP_WM16(HDPTX_I_BGR_EN, 1));
 	regmap_write(hdptx->grf, GRF_HDPTX_CON0, val);
 
 	usleep_range(10, 15);
 	reset_control_deassert(hdptx->rsts[RST_INIT].rstc);
 
 	usleep_range(10, 15);
-	val = HDPTX_I_PLL_EN << 16 | HDPTX_I_PLL_EN;
-	regmap_write(hdptx->grf, GRF_HDPTX_CON0, val);
+	regmap_write(hdptx->grf, GRF_HDPTX_CON0, FIELD_PREP_WM16(HDPTX_I_PLL_EN, 1));
 
 	usleep_range(10, 15);
 	reset_control_deassert(hdptx->rsts[RST_CMN].rstc);
@@ -1037,7 +1039,9 @@ static void rk_hdptx_phy_disable(struct rk_hdptx_phy *hdptx)
 	reset_control_assert(hdptx->rsts[RST_CMN].rstc);
 	reset_control_assert(hdptx->rsts[RST_INIT].rstc);
 
-	val = (HDPTX_I_PLL_EN | HDPTX_I_BIAS_EN | HDPTX_I_BGR_EN) << 16;
+	val = (FIELD_PREP_WM16(HDPTX_I_PLL_EN, 0) |
+	       FIELD_PREP_WM16(HDPTX_I_BIAS_EN, 0) |
+	       FIELD_PREP_WM16(HDPTX_I_BGR_EN, 0));
 	regmap_write(hdptx->grf, GRF_HDPTX_CON0, val);
 }
 
@@ -1135,7 +1139,7 @@ static int rk_hdptx_frl_lcpll_cmn_config(struct rk_hdptx_phy *hdptx)
 
 	rk_hdptx_pre_power_up(hdptx);
 
-	regmap_write(hdptx->grf, GRF_HDPTX_CON0, LC_REF_CLK_SEL << 16);
+	regmap_write(hdptx->grf, GRF_HDPTX_CON0, FIELD_PREP_WM16(LC_REF_CLK_SEL, 0));
 
 	rk_hdptx_multi_reg_write(hdptx, rk_hdptx_common_cmn_init_seq);
 	rk_hdptx_multi_reg_write(hdptx, rk_hdptx_frl_lcpll_cmn_init_seq);
@@ -1178,8 +1182,7 @@ static int rk_hdptx_frl_lcpll_ropll_cmn_config(struct rk_hdptx_phy *hdptx)
 	rk_hdptx_pre_power_up(hdptx);
 
 	/* ROPLL input reference clock from LCPLL (cascade mode) */
-	regmap_write(hdptx->grf, GRF_HDPTX_CON0,
-		     (LC_REF_CLK_SEL << 16) | LC_REF_CLK_SEL);
+	regmap_write(hdptx->grf, GRF_HDPTX_CON0, FIELD_PREP_WM16(LC_REF_CLK_SEL, 1));
 
 	rk_hdptx_multi_reg_write(hdptx, rk_hdptx_common_cmn_init_seq);
 	rk_hdptx_multi_reg_write(hdptx, rk_hdptx_frl_lcpll_ropll_cmn_init_seq);
@@ -1218,7 +1221,7 @@ static int rk_hdptx_tmds_ropll_cmn_config(struct rk_hdptx_phy *hdptx)
 
 	rk_hdptx_pre_power_up(hdptx);
 
-	regmap_write(hdptx->grf, GRF_HDPTX_CON0, LC_REF_CLK_SEL << 16);
+	regmap_write(hdptx->grf, GRF_HDPTX_CON0, FIELD_PREP_WM16(LC_REF_CLK_SEL, 0));
 
 	rk_hdptx_multi_reg_write(hdptx, rk_hdptx_common_cmn_init_seq);
 	rk_hdptx_multi_reg_write(hdptx, rk_hdptx_tmds_cmn_init_seq);
@@ -1336,11 +1339,9 @@ static void rk_hdptx_dp_reset(struct rk_hdptx_phy *hdptx)
 			   FIELD_PREP(LN_TX_DRV_EI_EN_MASK, 0));
 
 	regmap_write(hdptx->grf, GRF_HDPTX_CON0,
-		     HDPTX_I_PLL_EN << 16 | FIELD_PREP(HDPTX_I_PLL_EN, 0x0));
-	regmap_write(hdptx->grf, GRF_HDPTX_CON0,
-		     HDPTX_I_BIAS_EN << 16 | FIELD_PREP(HDPTX_I_BIAS_EN, 0x0));
-	regmap_write(hdptx->grf, GRF_HDPTX_CON0,
-		     HDPTX_I_BGR_EN << 16 | FIELD_PREP(HDPTX_I_BGR_EN, 0x0));
+		     FIELD_PREP_WM16(HDPTX_I_PLL_EN, 0) |
+		     FIELD_PREP_WM16(HDPTX_I_BIAS_EN, 0) |
+		     FIELD_PREP_WM16(HDPTX_I_BGR_EN, 0));
 }
 
 static int rk_hdptx_phy_consumer_get(struct rk_hdptx_phy *hdptx)
@@ -1616,9 +1617,8 @@ static int rk_hdptx_dp_aux_init(struct rk_hdptx_phy *hdptx)
 			   FIELD_PREP(OVRD_SB_VREG_EN_MASK, 0x1));
 
 	regmap_write(hdptx->grf, GRF_HDPTX_CON0,
-		     HDPTX_I_BGR_EN << 16 | FIELD_PREP(HDPTX_I_BGR_EN, 0x1));
-	regmap_write(hdptx->grf, GRF_HDPTX_CON0,
-		     HDPTX_I_BIAS_EN << 16 | FIELD_PREP(HDPTX_I_BIAS_EN, 0x1));
+		     FIELD_PREP_WM16(HDPTX_I_BGR_EN, 1) |
+		     FIELD_PREP_WM16(HDPTX_I_BIAS_EN, 1));
 	usleep_range(20, 25);
 
 	reset_control_deassert(hdptx->rsts[RST_INIT].rstc);
@@ -1665,7 +1665,7 @@ static int rk_hdptx_phy_power_on(struct phy *phy)
 
 	if (mode == PHY_MODE_DP) {
 		regmap_write(hdptx->grf, GRF_HDPTX_CON0,
-			     HDPTX_MODE_SEL << 16 | FIELD_PREP(HDPTX_MODE_SEL, 0x1));
+			     FIELD_PREP_WM16(HDPTX_MODE_SEL, 1));
 
 		for (lane = 0; lane < 4; lane++) {
 			regmap_update_bits(hdptx->regmap, LANE_REG(031e) + 0x400 * lane,
@@ -1693,7 +1693,7 @@ static int rk_hdptx_phy_power_on(struct phy *phy)
 
 		if (!ret) {
 			regmap_write(hdptx->grf, GRF_HDPTX_CON0,
-				     HDPTX_MODE_SEL << 16 | FIELD_PREP(HDPTX_MODE_SEL, 0x0));
+				     FIELD_PREP_WM16(HDPTX_MODE_SEL, 0));
 
 			if (hdptx->hdmi_cfg.mode == PHY_HDMI_MODE_FRL)
 				ret = rk_hdptx_frl_lcpll_mode_config(hdptx);
@@ -1828,8 +1828,7 @@ static int rk_hdptx_phy_set_rate(struct rk_hdptx_phy *hdptx,
 	u32 bw, status;
 	int ret;
 
-	regmap_write(hdptx->grf, GRF_HDPTX_CON0,
-		     HDPTX_I_PLL_EN << 16 | FIELD_PREP(HDPTX_I_PLL_EN, 0x0));
+	regmap_write(hdptx->grf, GRF_HDPTX_CON0, FIELD_PREP_WM16(HDPTX_I_PLL_EN, 0));
 
 	switch (dp->link_rate) {
 	case 1620:
@@ -1885,8 +1884,7 @@ static int rk_hdptx_phy_set_rate(struct rk_hdptx_phy *hdptx,
 	regmap_update_bits(hdptx->regmap, CMN_REG(0095), DP_TX_LINK_BW_MASK,
 			   FIELD_PREP(DP_TX_LINK_BW_MASK, bw));
 
-	regmap_write(hdptx->grf, GRF_HDPTX_CON0,
-		     HDPTX_I_PLL_EN << 16 | FIELD_PREP(HDPTX_I_PLL_EN, 0x1));
+	regmap_write(hdptx->grf, GRF_HDPTX_CON0, FIELD_PREP_WM16(HDPTX_I_PLL_EN, 1));
 
 	ret = regmap_read_poll_timeout(hdptx->grf, GRF_HDPTX_STATUS,
 				       status, FIELD_GET(HDPTX_O_PLL_LOCK_DONE, status),

-- 
2.54.0



^ permalink raw reply related

* [PATCH v3 6/6] phy: rockchip: samsung-hdptx: Consistently use bitfield macros
From: Cristian Ciocaltea @ 2026-06-11 12:31 UTC (permalink / raw)
  To: Vinod Koul, Neil Armstrong, Heiko Stuebner, Algea Cao,
	Dmitry Baryshkov
  Cc: kernel, linux-phy, linux-arm-kernel, linux-rockchip, linux-kernel,
	Thomas Niederprüm, Simon Wright
In-Reply-To: <20260611-hdptx-clk-fixes-v3-0-67b1b0c00e16@collabora.com>

Make the code more robust and improve readability by using the available
bitfield macros (e.g. FIELD_PREP, FIELD_GET) whenever possible, instead
of open coding the related bit operations.

Tested-by: Thomas Niederprüm <dubito@online.de>
Tested-by: Simon Wright <simon@symple.nz>
Signed-off-by: Cristian Ciocaltea <cristian.ciocaltea@collabora.com>
---
 drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c | 24 ++++++++++++++++-------
 1 file changed, 17 insertions(+), 7 deletions(-)

diff --git a/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c b/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c
index 5ed110e2adc7..a8a8cd176897 100644
--- a/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c
+++ b/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c
@@ -53,6 +53,12 @@
 /* CMN_REG(001e) */
 #define LCPLL_PI_EN_MASK		BIT(5)
 #define LCPLL_100M_CLK_EN_MASK		BIT(0)
+/* CMN_REG(0022) */
+#define ANA_LCPLL_PMS_PDIV_MASK		GENMASK(7, 4)
+#define ANA_LCPLL_PMS_REFDIV_MASK	GENMASK(3, 0)
+/* CMN_REG(0023) */
+#define LCPLL_PMS_SDIV_RBR_MASK		GENMASK(7, 4)
+#define LCPLL_PMS_SDIV_HBR_MASK		GENMASK(3, 0)
 /* CMN_REG(0025) */
 #define LCPLL_PMS_IQDIV_RSTN_MASK	BIT(4)
 /* CMN_REG(0028) */
@@ -1157,9 +1163,11 @@ static int rk_hdptx_frl_lcpll_cmn_config(struct rk_hdptx_phy *hdptx)
 	regmap_write(hdptx->regmap, CMN_REG(0020), cfg->pms_mdiv);
 	regmap_write(hdptx->regmap, CMN_REG(0021), cfg->pms_mdiv_afc);
 	regmap_write(hdptx->regmap, CMN_REG(0022),
-		     (cfg->pms_pdiv << 4) | cfg->pms_refdiv);
+		     FIELD_PREP(ANA_LCPLL_PMS_PDIV_MASK, cfg->pms_pdiv) |
+		     FIELD_PREP(ANA_LCPLL_PMS_REFDIV_MASK, cfg->pms_refdiv));
 	regmap_write(hdptx->regmap, CMN_REG(0023),
-		     (cfg->pms_sdiv << 4) | cfg->pms_sdiv);
+		     FIELD_PREP(LCPLL_PMS_SDIV_RBR_MASK, cfg->pms_sdiv) |
+		     FIELD_PREP(LCPLL_PMS_SDIV_HBR_MASK, cfg->pms_sdiv));
 	regmap_write(hdptx->regmap, CMN_REG(002a), cfg->sdm_deno);
 	regmap_write(hdptx->regmap, CMN_REG(002b), cfg->sdm_num_sign);
 	regmap_write(hdptx->regmap, CMN_REG(002c), cfg->sdm_num);
@@ -1229,8 +1237,10 @@ static int rk_hdptx_tmds_ropll_cmn_config(struct rk_hdptx_phy *hdptx)
 	regmap_write(hdptx->regmap, CMN_REG(0051), cfg->pms_mdiv);
 	regmap_write(hdptx->regmap, CMN_REG(0055), cfg->pms_mdiv_afc);
 	regmap_write(hdptx->regmap, CMN_REG(0059),
-		     (cfg->pms_pdiv << 4) | cfg->pms_refdiv);
-	regmap_write(hdptx->regmap, CMN_REG(005a), cfg->pms_sdiv << 4);
+		     FIELD_PREP(ANA_ROPLL_PMS_PDIV_MASK, cfg->pms_pdiv) |
+		     FIELD_PREP(ANA_ROPLL_PMS_REFDIV_MASK, cfg->pms_refdiv));
+	regmap_write(hdptx->regmap, CMN_REG(005a),
+		     FIELD_PREP(ROPLL_PMS_SDIV_RBR_MASK, cfg->pms_sdiv));
 
 	regmap_update_bits(hdptx->regmap, CMN_REG(005e), ROPLL_SDM_EN_MASK,
 			   FIELD_PREP(ROPLL_SDM_EN_MASK, cfg->sdm_en));
@@ -2177,7 +2187,7 @@ static u64 rk_hdptx_phy_clk_calc_rate_from_pll_cfg(struct rk_hdptx_phy *hdptx)
 		ret = regmap_read(hdptx->regmap, CMN_REG(0023), &val);
 		if (ret)
 			return 0;
-		lcpll_hw.pms_sdiv = val & 0xf;
+		lcpll_hw.pms_sdiv = FIELD_GET(LCPLL_PMS_SDIV_HBR_MASK, val);
 
 		ret = regmap_read(hdptx->regmap, CMN_REG(002B), &val);
 		if (ret)
@@ -2197,7 +2207,7 @@ static u64 rk_hdptx_phy_clk_calc_rate_from_pll_cfg(struct rk_hdptx_phy *hdptx)
 		ret = regmap_read(hdptx->regmap, CMN_REG(002D), &val);
 		if (ret)
 			return 0;
-		lcpll_hw.sdc_n = (val & LCPLL_SDC_N_MASK) >> 1;
+		lcpll_hw.sdc_n = FIELD_GET(LCPLL_SDC_N_MASK, val);
 
 		for (i = 0; i < ARRAY_SIZE(rk_hdptx_frl_lcpll_cfg); i++) {
 			const struct lcpll_config *cfg = &rk_hdptx_frl_lcpll_cfg[i];
@@ -2258,7 +2268,7 @@ static u64 rk_hdptx_phy_clk_calc_rate_from_pll_cfg(struct rk_hdptx_phy *hdptx)
 	ret = regmap_read(hdptx->regmap, CMN_REG(0086), &val);
 	if (ret)
 		return 0;
-	ropll_hw.pms_sdiv = ((val & PLL_PCG_POSTDIV_SEL_MASK) >> 4) + 1;
+	ropll_hw.pms_sdiv = FIELD_GET(PLL_PCG_POSTDIV_SEL_MASK, val) + 1;
 	bpc = (FIELD_GET(PLL_PCG_CLK_SEL_MASK, val) << 1) + 8;
 
 	fout = PLL_REF_CLK * ropll_hw.pms_mdiv;

-- 
2.54.0



^ permalink raw reply related

* Re: [PATCH v2 0/6] phy: rockchip: samsung-hdptx: Clock fixes and API transition cleanups
From: Cristian Ciocaltea @ 2026-06-11 12:34 UTC (permalink / raw)
  To: Vinod Koul
  Cc: Neil Armstrong, Heiko Stuebner, Algea Cao, Dmitry Baryshkov,
	kernel, linux-phy, linux-arm-kernel, linux-rockchip, linux-kernel,
	Thomas Niederprüm, Simon Wright
In-Reply-To: <aiqbX8kon8HtiSEl@vaman>

On 6/11/26 2:26 PM, Vinod Koul wrote:
> On 03-06-26, 13:27, Cristian Ciocaltea wrote:
>> On 5/20/26 10:05 PM, Cristian Ciocaltea wrote:
>>> Hi Vinod,
>>>
>>> On 5/11/26 9:21 PM, Cristian Ciocaltea wrote:
>>>> This series provides a set of bug fixes and cleanups for the Rockchip
>>>> Samsung HDPTX PHY driver.
>>>>
>>>> The first part of the series (i.e. PATCH 1 & 2) addresses clock rate
>>>> calculation and synchronization issues.  Specifically, it fixes edge
>>>> cases where the PHY PLL is pre-programmed by an external component (like
>>>> a bootloader) or when changing the color depth (bpc) while keeping the
>>>> modeline constant.  Because the Common Clock Framework .set_rate()
>>>> callback might not be invoked if the pixel clock remains unchanged, this
>>>> previously led to out-of-sync states between CCF and the actual HDMI PHY
>>>> configuration.
>>>>
>>>> The second part focuses on code cleanups and modernizing the register
>>>> access.  Now that dw_hdmi_qp driver has fully switched to using
>>>> phy_configure(), we can drop the deprecated TMDS rate setup workarounds
>>>> and the restrict_rate_change flag logic.  Finally, it refactors the
>>>> driver to consistently use standard bitfield macros.
>>>>
>>>> Signed-off-by: Cristian Ciocaltea <cristian.ciocaltea@collabora.com>
>>>> ---
>>>> Changes in v2:
>>>> - Collected Tested-by tags from Thomas and Simon
>>>> - Fixed a typo in commit description of patch 1
>>>> - Added a comment in patch 2 explaining why PLL config errors are
>>>>   ignored for rk_hdptx_phy_consumer_get()
>>>> - Added a missed FIELD_GET conversion for lcpll_hw.pms_sdiv in patch 6
>>>> - Rebased onto latest phy/fixes
>>>> - Link to v1: https://lore.kernel.org/r/20260227-hdptx-clk-fixes-v1-0-f998f2762d0f@collabora.com
>>>
>>> In case you missed my comments from last week on the Sashiko AI review findings
>>> - in short, I don't think there is anything to worry about and the series should
>>> be fine to apply as-is.  Please let me know if you would still prefer a new
>>> revision.
>> Kind reminder..
> 
> Please post a new revision based on phy/next

Done (also addressed the confirmed Sashiko reported issues):

https://lore.kernel.org/all/20260611-hdptx-clk-fixes-v3-0-67b1b0c00e16@collabora.com/

Thanks,
Cristian


^ permalink raw reply

* Re: [PATCH v11 0/6] gpio: siul2-s32g2: add initial GPIO driver
From: Linus Walleij @ 2026-06-11 12:35 UTC (permalink / raw)
  To: Khristine Andreea Barbulescu
  Cc: Bartosz Golaszewski, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Chester Lin, Matthias Brugger, Ghennadi Procopciuc,
	Larisa Grigore, Lee Jones, Shawn Guo, Sascha Hauer, Fabio Estevam,
	Dong Aisheng, Jacky Bai, Greg Kroah-Hartman, Rafael J. Wysocki,
	Srinivas Kandagatla, Alberto Ruiz, Christophe Lizzi, devicetree,
	Enric Balletbo, Eric Chanudet, imx, linux-arm-kernel, linux-gpio,
	linux-kernel, NXP S32 Linux Team, Pengutronix Kernel Team,
	Vincent Guittot
In-Reply-To: <20260610132116.1998140-1-khristineandreea.barbulescu@oss.nxp.com>

On Wed, Jun 10, 2026 at 3:21 PM Khristine Andreea Barbulescu
<khristineandreea.barbulescu@oss.nxp.com> wrote:

> This patch series adds support for basic GPIO
> operations using gpio-regmap.

The series:
Reviewed-by: Linus Walleij <linusw@kernel.org>

This is definitely merge material, I would disregard Sashiko nitpicking at this
point and just merge it. Any additional fixes can be handled in-tree.

Yours,
Linus Walleij


^ permalink raw reply

* [PATCH net-next 0/3] Introduce HSR/PRP HW offload support for PRU-ICSSM Ethernet driver
From: Parvathi Pudi @ 2026-06-11 12:33 UTC (permalink / raw)
  To: andrew+netdev, davem, edumazet, kuba, pabeni, danishanwar,
	parvathi, rogerq, pmohan, afd, basharath, arnd
  Cc: linux-kernel, netdev, linux-arm-kernel, pratheesh, j-rameshbabu,
	vigneshr, praneeth, srk, rogerq, m-malladi, krishna, mohan

Hi,

This series introduces HSR and PRP protocol HW offload support for ICSSM-Prueth driver.
HW offload support for HSR/PRP is implemented using dedicated HSR/PRP firmware running
on 2 PRU cores(PRU-ICSS) as a "DAN" available in AM57xx, AM437x and AM335x.

The following features are offloaded to HW in case of HSR and PRP:
1. L2 forwarding of a HSR frame via traditional store and forward or via cut-through (only for HSR)
2. Transmit frame duplication is offloaded to HW
3. Tag removal on the receive is offloaded to HW
4. Redundant duplicate packet discard on the receive is also offloaded to HW

In HW offload mode, redundant tag insertion in the transmit path will be still done by HSR driver
and firmware updates the LAN information available in the tag on the fly when PRU is transmitting
frame in that respective LAN.

HSR Test Setup:
--------------

     ___________           ______________           ___________
    |           | Link AB |              | Link BC |           |
  __|   AM57*   |_________|AM57/AM43/AM33|_________|   AM57*   |___
 |  | Station A |         |   Station B  |         | Station C |   |
 |  |___________|         |______________|         |___________|   |
 |                                                                 |
 |_________________________________________________________________|
                            Link CA

Steps to switch to HSR forward offload mode:
-------------------------------------------------
Example assuming eth1, eth2 ports of ICSSM on AM57x, AM437x and AM335x EVM's

  1) Bring down both slave interfaces
      ip link set eth1 down
      ip link set eth2 down

  2) Set matching MAC addresses on both slave interfaces
      ip link set eth1 address <mac-addr>
      ip link set eth2 address <mac-addr>

  3) Enable HSR offload for both interfaces
      ethtool -K eth1 hsr-fwd-offload on
      ethtool -K eth1 hsr-dup-offload on
      ethtool -K eth1 hsr-tag-rm-offload on

      ethtool -K eth2 hsr-fwd-offload on
      ethtool -K eth2 hsr-dup-offload on
      ethtool -K eth2 hsr-tag-rm-offload on

  4) Create HSR interface and add slave interfaces to it
      ip link add name hsr0 type hsr slave1 eth1 slave2 eth2 \
    supervision 45 version 1

  5) Add IP address to the HSR interface
      ip addr add <IP_ADDR>/24 dev hsr0

  6) Bring up the HSR interface
      ip link set hsr0 up

  7) Bring up the both slave ports
      ip link set eth1 up
      ip link set eth2 up

Switching back to default mode:
--------------------------------
  1) Delete HSR interface
      ip link delete hsr0

  2) Disable HSR port-to-port offloading mode, packet duplication
      ethtool -K eth1 hsr-fwd-offload off
      ethtool -K eth1 hsr-dup-offload off
      ethtool -K eth1 hsr-tag-rm-offload off

      ethtool -K eth2 hsr-fwd-offload off
      ethtool -K eth2 hsr-dup-offload off
      ethtool -K eth2 hsr-tag-rm-offload off

Testing the port-to-port frame forward offload feature:
-------------------------------------------------------
  1) Connect the LAN cables as shown in the test setup.
  2) Configure Station A and Station C in HSR non-offload mode.
  3) Configure Station B is HSR offload mode.
  4) Since HSR is a redundancy protocol, disconnect cable "Link CA",
     to ensure frames from Station A reach Station C only through
     Station B.
  5) Run iperf3 Server on Station C and client on station A.
  7) Check the CPU usage on Station B.

CPU usage report on Station B using mpstat when running UDP iperf3:
-------------------------------------------------------------------

AM57xx
------

  1) Non-Offload case
  -------------------
  CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest   %idle
  all    0.00    0.00    0.00    0.00    0.00   10.41    0.00    0.00   89.59
    0    0.00    0.00    0.00    0.00    0.00   20.88    0.00    0.00   79.12
    1    0.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00  100.00

  2) Offload case
  ---------------
  CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest   %idle
  all    0.00    0.00    0.10    0.00    0.00    0.73    0.00    0.00   99.17
    0    0.00    0.00    0.20    0.00    0.00    1.46    0.00    0.00   98.34
    1    0.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00  100.00

AM437x
------

  1) Non-Offload case
  -------------------
  CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest   %idle
  all    0.30    0.00    0.80    0.00    0.00   35.19    0.00    0.00   63.72
    0    0.30    0.00    0.80    0.00    0.00   35.19    0.00    0.00   63.72

  2) Offload case
  ---------------
  CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest   %idle
  all    0.10    0.00    0.31    0.10    0.00    1.74    0.00    0.00   97.75
    0    0.10    0.00    0.31    0.10    0.00    1.74    0.00    0.00   97.75

AM335x
------

  1) Non Offload case
  -------------------
  CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest   %idle
  all    0.30    0.00    1.10    0.00    0.00   90.32    0.00    0.00    8.28
    0    0.30    0.00    1.10    0.00    0.00   90.32    0.00    0.00    8.28

  2) Offload case
  ---------------
  CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest   %idle
  all    0.43    0.00    3.61    0.00    0.00   13.28    0.00    0.00   82.68
    0    0.43    0.00    3.61    0.00    0.00   13.28    0.00    0.00   82.68

PRP Test Setup:
---------------

     _________________        LAN-A        __________________
    |                 |eth1-----------eth1|                  |
    | AM57/AM437/AM335|                   | AM57/AM437/AM335 |
    |    station A    |eth2-----------eth2|    station B     |
    |_________________|       LAN-B       |__________________|

Steps to switch to PRP offload mode:
------------------------------------
Example assuming eth1, eth2 ports of ICSSM on AM57x, AM437x and AM335x EVM's

  1) Bring down both slave interfaces
      ip link set eth1 down
      ip link set eth2 down

  2) Set matching MAC addresses on both slave interfaces
      ip link set eth1 address <mac-addr>
      ip link set eth2 address <mac-addr>

  3) Enable PRP offload for both interfaces
      ethtool -K eth1 hsr-dup-offload on
      ethtool -K eth1 hsr-tag-rm-offload on

      ethtool -K eth2 hsr-dup-offload on
      ethtool -K eth2 hsr-tag-rm-offload on

  4) Create PRP interface and add slave interfaces to it
      ip link add name prp0 type hsr slave1 eth1 slave2 eth2 \
    supervision 45 proto 1

  5) Add IP address to the PRP interface
      ip addr add <IP_ADDR>/24 dev prp0

  6) Bring up the PRP interface
      ip link set prp0 up

  7) Bring up the both slave ports
      ip link set eth1 up
      ip link set eth2 up

Switching back to default mode:
--------------------------------
  1) Delete PRP interface
      ip link delete prp0

  2) Disable PRP offloading mode
      ethtool -K eth1 hsr-dup-offload off
      ethtool -K eth1 hsr-tag-rm-offload off

      ethtool -K eth2 hsr-dup-offload off
      ethtool -K eth2 hsr-tag-rm-offload off

Testing the PRP offload feature:
--------------------------------
  1) Connect eth1 of Station A to eth1 of Station B (LAN-A).
     Connect eth2 of Station A to eth2 of Station B (LAN-B).
  2) Configure Station A in PRP non-offload mode.
  3) Configure Station B in PRP offload mode.
  4) Run iperf3 Server on Station B and client on Station A.
  5) Check the CPU usage on Station B.
  6) Disconnect LAN-B cable to verify Station A frames still reach
     Station B over LAN-A with no traffic interruption.
  7) Reconnect LAN-B and disconnect LAN-A, verify the same.

CPU usage report on Station B using mpstat when running UDP iperf3:
-------------------------------------------------------------------

AM57x
-----

  1) Non Offload case
  -------------------
  CPU    %usr   %nice    %sys  %iowait    %irq   %soft  %steal  %guest   %idle
  all    2.04    0.00   18.85    0.00     0.00   27.83    0.00    0.00   51.27
    0    1.80    0.00   21.56    0.00     0.00   54.89    0.00    0.00   21.76
    1    2.29    0.00   16.14    0.00     0.00    0.80    0.00    0.00   80.78

  2) Offload case
  ---------------
  CPU    %usr   %nice    %sys  %iowait    %irq   %soft  %steal  %guest   %idle
  all    2.79    0.00   18.36    0.00     0.00   18.16    0.00    0.00   60.68
    0    3.89    0.00   22.16    0.00     0.00   36.13    0.00    0.00   37.82
    1    1.69    0.00   14.56    0.00     0.00    0.20    0.00    0.00   83.55

AM437x
------

  1) Non Offload case
  -------------------
  CPU    %usr   %nice    %sys   %iowait  %irq   %soft    %steal  %guest   %idle
  all    5.68    0.00    43.27   0.00    0.00    43.57    0.00     0.00    7.48
    0    5.68    0.00    43.27   0.00    0.00    43.57    0.00     0.00    7.48

  2) Offload case
  ---------------
  CPU    %usr   %nice    %sys   %iowait  %irq   %soft    %steal  %guest   %idle
  all    6.39    0.00    42.86   0.00    0.00   32.57    0.00      0.00   18.18
    0    6.39    0.00    42.86   0.00    0.00   32.57    0.00      0.00   18.18

AM335x
------

  1) Non Offload case
  -------------------
  CPU    %usr   %nice    %sys    %iowait  %irq   %soft    %steal  %guest   %idle
  all    2.29    0.00    14.04    0.00    0.00    75.50    0.00    0.00    8.17
    0    2.29    0.00    14.04    0.00    0.00    75.50    0.00    0.00    8.17

  2) Offload case
  ---------------
  CPU    %usr   %nice    %sys    %iowait  %irq   %soft    %steal  %guest   %idle
  all    5.70    0.00    48.50    0.00    0.00    29.00    0.00    0.00    16.80
    0    5.70    0.00    48.50    0.00    0.00    29.00    0.00    0.00    16.80

Note:
  hsr-tag-rm-offload and hsr-dup-offload are tightly coupled in the firmware implementation.
  They both need to be enabled / disabled together and hsr-tag-ins-offload is unsupported.

Roger Quadros (3):
  net: ti: icssm-prueth: Add HSR and PRP HW offload mode support for
    AM57xx, AM437x and AM335x
  net: ti: icssm-prueth: Add priority based RX IRQ handlers
  net: ti: icssm-prueth: Support duplicate HW offload feature for HSR
    and PRP

 drivers/net/ethernet/ti/Makefile              |   2 +-
 .../ethernet/ti/icssm/icssm_lre_firmware.h    | 141 ++++
 drivers/net/ethernet/ti/icssm/icssm_prueth.c  | 604 +++++++++++++++++-
 drivers/net/ethernet/ti/icssm/icssm_prueth.h  |  68 +-
 .../ethernet/ti/icssm/icssm_prueth_common.c   | 282 ++++++++
 .../net/ethernet/ti/icssm/icssm_prueth_lre.c  | 223 +++++++
 .../net/ethernet/ti/icssm/icssm_prueth_lre.h  |  19 +
 .../ethernet/ti/icssm/icssm_prueth_switch.c   | 310 ++++++++-
 .../ethernet/ti/icssm/icssm_prueth_switch.h   |   1 +
 drivers/net/ethernet/ti/icssm/icssm_switch.h  |  35 +-
 10 files changed, 1639 insertions(+), 46 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

* [PATCH net-next 3/3] net: ti: icssm-prueth: Support duplicate HW offload feature for HSR and PRP
From: Parvathi Pudi @ 2026-06-11 12:33 UTC (permalink / raw)
  To: andrew+netdev, davem, edumazet, kuba, pabeni, danishanwar,
	parvathi, rogerq, pmohan, afd, basharath, arnd
  Cc: linux-kernel, netdev, linux-arm-kernel, pratheesh, j-rameshbabu,
	vigneshr, praneeth, srk, rogerq, m-malladi, krishna, mohan
In-Reply-To: <20260611123636.376577-1-parvathi@couthit.com>

From: Roger Quadros <rogerq@ti.com>

In HSR and PRP modes each outgoing frame must be sent on both PRU slave
ports.

Previously the driver was writing the frame into each port's transmit queue
independently after updating the tags resulting in performing two OCMC
buffer copy operations.

Frame duplicate offloading is implemented with a common shared queue
between the two ports. The driver writes the frame once into OCMC RAM,
each port reads from the shared queue and replicates the transmission to
both PRU ports, synchronising between PRU ports are maintained within
firmware with appropriate handling.

For HSR the driver inspects the encapsulated ethertype in the HSR tag.
PTP frames (ETH_P_1588) are sent on the directed port only to avoid double
duplication and all other HSR frames are duplicated to both ports.
VLAN-tagged HSR frames are handled by advancing past the 4-byte VLAN header
before reading the HSR tag.

For PRP the driver checks the 6-byte RCT trailer for the ETH_P_PRP suffix
to identify redundancy-tagged frames. Frames without an RCT are sent on the
originating port only.

Signed-off-by: Roger Quadros <rogerq@ti.com>
Signed-off-by: Andrew F. Davis <afd@ti.com>
Signed-off-by: Parvathi Pudi <parvathi@couthit.com>
---
 drivers/net/ethernet/ti/icssm/icssm_prueth.c  | 218 +++++++++++-
 drivers/net/ethernet/ti/icssm/icssm_prueth.h  |   9 +-
 .../ethernet/ti/icssm/icssm_prueth_common.c   |   7 +-
 .../ethernet/ti/icssm/icssm_prueth_switch.c   | 310 +++++++++++++++++-
 .../ethernet/ti/icssm/icssm_prueth_switch.h   |   1 +
 drivers/net/ethernet/ti/icssm/icssm_switch.h  |  35 +-
 6 files changed, 549 insertions(+), 31 deletions(-)

diff --git a/drivers/net/ethernet/ti/icssm/icssm_prueth.c b/drivers/net/ethernet/ti/icssm/icssm_prueth.c
index 58a6935dd809..e2d66239380b 100644
--- a/drivers/net/ethernet/ti/icssm/icssm_prueth.c
+++ b/drivers/net/ethernet/ti/icssm/icssm_prueth.c
@@ -36,6 +36,7 @@
 #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
@@ -76,6 +77,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)
@@ -94,6 +121,17 @@ static void icssm_prueth_write_reg(struct prueth *prueth,
 static enum pruss_mem pruss_mem_ids[] = { PRUSS_MEM_DRAM0, PRUSS_MEM_DRAM1,
 					  PRUSS_MEM_SHRD_RAM2 };
 
+struct prp_txopt_rct {
+	__be16 sequence_nr;
+	__be16 lan_id_and_lsdu_size;
+	__be16 prp_suffix;
+};
+
+struct hsr_txopt_ethhdr {
+	struct ethhdr ethhdr;
+	struct hsr_tag hsr_tag;
+};
+
 static const struct prueth_queue_info queue_infos[][NUM_QUEUES] = {
 	[PRUETH_PORT_QUEUE_HOST] = {
 		[PRUETH_QUEUE1] = {
@@ -546,15 +584,25 @@ static int icssm_prueth_tx_enqueue(struct prueth_emac *emac,
 				   struct sk_buff *skb,
 				   enum prueth_queue_id queue_id)
 {
+	struct prueth_queue_desc __iomem *queue_desc_other_port = NULL;
 	struct prueth_queue_desc __iomem *queue_desc;
 	const struct prueth_queue_info *txqueue;
 	struct net_device *ndev = emac->ndev;
 	struct prueth *prueth = emac->prueth;
+	struct hsr_txopt_ethhdr *hsr_ethhdr;
 	unsigned int buffer_desc_count;
+	struct prueth_emac *other_emac;
 	int free_blocks, update_block;
+	struct vlan_ethhdr *vlan_hdr;
 	bool buffer_wrapped = false;
 	int write_block, read_block;
+	int free_blocks_other_port;
+	int read_block_other_port;
 	void *src_addr, *dst_addr;
+	u16 bd_rd_ptr_other_port;
+	struct ethhdr *ethhdr;
+	bool is_vlan = false;
+	bool link_up = false;
 	int pkt_block_size;
 	void __iomem *sram;
 	void __iomem *dram;
@@ -562,6 +610,14 @@ static int icssm_prueth_tx_enqueue(struct prueth_emac *emac,
 	u16 update_wr_ptr;
 	u32 wr_buf_desc;
 	void *ocmc_ram;
+	__be16 proto;
+	u8 *hdr;
+
+	other_emac = emac->prueth->emac[(emac->port_id == PRUETH_PORT_MII0) ?
+				PRUETH_PORT_MII1 - 1 : PRUETH_PORT_MII0 - 1];
+
+	if (prueth_is_lre(prueth) && (emac->link || other_emac->link))
+		link_up = true;
 
 	if (!PRUETH_IS_EMAC(prueth))
 		dram = prueth->mem[PRUETH_MEM_DRAM1].va;
@@ -579,7 +635,10 @@ static int icssm_prueth_tx_enqueue(struct prueth_emac *emac,
 	pktlen = skb->len;
 	/* Get the tx queue */
 	queue_desc = emac->tx_queue_descs + queue_id;
-	if (!PRUETH_IS_EMAC(prueth))
+	/* Tx queue context */
+	if (prueth_is_lre(prueth))
+		txqueue = &lre_queue_infos[txport][queue_id];
+	else if (PRUETH_IS_SWITCH(prueth))
 		txqueue = &sw_queue_infos[txport][queue_id];
 	else
 		txqueue = &queue_infos[txport][queue_id];
@@ -602,6 +661,29 @@ static int icssm_prueth_tx_enqueue(struct prueth_emac *emac,
 		free_blocks = buffer_desc_count;
 	}
 
+	/* Fetch queue state for the second LRE port */
+	if (prueth_is_lre(prueth) && link_up) {
+		queue_desc_other_port = emac->tx_queue_descs_other_port +
+					queue_id;
+		bd_rd_ptr_other_port = readw(&queue_desc_other_port->rd_ptr);
+
+		read_block_other_port = (bd_rd_ptr_other_port -
+					 txqueue->buffer_desc_offset) / BD_SIZE;
+
+		if (write_block > read_block_other_port) {
+			free_blocks_other_port = buffer_desc_count -
+						 write_block;
+			free_blocks_other_port += read_block_other_port;
+		} else if (write_block < read_block_other_port) {
+			free_blocks_other_port = read_block_other_port -
+						 write_block;
+		} else {
+			free_blocks_other_port = buffer_desc_count;
+		}
+
+		if (free_blocks_other_port < free_blocks)
+			free_blocks = free_blocks_other_port;
+	}
 	pkt_block_size = DIV_ROUND_UP(pktlen, ICSS_BLOCK_SIZE);
 	if (pkt_block_size > free_blocks) /* out of queue space */
 		return -ENOBUFS;
@@ -651,6 +733,57 @@ static int icssm_prueth_tx_enqueue(struct prueth_emac *emac,
 	if (PRUETH_IS_HSR(prueth))
 		wr_buf_desc |= BIT(PRUETH_BD_HSR_FRAME_SHIFT);
 
+	if (prueth_is_lre(prueth)) {
+		ethhdr = (struct ethhdr *)skb_mac_header(skb);
+		proto = ethhdr->h_proto;
+
+		if (proto == htons(ETH_P_8021Q)) {
+			vlan_hdr = (struct vlan_ethhdr *)ethhdr;
+			proto = vlan_hdr->h_vlan_encapsulated_proto;
+			is_vlan = true;
+		}
+
+		/* Extract HSR sequence number and LAN ID
+		 * from the tag for the Buffer Descriptor
+		 */
+		if (proto == htons(ETH_P_HSR)) {
+			hdr = skb_mac_header(skb);
+
+			if (is_vlan) {
+				hsr_ethhdr =
+					(struct hsr_txopt_ethhdr *)(hdr +
+								    VLAN_HLEN);
+			} else {
+				hsr_ethhdr = (struct hsr_txopt_ethhdr *)hdr;
+			}
+
+			/* PTP frames (ETH_P_1588) carry no LAN ID
+			 * in the HSR tag
+			 */
+			if (hsr_ethhdr->hsr_tag.encap_proto !=
+			    htons(ETH_P_1588)) {
+				wr_buf_desc |= PRUETH_BD_LAN_INFO_MASK;
+			} else {
+				wr_buf_desc |= (txport <<
+						PRUETH_BD_LAN_A_SHIFT);
+			}
+			wr_buf_desc |= PRUETH_BD_RED_PKT_MASK;
+		} else {
+			/* Read PRP RCT to extract sequence number and LAN ID */
+			struct prp_txopt_rct *rct =
+				(struct prp_txopt_rct *)(skb_tail_pointer(skb) -
+							 ICSSM_LRE_TAG_SIZE);
+
+			if (rct->prp_suffix == htons(ETH_P_PRP)) {
+				wr_buf_desc |= PRUETH_BD_LAN_INFO_MASK;
+				wr_buf_desc |= PRUETH_BD_RED_PKT_MASK;
+			} else {
+				wr_buf_desc |= (txport <<
+						PRUETH_BD_LAN_A_SHIFT);
+			}
+		}
+	}
+
 	sram = prueth->mem[PRUETH_MEM_SHARED_RAM].va;
 	if (!PRUETH_IS_EMAC(prueth))
 		writel(wr_buf_desc, sram + readw(&queue_desc->wr_ptr));
@@ -663,6 +796,10 @@ static int icssm_prueth_tx_enqueue(struct prueth_emac *emac,
 	update_wr_ptr = txqueue->buffer_desc_offset + (update_block * BD_SIZE);
 	writew(update_wr_ptr, &queue_desc->wr_ptr);
 
+	/* update the write pointer in queue descriptor of other port */
+	if (prueth_is_lre(prueth) && link_up)
+		writew(update_wr_ptr, &queue_desc_other_port->wr_ptr);
+
 	return 0;
 }
 
@@ -675,8 +812,10 @@ void icssm_parse_packet_info(struct prueth *prueth, u32 buffer_descriptor,
 	else
 		pkt_info->start_offset = false;
 
-	pkt_info->port = (buffer_descriptor & PRUETH_BD_PORT_MASK) >>
-			 PRUETH_BD_PORT_SHIFT;
+	/* Flag from BD to indicate packet is valid for HOST or not. */
+	pkt_info->host_recv_flag = !!(buffer_descriptor &
+				      PRUETH_BD_HOST_RECV_MASK);
+
 	pkt_info->length = (buffer_descriptor & PRUETH_BD_LENGTH_MASK) >>
 			   PRUETH_BD_LENGTH_SHIFT;
 	pkt_info->broadcast = !!(buffer_descriptor & PRUETH_BD_BROADCAST_MASK);
@@ -708,11 +847,13 @@ int icssm_emac_rx_packet(struct prueth_emac *emac, u16 *bd_rd_ptr,
 	int read_block, update_block;
 	unsigned int actual_pkt_len;
 	bool buffer_wrapped = false;
+	int adjust_for_hsr_tag = 0;
 	void *src_addr, *dst_addr;
 	u16 start_offset = 0;
 	struct sk_buff *skb;
 	int pkt_block_size;
 	void *ocmc_ram;
+	u16 type;
 
 	if (PRUETH_IS_HSR(emac->prueth))
 		start_offset = (pkt_info->start_offset ?
@@ -739,9 +880,19 @@ int icssm_emac_rx_packet(struct prueth_emac *emac, u16 *bd_rd_ptr,
 	/* calculate new pointer in ram */
 	*bd_rd_ptr = rxqueue->buffer_desc_offset + (update_block * BD_SIZE);
 
+	if (PRUETH_IS_HSR(emac->prueth)) {
+		if (!pkt_info->host_recv_flag)
+			return 0;
+	}
+
 	/* Exclude the HSR tag bytes already stripped by firmware, if any. */
 	actual_pkt_len = pkt_info->length - start_offset;
 
+	if (PRUETH_IS_HSR(emac->prueth)) {
+		if (!start_offset && !pkt_info->timestamp)
+			actual_pkt_len -= ICSSM_LRE_TAG_SIZE;
+	}
+
 	/* Allocate a socket buffer for this packet */
 	skb = netdev_alloc_skb_ip_align(ndev, actual_pkt_len);
 	if (!skb) {
@@ -762,6 +913,29 @@ int icssm_emac_rx_packet(struct prueth_emac *emac, u16 *bd_rd_ptr,
 		   (read_block * ICSS_BLOCK_SIZE);
 	src_addr += start_offset;
 
+	/* Copy destination and source MAC address */
+	memcpy(dst_addr, src_addr, PRUETH_ETHER_TYPE_OFFSET);
+	src_addr += PRUETH_ETHER_TYPE_OFFSET;
+	dst_addr += PRUETH_ETHER_TYPE_OFFSET;
+
+	adjust_for_hsr_tag += PRUETH_ETHER_TYPE_OFFSET;
+
+	/* Check for VLAN tag */
+	type = get_unaligned_be16(src_addr);
+
+	if (type == ETH_P_8021Q) {
+		memcpy(dst_addr, src_addr, VLAN_HLEN);
+		src_addr += VLAN_HLEN;
+		dst_addr += VLAN_HLEN;
+		adjust_for_hsr_tag += VLAN_HLEN;
+	}
+
+	/* HSR tag removal handling */
+	if (PRUETH_IS_HSR(emac->prueth)) {
+		if (!start_offset && !pkt_info->timestamp)
+			src_addr += ICSSM_LRE_TAG_SIZE;
+	}
+
 	/* Copy the data from PRU buffers(OCMC) to socket buffer(DRAM) */
 	if (buffer_wrapped) { /* wrapped around buffer */
 		int bytes = (buffer_desc_count - read_block) * ICSS_BLOCK_SIZE;
@@ -777,19 +951,25 @@ int icssm_emac_rx_packet(struct prueth_emac *emac, u16 *bd_rd_ptr,
 		/* If applicable, account for the HSR tag removed */
 		bytes -= start_offset;
 
+		if (PRUETH_IS_HSR(emac->prueth)) {
+			if (!start_offset && !pkt_info->timestamp)
+				bytes -= ICSSM_LRE_TAG_SIZE;
+		}
+
 		/* copy non-wrapped part */
-		memcpy(dst_addr, src_addr, bytes);
+		memcpy(dst_addr, src_addr, bytes - adjust_for_hsr_tag);
 
 		/* copy wrapped part */
-		dst_addr += bytes;
+		dst_addr += (bytes - adjust_for_hsr_tag);
 		remaining = actual_pkt_len - bytes;
 
 		src_addr = ocmc_ram + rxqueue->buffer_offset;
 		memcpy(dst_addr, src_addr, remaining);
 		src_addr += remaining;
 	} else {
-		memcpy(dst_addr, src_addr, actual_pkt_len);
-		src_addr += actual_pkt_len;
+		memcpy(dst_addr, src_addr, actual_pkt_len -
+		       adjust_for_hsr_tag);
+		src_addr += actual_pkt_len - adjust_for_hsr_tag;
 	}
 
 	if (PRUETH_IS_SWITCH(emac->prueth)) {
@@ -1313,11 +1493,20 @@ static enum netdev_tx icssm_emac_ndo_start_xmit(struct sk_buff *skb,
 						struct net_device *ndev)
 {
 	struct prueth_emac *emac = netdev_priv(ndev);
+	raw_spinlock_t *lock_queue;
 	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];
+
+	raw_spin_lock(lock_queue);
 	ret = icssm_prueth_tx_enqueue(emac, skb, qid);
+	raw_spin_unlock(lock_queue);
 	if (ret) {
 		if (ret != -ENOBUFS && netif_msg_tx_err(emac) &&
 		    net_ratelimit())
@@ -1682,6 +1871,9 @@ static int icssm_prueth_netdev_init(struct prueth *prueth,
 	spin_lock_init(&emac->lock);
 	spin_lock_init(&emac->addr_lock);
 
+	raw_spin_lock_init(&emac->host_queue_lock[0]);
+	raw_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)) {
@@ -1719,7 +1911,9 @@ static int icssm_prueth_netdev_init(struct prueth *prueth,
 
 	if (prueth->support_lre)
 		ndev->hw_features |=
-			(NETIF_F_HW_HSR_FWD | NETIF_F_HW_HSR_TAG_RM);
+			(NETIF_F_HW_HSR_FWD |
+			 NETIF_F_HW_HSR_TAG_RM |
+			 NETIF_F_HW_HSR_DUP);
 
 	ndev->dev.of_node = eth_node;
 	ndev->netdev_ops = &emac_netdev_ops;
@@ -1947,9 +2141,11 @@ static int icssm_prueth_hsr_port_link(struct net_device *ndev,
 		if (prueth->hsr_members & BIT(PRUETH_PORT_MII0) &&
 		    prueth->hsr_members & BIT(PRUETH_PORT_MII1)) {
 			if (!(emac0->ndev->features &
-			    (NETIF_F_HW_HSR_TAG_RM | NETIF_F_HW_HSR_FWD)) &&
+			    (NETIF_F_HW_HSR_TAG_RM | NETIF_F_HW_HSR_FWD |
+			     NETIF_F_HW_HSR_DUP)) &&
 			    !(emac1->ndev->features &
-			    (NETIF_F_HW_HSR_TAG_RM | NETIF_F_HW_HSR_FWD)))
+			    (NETIF_F_HW_HSR_TAG_RM | NETIF_F_HW_HSR_FWD |
+			     NETIF_F_HW_HSR_DUP)))
 				return -EOPNOTSUPP;
 
 			ret = icssm_prueth_change_mode(prueth, mode);
@@ -2275,6 +2471,8 @@ static int icssm_prueth_probe(struct platform_device *pdev)
 	}
 
 	prueth->support_lre = has_lre;
+	raw_spin_lock_init(&prueth->lre_host_queue_lock[0]);
+	raw_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 60dc451f79e5..b7f6919fbd45 100644
--- a/drivers/net/ethernet/ti/icssm/icssm_prueth.h
+++ b/drivers/net/ethernet/ti/icssm/icssm_prueth.h
@@ -104,7 +104,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;
@@ -239,6 +239,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;
@@ -262,6 +264,7 @@ struct prueth_emac {
 	spinlock_t lock;
 	spinlock_t addr_lock;   /* serialize access to VLAN/MC filter table */
 
+	raw_spinlock_t host_queue_lock[NUM_QUEUES / 2];
 	struct hrtimer tx_hrtimer;
 	struct prueth_emac_stats stats;
 	int offload_fwd_mark;
@@ -311,9 +314,13 @@ struct prueth {
 	u8 emac_configured;
 	u8 hsr_members;
 	u8 br_members;
+
+	/* Per-queue TX lock - LRE uses only the two high-priority queues */
+	raw_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 4820def5f9e1..27f94a0e0735 100644
--- a/drivers/net/ethernet/ti/icssm/icssm_prueth_common.c
+++ b/drivers/net/ethernet/ti/icssm/icssm_prueth_common.c
@@ -141,16 +141,13 @@ static int icssm_prueth_common_emac_rx_packets(struct prueth_emac *emac,
 			used++;
 		}
 
-		/* Zero the BD after consuming it, a misaligned rd_ptr
-		 * would otherwise mistake stale data for a valid incoming
-		 * frame.
+		/* Leave the BD intact after reading. Firmware reuses it to
+		 * forward the frame to the second LRE port.
 		 */
 		if (port == 0) {
-			writel(0, shared_ram + bd_rd_ptr);
 			writew(update_rd_ptr, &queue_desc->rd_ptr);
 			bd_rd_ptr = update_rd_ptr;
 		} else {
-			writel(0, shared_ram + bd_rd_ptr_o);
 			writew(update_rd_ptr, &queue_desc_o->rd_ptr);
 			bd_rd_ptr_o = update_rd_ptr;
 		}
diff --git a/drivers/net/ethernet/ti/icssm/icssm_prueth_switch.c b/drivers/net/ethernet/ti/icssm/icssm_prueth_switch.c
index 66866ea37913..1b2486170ab3 100644
--- a/drivers/net/ethernet/ti/icssm/icssm_prueth_switch.c
+++ b/drivers/net/ethernet/ti/icssm/icssm_prueth_switch.c
@@ -199,6 +199,189 @@ static const struct prueth_queue_info rx_queue_infos[][NUM_QUEUES] = {
 	},
 };
 
+/* Tx Queue context for HSR and PRP */
+const struct prueth_queue_info lre_queue_infos[][NUM_QUEUES] = {
+	[PRUETH_PORT_QUEUE_HOST] = {
+		[PRUETH_QUEUE1] = {
+			P0_Q1_BUFFER_OFFSET,
+			P0_QUEUE_DESC_OFFSET,
+			P0_Q1_BD_OFFSET,
+			P0_Q1_BD_OFFSET + ((HOST_QUEUE_1_SIZE - 1) * BD_SIZE),
+		},
+		[PRUETH_QUEUE2] = {
+			P0_Q2_BUFFER_OFFSET,
+			P0_QUEUE_DESC_OFFSET + 8,
+			P0_Q2_BD_OFFSET,
+			P0_Q2_BD_OFFSET + ((HOST_QUEUE_2_SIZE - 1) * BD_SIZE),
+		},
+		[PRUETH_QUEUE3] = {
+			P0_Q3_BUFFER_OFFSET,
+			P0_QUEUE_DESC_OFFSET + 16,
+			P0_Q3_BD_OFFSET,
+			P0_Q3_BD_OFFSET + ((HOST_QUEUE_3_SIZE - 1) * BD_SIZE),
+		},
+		[PRUETH_QUEUE4] = {
+			P0_Q4_BUFFER_OFFSET,
+			P0_QUEUE_DESC_OFFSET + 24,
+			P0_Q4_BD_OFFSET,
+			P0_Q4_BD_OFFSET + ((HOST_QUEUE_4_SIZE - 1) * BD_SIZE),
+		},
+	},
+	[PRUETH_PORT_QUEUE_MII0] = {
+		[PRUETH_QUEUE1] = {
+			P0_Q3_BUFFER_OFFSET,
+			P0_Q3_BUFFER_OFFSET +
+				((HOST_QUEUE_3_SIZE - 1) * ICSS_BLOCK_SIZE),
+			P0_Q3_BD_OFFSET,
+			P0_Q3_BD_OFFSET + ((HOST_QUEUE_3_SIZE - 1) * BD_SIZE),
+		},
+		[PRUETH_QUEUE2] = {
+			P0_Q4_BUFFER_OFFSET,
+			P0_Q4_BUFFER_OFFSET +
+				((HOST_QUEUE_4_SIZE - 1) * ICSS_BLOCK_SIZE),
+			P0_Q4_BD_OFFSET,
+			P0_Q4_BD_OFFSET + ((HOST_QUEUE_4_SIZE - 1) * BD_SIZE),
+		},
+		[PRUETH_QUEUE3] = {
+			P1_Q3_TXOPT_BUFFER_OFFSET,
+			P1_Q3_TXOPT_BUFFER_OFFSET +
+				((QUEUE_3_TXOPT_SIZE - 1) * ICSS_BLOCK_SIZE),
+			P1_Q3_TXOPT_BD_OFFSET,
+			P1_Q3_TXOPT_BD_OFFSET +
+				((QUEUE_3_TXOPT_SIZE - 1) * BD_SIZE),
+		},
+		[PRUETH_QUEUE4] = {
+			P2_Q1_TXOPT_BUFFER_OFFSET,
+			P2_Q1_TXOPT_BUFFER_OFFSET +
+				((QUEUE_4_TXOPT_SIZE - 1) * ICSS_BLOCK_SIZE),
+			P2_Q1_TXOPT_BD_OFFSET,
+			P2_Q1_TXOPT_BD_OFFSET +
+				((QUEUE_4_TXOPT_SIZE - 1) * BD_SIZE),
+		},
+	},
+	[PRUETH_PORT_QUEUE_MII1] = {
+		[PRUETH_QUEUE1] = {
+			P0_Q1_BUFFER_OFFSET,
+			P0_Q1_BUFFER_OFFSET +
+				((HOST_QUEUE_1_SIZE - 1) * ICSS_BLOCK_SIZE),
+			P0_Q1_BD_OFFSET,
+			P0_Q1_BD_OFFSET +
+				((HOST_QUEUE_1_SIZE - 1) * BD_SIZE),
+		},
+		[PRUETH_QUEUE2] = {
+			P0_Q2_BUFFER_OFFSET,
+			P0_Q2_BUFFER_OFFSET +
+				((HOST_QUEUE_2_SIZE - 1) * ICSS_BLOCK_SIZE),
+			P0_Q2_BD_OFFSET,
+			P0_Q2_BD_OFFSET +
+				((HOST_QUEUE_2_SIZE - 1) * BD_SIZE),
+		},
+		[PRUETH_QUEUE3] = {
+			P1_Q3_TXOPT_BUFFER_OFFSET,
+			P1_Q3_TXOPT_BUFFER_OFFSET +
+				((QUEUE_3_TXOPT_SIZE - 1) * ICSS_BLOCK_SIZE),
+			P1_Q3_TXOPT_BD_OFFSET,
+			P1_Q3_TXOPT_BD_OFFSET +
+				((QUEUE_3_TXOPT_SIZE - 1) * BD_SIZE),
+		},
+		[PRUETH_QUEUE4] = {
+			P2_Q1_TXOPT_BUFFER_OFFSET,
+			P2_Q1_TXOPT_BUFFER_OFFSET +
+				((QUEUE_4_TXOPT_SIZE - 1) * ICSS_BLOCK_SIZE),
+			P2_Q1_TXOPT_BD_OFFSET,
+			P2_Q1_TXOPT_BD_OFFSET +
+				((QUEUE_4_TXOPT_SIZE - 1) * BD_SIZE),
+		},
+
+	},
+};
+
+/* Rx Queue Context for HSR and PRP */
+static const struct prueth_queue_info lre_rx_queue_infos[][NUM_QUEUES] = {
+	[PRUETH_PORT_QUEUE_HOST] = {
+		[PRUETH_QUEUE1] = {
+			P0_Q1_BUFFER_OFFSET,
+			HOST_QUEUE_DESC_OFFSET,
+			P0_Q1_BD_OFFSET,
+			P0_Q1_BD_OFFSET + ((HOST_QUEUE_1_SIZE - 1) * BD_SIZE),
+		},
+		[PRUETH_QUEUE2] = {
+			P0_Q2_BUFFER_OFFSET,
+			HOST_QUEUE_DESC_OFFSET + 8,
+			P0_Q2_BD_OFFSET,
+			P0_Q2_BD_OFFSET + ((HOST_QUEUE_2_SIZE - 1) * BD_SIZE),
+		},
+		[PRUETH_QUEUE3] = {
+			P0_Q3_BUFFER_OFFSET,
+			HOST_QUEUE_DESC_OFFSET + 16,
+			P0_Q3_BD_OFFSET,
+			P0_Q3_BD_OFFSET + ((HOST_QUEUE_3_SIZE - 1) * BD_SIZE),
+		},
+		[PRUETH_QUEUE4] = {
+			P0_Q4_BUFFER_OFFSET,
+			HOST_QUEUE_DESC_OFFSET + 24,
+			P0_Q4_BD_OFFSET,
+			P0_Q4_BD_OFFSET + ((HOST_QUEUE_4_SIZE - 1) * BD_SIZE),
+		},
+	},
+	[PRUETH_PORT_QUEUE_MII0] = {
+		[PRUETH_QUEUE1] = {
+			P0_Q3_BUFFER_OFFSET,
+			P1_QUEUE_DESC_OFFSET,
+			P0_Q3_BD_OFFSET,
+			P0_Q3_BD_OFFSET + ((HOST_QUEUE_3_SIZE - 1) * BD_SIZE),
+		},
+		[PRUETH_QUEUE2] = {
+			P0_Q4_BUFFER_OFFSET,
+			P1_QUEUE_DESC_OFFSET + 8,
+			P0_Q4_BD_OFFSET,
+			P0_Q4_BD_OFFSET + ((HOST_QUEUE_4_SIZE - 1) * BD_SIZE),
+		},
+		[PRUETH_QUEUE3] = {
+			P1_Q3_TXOPT_BUFFER_OFFSET,
+			P1_QUEUE_DESC_OFFSET + 16,
+			P1_Q3_TXOPT_BD_OFFSET,
+			P1_Q3_TXOPT_BD_OFFSET +
+				((QUEUE_3_TXOPT_SIZE - 1) * BD_SIZE),
+		},
+		[PRUETH_QUEUE4] = {
+			P2_Q1_TXOPT_BUFFER_OFFSET,
+			P1_QUEUE_DESC_OFFSET + 24,
+			P2_Q1_TXOPT_BD_OFFSET,
+			P2_Q1_TXOPT_BD_OFFSET +
+				((QUEUE_4_TXOPT_SIZE - 1) * BD_SIZE),
+		},
+	},
+	[PRUETH_PORT_QUEUE_MII1] = {
+		[PRUETH_QUEUE1] = {
+			P0_Q1_BUFFER_OFFSET,
+			P2_QUEUE_DESC_OFFSET,
+			P0_Q1_BD_OFFSET,
+			P0_Q1_BD_OFFSET + ((HOST_QUEUE_1_SIZE - 1) * BD_SIZE),
+		},
+		[PRUETH_QUEUE2] = {
+			P0_Q2_BUFFER_OFFSET,
+			P2_QUEUE_DESC_OFFSET + 8,
+			P0_Q2_BD_OFFSET,
+			P0_Q2_BD_OFFSET + ((HOST_QUEUE_2_SIZE - 1) * BD_SIZE),
+		},
+		[PRUETH_QUEUE3] = {
+			P1_Q3_TXOPT_BUFFER_OFFSET,
+			P2_QUEUE_DESC_OFFSET + 16,
+			P1_Q3_TXOPT_BD_OFFSET,
+			P1_Q3_TXOPT_BD_OFFSET +
+				((QUEUE_3_TXOPT_SIZE - 1) * BD_SIZE),
+		},
+		[PRUETH_QUEUE4] = {
+			P2_Q1_TXOPT_BUFFER_OFFSET,
+			P2_QUEUE_DESC_OFFSET + 24,
+			P2_Q1_TXOPT_BD_OFFSET,
+			P2_Q1_TXOPT_BD_OFFSET +
+				((QUEUE_4_TXOPT_SIZE - 1) * BD_SIZE),
+		},
+	},
+};
+
 void icssm_prueth_sw_free_fdb_table(struct prueth *prueth)
 {
 	if (prueth->emac_configured)
@@ -856,8 +1039,12 @@ void icssm_prueth_sw_hostconfig(struct prueth *prueth)
 
 	/* queue information table */
 	dram = dram1_base + P0_Q1_RX_CONTEXT_OFFSET;
-	memcpy_toio(dram, sw_queue_infos[PRUETH_PORT_QUEUE_HOST],
-		    sizeof(sw_queue_infos[PRUETH_PORT_QUEUE_HOST]));
+	if (prueth_is_lre(prueth))
+		memcpy_toio(dram, lre_queue_infos[PRUETH_PORT_QUEUE_HOST],
+			    sizeof(lre_queue_infos[PRUETH_PORT_QUEUE_HOST]));
+	else
+		memcpy_toio(dram, sw_queue_infos[PRUETH_PORT_QUEUE_HOST],
+			    sizeof(sw_queue_infos[PRUETH_PORT_QUEUE_HOST]));
 
 	/* buffer descriptor offset table*/
 	dram = dram1_base + QUEUE_DESCRIPTOR_OFFSET_ADDR;
@@ -882,8 +1069,15 @@ void icssm_prueth_sw_hostconfig(struct prueth *prueth)
 
 	/* queue table */
 	dram = dram1_base + P0_QUEUE_DESC_OFFSET;
-	memcpy_toio(dram, queue_descs[PRUETH_PORT_QUEUE_HOST],
-		    sizeof(queue_descs[PRUETH_PORT_QUEUE_HOST]));
+	if (prueth_is_lre(prueth))
+		memcpy_toio(dram,
+			    hsr_prp_txopt_queue_descs[PRUETH_PORT_QUEUE_HOST],
+			    sizeof(hsr_prp_txopt_queue_descs
+				    [PRUETH_PORT_QUEUE_HOST]));
+	else
+		memcpy_toio(dram, queue_descs[PRUETH_PORT_QUEUE_HOST],
+			    sizeof(queue_descs[PRUETH_PORT_QUEUE_HOST]));
+
 }
 
 static int icssm_prueth_sw_port_config(struct prueth *prueth,
@@ -975,6 +1169,109 @@ static int icssm_prueth_sw_port_config(struct prueth *prueth,
 	return 0;
 }
 
+/* Configure TX/RX queue contexts and buffer descriptor tables for LRE port */
+static int icssm_prueth_lre_port_config(struct prueth *prueth,
+					enum prueth_port port_id)
+{
+	unsigned int tx_context_ofs_addr, rx_context_ofs, queue_desc_ofs;
+	void __iomem *dram, *dram_base, *dram_mac;
+	struct prueth_emac *emac;
+
+	emac = prueth->emac[port_id - 1];
+	switch (port_id) {
+	case PRUETH_PORT_MII0:
+		tx_context_ofs_addr     = TX_CONTEXT_P1_Q1_OFFSET_ADDR;
+		rx_context_ofs          = P1_Q1_RX_CONTEXT_OFFSET;
+		queue_desc_ofs          = P1_QUEUE_DESC_OFFSET;
+		/* for switch PORT MII0 mac addr is in DRAM0. */
+		dram_mac = prueth->mem[PRUETH_MEM_DRAM0].va;
+		break;
+	case PRUETH_PORT_MII1:
+		tx_context_ofs_addr     = TX_CONTEXT_P2_Q1_OFFSET_ADDR;
+		rx_context_ofs          = P2_Q1_RX_CONTEXT_OFFSET;
+		queue_desc_ofs          = P2_QUEUE_DESC_OFFSET;
+
+		/* for switch PORT MII1 mac addr is in DRAM1. */
+		dram_mac = prueth->mem[PRUETH_MEM_DRAM1].va;
+		break;
+	default:
+		netdev_err(emac->ndev, "invalid port\n");
+		return -EINVAL;
+	}
+
+	/* setup mac address */
+	memcpy_toio(dram_mac + PORT_MAC_ADDR, emac->mac_addr, ETH_ALEN);
+
+	/* Remaining switch port configs are in DRAM1 */
+	dram_base = prueth->mem[PRUETH_MEM_DRAM1].va;
+
+	/* queue information table */
+	memcpy_toio(dram_base + tx_context_ofs_addr,
+		    lre_queue_infos[port_id],
+		    sizeof(lre_queue_infos[port_id]));
+
+	memcpy_toio(dram_base + rx_context_ofs,
+		    lre_rx_queue_infos[port_id],
+		    sizeof(lre_rx_queue_infos[port_id]));
+
+	/* buffer descriptor offset table*/
+	dram = dram_base + QUEUE_DESCRIPTOR_OFFSET_ADDR +
+		(port_id * NUM_QUEUES * sizeof(u16));
+	writew(lre_queue_infos[port_id][PRUETH_QUEUE1].buffer_desc_offset,
+	       dram);
+	writew(lre_queue_infos[port_id][PRUETH_QUEUE2].buffer_desc_offset,
+	       dram + 2);
+	writew(lre_queue_infos[port_id][PRUETH_QUEUE3].buffer_desc_offset,
+	       dram + 4);
+	writew(lre_queue_infos[port_id][PRUETH_QUEUE4].buffer_desc_offset,
+	       dram + 6);
+
+	/* buffer offset table */
+	dram = dram_base + QUEUE_OFFSET_ADDR +
+		port_id * NUM_QUEUES * sizeof(u16);
+	writew(lre_queue_infos[port_id][PRUETH_QUEUE1].buffer_offset, dram);
+	writew(lre_queue_infos[port_id][PRUETH_QUEUE2].buffer_offset,
+	       dram + 2);
+	writew(lre_queue_infos[port_id][PRUETH_QUEUE3].buffer_offset,
+	       dram + 4);
+	writew(lre_queue_infos[port_id][PRUETH_QUEUE4].buffer_offset,
+	       dram + 6);
+
+	/* queue size lookup table */
+	dram = dram_base + QUEUE_SIZE_ADDR +
+		port_id * NUM_QUEUES * sizeof(u16);
+	writew(HOST_QUEUE_1_SIZE, dram);
+	writew(HOST_QUEUE_2_SIZE, dram + 2);
+	writew(QUEUE_3_TXOPT_SIZE, dram + 4);
+	writew(QUEUE_4_TXOPT_SIZE, dram + 6);
+
+	/* queue table */
+	memcpy_toio(dram_base + queue_desc_ofs,
+		    &hsr_prp_txopt_queue_descs[port_id][0],
+		    4 * sizeof(hsr_prp_txopt_queue_descs[port_id][0]));
+
+	/* In HSR/PRP mode both slave ports share the host receive queue
+	 * descriptor region (P0_QUEUE_DESC_OFFSET). The firmware arbitrates
+	 * ownership; the driver always reads from the same host-side descriptor
+	 * base regardless of which physical port the frame arrived on.
+	 */
+	emac->rx_queue_descs = dram_base + P0_QUEUE_DESC_OFFSET;
+	emac->tx_queue_descs = dram_base +
+		lre_rx_queue_infos[port_id][PRUETH_QUEUE1].queue_desc_offset;
+
+	if (port_id == PRUETH_PORT_MII0) {
+		emac->tx_queue_descs_other_port = dram_base +
+			lre_rx_queue_infos
+			[port_id + 1][PRUETH_QUEUE1].queue_desc_offset;
+	} else if (port_id == PRUETH_PORT_MII1) {
+		emac->tx_queue_descs_other_port = dram_base +
+			lre_rx_queue_infos
+			[port_id - 1][PRUETH_QUEUE1].queue_desc_offset;
+	}
+
+	return 0;
+}
+
 int icssm_prueth_sw_emac_config(struct prueth_emac *emac)
 {
 	struct prueth *prueth = emac->prueth;
@@ -989,7 +1286,10 @@ int icssm_prueth_sw_emac_config(struct prueth_emac *emac)
 	if (prueth->emac_configured & BIT(emac->port_id))
 		return 0;
 
-	ret = icssm_prueth_sw_port_config(prueth, emac->port_id);
+	if (prueth_is_lre(prueth))
+		ret = icssm_prueth_lre_port_config(prueth, emac->port_id);
+	else
+		ret = icssm_prueth_sw_port_config(prueth, emac->port_id);
 	if (ret)
 		return ret;
 
diff --git a/drivers/net/ethernet/ti/icssm/icssm_prueth_switch.h b/drivers/net/ethernet/ti/icssm/icssm_prueth_switch.h
index e6111bba166e..0f4595c6075f 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 5ba9ce14da44..089e43cadc25 100644
--- a/drivers/net/ethernet/ti/icssm/icssm_switch.h
+++ b/drivers/net/ethernet/ti/icssm/icssm_switch.h
@@ -24,6 +24,9 @@
 #define QUEUE_3_SIZE		97	/* Protocol specific */
 #define QUEUE_4_SIZE		97	/* NRT (IP,ARP, ICMP) */
 
+#define QUEUE_3_TXOPT_SIZE	194	/* Protocol specific - High Priority */
+#define QUEUE_4_TXOPT_SIZE	194	/* NRT(IP,ARP, ICMP) - Low Priority*/
+
 /* Host queue size (number of BDs). Each BD points to data buffer of 32 bytes.
  * HOST PORT QUEUES can buffer up to 4 full sized frames per queue
  */
@@ -49,20 +52,18 @@
  *				For RED, NodeTable lookup was successful.
  * 7		Flood		Packet should be flooded (destination MAC
  *				address found in FDB). For switch only.
- * 8..12	Block_length	number of valid bytes in this specific block.
- *				Will be <=32 bytes on last block of packet
+ * 8		RED_INFO	Set if the frame carries an HSR or PRP
+ *				redundancy tag
+ * 10		HostRecv	Set if the frame is destined for the host port
  * 13		More		"More" bit indicating that there are more blocks
  * 14		Shadow		indicates that "index" is pointing into shadow
  *				buffer
  * 15		TimeStamp	indicates that this packet has time stamp in
  *				separate buffer - only needed if PTP runs on
  *				host
- * 16..17	Port		different meaning for ingress and egress,
- *				Ingress: Port = 0 indicates phy port 1 and
- *				Port = 1 indicates phy port 2.
- *				Egress: 0 sends on phy port 1 and 1 sends on
- *				phy port 2. Port = 2 goes over MAC table
- *				look-up
+ * 16..17	LAN		Destination LAN for transmission:
+ *				bit 16 = LAN A, bit 17 = LAN B, set both to
+ *				duplicate to both LANs.
  * 18..28	Length		11 bit of total packet length which is put into
  *				first BD only so that host access only one BD
  * 29		VlanTag		indicates that packet has Length/Type field of
@@ -86,14 +87,21 @@
 #define PRUETH_BD_SW_FLOOD_MASK		BIT(7)
 #define PRUETH_BD_SW_FLOOD_SHIFT	7
 
+#define PRUETH_BD_RED_PKT_MASK		BIT(8)
+#define PRUETH_BD_RED_PKT		8
+
+#define PRUETH_BD_HOST_RECV_MASK	BIT(10)
+#define PRUETH_BD_HOST_RECV_SHIFT	10
+
 #define	PRUETH_BD_SHADOW_MASK		BIT(14)
 #define	PRUETH_BD_SHADOW_SHIFT		14
 
 #define PRUETH_BD_TIMESTAMP_MASK	BIT(15)
 #define PRUETH_BD_TIMESTAMP_SHIFT	15
 
-#define PRUETH_BD_PORT_MASK		GENMASK(17, 16)
-#define PRUETH_BD_PORT_SHIFT		16
+#define PRUETH_BD_LAN_INFO_MASK		GENMASK(17, 16)
+#define PRUETH_BD_LAN_A_SHIFT		16
+#define PRUETH_BD_LAN_B_SHIFT		17
 
 #define PRUETH_BD_LENGTH_MASK		GENMASK(28, 18)
 #define PRUETH_BD_LENGTH_SHIFT		18
@@ -298,6 +306,9 @@
 #define P0_Q4_BD_OFFSET		(P0_Q3_BD_OFFSET + HOST_QUEUE_3_SIZE * BD_SIZE)
 #define P0_Q3_BD_OFFSET		(P0_Q2_BD_OFFSET + HOST_QUEUE_2_SIZE * BD_SIZE)
 #define P0_Q2_BD_OFFSET		(P0_Q1_BD_OFFSET + HOST_QUEUE_1_SIZE * BD_SIZE)
+#define P1_Q3_TXOPT_BD_OFFSET	(P0_Q4_BD_OFFSET + HOST_QUEUE_4_SIZE * BD_SIZE)
+#define P2_Q1_TXOPT_BD_OFFSET	(P1_Q3_TXOPT_BD_OFFSET +	\
+				 QUEUE_3_TXOPT_SIZE * BD_SIZE)
 #define P0_Q1_BD_OFFSET		P0_BUFFER_DESC_OFFSET
 #define P0_BUFFER_DESC_OFFSET	SRAM_START_OFFSET
 
@@ -328,6 +339,10 @@
 				 ICSS_BLOCK_SIZE)
 #define P0_Q2_BUFFER_OFFSET	(P0_Q1_BUFFER_OFFSET + HOST_QUEUE_1_SIZE * \
 				 ICSS_BLOCK_SIZE)
+#define P1_Q3_TXOPT_BUFFER_OFFSET	(P0_Q4_BUFFER_OFFSET +	\
+					 HOST_QUEUE_4_SIZE * ICSS_BLOCK_SIZE)
+#define P2_Q1_TXOPT_BUFFER_OFFSET	(P1_Q3_TXOPT_BUFFER_OFFSET +	\
+					 QUEUE_3_TXOPT_SIZE * ICSS_BLOCK_SIZE)
 #define P0_COL_BUFFER_OFFSET	0xEE00
 #define P0_Q1_BUFFER_OFFSET	0x0000
 
-- 
2.43.0



^ permalink raw reply related

* [PATCH net-next 1/3] net: ti: icssm-prueth: Add HSR and PRP HW offload mode support for AM57xx, AM437x and AM335x
From: Parvathi Pudi @ 2026-06-11 12:33 UTC (permalink / raw)
  To: andrew+netdev, davem, edumazet, kuba, pabeni, danishanwar,
	parvathi, rogerq, pmohan, afd, basharath, arnd
  Cc: linux-kernel, netdev, linux-arm-kernel, pratheesh, j-rameshbabu,
	vigneshr, praneeth, srk, rogerq, m-malladi, krishna, mohan
In-Reply-To: <20260611123636.376577-1-parvathi@couthit.com>

From: Roger Quadros <rogerq@ti.com>

The PRU-ICSS subsystem on AM335x, AM437x and AM57xx SoCs supports dedicated
firmware implementing the IEC 62439-3 redundancy protocols: HSR and PRP.
Extend the ICSSM PRUETH driver to enable these operating modes in addition
to the existing dual-EMAC and RSTP switch configurations.

In both HSR and PRP modes, the two PRU Ethernet ports operate as LRE (Link
Redundancy Entity) slave ports, while the host port acts as the master.
In case of HW offload mode, Frame duplicate detection/discard for both HSR
and PRP and L2 forwarding in case of HSR are handled entirely by firmware
within the PRU cores.

For HSR, frames received on one PRU port are forwarded to the host and to
the peer PRU port (store-and-forward or cut-through), providing a redundant
ring path. For PRP, any one of the PRU port forwards received frames to the
host after firmware discards the duplicates.

The PRU-ICSS subsystem loads the Dual EMAC firmware by default. To enable
HSR or PRP functionality, the firmware must be changed accordingly. The
required reconfiguration steps are detailed below.

To switch from dual-EMAC to HSR (example: eth2 and eth3 as slave raw
ports):

$ ip link set eth2 down && ip link set eth3 down
$ ip link set eth2 address <mac-addr>
$ ip link set eth3 address <mac-addr>
$ ethtool -K eth2 hsr-tag-rm-offload on
$ ethtool -K eth2 hsr-fwd-offload on
$ ethtool -K eth2  hsr-dup-offload on
$ ethtool -K eth3 hsr-tag-rm-offload on
$ ethtool -K eth3 hsr-fwd-offload on
$ ethtool -K eth3 hsr-dup-offload on
$ ip link add name hsr0 type hsr slave1 eth2 slave2 eth3 supervison 45
  version 1
$ ip link set eth2 up
$ ip link set eth3 up

To switch from dual-EMAC to PRP (example: eth2 and eth3 as slave raw
ports):

$ ip link set eth2 down && ip link set eth3 down
$ ip link set eth2 address <mac-addr>
$ ip link set eth3 address <mac-addr>
$ ethtool -K eth2 hsr-tag-rm-offload on
$ ethtool -K eth2 hsr-dup-offload on
$ ethtool -K eth3 hsr-tag-rm-offload on
$ ethtool -K eth3 hsr-dup-offload on
$ ip link add name prp0 type hsr slave1 eth2 slave2 eth3 supervision 45
  proto 1
$ ip link set eth2 up
$ ip link set eth3 up

To revert back to dual-EMAC:

$ ip link delete hsr0 or prp0
$ ip link set eth2 down && ip link set eth3 down
$ ethtool -K eth2 hsr-tag-rm-offload off
$ ethtool -K eth2 hsr-fwd-offload off
$ ethtool -K eth2 hsr-dup-offload off
$ ethtool -K eth3 hsr-tag-rm-offload off
$ ethtool -K eth3 hsr-fwd-offload off
$ ethtool -K eth3 hsr-dup-offload off

Signed-off-by: Roger Quadros <rogerq@ti.com>
Signed-off-by: Andrew F. Davis <afd@ti.com>
Signed-off-by: Parvathi Pudi <parvathi@couthit.com>
---
 drivers/net/ethernet/ti/Makefile              |   2 +-
 .../ethernet/ti/icssm/icssm_lre_firmware.h    | 141 ++++++++++
 drivers/net/ethernet/ti/icssm/icssm_prueth.c  | 252 +++++++++++++++++-
 drivers/net/ethernet/ti/icssm/icssm_prueth.h  |  29 +-
 .../net/ethernet/ti/icssm/icssm_prueth_lre.c  | 210 +++++++++++++++
 .../net/ethernet/ti/icssm/icssm_prueth_lre.h  |  19 ++
 6 files changed, 640 insertions(+), 13 deletions(-)
 create mode 100644 drivers/net/ethernet/ti/icssm/icssm_lre_firmware.h
 create mode 100644 drivers/net/ethernet/ti/icssm/icssm_prueth_lre.c
 create mode 100644 drivers/net/ethernet/ti/icssm/icssm_prueth_lre.h

diff --git a/drivers/net/ethernet/ti/Makefile b/drivers/net/ethernet/ti/Makefile
index f4276c9a7762..e4a10d60e1a6 100644
--- a/drivers/net/ethernet/ti/Makefile
+++ b/drivers/net/ethernet/ti/Makefile
@@ -4,7 +4,7 @@
 #
 
 obj-$(CONFIG_TI_PRUETH) += icssm-prueth.o
-icssm-prueth-y := icssm/icssm_prueth.o icssm/icssm_prueth_switch.o icssm/icssm_switchdev.o
+icssm-prueth-y := icssm/icssm_prueth.o icssm/icssm_prueth_switch.o icssm/icssm_switchdev.o icssm/icssm_prueth_lre.o
 
 ti-cpsw-common-y += cpsw-common.o davinci_cpdma.o
 ti-cpsw-priv-y += cpsw_priv.o cpsw_ethtool.o
diff --git a/drivers/net/ethernet/ti/icssm/icssm_lre_firmware.h b/drivers/net/ethernet/ti/icssm/icssm_lre_firmware.h
new file mode 100644
index 000000000000..b5ab0ec87c5f
--- /dev/null
+++ b/drivers/net/ethernet/ti/icssm/icssm_lre_firmware.h
@@ -0,0 +1,141 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/* Copyright (C) 2017-2020 Texas Instruments Incorporated - http://www.ti.com */
+#ifndef __ICSS_LRE_FIRMWARE_H
+#define __ICSS_LRE_FIRMWARE_H
+
+#define ICSS_LRE_HSR_MODE_OFFSET		0x1E76
+#define ICSS_LRE_MODEH				0x01
+
+/* PRU0 DMEM */
+#define ICSS_LRE_DBG_START			0x1E00
+
+#define ICSS_LRE_DUPLICATE_HOST_TABLE		0x0200
+
+/* PRU1 DMEM */
+#define ICSS_LRE_DUPLICATE_PORT_TABLE_PRU0	0x0200
+#define ICSS_LRE_DUPLICATE_PORT_TABLE_PRU1	0x0E00
+
+/* Size and setup (N and M) of duplicate host table */
+#define ICSS_LRE_DUPLICATE_HOST_TABLE_SIZE	0x1C08
+/* Size and setup (N and M) of duplicate port table (HSR Only) */
+#define ICSS_LRE_DUPLICATE_PORT_TABLE_SIZE	0x1C1C
+/* Time after which an entry is removed from the duplicate
+ * table (10 ms resolution)
+ */
+#define ICSS_LRE_DUPLI_FORGET_TIME		0x1C24
+/* Time interval to check the port duplicate table */
+#define ICSS_LRE_DUPLI_PORT_CHECK_RESO		0x1C2C
+/* Time interval to check the host duplicate table */
+#define ICSS_LRE_DUPLI_HOST_CHECK_RESO		0x1C30
+/* NodeTable | Host | Port */
+#define ICSS_LRE_HOST_TIMER_CHECK_FLAGS		0x1C38
+/* Arbitration flag for the host duplicate table */
+#define ICSS_LRE_HOST_DUPLICATE_ARBITRATION	0x1C3C
+/* Supervision address in LRE */
+#define ICSS_LRE_SUP_ADDR			0x1C4C
+#define ICSS_LRE_SUP_ADDR_LOW			0x1C50
+
+/* Time in TimeTicks (1/100s) */
+#define ICSS_LRE_DUPLICATE_FORGET_TIME_400_MS	40
+#define ICSS_LRE_NODE_FORGET_TIME_60000_MS	6000
+#define ICSS_LRE_MAX_FORGET_TIME		0xFFDF
+
+#define ICSS_LRE_DUPLICATE_PORT_TABLE_DMEM_SIZE	0x0C00
+#define ICSS_LRE_DUPLICATE_HOST_TABLE_DMEM_SIZE	0x1800
+#define ICSS_LRE_STATS_DMEM_SIZE		0x0080
+#define ICSS_LRE_DEBUG_COUNTER_DMEM_SIZE	0x0050
+
+#define ICSS_LRE_DUPLICATE_HOST_TABLE_SIZE_INIT	0x800004 /* N = 128, M = 4 */
+#define ICSS_LRE_DUPLICATE_PORT_TABLE_SIZE_INIT	0x400004 /* N = 64, M = 4 */
+#define ICSS_LRE_MASTER_SLAVE_BUSY_BITS_CLEAR	0x0
+#define ICSS_LRE_TABLE_CHECK_RESOLUTION_10_MS	0xA
+#define ICSS_LRE_SUP_ADDRESS_INIT_OCTETS_HIGH	0x4E1501
+#define ICSS_LRE_SUP_ADDRESS_INIT_OCTETS_LOW	0x1
+
+/* SHARED RAM */
+
+/* 8 bytes of VLAN PCP to RX QUEUE MAPPING */
+#define ICSS_LRE_QUEUE_2_PCP_MAP_OFFSET		0x124
+#define ICSS_LRE_START				0x140
+
+/* Count of HSR/PRP tagged frames successfully transmitted on port A/B */
+#define ICSS_LRE_CNT_TX_A			(ICSS_LRE_START + 4)
+#define ICSS_LRE_DUPLICATE_DISCARD		(ICSS_LRE_START + 104)
+#define ICSS_LRE_TRANSPARENT_RECEPTION		(ICSS_LRE_START + 108)
+#define ICSS_LRE_CNT_NODES			(ICSS_LRE_START + 52)
+
+/* SRAM */
+#define ICSS_LRE_IEC62439_CONST_DUPLICATE_ACCEPT		0x01
+#define ICSS_LRE_IEC62439_CONST_DUPLICATE_DISCARD		0x02
+#define ICSS_LRE_IEC62439_CONST_TRANSP_RECEPTION_REMOVE_RCT	0x01
+#define ICSS_LRE_IEC62439_CONST_TRANSP_RECEPTION_PASS_RCT	0x02
+
+/* Enable/disable interrupts for high/low priority instead of per port.
+ * 0 = disabled (default), 1 = enabled
+ */
+#define ICSS_LRE_PRIORITY_INTRS_STATUS_OFFSET	0x1FAA
+/* Enable/disable timestamping of packets. 0 = disabled (default) 1 = enabled */
+#define ICSS_LRE_TIMESTAMP_PKTS_STATUS_OFFSET	0x1FAB
+#define ICSS_LRE_TIMESTAMP_ARRAY_OFFSET		0xC200
+
+/* HOST_TIMER_CHECK_FLAGS bits */
+#define ICSS_LRE_HOST_TIMER_NODE_TABLE_CHECK_BIT	BIT(0)
+#define ICSS_LRE_HOST_TIMER_NODE_TABLE_CLEAR_BIT	BIT(4)
+#define ICSS_LRE_HOST_TIMER_HOST_TABLE_CHECK_BIT	BIT(8)
+#define ICSS_LRE_HOST_TIMER_P1_TABLE_CHECK_BIT		BIT(16)
+#define ICSS_LRE_HOST_TIMER_P2_TABLE_CHECK_BIT		BIT(24)
+#define ICSS_LRE_HOST_TIMER_PORT_TABLE_CHECK_BITS \
+			(ICSS_LRE_HOST_TIMER_P1_TABLE_CHECK_BIT | \
+			 ICSS_LRE_HOST_TIMER_P2_TABLE_CHECK_BIT)
+
+/* PRU1 DMEM */
+/* Node table offsets are different for AM3/4 vs AM57/K2G, set by firmware */
+#define ICSS_LRE_V1_0_HASH_MASK                 0x3F
+#define ICSS_LRE_V1_0_INDEX_ARRAY_NT            0x60
+#define ICSS_LRE_V1_0_BIN_ARRAY                 0x1A00
+#define ICSS_LRE_V1_0_NODE_TABLE_NEW            0x1FC0
+#define ICSS_LRE_V1_0_INDEX_ARRAY_LOC           PRUETH_MEM_DRAM0
+#define ICSS_LRE_V1_0_BIN_ARRAY_LOC             PRUETH_MEM_DRAM0
+#define ICSS_LRE_V1_0_NODE_TABLE_LOC            PRUETH_MEM_SHARED_RAM
+#define ICSS_LRE_V1_0_INDEX_TBL_MAX_ENTRIES     64
+#define ICSS_LRE_V1_0_BIN_TBL_MAX_ENTRIES       128
+#define ICSS_LRE_V1_0_NODE_TBL_MAX_ENTRIES      128
+
+#define ICSS_LRE_V2_1_HASH_MASK                 0xFF
+#define ICSS_LRE_V2_1_INDEX_ARRAY_NT            0x3000
+#define ICSS_LRE_V2_1_BIN_ARRAY \
+	(ICSS_LRE_V2_1_INDEX_ARRAY_NT + \
+	(ICSS_LRE_V2_1_INDEX_TBL_MAX_ENTRIES * 6))
+#define ICSS_LRE_V2_1_NODE_TABLE_NEW \
+	(ICSS_LRE_V2_1_BIN_ARRAY + \
+	(ICSS_LRE_V2_1_BIN_TBL_MAX_ENTRIES * 8))
+#define ICSS_LRE_V2_1_INDEX_ARRAY_LOC           PRUETH_MEM_SHARED_RAM
+#define ICSS_LRE_V2_1_BIN_ARRAY_LOC             PRUETH_MEM_SHARED_RAM
+#define ICSS_LRE_V2_1_NODE_TABLE_LOC            PRUETH_MEM_SHARED_RAM
+#define ICSS_LRE_V2_1_INDEX_TBL_MAX_ENTRIES     256
+#define ICSS_LRE_V2_1_BIN_TBL_MAX_ENTRIES       256
+#define ICSS_LRE_V2_1_NODE_TBL_MAX_ENTRIES      256
+
+#define ICSS_LRE_NODE_FREE			0x10
+#define ICSS_LRE_NODE_TAKEN			0x01
+#define ICSS_LRE_NT_REM_NODE_TYPE_MASK		0x1F
+#define ICSS_LRE_NT_REM_NODE_TYPE_SHIFT		0x00
+
+#define ICSS_LRE_NT_REM_NODE_TYPE_SANA		0x01
+#define ICSS_LRE_NT_REM_NODE_TYPE_SANB		0x02
+#define ICSS_LRE_NT_REM_NODE_TYPE_SANAB		0x03
+#define ICSS_LRE_NT_REM_NODE_TYPE_DAN		0x04
+#define ICSS_LRE_NT_REM_NODE_TYPE_REDBOX	0x08
+#define ICSS_LRE_NT_REM_NODE_TYPE_VDAN		0x10
+
+#define ICSS_LRE_NT_REM_NODE_HSR_BIT		0x20 /* if set node is HSR */
+
+#define ICSS_LRE_NT_REM_NODE_DUP_MASK		0xC0
+#define ICSS_LRE_NT_REM_NODE_DUP_SHIFT		0x06
+
+/* Node entry duplicate type: DupAccept */
+#define ICSS_LRE_NT_REM_NODE_DUP_ACCEPT		0x40
+/* Node entry duplicate type: DupDiscard */
+#define ICSS_LRE_NT_REM_NODE_DUP_DISCARD	0x80
+
+#endif /* __ICSS_LRE_FIRMWARE_H */
diff --git a/drivers/net/ethernet/ti/icssm/icssm_prueth.c b/drivers/net/ethernet/ti/icssm/icssm_prueth.c
index b7e94244355a..6ebc52ce7123 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,27 @@
 #define TX_CLK_DELAY_100M	0x6
 #define HR_TIMER_TX_DELAY_US	100
 
+static const struct prueth_fw_offsets fw_offsets_v2_1;
+static void icssm_prueth_set_fw_offsets(struct prueth *prueth)
+{
+	/* Set Multicast filter control and table offsets */
+	if (PRUETH_IS_EMAC(prueth) || PRUETH_IS_SWITCH(prueth)) {
+		prueth->fw_offsets.mc_ctrl_offset  =
+			ICSS_EMAC_FW_MULTICAST_FILTER_CTRL_OFFSET;
+		prueth->fw_offsets.mc_filter_mask =
+			ICSS_EMAC_FW_MULTICAST_FILTER_MASK_OFFSET;
+		prueth->fw_offsets.mc_filter_tbl =
+			ICSS_EMAC_FW_MULTICAST_FILTER_TABLE;
+	} else {
+		prueth->fw_offsets.mc_ctrl_offset  =
+			ICSS_LRE_FW_MULTICAST_TABLE_SEARCH_OP_CONTROL_BIT;
+		prueth->fw_offsets.mc_filter_mask =
+			ICSS_LRE_FW_MULTICAST_FILTER_MASK;
+		prueth->fw_offsets.mc_filter_tbl =
+			ICSS_LRE_FW_MULTICAST_FILTER_TABLE;
+	}
+}
+
 static void icssm_prueth_write_reg(struct prueth *prueth,
 				   enum prueth_mem region,
 				   unsigned int reg, u32 val)
@@ -309,12 +331,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 +634,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 +655,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 +695,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 +725,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 +746,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 +760,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 +784,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 */
@@ -982,14 +1031,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;
 
-		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)
+				return ret;
+		} else {
+			/* LRE mode: set up duplicate-table check flags */
+			icssm_prueth_lre_config_check_flags(prueth);
+		}
 	} else {
 		icssm_prueth_emac_config(emac);
 	}
@@ -1079,6 +1134,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 +1180,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 +1325,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 +1353,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 +1369,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 +1388,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);
 }
 
@@ -1436,6 +1513,8 @@ 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,
 };
@@ -1589,6 +1668,10 @@ 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_F_HW_HSR_FWD | NETIF_F_HW_HSR_TAG_RM);
+
 	ndev->dev.of_node = eth_node;
 	ndev->netdev_ops = &emac_netdev_ops;
 
@@ -1741,6 +1824,104 @@ 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;
+	struct prueth_emac *emac0;
+	struct prueth_emac *emac1;
+	enum pruss_ethtype mode;
+	enum hsr_version ver;
+	unsigned long flags;
+	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;
+
+	emac0 = prueth->emac[PRUETH_MAC0];
+	emac1 = prueth->emac[PRUETH_MAC1];
+
+	spin_lock_irqsave(&emac->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(&emac->addr_lock, flags);
+			return -EOPNOTSUPP;
+		}
+	}
+
+	prueth->hsr_members |= BIT(emac->port_id);
+
+	spin_unlock_irqrestore(&emac->addr_lock, flags);
+
+	if (!prueth_is_lre(prueth)) {
+		if (prueth->hsr_members & BIT(PRUETH_PORT_MII0) &&
+		    prueth->hsr_members & BIT(PRUETH_PORT_MII1)) {
+			if (!(emac0->ndev->features &
+			    (NETIF_F_HW_HSR_TAG_RM | NETIF_F_HW_HSR_FWD)) &&
+			    !(emac1->ndev->features &
+			    (NETIF_F_HW_HSR_TAG_RM | NETIF_F_HW_HSR_FWD)))
+				return -EOPNOTSUPP;
+
+			ret = icssm_prueth_change_mode(prueth, mode);
+			if (ret < 0) {
+				dev_err(prueth->dev, "Failed to enable HSR mode\n");
+			} else {
+				dev_info(prueth->dev,
+					 "TI PRU ethernet now in %s mode\n",
+					 (mode == PRUSS_ETHTYPE_HSR) ?
+					 "HSR" : "PRP");
+			}
+		}
+	}
+
+	return ret;
+}
+
+static int icssm_prueth_hsr_port_unlink(struct net_device *ndev)
+{
+	struct prueth_emac *emac = netdev_priv(ndev);
+	struct prueth *prueth = emac->prueth;
+	unsigned long flags;
+	int ret = 0;
+
+	spin_lock_irqsave(&emac->addr_lock, flags);
+
+	prueth->hsr_members &= ~BIT(emac->port_id);
+
+	spin_unlock_irqrestore(&emac->addr_lock, flags);
+
+	if (prueth_is_lre(prueth)) {
+		ret = icssm_prueth_change_mode(prueth, PRUSS_ETHTYPE_EMAC);
+		if (ret < 0) {
+			dev_err(prueth->dev, "Failed to enable dual EMAC mode\n");
+			return ret;
+		}
+	}
+
+	spin_lock_irqsave(&emac->addr_lock, flags);
+
+	if (!prueth->hsr_members)
+		prueth->hsr_dev = NULL;
+
+	spin_unlock_irqrestore(&emac->addr_lock, flags);
+
+	return 0;
+}
+
 static int icssm_prueth_ndev_event(struct notifier_block *unused,
 				   unsigned long event, void *ptr)
 {
@@ -1754,6 +1935,16 @@ static int icssm_prueth_ndev_event(struct notifier_block *unused,
 	switch (event) {
 	case NETDEV_CHANGEUPPER:
 		info = ptr;
+		if ((ndev->features &
+		     (NETIF_F_HW_HSR_FWD | NETIF_F_HW_HSR_TAG_RM)) &&
+		    is_hsr_master(info->upper_dev)) {
+			if (info->linking)
+				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 +1987,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 +2002,7 @@ static int icssm_prueth_probe(struct platform_device *pdev)
 	platform_set_drvdata(pdev, prueth);
 	prueth->dev = dev;
 	prueth->fw_data = device_get_match_data(dev);
+	prueth->fw_offsets = fw_offsets_v2_1;
 
 	eth_ports_node = of_get_child_by_name(np, "ethernet-ports");
 	if (!eth_ports_node)
@@ -1955,6 +2148,16 @@ 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;
 	/* setup netdev interfaces */
 	if (eth0_node) {
 		ret = icssm_prueth_netdev_init(prueth, eth0_node);
@@ -2176,15 +2379,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 +2406,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 +2433,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..098d81599415 100644
--- a/drivers/net/ethernet/ti/icssm/icssm_prueth.h
+++ b/drivers/net/ethernet/ti/icssm/icssm_prueth.h
@@ -16,10 +16,13 @@
 #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)
@@ -42,6 +45,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 +91,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 +102,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 +178,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 +197,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 +264,18 @@ 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;
+
 	struct net_device *hw_bridge_dev;
+	struct net_device *hsr_dev;
 	struct fdb_tbl *fdb_tbl;
 
 	struct notifier_block prueth_netdevice_nb;
@@ -264,6 +285,7 @@ struct prueth {
 	unsigned int eth_type;
 	size_t ocmc_ram_size;
 	u8 emac_configured;
+	u8 hsr_members;
 	u8 br_members;
 };
 
@@ -277,4 +299,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..930bc59df4c5
--- /dev/null
+++ b/drivers/net/ethernet/ti/icssm/icssm_prueth_lre.c
@@ -0,0 +1,210 @@
+// 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)
+{
+	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

* [PATCH net-next 2/3] net: ti: icssm-prueth: Add priority based RX IRQ handlers
From: Parvathi Pudi @ 2026-06-11 12:33 UTC (permalink / raw)
  To: andrew+netdev, davem, edumazet, kuba, pabeni, danishanwar,
	parvathi, rogerq, pmohan, afd, basharath, arnd
  Cc: linux-kernel, netdev, linux-arm-kernel, pratheesh, j-rameshbabu,
	vigneshr, praneeth, srk, rogerq, m-malladi, krishna, mohan
In-Reply-To: <20260611123636.376577-1-parvathi@couthit.com>

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
infor to driver. The driver will read the RX HW timestamp from frame and
process the frame which has arrived first among the two ports, giving the
stack wire-arrival ordering.

Dual-EMAC mode continues to use per-port interrupts.

Signed-off-by: Roger Quadros <rogerq@ti.com>
Signed-off-by: Andrew F. Davis <afd@ti.com>
Signed-off-by: Basharath Hussain Khaja <basharath@couthit.com>
Signed-off-by: Parvathi Pudi <parvathi@couthit.com>
---
 drivers/net/ethernet/ti/Makefile              |   2 +-
 drivers/net/ethernet/ti/icssm/icssm_prueth.c  | 146 ++++++++-
 drivers/net/ethernet/ti/icssm/icssm_prueth.h  |  30 ++
 .../ethernet/ti/icssm/icssm_prueth_common.c   | 285 ++++++++++++++++++
 .../net/ethernet/ti/icssm/icssm_prueth_lre.c  |  13 +
 5 files changed, 462 insertions(+), 14 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 6ebc52ce7123..58a6935dd809 100644
--- a/drivers/net/ethernet/ti/icssm/icssm_prueth.c
+++ b/drivers/net/ethernet/ti/icssm/icssm_prueth.c
@@ -41,7 +41,21 @@
 #define TX_CLK_DELAY_100M	0x6
 #define HR_TIMER_TX_DELAY_US	100
 
-static const struct prueth_fw_offsets fw_offsets_v2_1;
+/* ICSSM (v2.1) - supports 64-bit IEP counter.
+ * Firmware stores packet timestamps using lower 32 bits
+ * which wraps at 0xffffffff.
+ */
+static const struct prueth_fw_offsets fw_offsets_v2_1 = {
+	.iep_wrap = 0xffffffff,
+};
+
+/* ICSSM (v1.0) - supports 32-bit IEP counter, which resets the
+ * counter every one second (nanosecond resolution).
+ */
+static const struct prueth_fw_offsets fw_offsets_v1_0 = {
+	.iep_wrap = NSEC_PER_SEC,
+};
+
 static void icssm_prueth_set_fw_offsets(struct prueth *prueth)
 {
 	/* Set Multicast filter control and table offsets */
@@ -1069,11 +1083,25 @@ static int icssm_emac_ndo_open(struct net_device *ndev)
 			goto iep_exit;
 	}
 
-	ret = icssm_emac_request_irqs(emac);
-	if (ret)
-		goto rproc_shutdown;
+	if (PRUETH_IS_EMAC(prueth)) {
+		napi_enable(&emac->napi);
+	} else {
+		if (!prueth->emac_configured &&
+		    (PRUETH_IS_SWITCH(prueth) || prueth_is_lre(prueth))) {
+			napi_enable(&prueth->napi_hpq);
+			napi_enable(&prueth->napi_lpq);
+		}
+	}
 
-	napi_enable(&emac->napi);
+	/* In switch and LRE modes the shared HPQ/LPQ IRQs are used,
+	 * register them here and reuse for both modes.
+	 */
+	if (PRUETH_IS_EMAC(prueth))
+		ret = icssm_emac_request_irqs(emac);
+	else
+		ret = icssm_prueth_common_request_irqs(emac);
+	if (ret)
+		goto disable_napi;
 
 	/* start PHY */
 	phy_start(emac->phydev);
@@ -1090,7 +1118,17 @@ static int icssm_emac_ndo_open(struct net_device *ndev)
 
 	return 0;
 
-rproc_shutdown:
+disable_napi:
+	if (PRUETH_IS_EMAC(prueth)) {
+		napi_disable(&emac->napi);
+	} else {
+		if (!prueth->emac_configured &&
+		    (PRUETH_IS_SWITCH(prueth) || prueth_is_lre(prueth))) {
+			napi_disable(&prueth->napi_lpq);
+			napi_disable(&prueth->napi_hpq);
+		}
+	}
+
 	if (!PRUETH_IS_EMAC(prueth))
 		icssm_prueth_sw_shutdown_prus(emac, ndev);
 	else
@@ -1122,12 +1160,26 @@ static int icssm_emac_ndo_stop(struct net_device *ndev)
 	/* disable the mac port */
 	icssm_prueth_port_enable(emac, false);
 
+	netif_stop_queue(ndev);
+
 	/* stop PHY */
 	phy_stop(emac->phydev);
 
-	napi_disable(&emac->napi);
 	hrtimer_cancel(&emac->tx_hrtimer);
 
+	if (PRUETH_IS_EMAC(prueth)) {
+		napi_disable(&emac->napi);
+		free_irq(emac->rx_irq, ndev);
+	} else {
+		if (!prueth->emac_configured &&
+		    (PRUETH_IS_SWITCH(prueth) || prueth_is_lre(prueth))) {
+			napi_disable(&prueth->napi_lpq);
+			napi_disable(&prueth->napi_hpq);
+		}
+		/* Free IRQs on last port before halting PRU */
+		icssm_prueth_common_free_irqs(emac);
+	}
+
 	/* stop the PRU */
 	if (!PRUETH_IS_EMAC(prueth))
 		icssm_prueth_sw_shutdown_prus(emac, ndev);
@@ -1137,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);
 
@@ -1675,11 +1724,28 @@ static int icssm_prueth_netdev_init(struct prueth *prueth,
 	ndev->dev.of_node = eth_node;
 	ndev->netdev_ops = &emac_netdev_ops;
 
-	netif_napi_add(ndev, &emac->napi, icssm_emac_napi_poll);
+	if (PRUETH_IS_EMAC(prueth))
+		netif_napi_add(ndev, &emac->napi, icssm_emac_napi_poll);
+
+	if ((prueth->support_lre || fw_data->support_switch) &&
+	    emac->port_id == PRUETH_PORT_MII0) {
+		netif_napi_add(ndev, &prueth->napi_hpq,
+			       icssm_prueth_lre_napi_poll_hpq);
+		netif_napi_add(ndev, &prueth->napi_lpq,
+			       icssm_prueth_lre_napi_poll_lpq);
+	}
 
 	hrtimer_setup(&emac->tx_hrtimer, &icssm_emac_tx_timer_callback,
 		      CLOCK_MONOTONIC, HRTIMER_MODE_REL_PINNED);
 
+	if ((prueth->support_lre || fw_data->support_switch) &&
+	    emac->port_id == PRUETH_PORT_MII0) {
+		prueth->hp->ndev = ndev;
+		prueth->hp->priority = 0;
+		prueth->lp->ndev = ndev;
+		prueth->lp->priority = 1;
+	}
+
 	return 0;
 free:
 	emac->ndev = NULL;
@@ -1691,6 +1757,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;
 
@@ -1704,7 +1771,16 @@ static void icssm_prueth_netdev_exit(struct prueth *prueth,
 
 	phy_disconnect(emac->phydev);
 
-	netif_napi_del(&emac->napi);
+	if (PRUETH_IS_EMAC(prueth)) {
+		netif_napi_del(&emac->napi);
+	} else {
+		if (emac->port_id == PRUETH_PORT_MII0 &&
+		    (fw_data->support_switch || prueth->support_lre)) {
+			netif_napi_del(&prueth->napi_hpq);
+			netif_napi_del(&prueth->napi_lpq);
+		}
+	}
+
 	prueth->emac[mac] = NULL;
 }
 
@@ -2002,7 +2078,13 @@ static int icssm_prueth_probe(struct platform_device *pdev)
 	platform_set_drvdata(pdev, prueth);
 	prueth->dev = dev;
 	prueth->fw_data = device_get_match_data(dev);
-	prueth->fw_offsets = fw_offsets_v2_1;
+
+	if (prueth->fw_data->fw_rev == FW_REV_V1_0)
+		prueth->fw_offsets = fw_offsets_v1_0;
+	else if (prueth->fw_data->fw_rev == FW_REV_V2_1)
+		prueth->fw_offsets = fw_offsets_v2_1;
+	else
+		return -EINVAL;
 
 	eth_ports_node = of_get_child_by_name(np, "ethernet-ports");
 	if (!eth_ports_node)
@@ -2157,6 +2239,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;
 	/* setup netdev interfaces */
 	if (eth0_node) {
@@ -2396,6 +2513,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,
 };
@@ -2423,6 +2541,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,
 };
@@ -2451,6 +2570,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 098d81599415..60dc451f79e5 100644
--- a/drivers/net/ethernet/ti/icssm/icssm_prueth.h
+++ b/drivers/net/ethernet/ti/icssm/icssm_prueth.h
@@ -178,11 +178,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 {
@@ -196,12 +207,14 @@ 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;
 };
@@ -254,6 +267,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;
@@ -269,6 +287,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;
@@ -300,6 +324,12 @@ void icssm_emac_mc_filter_bin_allow(struct prueth_emac *emac, u8 hash);
 void icssm_emac_mc_filter_bin_disallow(struct prueth_emac *emac, u8 hash);
 u8 icssm_emac_get_mc_hash(u8 *mac, u8 *mask);
 
+int icssm_prueth_lre_napi_poll_lpq(struct napi_struct *napi, int budget);
+int icssm_prueth_lre_napi_poll_hpq(struct napi_struct *napi, int budget);
+
+int icssm_prueth_common_request_irqs(struct prueth_emac *emac);
+void icssm_prueth_common_free_irqs(struct prueth_emac *emac);
+
 static inline bool prueth_is_lre(struct prueth *prueth)
 {
 	return PRUETH_IS_HSR(prueth) || PRUETH_IS_PRP(prueth);
diff --git a/drivers/net/ethernet/ti/icssm/icssm_prueth_common.c b/drivers/net/ethernet/ti/icssm/icssm_prueth_common.c
new file mode 100644
index 000000000000..4820def5f9e1
--- /dev/null
+++ b/drivers/net/ethernet/ti/icssm/icssm_prueth_common.c
@@ -0,0 +1,285 @@
+// 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 net_device_stats *ndevstats, *ndevstats_o, *ndevstats_p;
+	struct prueth_queue_desc __iomem *queue_desc, *queue_desc_o;
+	struct prueth_packet_info pkt_info, pkt_info_o, *pkt_info_p;
+	u32 rd_buf_desc, rd_buf_desc_o, pkt_ts, pkt_ts_o, iep_wrap;
+	int ret, used = 0, port, port0_q_empty, port1_q_empty;
+	struct prueth_emac *emac_p, *other_emac;
+	void __iomem *shared_ram, *ocmc_ram;
+	u8 overflow_cnt, overflow_cnt_o;
+	u16 *bd_rd_ptr_p, *bd_wr_ptr_p;
+	unsigned int emac_max_pktlen;
+	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];
+	ndevstats = &emac->ndev->stats;
+	ndevstats_o = &other_emac->ndev->stats;
+	emac_max_pktlen = EMAC_MAX_FRM_SUPPORT;
+
+	iep_wrap = prueth->fw_offsets.iep_wrap;
+	/* search host queues for packets */
+	queue_desc = emac->rx_queue_descs + qid1;
+	queue_desc_o = other_emac->rx_queue_descs + qid2;
+
+	rxqueue = &sw_queue_infos[PRUETH_PORT_HOST][qid1];
+	rxqueue_o = &sw_queue_infos[PRUETH_PORT_HOST][qid2];
+
+	overflow_cnt = readb(&queue_desc->overflow_cnt);
+	overflow_cnt_o = readb(&queue_desc_o->overflow_cnt);
+
+	if (overflow_cnt > 0) {
+		emac->ndev->stats.rx_over_errors += overflow_cnt;
+		writeb(0, &queue_desc->overflow_cnt);
+	}
+	if (overflow_cnt_o > 0) {
+		other_emac->ndev->stats.rx_over_errors += overflow_cnt_o;
+		writeb(0, &queue_desc_o->overflow_cnt);
+	}
+
+	bd_rd_ptr = readw(&queue_desc->rd_ptr);
+	bd_wr_ptr = readw(&queue_desc->wr_ptr);
+
+	bd_rd_ptr_o = readw(&queue_desc_o->rd_ptr);
+	bd_wr_ptr_o = readw(&queue_desc_o->wr_ptr);
+
+	port0_q_empty = (bd_rd_ptr == bd_wr_ptr);
+	port1_q_empty = (bd_rd_ptr_o == bd_wr_ptr_o);
+
+	while (!port0_q_empty || !port1_q_empty) {
+		rd_buf_desc = readl(shared_ram + bd_rd_ptr);
+		rd_buf_desc_o = readl(shared_ram + bd_rd_ptr_o);
+
+		icssm_parse_packet_info(prueth, rd_buf_desc, &pkt_info);
+		icssm_parse_packet_info(prueth, rd_buf_desc_o, &pkt_info_o);
+
+		pkt_ts = readl(ocmc_ram + ICSS_LRE_TIMESTAMP_ARRAY_OFFSET +
+			       bd_rd_ptr - SRAM_START_OFFSET);
+		pkt_ts_o = readl(ocmc_ram + ICSS_LRE_TIMESTAMP_ARRAY_OFFSET +
+				 bd_rd_ptr_o - SRAM_START_OFFSET);
+
+		if (!port0_q_empty && !port1_q_empty) {
+			/* Both ports have a pending frame, pick the
+			 * earlier one by comparing timestamps and
+			 * account for wraparound.
+			 */
+			if (pkt_ts > pkt_ts_o)
+				port = (pkt_ts - pkt_ts_o) > (iep_wrap / 2) ?
+					0 : 1;
+			else
+				port = (pkt_ts_o - pkt_ts) > (iep_wrap / 2) ?
+					1 : 0;
+
+		} else if (!port0_q_empty) {
+			/* Packet(s) in port0 queue only */
+			port = 0;
+		} else {
+			/* Packet(s) in port1 queue only */
+			port = 1;
+		}
+
+		/* Select correct data structures for queue/packet selected */
+		if (port == 0) {
+			pkt_info_p = &pkt_info;
+			bd_wr_ptr_p = &bd_wr_ptr;
+			bd_rd_ptr_p = &bd_rd_ptr;
+			emac_p = emac;
+			ndevstats_p = ndevstats;
+			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;
+			ndevstats_p = ndevstats_o;
+			rxqueue_p = rxqueue_o;
+		}
+
+		if ((*pkt_info_p).length == 0) {
+			/* A zero-length frame would stall the ring, rd_ptr
+			 * would never advance. Firmware should not produce
+			 * these, skip to wr_ptr and drop all remaining
+			 * frames in this queue.
+			 */
+			update_rd_ptr = *bd_wr_ptr_p;
+			ndevstats_p->rx_length_errors++;
+		} else if ((*pkt_info_p).length > emac_max_pktlen) {
+			/* Oversized frame: firmware should have filtered
+			 * these before they reach the host queue. Advance
+			 * the read pointer to skip it.
+			 */
+			update_rd_ptr = *bd_wr_ptr_p;
+			ndevstats_p->rx_length_errors++;
+		} else {
+			update_rd_ptr = *bd_rd_ptr_p;
+			ret = icssm_emac_rx_packet(emac_p, &update_rd_ptr,
+						   pkt_info_p, rxqueue_p);
+			if (ret)
+				return used;
+
+			used++;
+		}
+
+		/* Zero the BD after consuming it, a misaligned rd_ptr
+		 * would otherwise mistake stale data for a valid incoming
+		 * frame.
+		 */
+		if (port == 0) {
+			writel(0, shared_ram + bd_rd_ptr);
+			writew(update_rd_ptr, &queue_desc->rd_ptr);
+			bd_rd_ptr = update_rd_ptr;
+		} else {
+			writel(0, shared_ram + bd_rd_ptr_o);
+			writew(update_rd_ptr, &queue_desc_o->rd_ptr);
+			bd_rd_ptr_o = update_rd_ptr;
+		}
+
+		port0_q_empty = (bd_rd_ptr == bd_wr_ptr) ? 1 : 0;
+		port1_q_empty = (bd_rd_ptr_o == bd_wr_ptr_o) ? 1 : 0;
+
+		if (used >= quota)
+			return used;
+	}
+
+	return used;
+}
+
+int icssm_prueth_lre_napi_poll_lpq(struct napi_struct *napi, int budget)
+{
+	struct prueth_emac *emac;
+	struct net_device *ndev;
+	struct prueth *prueth;
+	int num_rx_packets;
+	u8 qid1, qid2;
+
+	prueth = container_of(napi, struct prueth, napi_lpq);
+	ndev = prueth->lp->ndev;
+	emac = netdev_priv(ndev);
+	qid1 = PRUETH_QUEUE2;
+	qid2 = PRUETH_QUEUE4;
+
+	num_rx_packets = icssm_prueth_common_emac_rx_packets(emac, budget,
+							     qid1, qid2);
+	if (num_rx_packets < budget && napi_complete_done(napi, num_rx_packets))
+		enable_irq(prueth->rx_lpq_irq);
+
+	return num_rx_packets;
+}
+
+int icssm_prueth_lre_napi_poll_hpq(struct napi_struct *napi, int budget)
+{
+	struct prueth_emac *emac;
+	struct net_device *ndev;
+	struct prueth *prueth;
+	int num_rx_packets;
+	u8 qid1, qid2;
+
+	prueth = container_of(napi, struct prueth, napi_hpq);
+	ndev = prueth->hp->ndev;
+	emac = netdev_priv(ndev);
+	qid1 = PRUETH_QUEUE1;
+	qid2 = PRUETH_QUEUE3;
+
+	num_rx_packets = icssm_prueth_common_emac_rx_packets(emac, budget,
+							     qid1, qid2);
+	if (num_rx_packets < budget && napi_complete_done(napi, num_rx_packets))
+		enable_irq(prueth->rx_hpq_irq);
+
+	return num_rx_packets;
+}
+
+static irqreturn_t icssm_prueth_common_emac_rx_hardirq(int irq, void *dev_id)
+{
+	struct prueth_ndev_priority *ndev_prio;
+	struct prueth_emac *emac;
+	struct net_device *ndev;
+	struct prueth *prueth;
+
+	ndev_prio = (struct prueth_ndev_priority *)dev_id;
+	ndev = ndev_prio->ndev;
+	emac = netdev_priv(ndev);
+	prueth = emac->prueth;
+
+	/* disable Rx system event */
+	if (ndev_prio->priority == 1) {
+		disable_irq_nosync(prueth->rx_lpq_irq);
+		napi_schedule(&prueth->napi_lpq);
+	} else {
+		disable_irq_nosync(prueth->rx_hpq_irq);
+		napi_schedule(&prueth->napi_hpq);
+	}
+
+	return IRQ_HANDLED;
+}
+
+int icssm_prueth_common_request_irqs(struct prueth_emac *emac)
+{
+	struct prueth *prueth = emac->prueth;
+	int ret;
+
+	/* Request irq when first port is initialized */
+	if (prueth->emac_configured)
+		return 0;
+
+	ret = request_irq(prueth->rx_hpq_irq,
+			  icssm_prueth_common_emac_rx_hardirq,
+			  IRQF_TRIGGER_HIGH, "eth_hp_int", prueth->hp);
+	if (ret) {
+		netdev_err(emac->ndev, "unable to request RX HPQ IRQ\n");
+		return ret;
+	}
+
+	ret = request_irq(prueth->rx_lpq_irq,
+			  icssm_prueth_common_emac_rx_hardirq,
+			  IRQF_TRIGGER_HIGH, "eth_lp_int", prueth->lp);
+	if (ret) {
+		netdev_err(emac->ndev, "unable to request RX LPQ IRQ\n");
+		goto free_rx_hpq_irq;
+	}
+
+	return 0;
+
+free_rx_hpq_irq:
+	free_irq(prueth->rx_hpq_irq, prueth->hp);
+
+	return ret;
+}
+
+/**
+ * icssm_prueth_common_free_irqs - free irq
+ *
+ * @emac: EMAC data structure
+ *
+ */
+void icssm_prueth_common_free_irqs(struct prueth_emac *emac)
+{
+	struct prueth *prueth = emac->prueth;
+
+	/* HSR/PRP: free irqs when last port is down */
+	if (prueth->emac_configured)
+		return;
+
+	free_irq(prueth->rx_lpq_irq, prueth->lp);
+	free_irq(prueth->rx_hpq_irq, prueth->hp);
+}
diff --git a/drivers/net/ethernet/ti/icssm/icssm_prueth_lre.c b/drivers/net/ethernet/ti/icssm/icssm_prueth_lre.c
index 930bc59df4c5..be6a503a5754 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)
-- 
2.43.0



^ permalink raw reply related

* Re: [PATCH] ARM: remove references to removed CONFIG_CPU_ARM92x_CPU_IDLE options
From: Linus Walleij @ 2026-06-11 12:38 UTC (permalink / raw)
  To: Ethan Nelson-Moore
  Cc: linux-arm-kernel, Nathan Chancellor, Kees Cook, Russell King
In-Reply-To: <20260611004700.395759-1-enelsonmoore@gmail.com>

On Thu, Jun 11, 2026 at 2:47 AM Ethan Nelson-Moore
<enelsonmoore@gmail.com> wrote:

> Several assembly files in arch/arm/mm contain comments referring to
> CONFIG_CPU_ARM92x_CPU_IDLE options, which have not existed in the kernel
> since 2.4.21. Remove them.
>
> Discovered while searching for CONFIG_* symbols referenced in code but
> not defined in any Kconfig file.
>
> Signed-off-by: Ethan Nelson-Moore <enelsonmoore@gmail.com>

Reviewed-by: Linus Walleij <linusw@kernel.org>

Please put this patch into Russell's patch tracker.

Yours,
Linus Walleij


^ permalink raw reply

* Re: [PATCH] pinctrl: meson: amlogic-a4: use nolock get range
From: Linus Walleij @ 2026-06-11 12:40 UTC (permalink / raw)
  To: xianwei.zhao
  Cc: Neil Armstrong, Kevin Hilman, Jerome Brunet, Martin Blumenstingl,
	linux-amlogic, linux-gpio, linux-arm-kernel, linux-kernel
In-Reply-To: <20260611-pinctrl-nolock-v1-1-aca022d4d60f@amlogic.com>

On Thu, Jun 11, 2026 at 9:10 AM Xianwei Zhao via B4 Relay
<devnull+xianwei.zhao.amlogic.com@kernel.org> wrote:

> From: Xianwei Zhao <xianwei.zhao@amlogic.com>
>
> Use pinctrl_find_gpio_range_from_pin_nolock() instead of
> pinctrl_find_gpio_range_from_pin() when configuring a pin or
> setting a GPIO value.
>
> This avoids taking the lock and allows the code to be safely
> called from interrupt context.
>
> Signed-off-by: Xianwei Zhao <xianwei.zhao@amlogic.com>

Patch applied!

Yours,
Linus Walleij


^ permalink raw reply

* Re: [PATCH v7 8/8] perf test: Add Arm CoreSight callchain test
From: Leo Yan @ 2026-06-11 12:42 UTC (permalink / raw)
  To: James Clark
  Cc: linux-arm-kernel, coresight, linux-perf-users,
	Arnaldo Carvalho de Melo, John Garry, Will Deacon, Mike Leach,
	Suzuki K Poulose, Namhyung Kim, Mark Rutland, Alexander Shishkin,
	Jiri Olsa, Ian Rogers, Adrian Hunter, Al Grant, Paschalis Mpeis,
	Amir Ayupov
In-Reply-To: <c6f6766b-ef3f-4260-9569-620d351027ff@linaro.org>

On Thu, Jun 11, 2026 at 10:11:37AM +0100, James Clark wrote:
> 
> 
> On 11/06/2026 8:57 am, Leo Yan wrote:
> > Add a CoreSight shell test for synthesized callchains.
> > 
> > The test uses the new callchain workload to generate trace and decodes
> > it with synthesis callchain. It then verifies that the instruction
> > samples show the expected callchain push and pop.
> > 
> > Use control FIFOs so tracing starts only around the workload, which
> > keeps the trace data small. The test is limited to arm64 systems with
> > the cs_etm event available.
> > 
> > After:
> > 
> >    perf test 136 -vvv
> >    136: CoreSight synthesized callchain:
> >    --- start ---
> >    test child forked, pid 3539
> >    ---- end(0) ----
> >    136: CoreSight synthesized callchain			: Ok
> > 
> > Assisted-by: Codex:GPT-5.5
> > Signed-off-by: Leo Yan <leo.yan@arm.com>
> > ---
> >   tools/perf/Documentation/perf-test.txt        |   6 +-
> >   tools/perf/tests/builtin-test.c               |   1 +
> >   tools/perf/tests/shell/coresight/callchain.sh | 168 ++++++++++++++++++++++++++
> >   tools/perf/tests/tests.h                      |   1 +
> >   tools/perf/tests/workloads/Build              |   2 +
> >   tools/perf/tests/workloads/callchain.c        |  24 ++++
> 
> Maybe "syscall" is a better name? There's not any difference between this
> one and others like the deterministic one I added with regards to the
> callchain.

The callchain workload has function calls in userspace and system call
for the switch between userspace and kernel.

Connecting to another comment for not static attribute to avoid LTO's
function name tweaking, I will rename functions with prefix
"callchain_", this can dismiss concern for naming conflict.

For above two reasons, I will keep the name.

Thanks,
Leo


^ permalink raw reply

* Re: [PATCH v5 08/10] ACPI: APEI: share GHES CPER helpers
From: Ahmed Tiba @ 2026-06-11 12:42 UTC (permalink / raw)
  To: Jonathan Cameron
  Cc: will, xueshuai, saket.dumbre, mchehab, dave, djbw, bp, tony.luck,
	guohanjun, lenb, skhan, vishal.l.verma, rafael, corbet, ira.weiny,
	dave.jiang, krzk+dt, robh, catalin.marinas, alison.schofield,
	conor+dt, linux-arm-kernel, Michael.Zhao2, linux-doc,
	linux-kernel, linux-cxl, Dmitry.Lamerov, devicetree, linux-acpi,
	linux-edac, acpica-devel
In-Reply-To: <20260529173229.18843384@jic23-huawei>

On 29/05/2026 17:32, Jonathan Cameron wrote:
> On Fri, 29 May 2026 10:50:48 +0100
> Ahmed Tiba <ahmed.tiba@arm.com> wrote:
> 
>> Wire GHES up to the helper routines in ghes_cper.c and remove the local
>> copies from ghes.c. This keeps the control flow identical while letting
>> the helpers be shared with other firmware-first providers.
>>
>> Signed-off-by: Ahmed Tiba <ahmed.tiba@arm.com>
> Mostly looks fine.  The one bit that rather makes this exercise of breaking
> out generic code look dodgy is the ifdefs in the generic file.
> 

As below.

> 
>> ---
>>   drivers/acpi/apei/ghes.c      | 416 +--------------------------------------
>>   drivers/acpi/apei/ghes_cper.c | 438 +++++++++++++++++++++++++++++++++++++++++-
>>   include/acpi/ghes_cper.h      |  20 ++
>>   3 files changed, 459 insertions(+), 415 deletions(-)
>>
>> diff --git a/drivers/acpi/apei/ghes.c b/drivers/acpi/apei/ghes.c
>> index 85be2ebf4d3e..f85b97c4db4c 100644
>> --- a/drivers/acpi/apei/ghes.c
>> +++ b/drivers/acpi/apei/ghes.c
> 
>>   
>>   static void __ghes_panic(struct ghes *ghes,
>> diff --git a/drivers/acpi/apei/ghes_cper.c b/drivers/acpi/apei/ghes_cper.c
>> index d7a666a163c3..0ff9d06eb78f 100644
>> --- a/drivers/acpi/apei/ghes_cper.c
>> +++ b/drivers/acpi/apei/ghes_cper.c
>> @@ -13,22 +13,32 @@
> 
>>   
>>   #include "apei-internal.h"
>>   
>> +ATOMIC_NOTIFIER_HEAD(ghes_report_chain);
>> +
>> +#ifndef CONFIG_ACPI_APEI
>> +void __weak arch_apei_report_mem_error(int sev, struct cper_sec_mem_err *mem_err) { }
>> +#endif
> This is non obvious enough that the reasoning for a new weak function should be mentioned in
> the patch description.  Why not stub it in include/acpi/apei.h?
> 

Agreed. I should have explained that in the changelog.

The weak arch_apei_report_mem_error() fallback was only meant to keep
the shared helper buildable when GHES_CPER_HELPERS is enabled without
ACPI_APEI, while preserving the current GHES behaviour when ACPI_APEI
is enabled.

I kept it local because this fallback is only needed by this helper 
split and I did not want to widen the APEI header API for that.

>> +
>>   static struct ghes_estatus_cache __rcu *ghes_estatus_caches[GHES_ESTATUS_CACHES_SIZE];
>>   static atomic_t ghes_estatus_cache_alloced;
> 
>> +void __ghes_print_estatus(const char *pfx,
>> +			  const struct acpi_hest_generic *generic,
>> +			  const struct acpi_hest_generic_status *estatus)
>> +{
>> +	static atomic_t seqno;
>> +	unsigned int curr_seqno;
>> +	char pfx_seq[64];
>> +
>> +	if (!pfx) {
>> +		if (ghes_severity(estatus->error_severity) <=
>> +		    GHES_SEV_CORRECTED)
>> +			pfx = KERN_WARNING;
>> +		else
>> +			pfx = KERN_ERR;
>> +	}
>> +	curr_seqno = atomic_inc_return(&seqno);
>> +	snprintf(pfx_seq, sizeof(pfx_seq), "%s{%u}" HW_ERR, pfx, curr_seqno);
>> +	printk("%sHardware error from APEI Generic Hardware Error Source: %d\n",
>> +	       pfx_seq, generic->header.source_id);
>> +	cper_estatus_print(pfx_seq, estatus);
>> +}
>> +
>> +int ghes_print_estatus(const char *pfx,
>> +		       const struct acpi_hest_generic *generic,
>> +		       const struct acpi_hest_generic_status *estatus)
>> +{
>> +	/* Not more than 2 messages every 5 seconds */
>> +	static DEFINE_RATELIMIT_STATE(ratelimit_corrected, 5 * HZ, 2);
>> +	static DEFINE_RATELIMIT_STATE(ratelimit_uncorrected, 5 * HZ, 2);
>> +	struct ratelimit_state *ratelimit;
>> +
>> +	if (ghes_severity(estatus->error_severity) <= GHES_SEV_CORRECTED)
>> +		ratelimit = &ratelimit_corrected;
>> +	else
>> +		ratelimit = &ratelimit_uncorrected;
>> +	if (__ratelimit(ratelimit)) {
>> +		__ghes_print_estatus(pfx, generic, estatus);
>> +		return 1;
>> +	}
>> +	return 0;
>> +}
>> +
>> +#ifdef CONFIG_ACPI_APEI
> 
> So after the effort to break the the generic stuff we end up with non generic
> bits in the broken out file?  Is there no way to avoid this?
> 
The intent here was not to create a new generic estatus core
but to keep the existing GHES control flow and lift the CPER helper flow
for reuse by the DT provider.

Splitting the remaining ACPI GHES specific read/clear path back out into
ghes.c would break that flow across files again. The CONFIG_ACPI_APEI
guard keeps that ACPI specific piece local while the DT side reuses the
same CPER parsing and status-dispatch path.

Best regards,
Ahmed


^ permalink raw reply

* Re: [PATCH RFC 0/2] pinctrl: Add support gpiod_to_irq
From: Linus Walleij @ 2026-06-11 12:51 UTC (permalink / raw)
  To: xianwei.zhao
  Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Neil Armstrong,
	Kevin Hilman, Jerome Brunet, Martin Blumenstingl, linux-amlogic,
	linux-gpio, devicetree, linux-kernel, linux-arm-kernel
In-Reply-To: <20260611-gpio-to-irq-v1-0-12201716f23f@amlogic.com>

Hi Xianwei,

thanks for your patches!

On Thu, Jun 11, 2026 at 9:54 AM Xianwei Zhao via B4 Relay
<devnull+xianwei.zhao.amlogic.com@kernel.org> wrote:

> Some users need to obtain an IRQ directly from a GPIO descriptor through gpiod_to_irq().
> Add the required DT binding and implementation to support this use case.
> Since this introduces a new DT property, the property is kept optional to
> maintain compatibility with existing SoCs and DTS files.

To me it looks like you have just re-implemented hierarchical
irqs.

Look into the section "Infrastructure helpers for GPIO irqchips"
in Documentation/driver-api/gpio/driver.rst, especially towards
the end.

Solve this by using GPIOLIB_IRQCHIP and a custom
child_to_parent_hwirq() callback to translate the GPIO into
an IRQ.

To just implement gpiod_to_irq() without any irqchip abstraction
is also broken: you can't force all users to just use this way
to get an IRQ it's excessively restricting.

Add

  interrupt-controller: true

  "#interrupt-cells":
    const: 2

to the pinctrl node as well so that DT users can simply request
the IRQ from the irqchip inside of the pin controller. It will
be hierarchical and lightweight but an irqchip nevertheless.

The GPIOLIB_IRQCHIP approach will help you to get this
right.

Yours,
Linus Walleij


^ permalink raw reply

* Re: [RFC PATCH v2 1/3] mm/huge_memory: make persistent huge zero folio read-only
From: Lance Yang @ 2026-06-11 12:50 UTC (permalink / raw)
  To: David Hildenbrand (Arm)
  Cc: akpm, xueyuan.chen21, linux-mm, linux-kernel, linux-arm-kernel,
	x86, catalin.marinas, will, tglx, mingo, bp, dave.hansen, luto,
	peterz, hpa, ljs, liam, vbabka, rppt, surenb, mhocko, ziy,
	baolin.wang, npache, ryan.roberts, dev.jain, baohua, yang, jannh,
	dave.hansen
In-Reply-To: <d943a03e-bf28-4086-8907-5a24665fb2f5@kernel.org>



On 2026/6/11 20:21, David Hildenbrand (Arm) wrote:
> On 6/11/26 13:58, Lance Yang wrote:
>>
>> On Thu, Jun 11, 2026 at 01:28:58PM +0200, David Hildenbrand (Arm) wrote:
>>> On 6/10/26 04:15, Lance Yang wrote:
>>>>
>>>>
>>>> Right, this came from the RFC v1 discussion[1]. David preferred a page-
>>>> range helper for possible future non-folio callers, not something folio-
>>>> only.
>>>>
>>>> Of course, we could also add a folio wrapper on top of that if needed :)
>>>
>>> Best to document that as part of the patch description: we don't really expect
>>> to have a lot of read-only folios in the near future (zero page is rather
>>> special; maybe it won't even be a folio in the future).
>>
>> Ah, good to know, thanks. Will spell that out in RFC v3.
>>
>> Maybe something like this?
>>
>> The huge zero page is pretty special case, and maybe it won't even be a
>> folio in the future. Since read-only folios are unlikely to become a
>> common thing, a page-range helper is the cleaner fit.
> 
> Right. And if read-only folios in FSes become a real thing, we can always add
> infrastructure for that.

Got it, that draws the line nicely :P


^ permalink raw reply

* [PATCH] wifi: mt76: fix airoha_npu dependency tracking
From: Arnd Bergmann @ 2026-06-11 12:58 UTC (permalink / raw)
  To: Felix Fietkau, Lorenzo Bianconi, Ryder Lee, Matthias Brugger,
	AngeloGioacchino Del Regno
  Cc: Arnd Bergmann, Shayne Chen, Sean Wang, Rex Lu, linux-wireless,
	linux-kernel, linux-arm-kernel, linux-mediatek

From: Arnd Bergmann <arnd@arndb.de>

There is a new build failure with MT7996E=m MT76_CORE=y and NET_AIROHA_NPU=m:

ld.lld: error: undefined symbol: airoha_npu_get
ld.lld: error: undefined symbol: airoha_npu_put
>>> referenced by npu.c
>>>               drivers/net/wireless/mediatek/mt76/npu.o:(mt76_npu_init) in archive vmlinux.a

Fix this by reworking the dependency for the MT7996_NPU to only
allow enabling that when mt76_core can link against the npu driver.

To make sure this gets caught more easily in the future when additional
mt76 variants need the same dependency, also turn CONFIG_MT76_NPU into
a tristate symbol that has the same dependency.

Fixes: 7fb554b1b623 ("wifi: mt76: Introduce the NPU generic layer")
Signed-off-by: Arnd Bergmann <arnd@arndb.de>
---
 drivers/net/wireless/mediatek/mt76/Kconfig        | 4 ++--
 drivers/net/wireless/mediatek/mt76/Makefile       | 6 +++++-
 drivers/net/wireless/mediatek/mt76/mt76.h         | 2 +-
 drivers/net/wireless/mediatek/mt76/mt7996/Kconfig | 2 +-
 4 files changed, 9 insertions(+), 5 deletions(-)

diff --git a/drivers/net/wireless/mediatek/mt76/Kconfig b/drivers/net/wireless/mediatek/mt76/Kconfig
index 502303622a53..d941e67a222d 100644
--- a/drivers/net/wireless/mediatek/mt76/Kconfig
+++ b/drivers/net/wireless/mediatek/mt76/Kconfig
@@ -38,8 +38,8 @@ config MT792x_USB
 	select MT76_USB
 
 config MT76_NPU
-	bool
-	depends on MT76_CORE
+	tristate
+	depends on NET_AIROHA_NPU=y || MT76=NET_AIROHA_NPU
 
 source "drivers/net/wireless/mediatek/mt76/mt76x0/Kconfig"
 source "drivers/net/wireless/mediatek/mt76/mt76x2/Kconfig"
diff --git a/drivers/net/wireless/mediatek/mt76/Makefile b/drivers/net/wireless/mediatek/mt76/Makefile
index 1d42adfe8030..cacdd2b13d05 100644
--- a/drivers/net/wireless/mediatek/mt76/Makefile
+++ b/drivers/net/wireless/mediatek/mt76/Makefile
@@ -12,7 +12,11 @@ mt76-y := \
 	mmio.o util.o trace.o dma.o mac80211.o debugfs.o eeprom.o \
 	tx.o agg-rx.o mcu.o wed.o scan.o channel.o
 
-mt76-$(CONFIG_MT76_NPU) += npu.o
+ifdef CONFIG_MT76_NPU
+# CONFIG_MT76_NPU is tristate to simplify dependency tracking,
+# but it behaves as a bool symbol here.
+mt76-y += npu.o
+endif
 mt76-$(CONFIG_PCI) += pci.o
 mt76-$(CONFIG_NL80211_TESTMODE) += testmode.o
 
diff --git a/drivers/net/wireless/mediatek/mt76/mt76.h b/drivers/net/wireless/mediatek/mt76/mt76.h
index 07955555f84d..60bd155cc7d5 100644
--- a/drivers/net/wireless/mediatek/mt76/mt76.h
+++ b/drivers/net/wireless/mediatek/mt76/mt76.h
@@ -1647,7 +1647,7 @@ int mt76_testmode_dump(struct ieee80211_hw *hw, struct sk_buff *skb,
 int mt76_testmode_set_state(struct mt76_phy *phy, enum mt76_testmode_state state);
 int mt76_testmode_alloc_skb(struct mt76_phy *phy, u32 len);
 
-#ifdef CONFIG_MT76_NPU
+#if IS_ENABLED(CONFIG_MT76_NPU)
 void mt76_npu_check_ppe(struct mt76_dev *dev, struct sk_buff *skb,
 			u32 info);
 int mt76_npu_dma_add_buf(struct mt76_phy *phy, struct mt76_queue *q,
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/Kconfig b/drivers/net/wireless/mediatek/mt76/mt7996/Kconfig
index 5503d03bf62c..5742bce12fbb 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/Kconfig
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/Kconfig
@@ -16,6 +16,6 @@ config MT7996E
 config MT7996_NPU
 	bool "MT7996 (PCIe) NPU support"
 	depends on MT7996E
-	depends on NET_AIROHA_NPU=y || MT7996E=NET_AIROHA_NPU
+	depends on NET_AIROHA_NPU=y || MT76_CORE=NET_AIROHA_NPU
 	select MT76_NPU
 	default n
-- 
2.39.5



^ permalink raw reply related

* Re: [PATCH v3 2/2] clk: amlogic: Add A9 AO clock controller driver
From: Jian Hu @ 2026-06-11 13:01 UTC (permalink / raw)
  To: Jerome Brunet, Jian Hu via B4 Relay
  Cc: Neil Armstrong, Michael Turquette, Stephen Boyd, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Xianwei Zhao, Kevin Hilman,
	Martin Blumenstingl, linux-amlogic, linux-clk, devicetree,
	linux-kernel, linux-arm-kernel
In-Reply-To: <1jjys6fuhz.fsf@starbuckisacylon.baylibre.com>

On 6/10/2026 8:30 PM, Jerome Brunet wrote:
> [ EXTERNAL EMAIL ]
>
> On mer. 10 juin 2026 at 16:23, Jian Hu via B4 Relay <devnull+jian.hu.amlogic.com@kernel.org> wrote:
>
>> From: Jian Hu <jian.hu@amlogic.com>
>>
>> Add the Always-on clock controller driver for the Amlogic A9 SoC family.
>>
>> Signed-off-by: Jian Hu <jian.hu@amlogic.com>
>> ---
>>   drivers/clk/meson/Kconfig    |  13 ++
>>   drivers/clk/meson/Makefile   |   1 +
>>   drivers/clk/meson/a9-aoclk.c | 431 +++++++++++++++++++++++++++++++++++++++++++
>>   3 files changed, 445 insertions(+)
>>
>> diff --git a/drivers/clk/meson/Kconfig b/drivers/clk/meson/Kconfig
>> index cf8cf3f9e4ee..b71299898197 100644
>> --- a/drivers/clk/meson/Kconfig
>> +++ b/drivers/clk/meson/Kconfig
>> @@ -132,6 +132,19 @@ config COMMON_CLK_A1_PERIPHERALS
>>          device, A1 SoC Family. Say Y if you want A1 Peripherals clock
>>          controller to work.
>>
>> +config COMMON_CLK_A9_AO
>> +     tristate "Amlogic A9 SoC AO clock controller support"
>> +     depends on ARM64 || COMPILE_TEST
>> +     default ARCH_MESON
>> +     select COMMON_CLK_MESON_REGMAP
>> +     select COMMON_CLK_MESON_CLKC_UTILS
>> +     select COMMON_CLK_MESON_DUALDIV
>> +     imply COMMON_CLK_SCMI
>> +     help
>> +       Support for the AO clock controller on Amlogic A311Y3 based
>> +       device, AKA A9.
>> +       Say Y if you want A9 AO clock controller to work.
>> +
>>   config COMMON_CLK_C3_PLL
>>        tristate "Amlogic C3 PLL clock controller"
>>        depends on ARM64
>> diff --git a/drivers/clk/meson/Makefile b/drivers/clk/meson/Makefile
>> index c6719694a242..f89d027c282c 100644
>> --- a/drivers/clk/meson/Makefile
>> +++ b/drivers/clk/meson/Makefile
>> @@ -19,6 +19,7 @@ obj-$(CONFIG_COMMON_CLK_AXG) += axg.o axg-aoclk.o
>>   obj-$(CONFIG_COMMON_CLK_AXG_AUDIO) += axg-audio.o
>>   obj-$(CONFIG_COMMON_CLK_A1_PLL) += a1-pll.o
>>   obj-$(CONFIG_COMMON_CLK_A1_PERIPHERALS) += a1-peripherals.o
>> +obj-$(CONFIG_COMMON_CLK_A9_AO) += a9-aoclk.o
>>   obj-$(CONFIG_COMMON_CLK_C3_PLL) += c3-pll.o
>>   obj-$(CONFIG_COMMON_CLK_C3_PERIPHERALS) += c3-peripherals.o
>>   obj-$(CONFIG_COMMON_CLK_GXBB) += gxbb.o gxbb-aoclk.o
>> diff --git a/drivers/clk/meson/a9-aoclk.c b/drivers/clk/meson/a9-aoclk.c
>> new file mode 100644
>> index 000000000000..dd9fd8d24702
>> --- /dev/null
>> +++ b/drivers/clk/meson/a9-aoclk.c
>> @@ -0,0 +1,431 @@
>> +// SPDX-License-Identifier: (GPL-2.0-only OR MIT)
>> +/*
>> + * Copyright (C) 2026 Amlogic, Inc. All rights reserved
>> + */
>> +
>> +#include <dt-bindings/clock/amlogic,a9-aoclkc.h>
>> +#include <linux/clk-provider.h>
>> +#include <linux/platform_device.h>
>> +#include "clk-regmap.h"
>> +#include "clk-dualdiv.h"
>> +#include "meson-clkc-utils.h"
>> +
>> +#define AO_OSCIN_CTRL                        0x00
>> +#define AO_SYS_CLK0                  0x04
>> +#define AO_PWM_CLK_A_CTRL            0x1c
>> +#define AO_PWM_CLK_B_CTRL            0x20
>> +#define AO_PWM_CLK_C_CTRL            0x24
>> +#define AO_PWM_CLK_D_CTRL            0x28
>> +#define AO_PWM_CLK_E_CTRL            0x2c
>> +#define AO_PWM_CLK_F_CTRL            0x30
>> +#define AO_PWM_CLK_G_CTRL            0x34
>> +#define AO_CEC_CTRL0                 0x38
>> +#define AO_CEC_CTRL1                 0x3c
>> +#define AO_RTC_BY_OSCIN_CTRL0                0x50
>> +#define AO_RTC_BY_OSCIN_CTRL1                0x54
>> +
>> +#define A9_COMP_SEL(_name, _reg, _shift, _mask, _pdata) \
>> +     MESON_COMP_SEL(a9_ao_, _name, _reg, _shift, _mask, _pdata, NULL, 0, 0)
>> +
>> +#define A9_COMP_DIV(_name, _reg, _shift, _width) \
>> +     MESON_COMP_DIV(a9_ao_, _name, _reg, _shift, _width, 0, CLK_SET_RATE_PARENT)
>> +
>> +#define A9_COMP_GATE(_name, _reg, _bit) \
>> +     MESON_COMP_GATE(a9_ao_, _name, _reg, _bit, CLK_SET_RATE_PARENT)
>> +
>> +static struct clk_regmap a9_ao_xtal_in = {
>> +     .data = &(struct clk_regmap_gate_data){
>> +             .offset = AO_OSCIN_CTRL,
>> +             .bit_idx = 3,
>> +     },
>> +     .hw.init = &(struct clk_init_data) {
>> +             .name = "ao_xtal_in",
>> +             .ops = &clk_regmap_gate_ops,
>> +             .parent_data = &(const struct clk_parent_data) {
>> +                     .fw_name = "xtal",
>> +             },
>> +             .num_parents = 1,
>> +             /*
>> +              * ao_sys can select different clock sources. One possible clock path is:
>> +              *   ao_xtal_in->ao_xtal->ao_sys-> ao sys gate clocks
>> +              *
>> +              * ao_xtal_in is in the parent chain of AO sys gate clocks.
>> +              * Since some downstream clocks are marked CLK_IS_CRITICAL,
>> +              * ao_xtal_in must remain enabled and is therefore marked
>> +              * CLK_IS_CRITICAL as well.
>> +              */
>> +             .flags = CLK_IS_CRITICAL,
> Please allow some time for me to reply before reposting.
> See my answer on v2.
>

Sorry for reposting too quickly.

I'll allow more time for review feedback before sending the next revision.

I've seen your reply on v2 and will drop this flag in the next revision.

>> +     },
>> +};
>> +
>> +static struct clk_regmap a9_ao_xtal = {
>> +     .data = &(struct clk_regmap_mux_data) {
>> +             .offset = AO_OSCIN_CTRL,
>> +             .mask = 0x1,
>> +             .shift = 0,
>> +     },
>> +     /* ext_32k is from external PAD, do not automatically reparent */
>> +     .hw.init = CLK_HW_INIT_PARENTS_DATA("ao_xtal",
>> +                     ((const struct clk_parent_data []) {
>> +                             { .hw = &a9_ao_xtal_in.hw },
>> +                             { .fw_name = "ext_32k" }
>> +                     }), &clk_regmap_mux_ops, CLK_SET_RATE_NO_REPARENT),
> I hope my view on this is clear as well.
> Let me know if it isn't
>

Understood. I will drop all CLK_HW_INIT* macros and revert to explicit
struct clk_init_data initializers for the A9 clock controllers.


[ ... ]

> --
> Jerome


--

Jian



^ permalink raw reply

* [RFC PATCH 0/6] arm64: mm: Introducing ROX CACHE to ARM64 systems with bbml2 no abort
From: Adrian Barnaś @ 2026-06-11 13:01 UTC (permalink / raw)
  To: linux-arm-kernel
  Cc: linux-mm, Adrian Barnaś, Catalin Marinas, Will Deacon,
	Ryan Roberts, David Hildenbrand, Mike Rapoport (Microsoft),
	Ard Biesheuvel, Christoph Lameter, Yang Shi, Brendan Jackman

Hi All,

I would like to propose the introduction of the EXECMEM_ROX_CACHE feature
for ARM64.

When the `rodata=on` kernel parameter is set, all executable and read-only
aliases in the linear map are forced to be read-only. Originally, this
forced PTE-level mappings across the entire linear map, causing a
significant performance regression (greater than 10%) for some workloads
due to increased TLB miss rates and deeper page table walk.

The `feat_bbml2_no_abort` (FEAT_BBML3 in ARMv9.7) feature can be
utilized to mitigate this regression. Because we can split memory
mappings without triggering TLB conflict aborts, kernel memory permission
adjustments become possible after early boot without forcing PTE-level
mappings across the entire linear map.

However, when applying read-only permissions to kernel module section the
linear map can still became fragmented due to the scattered physical layout
of the underlying pages. To address this, EXECMEM_ROX_CACHE, which was
initially enabled on the x86 architecture [1] for this purpose, can be
used on ARM64 as well.

EXECMEM_ROX_CACHE works by preallocating PMD-sized contiguous blocks to
act as a cache for .module.text memory. These blocks are initially
poisoned and made read-only-execute (which simultaneously makes the
linear alias of this region read-only). When loading a .module.text
section into memory, the requested cache region is made RW, the bytes
are copied, and ROX permissions are restored.

To take full advantage of this approach, after restoring RO permissions
on the PMD-sized linear alias block, the PTE mappings are coalesced back
into a single PMD entry.

Testing on an Android device running 6.18 based kernel with rodata=on
shows an average 20% reduction of level 3 page table entries for the
linear mapping.

This implementation currently works around some limitations of the
`set_memory_xx` API, which might be relevant when considering the
refactoring proposed here [2]:

* Because the execmem_cache operates outside the linear map, its
  permissions could theoretically remain untouched (poisoned and RO)
  until the cache block is fully emptied and freed. However, we currently
  lack an API to interact exclusively with the vmalloc area (e.g., setting
  it to RW) without simultaneously setting the linear alias to RW.

* Additionally, set_direct_map_valid() has somewhat confusing semantics.
  It is used while "cleaning" a cache block (after confirming it is
  entirely empty). The linear map region should be returned to its default
  state to restore writability, but set_valid might just set the "valid"
  attribute (as is the case for ARM64, which I have temporarily addressed
  here with a workaround).

I would be glad to hear your feedback on these changes.

Best regards,
Adrian

[1]: https://lore.kernel.org/all/20241023162711.2579610-1-rppt@kernel.org/
[2]: https://lore.kernel.org/all/20260219175113.618562-1-jackmanb@google.com/

Cc: Catalin Marinas <catalin.marinas@arm.com>
Cc: Will Deacon <will@kernel.org>
Cc: Ryan Roberts <ryan.roberts@arm.com>
Cc: David Hildenbrand <david@kernel.org>
Cc: "Mike Rapoport (Microsoft)" <rppt@kernel.org>
Cc: Ard Biesheuvel <ardb@kernel.org>
Cc: Christoph Lameter <cl@gentwo.org>
Cc: Yang Shi <yang@os.amperecomputing.com>
Cc: Brendan Jackman <jackmanb@google.com>

--->8

Adrian Barnaś (6):
  arm64: mm: explicitly declare module and ftrace execmem regions
  arm64: mm: allow huge vmap permission adjustments with bbml2_no_abort
  arm64: mm: fix restoring linear map permissions on execmem cache clean
  arm64: mm: add helper to fill execmem with trapping instructions
  arm64: execmem: enable EXECMEM_ROX_CACHE on supported CPUs
  arm64: mm: support PMD page coalescing in the linear map

 arch/arm64/Kconfig           |  1 +
 arch/arm64/include/asm/mmu.h |  1 +
 arch/arm64/mm/init.c         | 54 +++++++++++++++++++-
 arch/arm64/mm/mmu.c          | 95 ++++++++++++++++++++++++++++++++++++
 arch/arm64/mm/pageattr.c     | 46 +++++++++++++----
 5 files changed, 186 insertions(+), 11 deletions(-)

--
2.54.0.1136.gdb2ca164c4-goog



^ permalink raw reply

* [RFC PATCH 1/6] arm64: mm: explicitly declare module and ftrace execmem regions
From: Adrian Barnaś @ 2026-06-11 13:01 UTC (permalink / raw)
  To: linux-arm-kernel
  Cc: linux-mm, Adrian Barnaś, Catalin Marinas, Will Deacon,
	Ryan Roberts, David Hildenbrand, Mike Rapoport (Microsoft),
	Ard Biesheuvel, Christoph Lameter, Yang Shi, Brendan Jackman
In-Reply-To: <20260611130144.1385343-1-abarnas@google.com>

Replace the reliance on the EXECMEM_DEFAULT fallback by explicitly defining
the execution memory (execmem) regions for MODULE_TEXT, MODULE_DATA, and
FTRACE in execmem_arch_setup().

Signed-off-by: Adrian Barnaś <abarnas@google.com>
---
 arch/arm64/mm/init.c | 18 +++++++++++++++++-
 1 file changed, 17 insertions(+), 1 deletion(-)

diff --git a/arch/arm64/mm/init.c b/arch/arm64/mm/init.c
index 96711b8578fd..c673a9a839dd 100644
--- a/arch/arm64/mm/init.c
+++ b/arch/arm64/mm/init.c
@@ -519,7 +519,7 @@ struct execmem_info __init *execmem_arch_setup(void)
 
 	execmem_info = (struct execmem_info){
 		.ranges = {
-			[EXECMEM_DEFAULT] = {
+			[EXECMEM_MODULE_TEXT] = {
 				.start	= start,
 				.end	= end,
 				.pgprot	= PAGE_KERNEL,
@@ -533,12 +533,28 @@ struct execmem_info __init *execmem_arch_setup(void)
 				.pgprot	= PAGE_KERNEL_ROX,
 				.alignment = 1,
 			},
+			[EXECMEM_FTRACE] = {
+				.start	= VMALLOC_START,
+				.end	= VMALLOC_END,
+				.pgprot	= PAGE_KERNEL,
+				.alignment = 1,
+				.fallback_start	= fallback_start,
+				.fallback_end	= fallback_end,
+			},
 			[EXECMEM_BPF] = {
 				.start	= VMALLOC_START,
 				.end	= VMALLOC_END,
 				.pgprot	= PAGE_KERNEL,
 				.alignment = 1,
 			},
+			[EXECMEM_MODULE_DATA] = {
+				.start  = start,
+				.end    = end,
+				.pgprot = PAGE_KERNEL,
+				.alignment = 1,
+				.fallback_start	= fallback_start,
+				.fallback_end	= fallback_end,
+			},
 		},
 	};
 
-- 
2.54.0.1136.gdb2ca164c4-goog



^ permalink raw reply related

* [RFC PATCH 2/6] arm64: mm: allow huge vmap permission adjustments with bbml2_no_abort
From: Adrian Barnaś @ 2026-06-11 13:01 UTC (permalink / raw)
  To: linux-arm-kernel
  Cc: linux-mm, Adrian Barnaś, Catalin Marinas, Will Deacon,
	Ryan Roberts, David Hildenbrand, Mike Rapoport (Microsoft),
	Ard Biesheuvel, Christoph Lameter, Yang Shi, Brendan Jackman
In-Reply-To: <20260611130144.1385343-1-abarnas@google.com>

Remove the protection against huge vmap permission adjustments on
systems that support the bbml2_no_abort CPU feature.

Splitting live kernel VA section mappings into page mappings was
restricted because it could cause TLB Conflict Aborts. This forced
permission adjustments on memory allocated with VM_ALLOW_HUGE_VMAP to be
rejected, resulting in performance drops (e.g., when enforcing rodata=on
disables huge mappings).

The bbml2_no_abort feature (which mirrors the architectural guarantees of
FEAT_BBML3) ensures that changing between table and block sizes without
following a break-before-make sequence will not generate a TLB Conflict
Abort. This hardware guarantee makes it safe to allow dynamic permission
adjustments on huge vmap regions.

Signed-off-by: Adrian Barnaś <abarnas@google.com>
---
 arch/arm64/mm/pageattr.c | 22 ++++++++++++++--------
 1 file changed, 14 insertions(+), 8 deletions(-)

diff --git a/arch/arm64/mm/pageattr.c b/arch/arm64/mm/pageattr.c
index 358d1dc9a576..88720bbba892 100644
--- a/arch/arm64/mm/pageattr.c
+++ b/arch/arm64/mm/pageattr.c
@@ -157,23 +157,29 @@ static int change_memory_common(unsigned long addr, int numpages,
 	}
 
 	/*
-	 * Kernel VA mappings are always live, and splitting live section
-	 * mappings into page mappings may cause TLB conflicts. This means
-	 * we have to ensure that changing the permission bits of the range
-	 * we are operating on does not result in such splitting.
-	 *
 	 * Let's restrict ourselves to mappings created by vmalloc (or vmap).
-	 * Disallow VM_ALLOW_HUGE_VMAP mappings to guarantee that only page
-	 * mappings are updated and splitting is never needed.
 	 *
 	 * So check whether the [addr, addr + size) interval is entirely
 	 * covered by precisely one VM area that has the VM_ALLOC flag set.
 	 */
 	area = find_vm_area((void *)addr);
+
 	if (!area ||
 	    ((unsigned long)kasan_reset_tag((void *)end) >
 	     (unsigned long)kasan_reset_tag(area->addr) + area->size) ||
-	    ((area->flags & (VM_ALLOC | VM_ALLOW_HUGE_VMAP)) != VM_ALLOC))
+	    !(area->flags & VM_ALLOC))
+		return -EINVAL;
+
+	/*
+	 * Kernel VA mappings are always live, and splitting live section
+	 * mappings into page mappings may cause TLB conflicts if bbml2_noabort
+	 * is not present.
+	 *
+	 * While bbml2_noabort is not present disallow VM_ALLOW_HUGE_VMAP mappings
+	 * to guarantee that only page mappings are updated and splitting is not
+	 * needed.
+	 */
+	if (!system_supports_bbml2_noabort() && (area->flags & (VM_ALLOW_HUGE_VMAP)))
 		return -EINVAL;
 
 	if (!numpages)
-- 
2.54.0.1136.gdb2ca164c4-goog



^ permalink raw reply related

* [RFC PATCH 3/6] arm64: mm: fix restoring linear map permissions on execmem cache clean
From: Adrian Barnaś @ 2026-06-11 13:01 UTC (permalink / raw)
  To: linux-arm-kernel
  Cc: linux-mm, Adrian Barnaś, Catalin Marinas, Will Deacon,
	Ryan Roberts, David Hildenbrand, Mike Rapoport (Microsoft),
	Ard Biesheuvel, Christoph Lameter, Yang Shi, Brendan Jackman
In-Reply-To: <20260611130144.1385343-1-abarnas@google.com>

Strip the read-only attribute from the selected memory range when
restoring the linear map after an execmem cache clean.

An execmem cache clean is performed when a cache block becomes empty
after unloading a module. When making the memory valid again, the linear
memory alias must also have its read-only attribute cleared.

Without this change, the linear memory alias remains read-only even
after the execmem cache block itself is freed, which prevents subsequent
allocations from writing to that memory.

Signed-off-by: Adrian Barnaś <abarnas@google.com>
---
 arch/arm64/mm/pageattr.c | 17 ++++++++++++++++-
 1 file changed, 16 insertions(+), 1 deletion(-)

diff --git a/arch/arm64/mm/pageattr.c b/arch/arm64/mm/pageattr.c
index 88720bbba892..eaefdf90b0d5 100644
--- a/arch/arm64/mm/pageattr.c
+++ b/arch/arm64/mm/pageattr.c
@@ -239,6 +239,13 @@ int set_memory_x(unsigned long addr, int numpages)
 					__pgprot(PTE_PXN));
 }
 
+static int set_memory_default(unsigned long addr, int numpages)
+{
+	return __change_memory_common(addr, PAGE_SIZE * numpages,
+				      __pgprot(PTE_VALID),
+				      __pgprot(PTE_RDONLY));
+}
+
 int set_memory_valid(unsigned long addr, int numpages, int enable)
 {
 	if (enable)
@@ -362,7 +369,15 @@ int set_direct_map_valid_noflush(struct page *page, unsigned nr, bool valid)
 	if (!can_set_direct_map())
 		return 0;
 
-	return set_memory_valid(addr, nr, valid);
+	/*
+	 * Execmem cache uses this function to reset permissions on linear mapping
+	 * when freeing unused cache block. On x86 it makes memory RW which is
+	 * desirable. On ARM64 set_memory_valid() just change valid bit which
+	 * leave direct mapping read-only so use set_memory_default instead.
+	 */
+
+	return valid ? set_memory_default(addr, nr) :
+		       set_memory_valid(addr, nr, false);
 }
 
 #ifdef CONFIG_DEBUG_PAGEALLOC
-- 
2.54.0.1136.gdb2ca164c4-goog



^ permalink raw reply related

* [RFC PATCH 4/6] arm64: mm: add helper to fill execmem with trapping instructions
From: Adrian Barnaś @ 2026-06-11 13:01 UTC (permalink / raw)
  To: linux-arm-kernel
  Cc: linux-mm, Adrian Barnaś, Catalin Marinas, Will Deacon,
	Ryan Roberts, David Hildenbrand, Mike Rapoport (Microsoft),
	Ard Biesheuvel, Christoph Lameter, Yang Shi, Brendan Jackman
In-Reply-To: <20260611130144.1385343-1-abarnas@google.com>

Implement the architecture-specific execmem_fill_trapping_insns() helper
to poison executable memory regions.

When CONFIG_ARCH_HAS_EXECMEM_ROX is enabled, the execmem subsystem
requires a way to fill unused or freed executable memory with
architecture-specific trapping instructions. This implementation fills
the specified region with AARCH64_BREAK_FAULT instructions and flushes
the icache to ensure the traps are immediately visible to execution.

Signed-off-by: Adrian Barnaś <abarnas@google.com>
---
 arch/arm64/mm/init.c | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

diff --git a/arch/arm64/mm/init.c b/arch/arm64/mm/init.c
index c673a9a839dd..71aa745e0bef 100644
--- a/arch/arm64/mm/init.c
+++ b/arch/arm64/mm/init.c
@@ -408,6 +408,20 @@ void dump_mem_limit(void)
 }
 
 #ifdef CONFIG_EXECMEM
+
+#ifdef CONFIG_ARCH_HAS_EXECMEM_ROX
+void execmem_fill_trapping_insns(void *ptr, size_t size)
+{
+	int nr_inst = size / AARCH64_INSN_SIZE;
+	__le32 *updptr = ptr;
+
+	for (int i = 0; i < nr_inst; i++)
+		updptr[i] = cpu_to_le32(AARCH64_BREAK_FAULT);
+
+	flush_icache_range((unsigned long)ptr, (unsigned long)ptr + size);
+}
+#endif
+
 static u64 module_direct_base __ro_after_init = 0;
 static u64 module_plt_base __ro_after_init = 0;
 
-- 
2.54.0.1136.gdb2ca164c4-goog



^ permalink raw reply related


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