kernel: backport rtl8365mb DSA driver improvements

Backport several fixes and enhancements for the rtl8365mb DSA driver
from upstream.

The backport includes the following changes:
- Fix for rtl8_4 tag (from v7.1)
- VLAN and Forwarding offload (from v7.2)

Before this backport, all bridge forwarding was handled by the CPU,
causing a significant performance drop between LAN ports, similar
to standard WAN/LAN routing. Enabling hardware forwarding offload
alleviates CPU overhead and restores line-rate switching performance.

Link: https://patch.msgid.link/20260408-realtek_fixes-v1-0-915ff1404d56@gmail.com
Link: https://patch.msgid.link/20260606-realtek_forward-v13-0-b9e409687cbe@gmail.com
Signed-off-by: Luiz Angelo Daros de Luca <luizluca@gmail.com>
Link: https://github.com/openwrt/openwrt/pull/23738
Signed-off-by: Robert Marko <robimarko@gmail.com>
This commit is contained in:
Luiz Angelo Daros de Luca
2026-06-11 14:49:43 +02:00
committed by Robert Marko
parent 4fa4766986
commit 0bba5f31fc
22 changed files with 17818 additions and 0 deletions
@@ -0,0 +1,70 @@
From 297e1f411ed4927a912c7e207ba6f978cb1f9f0e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Alvin=20=C5=A0ipraga?= <alsi@bang-olufsen.dk>
Date: Wed, 8 Apr 2026 17:31:01 -0300
Subject: [PATCH 1/2] net: dsa: tag_rtl8_4: update format description
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Document the updated tag layout fields (EFID, VSEL/VIDX) and clarify
which bits are set/cleared when emitting tags.
Co-developed-by: Alvin Šipraga <alsi@bang-olufsen.dk>
Signed-off-by: Alvin Šipraga <alsi@bang-olufsen.dk>
Signed-off-by: Luiz Angelo Daros de Luca <luizluca@gmail.com>
Reviewed-by: Linus Walleij <linusw@kernel.org>
Link: https://patch.msgid.link/20260408-realtek_fixes-v1-1-915ff1404d56@gmail.com
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
---
net/dsa/tag_rtl8_4.c | 17 ++++++++++-------
1 file changed, 10 insertions(+), 7 deletions(-)
--- a/net/dsa/tag_rtl8_4.c
+++ b/net/dsa/tag_rtl8_4.c
@@ -17,8 +17,8 @@
* | (8-bit) | (8-bit) |
* | Protocol [0x04] | REASON | b
* |-----------------------------------+-----------------------------------| y
- * | (1) | (1) | (2) | (1) | (3) | (1) | (1) | (1) | (5) | t
- * | FID_EN | X | FID | PRI_EN | PRI | KEEP | X | LEARN_DIS | X | e
+ * | (1) | (3) | (1) | (3) | (1) | (1) | (1) | (5) | t
+ * | EFID_EN | EFID | PRI_EN | PRI | KEEP | VSEL | LEARN_DIS | VIDX | e
* |-----------------------------------+-----------------------------------| s
* | (1) | (15-bit) | |
* | ALLOW | TX/RX | v
@@ -32,19 +32,22 @@
* EtherType | note that Realtek uses the same EtherType for
* | other incompatible tag formats (e.g. tag_rtl4_a.c)
* Protocol | 0x04: indicates that this tag conforms to this format
- * X | reserved
* ------------+-------------
* REASON | reason for forwarding packet to CPU
* | 0: packet was forwarded or flooded to CPU
* | 80: packet was trapped to CPU
- * FID_EN | 1: packet has an FID
- * | 0: no FID
- * FID | FID of packet (if FID_EN=1)
+ * EFID_EN | 1: packet has an EFID
+ * | 0: no EFID
+ * EFID | Extended filter ID (EFID) of packet (if EFID_EN=1)
* PRI_EN | 1: force priority of packet
* | 0: don't force priority
* PRI | priority of packet (if PRI_EN=1)
* KEEP | preserve packet VLAN tag format
+ * VSEL | 0: switch should classify packet according to VLAN tag
+ * | 1: switch should classify packet according to VLAN membership
+ * | configuration with index VIDX
* LEARN_DIS | don't learn the source MAC address of the packet
+ * VIDX | index of a VLAN membership configuration to use with VSEL
* ALLOW | 1: treat TX/RX field as an allowance port mask, meaning the
* | packet may only be forwarded to ports specified in the
* | mask
@@ -112,7 +115,7 @@ static void rtl8_4_write_tag(struct sk_b
/* Set Protocol; zero REASON */
tag16[1] = htons(FIELD_PREP(RTL8_4_PROTOCOL, RTL8_4_PROTOCOL_RTL8365MB));
- /* Zero FID_EN, FID, PRI_EN, PRI, KEEP; set LEARN_DIS */
+ /* Zero EFID_EN, EFID, PRI_EN, PRI, VSEL, VIDX, KEEP; set LEARN_DIS */
tag16[2] = htons(FIELD_PREP(RTL8_4_LEARN_DIS, 1));
/* Zero ALLOW; set RX (CPU->switch) forwarding port mask */
@@ -0,0 +1,48 @@
From 82f37bd9a4d779495479c0c13152208d5400c8a4 Mon Sep 17 00:00:00 2001
From: Luiz Angelo Daros de Luca <luizluca@gmail.com>
Date: Wed, 8 Apr 2026 17:31:02 -0300
Subject: [PATCH 2/2] net: dsa: tag_rtl8_4: set KEEP flag
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
KEEP=1 is needed because we should respect the format of the packet as
the kernel sends it to us. Unless tx forward offloading is used, the
kernel is giving us the packet exactly as it should leave the specified
port on the wire. Until now this was not needed because the ports were
always functioning in a standalone mode in a VLAN-unaware way, so the
switch would not tag or untag frames anyway. But arguably it should have
been KEEP=1 all along.
Co-developed-by: Alvin Šipraga <alsi@bang-olufsen.dk>
Signed-off-by: Alvin Šipraga <alsi@bang-olufsen.dk>
Signed-off-by: Luiz Angelo Daros de Luca <luizluca@gmail.com>
Reviewed-by: Linus Walleij <linusw@kernel.org>
Link: https://patch.msgid.link/20260408-realtek_fixes-v1-2-915ff1404d56@gmail.com
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
---
net/dsa/tag_rtl8_4.c | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
--- a/net/dsa/tag_rtl8_4.c
+++ b/net/dsa/tag_rtl8_4.c
@@ -99,6 +99,7 @@
#define RTL8_4_REASON_TRAP 80
#define RTL8_4_LEARN_DIS BIT(5)
+#define RTL8_4_KEEP BIT(7)
#define RTL8_4_TX GENMASK(3, 0)
#define RTL8_4_RX GENMASK(10, 0)
@@ -115,8 +116,9 @@ static void rtl8_4_write_tag(struct sk_b
/* Set Protocol; zero REASON */
tag16[1] = htons(FIELD_PREP(RTL8_4_PROTOCOL, RTL8_4_PROTOCOL_RTL8365MB));
- /* Zero EFID_EN, EFID, PRI_EN, PRI, VSEL, VIDX, KEEP; set LEARN_DIS */
- tag16[2] = htons(FIELD_PREP(RTL8_4_LEARN_DIS, 1));
+ /* Zero EFID_EN, EFID, PRI_EN, PRI, VSEL, VIDX; set KEEP, LEARN_DIS */
+ tag16[2] = htons(FIELD_PREP(RTL8_4_LEARN_DIS, 1) |
+ FIELD_PREP(RTL8_4_KEEP, 1));
/* Zero ALLOW; set RX (CPU->switch) forwarding port mask */
tag16[3] = htons(FIELD_PREP(RTL8_4_RX, BIT(dp->index)));
@@ -0,0 +1,154 @@
From a543687227d86b1f4401f395abaf7f3f710fe964 Mon Sep 17 00:00:00 2001
From: Luiz Angelo Daros de Luca <luizluca@gmail.com>
Date: Sat, 6 Jun 2026 05:29:25 -0300
Subject: net: dsa: realtek: rtl8365mb: use ERR_PTR
Convert numeric error codes into human-readable strings by using %pe
together with ERR_PTR() in dev_err() messages. Also use dev_err_probe()
instead of checking for -EPROBE_DEFER.
Reviewed-by: Linus Walleij <linusw@kernel.org>
Reviewed-by: Mieczyslaw Nalewaj <namiltd@yahoo.com>
Signed-off-by: Luiz Angelo Daros de Luca <luizluca@gmail.com>
Link: https://patch.msgid.link/20260606-realtek_forward-v13-1-b9e409687cbe@gmail.com
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
---
drivers/net/dsa/realtek/rtl8365mb.c | 51 +++++++++++++++++++++----------------
1 file changed, 29 insertions(+), 22 deletions(-)
--- a/drivers/net/dsa/realtek/rtl8365mb.c
+++ b/drivers/net/dsa/realtek/rtl8365mb.c
@@ -803,8 +803,8 @@ static int rtl8365mb_phy_read(struct rea
ret = rtl8365mb_phy_ocp_read(priv, phy, ocp_addr, &val);
if (ret) {
dev_err(priv->dev,
- "failed to read PHY%d reg %02x @ %04x, ret %d\n", phy,
- regnum, ocp_addr, ret);
+ "failed to read PHY%d reg %02x @ %04x, ret %pe\n", phy,
+ regnum, ocp_addr, ERR_PTR(ret));
return ret;
}
@@ -831,8 +831,8 @@ static int rtl8365mb_phy_write(struct re
ret = rtl8365mb_phy_ocp_write(priv, phy, ocp_addr, val);
if (ret) {
dev_err(priv->dev,
- "failed to write PHY%d reg %02x @ %04x, ret %d\n", phy,
- regnum, ocp_addr, ret);
+ "failed to write PHY%d reg %02x @ %04x, ret %pe\n", phy,
+ regnum, ocp_addr, ERR_PTR(ret));
return ret;
}
@@ -1082,8 +1082,8 @@ static void rtl8365mb_phylink_mac_config
ret = rtl8365mb_ext_config_rgmii(priv, port, state->interface);
if (ret)
dev_err(priv->dev,
- "failed to configure RGMII mode on port %d: %d\n",
- port, ret);
+ "failed to configure RGMII mode on port %d: %pe\n",
+ port, ERR_PTR(ret));
return;
}
@@ -1112,8 +1112,8 @@ static void rtl8365mb_phylink_mac_link_d
false, false);
if (ret)
dev_err(priv->dev,
- "failed to reset forced mode on port %d: %d\n",
- port, ret);
+ "failed to reset forced mode on port %d: %pe\n",
+ port, ERR_PTR(ret));
return;
}
@@ -1143,8 +1143,8 @@ static void rtl8365mb_phylink_mac_link_u
rx_pause);
if (ret)
dev_err(priv->dev,
- "failed to force mode on port %d: %d\n", port,
- ret);
+ "failed to force mode on port %d: %pe\n", port,
+ ERR_PTR(ret));
return;
}
@@ -1299,8 +1299,8 @@ static void rtl8365mb_get_ethtool_stats(
mib->length, &data[i]);
if (ret) {
dev_err(priv->dev,
- "failed to read port %d counters: %d\n", port,
- ret);
+ "failed to read port %d counters: %pe\n", port,
+ ERR_PTR(ret));
break;
}
}
@@ -1652,7 +1652,8 @@ static irqreturn_t rtl8365mb_irq(int irq
return IRQ_HANDLED;
out_error:
- dev_err(priv->dev, "failed to read interrupt status: %d\n", ret);
+ dev_err(priv->dev, "failed to read interrupt status: %pe\n",
+ ERR_PTR(ret));
out_none:
return IRQ_NONE;
@@ -1725,10 +1726,13 @@ static int rtl8365mb_irq_setup(struct re
/* rtl8365mb IRQs cascade off this one */
irq = of_irq_get(intc, 0);
if (irq <= 0) {
- if (irq != -EPROBE_DEFER)
- dev_err(priv->dev, "failed to get parent irq: %d\n",
- irq);
- ret = irq ? irq : -EINVAL;
+ if (!irq) {
+ dev_err(priv->dev, "failed to map IRQ\n");
+ ret = -EINVAL;
+ } else {
+ ret = dev_err_probe(priv->dev, irq,
+ "failed to get parent irq\n");
+ }
goto out_put_node;
}
@@ -1790,7 +1794,8 @@ static int rtl8365mb_irq_setup(struct re
ret = request_threaded_irq(irq, NULL, rtl8365mb_irq, IRQF_ONESHOT,
"rtl8365mb", priv);
if (ret) {
- dev_err(priv->dev, "failed to request irq: %d\n", ret);
+ dev_err(priv->dev, "failed to request irq: %pe\n",
+ ERR_PTR(ret));
goto out_remove_irqdomain;
}
@@ -1966,14 +1971,16 @@ static int rtl8365mb_setup(struct dsa_sw
ret = rtl8365mb_reset_chip(priv);
if (ret) {
- dev_err(priv->dev, "failed to reset chip: %d\n", ret);
+ dev_err(priv->dev, "failed to reset chip: %pe\n",
+ ERR_PTR(ret));
goto out_error;
}
/* Configure switch to vendor-defined initial state */
ret = rtl8365mb_switch_init(priv);
if (ret) {
- dev_err(priv->dev, "failed to initialize switch: %d\n", ret);
+ dev_err(priv->dev, "failed to initialize switch: %pe\n",
+ ERR_PTR(ret));
goto out_error;
}
@@ -2091,8 +2098,8 @@ static int rtl8365mb_detect(struct realt
ret = rtl8365mb_get_chip_id_and_ver(priv->map, &chip_id, &chip_ver);
if (ret) {
- dev_err(priv->dev, "failed to read chip id and version: %d\n",
- ret);
+ dev_err(priv->dev, "failed to read chip id and version: %pe\n",
+ ERR_PTR(ret));
return ret;
}
@@ -0,0 +1,59 @@
From 36c572fd60d6bf2f9631088833c162203907c9ca Mon Sep 17 00:00:00 2001
From: Luiz Angelo Daros de Luca <luizluca@gmail.com>
Date: Sat, 6 Jun 2026 05:29:26 -0300
Subject: net: dsa: realtek: rtl8365mb: reject unsupported topologies
Explicitly enforce the presence of a CPU port (-EINVAL) and reject DSA
cascade links (-EOPNOTSUPP) during setup to prevent silent failures.
These topologies were already non-functional. Without a CPU port, the
driver does not activate CPU tagging. Additionally, the switch hardware
was not designed to be cascaded, and DSA links never worked because
CPU tagging is not enabled for them.
Reviewed-by: Mieczyslaw Nalewaj <namiltd@yahoo.com>
Reviewed-by: Linus Walleij <linusw@kernel.org>
Signed-off-by: Luiz Angelo Daros de Luca <luizluca@gmail.com>
Link: https://patch.msgid.link/20260606-realtek_forward-v13-2-b9e409687cbe@gmail.com
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
---
drivers/net/dsa/realtek/rtl8365mb.c | 21 +++++++++++++++++++++
1 file changed, 21 insertions(+)
--- a/drivers/net/dsa/realtek/rtl8365mb.c
+++ b/drivers/net/dsa/realtek/rtl8365mb.c
@@ -1991,6 +1991,20 @@ static int rtl8365mb_setup(struct dsa_sw
else if (ret)
dev_info(priv->dev, "no interrupt support\n");
+ for (i = 0; i < priv->num_ports; i++) {
+ /* Cascading (DSA links) is not supported yet.
+ * Historically, the driver has always been broken
+ * without a dedicated CPU port because CPU tagging
+ * would be disabled, rendering the switch entirely
+ * non-functional for DSA operations.
+ */
+ if (dsa_is_dsa_port(ds, i)) {
+ dev_err(priv->dev, "Cascading (DSA link) not supported\n");
+ ret = -EOPNOTSUPP;
+ goto out_teardown_irq;
+ }
+ }
+
/* Configure CPU tagging */
dsa_switch_for_each_cpu_port(cpu_dp, ds) {
cpu->mask |= BIT(cpu_dp->index);
@@ -1999,6 +2013,13 @@ static int rtl8365mb_setup(struct dsa_sw
cpu->trap_port = cpu_dp->index;
}
cpu->enable = cpu->mask > 0;
+
+ if (!cpu->enable) {
+ dev_err(priv->dev, "no CPU port defined\n");
+ ret = -EINVAL;
+ goto out_teardown_irq;
+ }
+
ret = rtl8365mb_cpu_config(priv);
if (ret)
goto out_teardown_irq;
@@ -0,0 +1,347 @@
From 51a4a22301b023610a1321614e8dc61251ef9064 Mon Sep 17 00:00:00 2001
From: Luiz Angelo Daros de Luca <luizluca@gmail.com>
Date: Sat, 6 Jun 2026 05:29:27 -0300
Subject: net: dsa: realtek: rtl8365mb: use dsa helpers for port iteration
Convert open-coded port iteration loops to use the DSA helpers and
restructure rtl8365mb_setup() into clear blocking, user, and
CPU port phases.
As part of this refactoring, unused ports are explicitly placed into a
blocked, isolated state with learning disabled, ensuring safe default
hardware behavior. The driver also does not allocate a virtual IRQ
mapping for unused ports. To accommodate this, a guard check is added to
the interrupt handler (rtl8365mb_irq) to safely skip ports without a
valid IRQ mapping. The irq domain teardown, however, does clean all
ports as external PHYs may still map the IRQ.
Furthermore, since the new initialization loop starts with all ports
administratively isolated by default, CPU port forwarding and isolation
masks are explicitly configured at the end of the setup phase to prevent
egress traffic from being blocked.
Suggested-by: Abdulkader Alrezej <abdulkader.alrezej@gmail.com>
Reviewed-by: Linus Walleij <linusw@kernel.org>
Reviewed-by: Mieczyslaw Nalewaj <namiltd@yahoo.com>
Signed-off-by: Luiz Angelo Daros de Luca <luizluca@gmail.com>
Link: https://patch.msgid.link/20260606-realtek_forward-v13-3-b9e409687cbe@gmail.com
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
---
drivers/net/dsa/realtek/rtl8365mb.c | 166 +++++++++++++++++++++++-------------
1 file changed, 105 insertions(+), 61 deletions(-)
--- a/drivers/net/dsa/realtek/rtl8365mb.c
+++ b/drivers/net/dsa/realtek/rtl8365mb.c
@@ -1554,18 +1554,15 @@ static void rtl8365mb_stats_setup(struct
{
struct rtl8365mb *mb = priv->chip_data;
struct dsa_switch *ds = &priv->ds;
- int i;
+ struct dsa_port *dp;
/* Per-chip global mutex to protect MIB counter access, since doing
* so requires accessing a series of registers in a particular order.
*/
mutex_init(&mb->mib_lock);
- for (i = 0; i < priv->num_ports; i++) {
- struct rtl8365mb_port *p = &mb->ports[i];
-
- if (dsa_is_unused_port(ds, i))
- continue;
+ dsa_switch_for_each_available_port(dp, ds) {
+ struct rtl8365mb_port *p = &mb->ports[dp->index];
/* Per-port spinlock to protect the stats64 data */
spin_lock_init(&p->stats_lock);
@@ -1581,13 +1578,10 @@ static void rtl8365mb_stats_teardown(str
{
struct rtl8365mb *mb = priv->chip_data;
struct dsa_switch *ds = &priv->ds;
- int i;
-
- for (i = 0; i < priv->num_ports; i++) {
- struct rtl8365mb_port *p = &mb->ports[i];
+ struct dsa_port *dp;
- if (dsa_is_unused_port(ds, i))
- continue;
+ dsa_switch_for_each_available_port(dp, ds) {
+ struct rtl8365mb_port *p = &mb->ports[dp->index];
cancel_delayed_work_sync(&p->mib_work);
}
@@ -1646,6 +1640,9 @@ static irqreturn_t rtl8365mb_irq(int irq
for_each_set_bit(line, &line_changes, priv->num_ports) {
int child_irq = irq_find_mapping(priv->irqdomain, line);
+ if (!child_irq)
+ continue;
+
handle_nested_irq(child_irq);
}
@@ -1667,10 +1664,14 @@ static struct irq_chip rtl8365mb_irq_chi
static int rtl8365mb_irq_map(struct irq_domain *domain, unsigned int irq,
irq_hw_number_t hwirq)
{
- irq_set_chip_data(irq, domain->host_data);
+ struct realtek_priv *priv = domain->host_data;
+ struct rtl8365mb *mb = priv->chip_data;
+
+ irq_set_chip_data(irq, priv);
irq_set_chip_and_handler(irq, &rtl8365mb_irq_chip, handle_simple_irq);
irq_set_nested_thread(irq, 1);
irq_set_noprobe(irq);
+ irq_set_parent(irq, mb->irq);
return 0;
}
@@ -1709,13 +1710,14 @@ static int rtl8365mb_irq_disable(struct
static int rtl8365mb_irq_setup(struct realtek_priv *priv)
{
struct rtl8365mb *mb = priv->chip_data;
+ struct dsa_switch *ds = &priv->ds;
struct device_node *intc;
+ struct dsa_port *dp;
u32 irq_trig;
int virq;
int irq;
u32 val;
int ret;
- int i;
intc = of_get_child_by_name(priv->dev->of_node, "interrupt-controller");
if (!intc) {
@@ -1736,6 +1738,9 @@ static int rtl8365mb_irq_setup(struct re
goto out_put_node;
}
+ /* Store the irq so that we know to map and free it during teardown */
+ mb->irq = irq;
+
priv->irqdomain = irq_domain_add_linear(intc, priv->num_ports,
&rtl8365mb_irqdomain_ops, priv);
if (!priv->irqdomain) {
@@ -1744,8 +1749,8 @@ static int rtl8365mb_irq_setup(struct re
goto out_put_node;
}
- for (i = 0; i < priv->num_ports; i++) {
- virq = irq_create_mapping(priv->irqdomain, i);
+ dsa_switch_for_each_available_port(dp, ds) {
+ virq = irq_create_mapping(priv->irqdomain, dp->index);
if (!virq) {
dev_err(priv->dev,
"failed to create irq domain mapping\n");
@@ -1799,9 +1804,6 @@ static int rtl8365mb_irq_setup(struct re
goto out_remove_irqdomain;
}
- /* Store the irq so that we know to free it during teardown */
- mb->irq = irq;
-
ret = rtl8365mb_irq_enable(priv);
if (ret)
goto out_free_irq;
@@ -1812,18 +1814,20 @@ static int rtl8365mb_irq_setup(struct re
out_free_irq:
free_irq(mb->irq, priv);
- mb->irq = 0;
out_remove_irqdomain:
- for (i = 0; i < priv->num_ports; i++) {
- virq = irq_find_mapping(priv->irqdomain, i);
- irq_dispose_mapping(virq);
+ dsa_switch_for_each_port(dp, ds) {
+ virq = irq_find_mapping(priv->irqdomain, dp->index);
+
+ if (virq)
+ irq_dispose_mapping(virq);
}
irq_domain_remove(priv->irqdomain);
priv->irqdomain = NULL;
out_put_node:
+ mb->irq = 0;
of_node_put(intc);
return ret;
@@ -1832,8 +1836,9 @@ out_put_node:
static void rtl8365mb_irq_teardown(struct realtek_priv *priv)
{
struct rtl8365mb *mb = priv->chip_data;
+ struct dsa_switch *ds = &priv->ds;
+ struct dsa_port *dp;
int virq;
- int i;
if (mb->irq) {
free_irq(mb->irq, priv);
@@ -1841,9 +1846,15 @@ static void rtl8365mb_irq_teardown(struc
}
if (priv->irqdomain) {
- for (i = 0; i < priv->num_ports; i++) {
- virq = irq_find_mapping(priv->irqdomain, i);
- irq_dispose_mapping(virq);
+ /* Unused ports with a linked PHY still have an active IRQ
+ * mapping that must be disposed of during teardown. Loop
+ * through all ports.
+ */
+ dsa_switch_for_each_port(dp, ds) {
+ virq = irq_find_mapping(priv->irqdomain, dp->index);
+
+ if (virq)
+ irq_dispose_mapping(virq);
}
irq_domain_remove(priv->irqdomain);
@@ -1961,10 +1972,11 @@ static int rtl8365mb_setup(struct dsa_sw
{
struct realtek_priv *priv = ds->priv;
struct rtl8365mb_cpu *cpu;
- struct dsa_port *cpu_dp;
+ u32 downports_mask = 0;
+ u32 upports_mask = 0;
struct rtl8365mb *mb;
+ struct dsa_port *dp;
int ret;
- int i;
mb = priv->chip_data;
cpu = &mb->cpu;
@@ -1991,67 +2003,99 @@ static int rtl8365mb_setup(struct dsa_sw
else if (ret)
dev_info(priv->dev, "no interrupt support\n");
- for (i = 0; i < priv->num_ports; i++) {
+ dsa_switch_for_each_port(dp, ds) {
/* Cascading (DSA links) is not supported yet.
* Historically, the driver has always been broken
* without a dedicated CPU port because CPU tagging
* would be disabled, rendering the switch entirely
* non-functional for DSA operations.
*/
- if (dsa_is_dsa_port(ds, i)) {
+ if (dsa_port_is_dsa(dp)) {
dev_err(priv->dev, "Cascading (DSA link) not supported\n");
ret = -EOPNOTSUPP;
goto out_teardown_irq;
}
}
- /* Configure CPU tagging */
- dsa_switch_for_each_cpu_port(cpu_dp, ds) {
- cpu->mask |= BIT(cpu_dp->index);
+ /* Start with all ports blocked, including unused ports */
+ dsa_switch_for_each_port(dp, ds) {
+ struct rtl8365mb_port *p = &mb->ports[dp->index];
- if (cpu->trap_port == RTL8365MB_MAX_NUM_PORTS)
- cpu->trap_port = cpu_dp->index;
- }
- cpu->enable = cpu->mask > 0;
+ /* Set the initial STP state of all ports to DISABLED, otherwise
+ * ports will still forward frames to the CPU despite being
+ * administratively down by default.
+ */
+ rtl8365mb_port_stp_state_set(ds, dp->index, BR_STATE_DISABLED);
- if (!cpu->enable) {
- dev_err(priv->dev, "no CPU port defined\n");
- ret = -EINVAL;
- goto out_teardown_irq;
- }
+ /* Start with all port completely isolated */
+ ret = rtl8365mb_port_set_isolation(priv, dp->index, 0);
+ if (ret)
+ goto out_teardown_irq;
- ret = rtl8365mb_cpu_config(priv);
- if (ret)
- goto out_teardown_irq;
+ /* Disable learning */
+ ret = rtl8365mb_port_set_learning(priv, dp->index, false);
+ if (ret)
+ goto out_teardown_irq;
+
+ /* Set up per-port private data */
+ p->priv = priv;
+ p->index = dp->index;
- /* Configure ports */
- for (i = 0; i < priv->num_ports; i++) {
- struct rtl8365mb_port *p = &mb->ports[i];
+ /* Collect CPU ports. If we support cascade switches, it should
+ * also include the upstream DSA ports.
+ */
+ if (!dsa_port_is_cpu(dp))
+ continue;
+
+ upports_mask |= BIT(dp->index);
+ }
- if (dsa_is_unused_port(ds, i))
+ /* Configure user ports */
+ dsa_switch_for_each_port(dp, ds) {
+ if (!dsa_port_is_user(dp))
continue;
/* Forward only to the CPU */
- ret = rtl8365mb_port_set_isolation(priv, i, cpu->mask);
+ ret = rtl8365mb_port_set_isolation(priv, dp->index,
+ upports_mask);
if (ret)
goto out_teardown_irq;
- /* Disable learning */
- ret = rtl8365mb_port_set_learning(priv, i, false);
+ /* If we support cascade switches, it should also include the
+ * downstream DSA ports.
+ */
+ downports_mask |= BIT(dp->index);
+ }
+
+ /* Configure CPU tagging */
+ /* If we support cascade switches, it should also include the upstream
+ * DSA ports.
+ */
+ dsa_switch_for_each_cpu_port(dp, ds) {
+ /* Use the first CPU port as trap_port */
+ if (cpu->trap_port == RTL8365MB_MAX_NUM_PORTS)
+ cpu->trap_port = dp->index;
+
+ /* Forward to all user ports */
+ ret = rtl8365mb_port_set_isolation(priv, dp->index,
+ downports_mask);
if (ret)
goto out_teardown_irq;
+ }
- /* Set the initial STP state of all ports to DISABLED, otherwise
- * ports will still forward frames to the CPU despite being
- * administratively down by default.
- */
- rtl8365mb_port_stp_state_set(ds, i, BR_STATE_DISABLED);
+ cpu->mask = upports_mask;
+ cpu->enable = cpu->mask > 0;
- /* Set up per-port private data */
- p->priv = priv;
- p->index = i;
+ if (!cpu->enable) {
+ dev_err(priv->dev, "no CPU port defined\n");
+ ret = -EINVAL;
+ goto out_teardown_irq;
}
+ ret = rtl8365mb_cpu_config(priv);
+ if (ret)
+ goto out_teardown_irq;
+
ret = rtl8365mb_port_change_mtu(ds, cpu->trap_port, ETH_DATA_LEN);
if (ret)
goto out_teardown_irq;
@@ -0,0 +1,394 @@
From fbafdd3b224a03b7b335de144f44a600de937586 Mon Sep 17 00:00:00 2001
From: Alvin Šipraga <alsi@bang-olufsen.dk>
Date: Sat, 6 Jun 2026 05:29:29 -0300
Subject: net: dsa: realtek: rtl8365mb: add table lookup interface
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Add a generic table lookup interface to centralize access to
the RTL8365MB internal tables.
This interface abstracts the low-level table access logic and
will be used by subsequent commits to implement FDB and VLAN
operations.
Signed-off-by: Alvin Šipraga <alsi@bang-olufsen.dk>
Reviewed-by: Linus Walleij <linusw@kernel.org>
Reviewed-by: Mieczyslaw Nalewaj <namiltd@yahoo.com>
Co-developed-by: Luiz Angelo Daros de Luca <luizluca@gmail.com>
Signed-off-by: Luiz Angelo Daros de Luca <luizluca@gmail.com>
Link: https://patch.msgid.link/20260606-realtek_forward-v13-5-b9e409687cbe@gmail.com
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
---
drivers/net/dsa/realtek/Makefile | 1 +
drivers/net/dsa/realtek/rtl8365mb_table.c | 214 ++++++++++++++++++++++++++++++
drivers/net/dsa/realtek/rtl8365mb_table.h | 138 +++++++++++++++++++
3 files changed, 353 insertions(+)
--- a/drivers/net/dsa/realtek/Makefile
+++ b/drivers/net/dsa/realtek/Makefile
@@ -17,4 +17,5 @@ rtl8366-objs += rtl8366rb-leds.o
endif
obj-$(CONFIG_NET_DSA_REALTEK_RTL8365MB) += rtl8365mb.o
rtl8365mb-objs := rtl8365mb_main.o \
+ rtl8365mb_table.o \
# end of rtl8365mb-objs
--- /dev/null
+++ b/drivers/net/dsa/realtek/rtl8365mb_table.c
@@ -0,0 +1,214 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Look-up table query interface for the rtl8365mb switch family
+ *
+ * Copyright (C) 2022 Alvin Šipraga <alsi@bang-olufsen.dk>
+ */
+
+#include "rtl8365mb_table.h"
+#include <linux/regmap.h>
+
+/* Table access control register */
+#define RTL8365MB_TABLE_CTRL_REG 0x0500
+/* Should be one of rtl8365mb_table enum members */
+#define RTL8365MB_TABLE_CTRL_TABLE_MASK GENMASK(2, 0)
+/* Should be one of rtl8365mb_table_op enum members */
+#define RTL8365MB_TABLE_CTRL_OP_MASK GENMASK(3, 3)
+/* Should be one of rtl8365mb_table_l2_method enum members */
+#define RTL8365MB_TABLE_CTRL_METHOD_MASK GENMASK(6, 4)
+#define RTL8365MB_TABLE_CTRL_PORT_MASK GENMASK(11, 8)
+
+/* Table access address register */
+#define RTL8365MB_TABLE_ACCESS_ADDR_REG 0x0501
+#define RTL8365MB_TABLE_ADDR_MASK GENMASK(12, 0)
+
+/* Table status register */
+#define RTL8365MB_TABLE_STATUS_REG 0x0502
+#define RTL8365MB_TABLE_STATUS_ADDRESS_MASK GENMASK(10, 0)
+/* set for L3, unset for L2 */
+#define RTL8365MB_TABLE_STATUS_ADDR_TYPE_MASK GENMASK(11, 11)
+#define RTL8365MB_TABLE_STATUS_HIT_STATUS_MASK GENMASK(12, 12)
+#define RTL8365MB_TABLE_STATUS_BUSY_FLAG_MASK GENMASK(13, 13)
+#define RTL8365MB_TABLE_STATUS_ADDRESS_EXT_MASK GENMASK(14, 14)
+
+/* Table read/write registers */
+#define RTL8365MB_TABLE_WRITE_BASE 0x0510
+#define RTL8365MB_TABLE_WRITE_REG(_x) \
+ (RTL8365MB_TABLE_WRITE_BASE + (_x))
+#define RTL8365MB_TABLE_READ_BASE 0x0520
+#define RTL8365MB_TABLE_READ_REG(_x) \
+ (RTL8365MB_TABLE_READ_BASE + (_x))
+#define RTL8365MB_TABLE_10TH_DATA_MASK GENMASK(3, 0)
+#define RTL8365MB_TABLE_WRITE_10TH_REG \
+ RTL8365MB_TABLE_WRITE_REG(RTL8365MB_TABLE_ENTRY_MAX_SIZE - 1)
+
+static int rtl8365mb_table_poll_busy(struct realtek_priv *priv)
+{
+ u32 val;
+
+ return regmap_read_poll_timeout(priv->map_nolock,
+ RTL8365MB_TABLE_STATUS_REG, val,
+ !FIELD_GET(RTL8365MB_TABLE_STATUS_BUSY_FLAG_MASK, val),
+ 10, 10000);
+}
+
+int rtl8365mb_table_query(struct realtek_priv *priv,
+ enum rtl8365mb_table table,
+ enum rtl8365mb_table_op op, u16 *addr,
+ enum rtl8365mb_table_l2_method method,
+ u16 port, u16 *data, size_t size)
+{
+ bool addr_as_input = true;
+ bool write_data = false;
+ int ret = 0;
+ u32 cmd;
+ u32 val;
+ u32 hit;
+
+ /* Prepare target table and operation (read or write) */
+ cmd = 0;
+ cmd |= FIELD_PREP(RTL8365MB_TABLE_CTRL_TABLE_MASK, table);
+ cmd |= FIELD_PREP(RTL8365MB_TABLE_CTRL_OP_MASK, op);
+ if (op == RTL8365MB_TABLE_OP_READ && table == RTL8365MB_TABLE_L2) {
+ cmd |= FIELD_PREP(RTL8365MB_TABLE_CTRL_METHOD_MASK, method);
+ switch (method) {
+ case RTL8365MB_TABLE_L2_METHOD_MAC:
+ /*
+ * Method MAC requires as input the same L2 table format
+ * you'll get as result. However, it might only use mac
+ * address and FID/VID fields.
+ */
+ write_data = true;
+
+ /* METHOD_MAC does not use addr as input, but may return
+ * the matched index.
+ */
+ addr_as_input = false;
+
+ break;
+ case RTL8365MB_TABLE_L2_METHOD_ADDR:
+ case RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT:
+ case RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT_UC:
+ case RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT_MC:
+ break;
+ case RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT_UC_PORT:
+ cmd |= FIELD_PREP(RTL8365MB_TABLE_CTRL_PORT_MASK, port);
+ break;
+ default:
+ return -EINVAL;
+ }
+ } else if (op == RTL8365MB_TABLE_OP_WRITE) {
+ write_data = true;
+
+ /* Writing to L2 does not use addr as input, as the table index
+ * is derived from key fields.
+ */
+ if (table == RTL8365MB_TABLE_L2)
+ addr_as_input = false;
+ }
+
+ /* To prevent concurrent access to the look-up tables, take the regmap
+ * lock manually and access via the map_nolock regmap.
+ */
+ mutex_lock(&priv->map_lock);
+
+ /* Protect from a busy table access (i.e. previous access timeouts) */
+ ret = rtl8365mb_table_poll_busy(priv);
+ if (ret)
+ goto out;
+
+ /* Write entry data if writing to the table (or L2_METHOD_MAC) */
+ if (write_data) {
+ /* bulk write data up to 9th word */
+ ret = regmap_bulk_write(priv->map_nolock,
+ RTL8365MB_TABLE_WRITE_BASE,
+ data,
+ min_t(size_t, size,
+ RTL8365MB_TABLE_ENTRY_MAX_SIZE -
+ 1));
+ if (ret)
+ goto out;
+
+ /* 10th register uses only 4 least significant bits */
+ if (size == RTL8365MB_TABLE_ENTRY_MAX_SIZE) {
+ val = FIELD_PREP(RTL8365MB_TABLE_10TH_DATA_MASK,
+ data[size - 1]);
+ ret = regmap_update_bits(priv->map_nolock,
+ RTL8365MB_TABLE_WRITE_10TH_REG,
+ RTL8365MB_TABLE_10TH_DATA_MASK,
+ val);
+ }
+
+ if (ret)
+ goto out;
+ }
+
+ /* Write address (if needed) */
+ if (addr_as_input) {
+ ret = regmap_write(priv->map_nolock,
+ RTL8365MB_TABLE_ACCESS_ADDR_REG,
+ FIELD_PREP(RTL8365MB_TABLE_ADDR_MASK,
+ *addr));
+ if (ret)
+ goto out;
+ }
+
+ /* Execute */
+ ret = regmap_write(priv->map_nolock, RTL8365MB_TABLE_CTRL_REG, cmd);
+ if (ret)
+ goto out;
+
+ /* Poll for completion */
+ ret = rtl8365mb_table_poll_busy(priv);
+ if (ret)
+ goto out;
+
+ /* For both reads and writes to the L2 table, check status */
+ if (table == RTL8365MB_TABLE_L2) {
+ ret = regmap_read(priv->map_nolock, RTL8365MB_TABLE_STATUS_REG,
+ &val);
+ if (ret)
+ goto out;
+
+ /* Did the query find an entry? */
+ hit = FIELD_GET(RTL8365MB_TABLE_STATUS_HIT_STATUS_MASK, val);
+ if (!hit) {
+ ret = -ENOENT;
+ goto out;
+ }
+
+ /* If so, extract the address */
+ *addr = 0;
+ *addr |= FIELD_GET(RTL8365MB_TABLE_STATUS_ADDRESS_MASK, val);
+ *addr |= FIELD_GET(RTL8365MB_TABLE_STATUS_ADDRESS_EXT_MASK, val)
+ << 11;
+ /* only set if it is a L3 address */
+ *addr |= FIELD_GET(RTL8365MB_TABLE_STATUS_ADDR_TYPE_MASK, val)
+ << 12;
+ }
+
+ /* Finally, get the table entry if we were reading */
+ if (op == RTL8365MB_TABLE_OP_READ) {
+ ret = regmap_bulk_read(priv->map_nolock,
+ RTL8365MB_TABLE_READ_BASE,
+ data, size);
+ if (ret)
+ goto out;
+
+ /* For the biggest table entries, the uppermost table
+ * entry register has space for only one nibble. Mask
+ * out the remainder bits. Empirically I saw nothing
+ * wrong with omitting this mask, but it may prevent
+ * unwanted behaviour. FYI.
+ */
+ if (size == RTL8365MB_TABLE_ENTRY_MAX_SIZE) {
+ val = FIELD_GET(RTL8365MB_TABLE_10TH_DATA_MASK,
+ data[size - 1]);
+ data[size - 1] = val;
+ }
+ }
+
+out:
+ mutex_unlock(&priv->map_lock);
+
+ return ret;
+}
--- /dev/null
+++ b/drivers/net/dsa/realtek/rtl8365mb_table.h
@@ -0,0 +1,138 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/* Look-up table query interface for the rtl8365mb switch family
+ *
+ * Copyright (C) 2022 Alvin Šipraga <alsi@bang-olufsen.dk>
+ */
+
+#ifndef _REALTEK_RTL8365MB_TABLE_H
+#define _REALTEK_RTL8365MB_TABLE_H
+
+#include <linux/if_ether.h>
+#include <linux/types.h>
+
+#include "realtek.h"
+
+#define RTL8365MB_TABLE_ENTRY_MAX_SIZE 10
+
+/*
+ * enum rtl8365mb_table - available switch tables
+ * @RTL8365MB_TABLE_ACL_RULE: ACL rules
+ * @RTL8365MB_TABLE_ACL_ACTION: ACL actions
+ * @RTL8365MB_TABLE_CVLAN: VLAN4k configurations
+ * @RTL8365MB_TABLE_L2: filtering database (2K hash table)
+ * @RTL8365MB_TABLE_IGMP_GROUP: IGMP group database (readonly)
+ *
+ * NOTE: Don't change the enum values. They must concur with the field
+ * described by @RTL8365MB_TABLE_CTRL_TABLE_MASK.
+ */
+enum rtl8365mb_table {
+ RTL8365MB_TABLE_ACL_RULE = 1,
+ RTL8365MB_TABLE_ACL_ACTION = 2,
+ RTL8365MB_TABLE_CVLAN = 3,
+ RTL8365MB_TABLE_L2 = 4,
+ RTL8365MB_TABLE_IGMP_GROUP = 5,
+};
+
+/*
+ * enum rtl8365mb_table_op - table query operation
+ * @RTL8365MB_TABLE_OP_READ: read an entry from the target table
+ * @RTL8365MB_TABLE_OP_WRITE: write an entry to the target table
+ *
+ * NOTE: Don't change the enum values. They must concur with the field
+ * described by @RTL8365MB_TABLE_CTRL_OP_MASK.
+ */
+enum rtl8365mb_table_op {
+ RTL8365MB_TABLE_OP_READ = 0,
+ RTL8365MB_TABLE_OP_WRITE = 1,
+};
+
+/*
+ * enum rtl8365mb_table_l2_method - look-up method for read queries of L2 table
+ * @RTL8365MB_TABLE_L2_METHOD_MAC: look-up by source MAC address and FID (or
+ * VID)
+ * @RTL8365MB_TABLE_L2_METHOD_ADDR: look-up by entry address
+ * @RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT: look-up next entry starting from the
+ * supplied address
+ * @RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT_UC: same as ADDR_NEXT but search only
+ * unicast addresses
+ * @RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT_MC: same as ADDR_NEXT but search only
+ * multicast addresses
+ * @RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT_UC_PORT: same as ADDR_NEXT_UC but
+ * search only entries with matching source port
+ *
+ * NOTE: Don't change the enum values. They must concur with the field
+ * described by @RTL8365MB_TABLE_CTRL_METHOD_MASK
+ */
+enum rtl8365mb_table_l2_method {
+ RTL8365MB_TABLE_L2_METHOD_MAC = 0,
+ RTL8365MB_TABLE_L2_METHOD_ADDR = 1,
+ RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT = 2,
+ RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT_UC = 3,
+ RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT_MC = 4,
+ /*
+ * RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT_MC_L3 = 5,
+ * RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT_MC_L2L3 = 6,
+ */
+ RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT_UC_PORT = 7,
+};
+
+/*
+ * rtl8365mb_table_query() - read from or write to a switch table
+ * @priv: driver context
+ * @table: target table, see &enum rtl8365mb_table
+ * @op: read or write operation, see &enum rtl8365mb_table_op
+ * @addr: table address. For indexed tables, this selects the entry to access.
+ * For L2 read queries, it is ignored as input for MAC-based lookup
+ * methods and used as input for address-based lookup methods. On
+ * successful L2 queries, it is updated with the matched entry address.
+ * @method: L2 table lookup method, see &enum rtl8365mb_table_l2_method.
+ * Ignored for non-L2 tables.
+ * @port: for L2 read queries using method
+ * %RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT_UC_PORT, restrict the search
+ * to entries associated with this source port. Ignored otherwise.
+ * @data: data buffer used to read from or write to the table. For L2 MAC
+ * lookups, this buffer provides the lookup key and receives the
+ * matched entry contents on success.
+ * @size: size of @data in 16-bit words. The caller must ensure that @size
+ * matches the target table's entry size and does not exceed
+ * RTL8365MB_TABLE_ENTRY_MAX_SIZE.
+ *
+ * This function provides unified access to the internal tables of the switch.
+ * All tables except the L2 table are simple indexed tables, where @addr
+ * selects the entry and @op determines whether the access is a read or a
+ * write operation.
+ *
+ * The content of @data is used as input when writing to tables or when
+ * specifying the lookup key for L2 MAC searches, and as output for all
+ * successful read operations. It remains unchanged during write operations or
+ * failed read operations that return %-ENOENT. For other errors during read
+ * operations, it is undefined.
+ *
+ * The L2 table is a hash table and supports multiple lookup methods. For
+ * %RTL8365MB_TABLE_L2_METHOD_MAC, an entry is searched based on the MAC
+ * address and FID/VID fields provided in @data, using the same format as
+ * an L2 table entry. Address-based methods either read a specific entry
+ * (%RTL8365MB_TABLE_L2_METHOD_ADDR) or iterate over valid entries starting
+ * from @addr (%RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT and variants). When using
+ * %RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT_UC_PORT, only entries associated with
+ * the specified @port are considered.
+ *
+ * On successful L2 operations, @addr is updated with the matched table address
+ * or allocated entry address. If no matching entry is found, or if an L2 write
+ * operation fails (e.g., due to a full table during addition or a missing entry
+ * during deletion), %-ENOENT is returned and @addr remains unchanged. It is the
+ * caller's responsibility to map the returned error to the appropriate
+ * semantic error.
+ *
+ * @size must match the size of the target table entry, expressed in 16-bit
+ * words.
+ *
+ * Return: 0 on success, or a negative error code on failure.
+ */
+int rtl8365mb_table_query(struct realtek_priv *priv,
+ enum rtl8365mb_table table,
+ enum rtl8365mb_table_op op, u16 *addr,
+ enum rtl8365mb_table_l2_method method,
+ u16 port, u16 *data, size_t size);
+
+#endif /* _REALTEK_RTL8365MB_TABLE_H */
@@ -0,0 +1,335 @@
From 183bd68b1fe1d5ad584355a7449eea32da79334a Mon Sep 17 00:00:00 2001
From: Alvin Šipraga <alsi@bang-olufsen.dk>
Date: Sat, 6 Jun 2026 05:29:32 -0300
Subject: net: dsa: realtek: rtl8365mb: add port_bridge_{join,leave}
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Implement hardware offloading of bridge functionality. This is achieved
by using the per-port isolation registers, which contain a forwarding
port mask. The switch will refuse to forward packets ingressed on a
given port to a port which is not in its forwarding mask.
For each bridge that is offloaded, use the DSA-provided bridge number
for the Extended Filtering ID (EFID). When using Independent VLAN
Learning (IVL), the forwarding database is keyed with the tuple
{VID, MAC, EFID}. There are 8 EFIDs available (0~7), but we reserve the
default EFID 0 for standalone ports where learning is disabled. This
fits nicely because DSA indexes the bridge number starting from 1.
Because of the limited number of EFIDs, we have to set the
max_num_bridges property of our switch to 7: we can't offload more than
that or we will fail to offer IVL as at least two bridges would end up
having to share an EFID.
All ports start isolated, forwarding exclusively to CPU ports, and
with VLAN transparent, ignoring VLAN membership. Once a member in a
bridge, the port isolation is expanded to include the bridge members.
When that bridge enables VLAN filtering, the VLAN transparent feature is
disabled, letting the switch filter based on VLAN setup.
Signed-off-by: Alvin Šipraga <alsi@bang-olufsen.dk>
Reviewed-by: Linus Walleij <linusw@kernel.org>
Reviewed-by: Mieczyslaw Nalewaj <namiltd@yahoo.com>
Co-developed-by: Luiz Angelo Daros de Luca <luizluca@gmail.com>
Signed-off-by: Luiz Angelo Daros de Luca <luizluca@gmail.com>
Link: https://patch.msgid.link/20260606-realtek_forward-v13-8-b9e409687cbe@gmail.com
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
---
drivers/net/dsa/realtek/realtek.h | 7 ++
drivers/net/dsa/realtek/rtl8365mb_main.c | 47 ++++++++-
drivers/net/dsa/realtek/rtl83xx.c | 169 +++++++++++++++++++++++++++++++
drivers/net/dsa/realtek/rtl83xx.h | 7 ++
4 files changed, 229 insertions(+), 1 deletion(-)
--- a/drivers/net/dsa/realtek/realtek.h
+++ b/drivers/net/dsa/realtek/realtek.h
@@ -130,6 +130,13 @@ struct realtek_ops {
int (*enable_vlan)(struct realtek_priv *priv, bool enable);
int (*enable_vlan4k)(struct realtek_priv *priv, bool enable);
int (*enable_port)(struct realtek_priv *priv, int port, bool enable);
+ int (*port_add_isolation)(struct realtek_priv *priv, int port,
+ u32 mask);
+ int (*port_remove_isolation)(struct realtek_priv *priv, int port,
+ u32 mask);
+ int (*port_set_efid)(struct realtek_priv *priv, int port, u32 efid);
+ int (*port_set_learning)(struct realtek_priv *priv, int port,
+ bool enable);
int (*l2_add_uc)(struct realtek_priv *priv, int port,
const unsigned char addr[ETH_ALEN],
u16 efid, u16 vid);
--- a/drivers/net/dsa/realtek/rtl8365mb_main.c
+++ b/drivers/net/dsa/realtek/rtl8365mb_main.c
@@ -1568,10 +1568,44 @@ static int rtl8365mb_port_set_learning(s
enable ? RTL8365MB_LEARN_LIMIT_MAX : 0);
}
+static int rtl8365mb_port_set_efid(struct realtek_priv *priv, int port,
+ u32 efid)
+{
+ return regmap_update_bits(priv->map, RTL8365MB_PORT_EFID_REG(port),
+ RTL8365MB_PORT_EFID_MASK(port),
+ efid << RTL8365MB_PORT_EFID_OFFSET(port));
+}
+
+/* Port isolation manipulation functions.
+ *
+ * The port isolation register controls the forwarding mask of a given
+ * port. The switch will not forward packets ingressed on a given port
+ * to ports which are not enabled in its forwarding mask.
+ *
+ * The port forwarding mask has the highest priority in forwarding
+ * decisions. The only exception to this rule is when the switch
+ * receives a packet on its CPU port with ALLOW=0. In that case the TX
+ * field of the CPU tag will override the forwarding port mask.
+ */
static int rtl8365mb_port_set_isolation(struct realtek_priv *priv, int port,
u32 mask)
{
- return regmap_write(priv->map, RTL8365MB_PORT_ISOLATION_REG(port), mask);
+ return regmap_write(priv->map, RTL8365MB_PORT_ISOLATION_REG(port),
+ mask);
+}
+
+static int rtl8365mb_port_add_isolation(struct realtek_priv *priv, int port,
+ u32 mask)
+{
+ return regmap_update_bits(priv->map, RTL8365MB_PORT_ISOLATION_REG(port),
+ mask, mask);
+}
+
+static int rtl8365mb_port_remove_isolation(struct realtek_priv *priv, int port,
+ u32 mask)
+{
+ return regmap_update_bits(priv->map, RTL8365MB_PORT_ISOLATION_REG(port),
+ mask, 0);
}
static int rtl8365mb_mib_counter_read(struct realtek_priv *priv, int port,
@@ -2378,6 +2412,11 @@ static int rtl8365mb_setup(struct dsa_sw
if (ret)
goto out_teardown_irq;
+ /* Set the default EFID 0 for standalone mode */
+ ret = rtl8365mb_port_set_efid(priv, dp->index, 0);
+ if (ret)
+ goto out_teardown_irq;
+
/* Disable learning */
ret = rtl8365mb_port_set_learning(priv, dp->index, false);
if (ret)
@@ -2567,6 +2606,8 @@ static const struct dsa_switch_ops rtl83
.setup = rtl8365mb_setup,
.teardown = rtl8365mb_teardown,
.phylink_get_caps = rtl8365mb_phylink_get_caps,
+ .port_bridge_join = rtl83xx_port_bridge_join,
+ .port_bridge_leave = rtl83xx_port_bridge_leave,
.port_stp_state_set = rtl8365mb_port_stp_state_set,
.port_fast_age = rtl83xx_port_fast_age,
.port_fdb_add = rtl83xx_port_fdb_add,
@@ -2590,6 +2631,10 @@ static const struct dsa_switch_ops rtl83
static const struct realtek_ops rtl8365mb_ops = {
.detect = rtl8365mb_detect,
+ .port_add_isolation = rtl8365mb_port_add_isolation,
+ .port_remove_isolation = rtl8365mb_port_remove_isolation,
+ .port_set_efid = rtl8365mb_port_set_efid,
+ .port_set_learning = rtl8365mb_port_set_learning,
.l2_add_uc = rtl8365mb_l2_add_uc,
.l2_del_uc = rtl8365mb_l2_del_uc,
.l2_get_next_uc = rtl8365mb_l2_get_next_uc,
--- a/drivers/net/dsa/realtek/rtl83xx.c
+++ b/drivers/net/dsa/realtek/rtl83xx.c
@@ -329,6 +329,175 @@ void rtl83xx_reset_deassert(struct realt
}
/**
+ * rtl83xx_port_bridge_join() - join a port to a bridge
+ * @ds: DSA switch instance
+ * @port: port index
+ * @bridge: bridge being joined
+ * @tx_forward_offload: if the switch can offload TX forwarding
+ * @extack: netlink extended ack for reporting errors
+ *
+ * This function handles joining a port to a bridge. It updates the port
+ * isolation masks and EFID.
+ *
+ * Context: Can sleep.
+ * Return: 0 on success, negative value for failure.
+ */
+int rtl83xx_port_bridge_join(struct dsa_switch *ds, int port,
+ struct dsa_bridge bridge,
+ bool *tx_forward_offload,
+ struct netlink_ext_ack *extack)
+{
+ struct realtek_priv *priv = ds->priv;
+ struct dsa_port *dp;
+ u32 mask = 0;
+ int ret;
+
+ if (!priv->ops->port_add_isolation)
+ return -EOPNOTSUPP;
+
+ if (!priv->ops->port_set_learning)
+ return -EOPNOTSUPP;
+
+ dev_dbg(priv->dev, "bridge %d join port %d\n", bridge.num, port);
+
+ /* Add this port to the isolation group of every other port
+ * offloading this bridge.
+ */
+ dsa_switch_for_each_user_port(dp, ds) {
+ /* Handle this port after */
+ if (dp->index == port)
+ continue;
+
+ /* Skip ports that are not in this bridge */
+ if (!dsa_port_offloads_bridge(dp, &bridge))
+ continue;
+
+ ret = priv->ops->port_add_isolation(priv, dp->index, BIT(port));
+ if (ret)
+ goto undo_isolation;
+
+ mask |= BIT(dp->index);
+ }
+
+ /* If we support cascade switches, it should also include the
+ * downstream DSA ports to the isolation group.
+ */
+
+ /* Add those ports to the isolation group of this port */
+ ret = priv->ops->port_add_isolation(priv, port, mask);
+ if (ret)
+ goto undo_isolation;
+
+ /* Use the bridge number as the EFID for this port */
+ if (priv->ops->port_set_efid) {
+ ret = priv->ops->port_set_efid(priv, port, bridge.num);
+ if (ret)
+ goto undo_self_isolation;
+ }
+
+ ret = priv->ops->port_set_learning(priv, port, true);
+ if (ret)
+ goto undo_efid;
+
+ return 0;
+
+undo_efid:
+ if (priv->ops->port_set_efid)
+ priv->ops->port_set_efid(priv, port, 0);
+
+undo_self_isolation:
+ priv->ops->port_remove_isolation(priv, port, mask);
+
+undo_isolation:
+ dsa_switch_for_each_port(dp, ds) {
+ if (mask & BIT(dp->index))
+ priv->ops->port_remove_isolation(priv, dp->index,
+ BIT(port));
+ }
+
+ return ret;
+}
+EXPORT_SYMBOL_NS_GPL(rtl83xx_port_bridge_join, REALTEK_DSA);
+
+/**
+ * rtl83xx_port_bridge_leave() - leave a bridge
+ * @ds: DSA switch instance
+ * @port: port index
+ * @bridge: bridge being left
+ *
+ * This function handles removing a port from a bridge. It updates the port
+ * isolation masks and EFID.
+ *
+ * Context: Can sleep.
+ * Return: nothing
+ */
+void rtl83xx_port_bridge_leave(struct dsa_switch *ds, int port,
+ struct dsa_bridge bridge)
+{
+ struct realtek_priv *priv = ds->priv;
+ struct dsa_port *dp;
+ u32 mask = 0;
+ int ret;
+
+ if (!priv->ops->port_remove_isolation)
+ return;
+
+ if (!priv->ops->port_set_learning)
+ return;
+
+ dev_dbg(priv->dev, "bridge %d leave port %d\n", bridge.num, port);
+
+ /* Remove this port from the isolation group of every other
+ * port offloading this bridge.
+ */
+ dsa_switch_for_each_user_port(dp, ds) {
+ /* Handle this port after */
+ if (dp->index == port)
+ continue;
+
+ /* Skip ports that are not in this bridge */
+ if (!dsa_port_offloads_bridge(dp, &bridge))
+ continue;
+
+ ret = priv->ops->port_remove_isolation(priv, dp->index,
+ BIT(port));
+ if (ret)
+ dev_err(priv->dev,
+ "failed to isolate port %d from port %d: %pe\n",
+ port, dp->index, ERR_PTR(ret));
+
+ mask |= BIT(dp->index);
+ }
+
+ /* If we support cascade switches, it should also exclude the
+ * downstream DSA ports from the isolation group.
+ */
+
+ ret = priv->ops->port_set_learning(priv, port, false);
+ if (ret)
+ dev_err(priv->dev,
+ "failed to disable learning on port %d: %pe\n",
+ port, ERR_PTR(ret));
+
+ /* Remove those ports from the isolation group of this port */
+ ret = priv->ops->port_remove_isolation(priv, port, mask);
+ if (ret)
+ dev_err(priv->dev,
+ "failed to remove isolation mask from port %d: %pe\n",
+ port, ERR_PTR(ret));
+
+ /* Revert to the default EFID 0 for standalone mode */
+ if (priv->ops->port_set_efid) {
+ ret = priv->ops->port_set_efid(priv, port, 0);
+ if (ret)
+ dev_err(priv->dev,
+ "failed to clear EFID on port %d: %pe\n",
+ port, ERR_PTR(ret));
+ }
+}
+EXPORT_SYMBOL_NS_GPL(rtl83xx_port_bridge_leave, REALTEK_DSA);
+
+/**
* rtl83xx_port_fast_age() - flush dynamic FDB entries learned on a port
* @ds: DSA switch instance
* @port: port index
--- a/drivers/net/dsa/realtek/rtl83xx.h
+++ b/drivers/net/dsa/realtek/rtl83xx.h
@@ -21,6 +21,13 @@ void rtl83xx_remove(struct realtek_priv
void rtl83xx_reset_assert(struct realtek_priv *priv);
void rtl83xx_reset_deassert(struct realtek_priv *priv);
+int rtl83xx_port_bridge_join(struct dsa_switch *ds, int port,
+ struct dsa_bridge bridge,
+ bool *tx_forward_offload,
+ struct netlink_ext_ack *extack);
+void rtl83xx_port_bridge_leave(struct dsa_switch *ds, int port,
+ struct dsa_bridge bridge);
+
void rtl83xx_port_fast_age(struct dsa_switch *ds, int port);
int rtl83xx_port_fdb_add(struct dsa_switch *ds, int port,
const unsigned char *addr, u16 vid,
@@ -0,0 +1,279 @@
From 660a9e399ab02c0cb86d277ed6b0c9d10c350fdd Mon Sep 17 00:00:00 2001
From: Luiz Angelo Daros de Luca <luizluca@gmail.com>
Date: Sat, 6 Jun 2026 05:29:33 -0300
Subject: net: dsa: realtek: rtl8365mb: add bridge port flags
Implement support for bridge port flags to control learning and flooding
behavior. This patch maps hardware functionalities to the following
bridge flags:
- BR_LEARNING
- BR_FLOOD
- BR_MCAST_FLOOD
- BR_BCAST_FLOOD
By default, all flooding types are enabled during port setup to ensure
standard bridge behavior.
Reviewed-by: Linus Walleij <linusw@kernel.org>
Reviewed-by: Mieczyslaw Nalewaj <namiltd@yahoo.com>
Signed-off-by: Luiz Angelo Daros de Luca <luizluca@gmail.com>
Link: https://patch.msgid.link/20260606-realtek_forward-v13-9-b9e409687cbe@gmail.com
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
---
drivers/net/dsa/realtek/realtek.h | 6 ++
drivers/net/dsa/realtek/rtl8365mb_main.c | 68 +++++++++++++++++++++
drivers/net/dsa/realtek/rtl83xx.c | 101 +++++++++++++++++++++++++++++++
drivers/net/dsa/realtek/rtl83xx.h | 4 ++
4 files changed, 179 insertions(+)
--- a/drivers/net/dsa/realtek/realtek.h
+++ b/drivers/net/dsa/realtek/realtek.h
@@ -137,6 +137,12 @@ struct realtek_ops {
int (*port_set_efid)(struct realtek_priv *priv, int port, u32 efid);
int (*port_set_learning)(struct realtek_priv *priv, int port,
bool enable);
+ int (*port_set_ucast_flood)(struct realtek_priv *priv, int port,
+ bool enable);
+ int (*port_set_mcast_flood)(struct realtek_priv *priv, int port,
+ bool enable);
+ int (*port_set_bcast_flood)(struct realtek_priv *priv, int port,
+ bool enable);
int (*l2_add_uc)(struct realtek_priv *priv, int port,
const unsigned char addr[ETH_ALEN],
u16 efid, u16 vid);
--- a/drivers/net/dsa/realtek/rtl8365mb_main.c
+++ b/drivers/net/dsa/realtek/rtl8365mb_main.c
@@ -307,6 +307,21 @@
#define RTL8365MB_MSTI_CTRL_PORT_STATE_MASK(_physport) \
(0x3 << RTL8365MB_MSTI_CTRL_PORT_STATE_OFFSET((_physport)))
+/* Unknown unicast DA flooding port mask */
+#define RTL8365MB_UNKNOWN_UNICAST_FLOODING_PMASK_REG 0x0890
+#define RTL8365MB_UNKNOWN_UNICAST_FLOODING_PMASK_MASK 0x07FF
+
+/* Unknown multicast DA flooding port mask */
+#define RTL8365MB_UNKNOWN_MULTICAST_FLOODING_PMASK_REG 0x0891
+#define RTL8365MB_UNKNOWN_MULTICAST_FLOODING_PMASK_MASK 0x07FF
+
+/* Broadcast flooding port mask */
+#define RTL8365MB_UNKNOWN_BROADCAST_FLOODING_PMASK_REG 0x0892
+#define RTL8365MB_UNKNOWN_BROADCAST_FLOODING_PMASK_MASK 0x07FF
+
+#define RTL8365MB_SUPPORTED_BRIDGE_FLAGS \
+ (BR_LEARNING | BR_FLOOD | BR_MCAST_FLOOD | BR_BCAST_FLOOD)
+
/* Miscellaneous port configuration register, incl. VLAN egress mode */
#define RTL8365MB_PORT_MISC_CFG_REG_BASE 0x000E
#define RTL8365MB_PORT_MISC_CFG_REG(_p) \
@@ -1568,6 +1583,49 @@ static int rtl8365mb_port_set_learning(s
enable ? RTL8365MB_LEARN_LIMIT_MAX : 0);
}
+static int rtl8365mb_port_set_ucast_flood(struct realtek_priv *priv, int port,
+ bool enable)
+{
+ /* Frames with unknown unicast DA will be flooded to a programmable
+ * port mask that by default includes all ports. Add or remove
+ * the specified port from this port mask accordingly.
+ */
+ return regmap_update_bits(priv->map,
+ RTL8365MB_UNKNOWN_UNICAST_FLOODING_PMASK_REG,
+ BIT(port), enable ? BIT(port) : 0);
+}
+
+static int rtl8365mb_port_set_mcast_flood(struct realtek_priv *priv, int port,
+ bool enable)
+{
+ return regmap_update_bits(priv->map,
+ RTL8365MB_UNKNOWN_MULTICAST_FLOODING_PMASK_REG,
+ BIT(port), enable ? BIT(port) : 0);
+}
+
+static int rtl8365mb_port_set_bcast_flood(struct realtek_priv *priv, int port,
+ bool enable)
+{
+ return regmap_update_bits(priv->map,
+ RTL8365MB_UNKNOWN_BROADCAST_FLOODING_PMASK_REG,
+ BIT(port), enable ? BIT(port) : 0);
+}
+
+static int rtl8365mb_port_pre_bridge_flags(struct dsa_switch *ds, int port,
+ struct switchdev_brport_flags flags,
+ struct netlink_ext_ack *extack)
+{
+ struct realtek_priv *priv = ds->priv;
+
+ dev_dbg(priv->dev, "pre_bridge_flags port:%d flags:%lx supported:%lx\n",
+ port, flags.mask, RTL8365MB_SUPPORTED_BRIDGE_FLAGS);
+
+ if (flags.mask & ~RTL8365MB_SUPPORTED_BRIDGE_FLAGS)
+ return -EINVAL;
+
+ return 0;
+}
+
static int rtl8365mb_port_set_efid(struct realtek_priv *priv, int port,
u32 efid)
{
@@ -2422,6 +2480,11 @@ static int rtl8365mb_setup(struct dsa_sw
if (ret)
goto out_teardown_irq;
+ /* Enable all types of flooding */
+ ret = rtl83xx_setup_port_flood_control(priv, dp->index);
+ if (ret)
+ goto out_teardown_irq;
+
/* Set up per-port private data */
p->priv = priv;
p->index = dp->index;
@@ -2608,6 +2671,8 @@ static const struct dsa_switch_ops rtl83
.phylink_get_caps = rtl8365mb_phylink_get_caps,
.port_bridge_join = rtl83xx_port_bridge_join,
.port_bridge_leave = rtl83xx_port_bridge_leave,
+ .port_pre_bridge_flags = rtl8365mb_port_pre_bridge_flags,
+ .port_bridge_flags = rtl83xx_port_bridge_flags,
.port_stp_state_set = rtl8365mb_port_stp_state_set,
.port_fast_age = rtl83xx_port_fast_age,
.port_fdb_add = rtl83xx_port_fdb_add,
@@ -2635,6 +2700,9 @@ static const struct realtek_ops rtl8365m
.port_remove_isolation = rtl8365mb_port_remove_isolation,
.port_set_efid = rtl8365mb_port_set_efid,
.port_set_learning = rtl8365mb_port_set_learning,
+ .port_set_ucast_flood = rtl8365mb_port_set_ucast_flood,
+ .port_set_mcast_flood = rtl8365mb_port_set_mcast_flood,
+ .port_set_bcast_flood = rtl8365mb_port_set_bcast_flood,
.l2_add_uc = rtl8365mb_l2_add_uc,
.l2_del_uc = rtl8365mb_l2_del_uc,
.l2_get_next_uc = rtl8365mb_l2_get_next_uc,
--- a/drivers/net/dsa/realtek/rtl83xx.c
+++ b/drivers/net/dsa/realtek/rtl83xx.c
@@ -3,6 +3,7 @@
#include <linux/module.h>
#include <linux/regmap.h>
#include <linux/of_mdio.h>
+#include <linux/if_bridge.h>
#include <linux/etherdevice.h>
#include "realtek.h"
@@ -787,6 +788,106 @@ int rtl83xx_port_mdb_del(struct dsa_swit
}
EXPORT_SYMBOL_NS_GPL(rtl83xx_port_mdb_del, REALTEK_DSA);
+/**
+ * rtl83xx_port_bridge_flags() - set port bridge flags
+ * @ds: DSA switch instance
+ * @port: port index
+ * @flags: bridge port flags
+ * @extack: netlink extended ack for reporting errors
+ *
+ * This function handles setting bridge port flags like learning and flooding.
+ *
+ * Context: Can sleep.
+ * Return: 0 on success, negative value for failure.
+ */
+int rtl83xx_port_bridge_flags(struct dsa_switch *ds, int port,
+ struct switchdev_brport_flags flags,
+ struct netlink_ext_ack *extack)
+{
+ struct realtek_priv *priv = ds->priv;
+ bool enable;
+ int ret;
+
+ if (flags.mask & BR_LEARNING) {
+ if (!priv->ops->port_set_learning)
+ return -EOPNOTSUPP;
+
+ enable = !!(flags.val & BR_LEARNING);
+ ret = priv->ops->port_set_learning(priv, port, enable);
+ if (ret)
+ return ret;
+ }
+
+ if (flags.mask & BR_FLOOD) {
+ if (!priv->ops->port_set_ucast_flood)
+ return -EOPNOTSUPP;
+
+ enable = !!(flags.val & BR_FLOOD);
+ ret = priv->ops->port_set_ucast_flood(priv, port, enable);
+ if (ret)
+ return ret;
+ }
+
+ if (flags.mask & BR_MCAST_FLOOD) {
+ if (!priv->ops->port_set_mcast_flood)
+ return -EOPNOTSUPP;
+
+ enable = !!(flags.val & BR_MCAST_FLOOD);
+ ret = priv->ops->port_set_mcast_flood(priv, port, enable);
+ if (ret)
+ return ret;
+ }
+
+ if (flags.mask & BR_BCAST_FLOOD) {
+ if (!priv->ops->port_set_bcast_flood)
+ return -EOPNOTSUPP;
+
+ enable = !!(flags.val & BR_BCAST_FLOOD);
+ ret = priv->ops->port_set_bcast_flood(priv, port, enable);
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+EXPORT_SYMBOL_NS_GPL(rtl83xx_port_bridge_flags, REALTEK_DSA);
+
+/**
+ * rtl83xx_setup_port_flood_control() - setup default flood control for a port
+ * @priv: realtek_priv pointer
+ * @port: port index
+ *
+ * This function enables flooding for a given port.
+ *
+ * Context: Can sleep.
+ * Return: 0 on success, negative value for failure.
+ */
+int rtl83xx_setup_port_flood_control(struct realtek_priv *priv, int port)
+{
+ int ret;
+
+ if (priv->ops->port_set_ucast_flood) {
+ ret = priv->ops->port_set_ucast_flood(priv, port, true);
+ if (ret)
+ return ret;
+ }
+
+ if (priv->ops->port_set_mcast_flood) {
+ ret = priv->ops->port_set_mcast_flood(priv, port, true);
+ if (ret)
+ return ret;
+ }
+
+ if (priv->ops->port_set_bcast_flood) {
+ ret = priv->ops->port_set_bcast_flood(priv, port, true);
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+EXPORT_SYMBOL_NS_GPL(rtl83xx_setup_port_flood_control, REALTEK_DSA);
+
MODULE_AUTHOR("Luiz Angelo Daros de Luca <luizluca@gmail.com>");
MODULE_AUTHOR("Linus Walleij <linus.walleij@linaro.org>");
MODULE_DESCRIPTION("Realtek DSA switches common module");
--- a/drivers/net/dsa/realtek/rtl83xx.h
+++ b/drivers/net/dsa/realtek/rtl83xx.h
@@ -27,6 +27,10 @@ int rtl83xx_port_bridge_join(struct dsa_
struct netlink_ext_ack *extack);
void rtl83xx_port_bridge_leave(struct dsa_switch *ds, int port,
struct dsa_bridge bridge);
+int rtl83xx_port_bridge_flags(struct dsa_switch *ds, int port,
+ struct switchdev_brport_flags flags,
+ struct netlink_ext_ack *extack);
+int rtl83xx_setup_port_flood_control(struct realtek_priv *priv, int port);
void rtl83xx_port_fast_age(struct dsa_switch *ds, int port);
int rtl83xx_port_fdb_add(struct dsa_switch *ds, int port,
@@ -0,0 +1,70 @@
From 297e1f411ed4927a912c7e207ba6f978cb1f9f0e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Alvin=20=C5=A0ipraga?= <alsi@bang-olufsen.dk>
Date: Wed, 8 Apr 2026 17:31:01 -0300
Subject: [PATCH 1/2] net: dsa: tag_rtl8_4: update format description
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Document the updated tag layout fields (EFID, VSEL/VIDX) and clarify
which bits are set/cleared when emitting tags.
Co-developed-by: Alvin Šipraga <alsi@bang-olufsen.dk>
Signed-off-by: Alvin Šipraga <alsi@bang-olufsen.dk>
Signed-off-by: Luiz Angelo Daros de Luca <luizluca@gmail.com>
Reviewed-by: Linus Walleij <linusw@kernel.org>
Link: https://patch.msgid.link/20260408-realtek_fixes-v1-1-915ff1404d56@gmail.com
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
---
net/dsa/tag_rtl8_4.c | 17 ++++++++++-------
1 file changed, 10 insertions(+), 7 deletions(-)
--- a/net/dsa/tag_rtl8_4.c
+++ b/net/dsa/tag_rtl8_4.c
@@ -17,8 +17,8 @@
* | (8-bit) | (8-bit) |
* | Protocol [0x04] | REASON | b
* |-----------------------------------+-----------------------------------| y
- * | (1) | (1) | (2) | (1) | (3) | (1) | (1) | (1) | (5) | t
- * | FID_EN | X | FID | PRI_EN | PRI | KEEP | X | LEARN_DIS | X | e
+ * | (1) | (3) | (1) | (3) | (1) | (1) | (1) | (5) | t
+ * | EFID_EN | EFID | PRI_EN | PRI | KEEP | VSEL | LEARN_DIS | VIDX | e
* |-----------------------------------+-----------------------------------| s
* | (1) | (15-bit) | |
* | ALLOW | TX/RX | v
@@ -32,19 +32,22 @@
* EtherType | note that Realtek uses the same EtherType for
* | other incompatible tag formats (e.g. tag_rtl4_a.c)
* Protocol | 0x04: indicates that this tag conforms to this format
- * X | reserved
* ------------+-------------
* REASON | reason for forwarding packet to CPU
* | 0: packet was forwarded or flooded to CPU
* | 80: packet was trapped to CPU
- * FID_EN | 1: packet has an FID
- * | 0: no FID
- * FID | FID of packet (if FID_EN=1)
+ * EFID_EN | 1: packet has an EFID
+ * | 0: no EFID
+ * EFID | Extended filter ID (EFID) of packet (if EFID_EN=1)
* PRI_EN | 1: force priority of packet
* | 0: don't force priority
* PRI | priority of packet (if PRI_EN=1)
* KEEP | preserve packet VLAN tag format
+ * VSEL | 0: switch should classify packet according to VLAN tag
+ * | 1: switch should classify packet according to VLAN membership
+ * | configuration with index VIDX
* LEARN_DIS | don't learn the source MAC address of the packet
+ * VIDX | index of a VLAN membership configuration to use with VSEL
* ALLOW | 1: treat TX/RX field as an allowance port mask, meaning the
* | packet may only be forwarded to ports specified in the
* | mask
@@ -112,7 +115,7 @@ static void rtl8_4_write_tag(struct sk_b
/* Set Protocol; zero REASON */
tag16[1] = htons(FIELD_PREP(RTL8_4_PROTOCOL, RTL8_4_PROTOCOL_RTL8365MB));
- /* Zero FID_EN, FID, PRI_EN, PRI, KEEP; set LEARN_DIS */
+ /* Zero EFID_EN, EFID, PRI_EN, PRI, VSEL, VIDX, KEEP; set LEARN_DIS */
tag16[2] = htons(FIELD_PREP(RTL8_4_LEARN_DIS, 1));
/* Zero ALLOW; set RX (CPU->switch) forwarding port mask */
@@ -0,0 +1,48 @@
From 82f37bd9a4d779495479c0c13152208d5400c8a4 Mon Sep 17 00:00:00 2001
From: Luiz Angelo Daros de Luca <luizluca@gmail.com>
Date: Wed, 8 Apr 2026 17:31:02 -0300
Subject: [PATCH 2/2] net: dsa: tag_rtl8_4: set KEEP flag
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
KEEP=1 is needed because we should respect the format of the packet as
the kernel sends it to us. Unless tx forward offloading is used, the
kernel is giving us the packet exactly as it should leave the specified
port on the wire. Until now this was not needed because the ports were
always functioning in a standalone mode in a VLAN-unaware way, so the
switch would not tag or untag frames anyway. But arguably it should have
been KEEP=1 all along.
Co-developed-by: Alvin Šipraga <alsi@bang-olufsen.dk>
Signed-off-by: Alvin Šipraga <alsi@bang-olufsen.dk>
Signed-off-by: Luiz Angelo Daros de Luca <luizluca@gmail.com>
Reviewed-by: Linus Walleij <linusw@kernel.org>
Link: https://patch.msgid.link/20260408-realtek_fixes-v1-2-915ff1404d56@gmail.com
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
---
net/dsa/tag_rtl8_4.c | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
--- a/net/dsa/tag_rtl8_4.c
+++ b/net/dsa/tag_rtl8_4.c
@@ -99,6 +99,7 @@
#define RTL8_4_REASON_TRAP 80
#define RTL8_4_LEARN_DIS BIT(5)
+#define RTL8_4_KEEP BIT(7)
#define RTL8_4_TX GENMASK(3, 0)
#define RTL8_4_RX GENMASK(10, 0)
@@ -115,8 +116,9 @@ static void rtl8_4_write_tag(struct sk_b
/* Set Protocol; zero REASON */
tag16[1] = htons(FIELD_PREP(RTL8_4_PROTOCOL, RTL8_4_PROTOCOL_RTL8365MB));
- /* Zero EFID_EN, EFID, PRI_EN, PRI, VSEL, VIDX, KEEP; set LEARN_DIS */
- tag16[2] = htons(FIELD_PREP(RTL8_4_LEARN_DIS, 1));
+ /* Zero EFID_EN, EFID, PRI_EN, PRI, VSEL, VIDX; set KEEP, LEARN_DIS */
+ tag16[2] = htons(FIELD_PREP(RTL8_4_LEARN_DIS, 1) |
+ FIELD_PREP(RTL8_4_KEEP, 1));
/* Zero ALLOW; set RX (CPU->switch) forwarding port mask */
tag16[3] = htons(FIELD_PREP(RTL8_4_RX, BIT(dp->index)));
@@ -0,0 +1,154 @@
From a543687227d86b1f4401f395abaf7f3f710fe964 Mon Sep 17 00:00:00 2001
From: Luiz Angelo Daros de Luca <luizluca@gmail.com>
Date: Sat, 6 Jun 2026 05:29:25 -0300
Subject: net: dsa: realtek: rtl8365mb: use ERR_PTR
Convert numeric error codes into human-readable strings by using %pe
together with ERR_PTR() in dev_err() messages. Also use dev_err_probe()
instead of checking for -EPROBE_DEFER.
Reviewed-by: Linus Walleij <linusw@kernel.org>
Reviewed-by: Mieczyslaw Nalewaj <namiltd@yahoo.com>
Signed-off-by: Luiz Angelo Daros de Luca <luizluca@gmail.com>
Link: https://patch.msgid.link/20260606-realtek_forward-v13-1-b9e409687cbe@gmail.com
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
---
drivers/net/dsa/realtek/rtl8365mb.c | 51 +++++++++++++++++++++----------------
1 file changed, 29 insertions(+), 22 deletions(-)
--- a/drivers/net/dsa/realtek/rtl8365mb.c
+++ b/drivers/net/dsa/realtek/rtl8365mb.c
@@ -803,8 +803,8 @@ static int rtl8365mb_phy_read(struct rea
ret = rtl8365mb_phy_ocp_read(priv, phy, ocp_addr, &val);
if (ret) {
dev_err(priv->dev,
- "failed to read PHY%d reg %02x @ %04x, ret %d\n", phy,
- regnum, ocp_addr, ret);
+ "failed to read PHY%d reg %02x @ %04x, ret %pe\n", phy,
+ regnum, ocp_addr, ERR_PTR(ret));
return ret;
}
@@ -831,8 +831,8 @@ static int rtl8365mb_phy_write(struct re
ret = rtl8365mb_phy_ocp_write(priv, phy, ocp_addr, val);
if (ret) {
dev_err(priv->dev,
- "failed to write PHY%d reg %02x @ %04x, ret %d\n", phy,
- regnum, ocp_addr, ret);
+ "failed to write PHY%d reg %02x @ %04x, ret %pe\n", phy,
+ regnum, ocp_addr, ERR_PTR(ret));
return ret;
}
@@ -1082,8 +1082,8 @@ static void rtl8365mb_phylink_mac_config
ret = rtl8365mb_ext_config_rgmii(priv, port, state->interface);
if (ret)
dev_err(priv->dev,
- "failed to configure RGMII mode on port %d: %d\n",
- port, ret);
+ "failed to configure RGMII mode on port %d: %pe\n",
+ port, ERR_PTR(ret));
return;
}
@@ -1112,8 +1112,8 @@ static void rtl8365mb_phylink_mac_link_d
false, false);
if (ret)
dev_err(priv->dev,
- "failed to reset forced mode on port %d: %d\n",
- port, ret);
+ "failed to reset forced mode on port %d: %pe\n",
+ port, ERR_PTR(ret));
return;
}
@@ -1143,8 +1143,8 @@ static void rtl8365mb_phylink_mac_link_u
rx_pause);
if (ret)
dev_err(priv->dev,
- "failed to force mode on port %d: %d\n", port,
- ret);
+ "failed to force mode on port %d: %pe\n", port,
+ ERR_PTR(ret));
return;
}
@@ -1299,8 +1299,8 @@ static void rtl8365mb_get_ethtool_stats(
mib->length, &data[i]);
if (ret) {
dev_err(priv->dev,
- "failed to read port %d counters: %d\n", port,
- ret);
+ "failed to read port %d counters: %pe\n", port,
+ ERR_PTR(ret));
break;
}
}
@@ -1652,7 +1652,8 @@ static irqreturn_t rtl8365mb_irq(int irq
return IRQ_HANDLED;
out_error:
- dev_err(priv->dev, "failed to read interrupt status: %d\n", ret);
+ dev_err(priv->dev, "failed to read interrupt status: %pe\n",
+ ERR_PTR(ret));
out_none:
return IRQ_NONE;
@@ -1725,10 +1726,13 @@ static int rtl8365mb_irq_setup(struct re
/* rtl8365mb IRQs cascade off this one */
irq = of_irq_get(intc, 0);
if (irq <= 0) {
- if (irq != -EPROBE_DEFER)
- dev_err(priv->dev, "failed to get parent irq: %d\n",
- irq);
- ret = irq ? irq : -EINVAL;
+ if (!irq) {
+ dev_err(priv->dev, "failed to map IRQ\n");
+ ret = -EINVAL;
+ } else {
+ ret = dev_err_probe(priv->dev, irq,
+ "failed to get parent irq\n");
+ }
goto out_put_node;
}
@@ -1790,7 +1794,8 @@ static int rtl8365mb_irq_setup(struct re
ret = request_threaded_irq(irq, NULL, rtl8365mb_irq, IRQF_ONESHOT,
"rtl8365mb", priv);
if (ret) {
- dev_err(priv->dev, "failed to request irq: %d\n", ret);
+ dev_err(priv->dev, "failed to request irq: %pe\n",
+ ERR_PTR(ret));
goto out_remove_irqdomain;
}
@@ -1966,14 +1971,16 @@ static int rtl8365mb_setup(struct dsa_sw
ret = rtl8365mb_reset_chip(priv);
if (ret) {
- dev_err(priv->dev, "failed to reset chip: %d\n", ret);
+ dev_err(priv->dev, "failed to reset chip: %pe\n",
+ ERR_PTR(ret));
goto out_error;
}
/* Configure switch to vendor-defined initial state */
ret = rtl8365mb_switch_init(priv);
if (ret) {
- dev_err(priv->dev, "failed to initialize switch: %d\n", ret);
+ dev_err(priv->dev, "failed to initialize switch: %pe\n",
+ ERR_PTR(ret));
goto out_error;
}
@@ -2091,8 +2098,8 @@ static int rtl8365mb_detect(struct realt
ret = rtl8365mb_get_chip_id_and_ver(priv->map, &chip_id, &chip_ver);
if (ret) {
- dev_err(priv->dev, "failed to read chip id and version: %d\n",
- ret);
+ dev_err(priv->dev, "failed to read chip id and version: %pe\n",
+ ERR_PTR(ret));
return ret;
}
@@ -0,0 +1,59 @@
From 36c572fd60d6bf2f9631088833c162203907c9ca Mon Sep 17 00:00:00 2001
From: Luiz Angelo Daros de Luca <luizluca@gmail.com>
Date: Sat, 6 Jun 2026 05:29:26 -0300
Subject: net: dsa: realtek: rtl8365mb: reject unsupported topologies
Explicitly enforce the presence of a CPU port (-EINVAL) and reject DSA
cascade links (-EOPNOTSUPP) during setup to prevent silent failures.
These topologies were already non-functional. Without a CPU port, the
driver does not activate CPU tagging. Additionally, the switch hardware
was not designed to be cascaded, and DSA links never worked because
CPU tagging is not enabled for them.
Reviewed-by: Mieczyslaw Nalewaj <namiltd@yahoo.com>
Reviewed-by: Linus Walleij <linusw@kernel.org>
Signed-off-by: Luiz Angelo Daros de Luca <luizluca@gmail.com>
Link: https://patch.msgid.link/20260606-realtek_forward-v13-2-b9e409687cbe@gmail.com
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
---
drivers/net/dsa/realtek/rtl8365mb.c | 21 +++++++++++++++++++++
1 file changed, 21 insertions(+)
--- a/drivers/net/dsa/realtek/rtl8365mb.c
+++ b/drivers/net/dsa/realtek/rtl8365mb.c
@@ -1991,6 +1991,20 @@ static int rtl8365mb_setup(struct dsa_sw
else if (ret)
dev_info(priv->dev, "no interrupt support\n");
+ for (i = 0; i < priv->num_ports; i++) {
+ /* Cascading (DSA links) is not supported yet.
+ * Historically, the driver has always been broken
+ * without a dedicated CPU port because CPU tagging
+ * would be disabled, rendering the switch entirely
+ * non-functional for DSA operations.
+ */
+ if (dsa_is_dsa_port(ds, i)) {
+ dev_err(priv->dev, "Cascading (DSA link) not supported\n");
+ ret = -EOPNOTSUPP;
+ goto out_teardown_irq;
+ }
+ }
+
/* Configure CPU tagging */
dsa_switch_for_each_cpu_port(cpu_dp, ds) {
cpu->mask |= BIT(cpu_dp->index);
@@ -1999,6 +2013,13 @@ static int rtl8365mb_setup(struct dsa_sw
cpu->trap_port = cpu_dp->index;
}
cpu->enable = cpu->mask > 0;
+
+ if (!cpu->enable) {
+ dev_err(priv->dev, "no CPU port defined\n");
+ ret = -EINVAL;
+ goto out_teardown_irq;
+ }
+
ret = rtl8365mb_cpu_config(priv);
if (ret)
goto out_teardown_irq;
@@ -0,0 +1,347 @@
From 51a4a22301b023610a1321614e8dc61251ef9064 Mon Sep 17 00:00:00 2001
From: Luiz Angelo Daros de Luca <luizluca@gmail.com>
Date: Sat, 6 Jun 2026 05:29:27 -0300
Subject: net: dsa: realtek: rtl8365mb: use dsa helpers for port iteration
Convert open-coded port iteration loops to use the DSA helpers and
restructure rtl8365mb_setup() into clear blocking, user, and
CPU port phases.
As part of this refactoring, unused ports are explicitly placed into a
blocked, isolated state with learning disabled, ensuring safe default
hardware behavior. The driver also does not allocate a virtual IRQ
mapping for unused ports. To accommodate this, a guard check is added to
the interrupt handler (rtl8365mb_irq) to safely skip ports without a
valid IRQ mapping. The irq domain teardown, however, does clean all
ports as external PHYs may still map the IRQ.
Furthermore, since the new initialization loop starts with all ports
administratively isolated by default, CPU port forwarding and isolation
masks are explicitly configured at the end of the setup phase to prevent
egress traffic from being blocked.
Suggested-by: Abdulkader Alrezej <abdulkader.alrezej@gmail.com>
Reviewed-by: Linus Walleij <linusw@kernel.org>
Reviewed-by: Mieczyslaw Nalewaj <namiltd@yahoo.com>
Signed-off-by: Luiz Angelo Daros de Luca <luizluca@gmail.com>
Link: https://patch.msgid.link/20260606-realtek_forward-v13-3-b9e409687cbe@gmail.com
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
---
drivers/net/dsa/realtek/rtl8365mb.c | 166 +++++++++++++++++++++++-------------
1 file changed, 105 insertions(+), 61 deletions(-)
--- a/drivers/net/dsa/realtek/rtl8365mb.c
+++ b/drivers/net/dsa/realtek/rtl8365mb.c
@@ -1554,18 +1554,15 @@ static void rtl8365mb_stats_setup(struct
{
struct rtl8365mb *mb = priv->chip_data;
struct dsa_switch *ds = &priv->ds;
- int i;
+ struct dsa_port *dp;
/* Per-chip global mutex to protect MIB counter access, since doing
* so requires accessing a series of registers in a particular order.
*/
mutex_init(&mb->mib_lock);
- for (i = 0; i < priv->num_ports; i++) {
- struct rtl8365mb_port *p = &mb->ports[i];
-
- if (dsa_is_unused_port(ds, i))
- continue;
+ dsa_switch_for_each_available_port(dp, ds) {
+ struct rtl8365mb_port *p = &mb->ports[dp->index];
/* Per-port spinlock to protect the stats64 data */
spin_lock_init(&p->stats_lock);
@@ -1581,13 +1578,10 @@ static void rtl8365mb_stats_teardown(str
{
struct rtl8365mb *mb = priv->chip_data;
struct dsa_switch *ds = &priv->ds;
- int i;
-
- for (i = 0; i < priv->num_ports; i++) {
- struct rtl8365mb_port *p = &mb->ports[i];
+ struct dsa_port *dp;
- if (dsa_is_unused_port(ds, i))
- continue;
+ dsa_switch_for_each_available_port(dp, ds) {
+ struct rtl8365mb_port *p = &mb->ports[dp->index];
cancel_delayed_work_sync(&p->mib_work);
}
@@ -1646,6 +1640,9 @@ static irqreturn_t rtl8365mb_irq(int irq
for_each_set_bit(line, &line_changes, priv->num_ports) {
int child_irq = irq_find_mapping(priv->irqdomain, line);
+ if (!child_irq)
+ continue;
+
handle_nested_irq(child_irq);
}
@@ -1667,10 +1664,14 @@ static struct irq_chip rtl8365mb_irq_chi
static int rtl8365mb_irq_map(struct irq_domain *domain, unsigned int irq,
irq_hw_number_t hwirq)
{
- irq_set_chip_data(irq, domain->host_data);
+ struct realtek_priv *priv = domain->host_data;
+ struct rtl8365mb *mb = priv->chip_data;
+
+ irq_set_chip_data(irq, priv);
irq_set_chip_and_handler(irq, &rtl8365mb_irq_chip, handle_simple_irq);
irq_set_nested_thread(irq, 1);
irq_set_noprobe(irq);
+ irq_set_parent(irq, mb->irq);
return 0;
}
@@ -1709,13 +1710,14 @@ static int rtl8365mb_irq_disable(struct
static int rtl8365mb_irq_setup(struct realtek_priv *priv)
{
struct rtl8365mb *mb = priv->chip_data;
+ struct dsa_switch *ds = &priv->ds;
struct device_node *intc;
+ struct dsa_port *dp;
u32 irq_trig;
int virq;
int irq;
u32 val;
int ret;
- int i;
intc = of_get_child_by_name(priv->dev->of_node, "interrupt-controller");
if (!intc) {
@@ -1736,6 +1738,9 @@ static int rtl8365mb_irq_setup(struct re
goto out_put_node;
}
+ /* Store the irq so that we know to map and free it during teardown */
+ mb->irq = irq;
+
priv->irqdomain = irq_domain_create_linear(of_fwnode_handle(intc), priv->num_ports,
&rtl8365mb_irqdomain_ops, priv);
if (!priv->irqdomain) {
@@ -1744,8 +1749,8 @@ static int rtl8365mb_irq_setup(struct re
goto out_put_node;
}
- for (i = 0; i < priv->num_ports; i++) {
- virq = irq_create_mapping(priv->irqdomain, i);
+ dsa_switch_for_each_available_port(dp, ds) {
+ virq = irq_create_mapping(priv->irqdomain, dp->index);
if (!virq) {
dev_err(priv->dev,
"failed to create irq domain mapping\n");
@@ -1799,9 +1804,6 @@ static int rtl8365mb_irq_setup(struct re
goto out_remove_irqdomain;
}
- /* Store the irq so that we know to free it during teardown */
- mb->irq = irq;
-
ret = rtl8365mb_irq_enable(priv);
if (ret)
goto out_free_irq;
@@ -1812,18 +1814,20 @@ static int rtl8365mb_irq_setup(struct re
out_free_irq:
free_irq(mb->irq, priv);
- mb->irq = 0;
out_remove_irqdomain:
- for (i = 0; i < priv->num_ports; i++) {
- virq = irq_find_mapping(priv->irqdomain, i);
- irq_dispose_mapping(virq);
+ dsa_switch_for_each_port(dp, ds) {
+ virq = irq_find_mapping(priv->irqdomain, dp->index);
+
+ if (virq)
+ irq_dispose_mapping(virq);
}
irq_domain_remove(priv->irqdomain);
priv->irqdomain = NULL;
out_put_node:
+ mb->irq = 0;
of_node_put(intc);
return ret;
@@ -1832,8 +1836,9 @@ out_put_node:
static void rtl8365mb_irq_teardown(struct realtek_priv *priv)
{
struct rtl8365mb *mb = priv->chip_data;
+ struct dsa_switch *ds = &priv->ds;
+ struct dsa_port *dp;
int virq;
- int i;
if (mb->irq) {
free_irq(mb->irq, priv);
@@ -1841,9 +1846,15 @@ static void rtl8365mb_irq_teardown(struc
}
if (priv->irqdomain) {
- for (i = 0; i < priv->num_ports; i++) {
- virq = irq_find_mapping(priv->irqdomain, i);
- irq_dispose_mapping(virq);
+ /* Unused ports with a linked PHY still have an active IRQ
+ * mapping that must be disposed of during teardown. Loop
+ * through all ports.
+ */
+ dsa_switch_for_each_port(dp, ds) {
+ virq = irq_find_mapping(priv->irqdomain, dp->index);
+
+ if (virq)
+ irq_dispose_mapping(virq);
}
irq_domain_remove(priv->irqdomain);
@@ -1961,10 +1972,11 @@ static int rtl8365mb_setup(struct dsa_sw
{
struct realtek_priv *priv = ds->priv;
struct rtl8365mb_cpu *cpu;
- struct dsa_port *cpu_dp;
+ u32 downports_mask = 0;
+ u32 upports_mask = 0;
struct rtl8365mb *mb;
+ struct dsa_port *dp;
int ret;
- int i;
mb = priv->chip_data;
cpu = &mb->cpu;
@@ -1991,67 +2003,99 @@ static int rtl8365mb_setup(struct dsa_sw
else if (ret)
dev_info(priv->dev, "no interrupt support\n");
- for (i = 0; i < priv->num_ports; i++) {
+ dsa_switch_for_each_port(dp, ds) {
/* Cascading (DSA links) is not supported yet.
* Historically, the driver has always been broken
* without a dedicated CPU port because CPU tagging
* would be disabled, rendering the switch entirely
* non-functional for DSA operations.
*/
- if (dsa_is_dsa_port(ds, i)) {
+ if (dsa_port_is_dsa(dp)) {
dev_err(priv->dev, "Cascading (DSA link) not supported\n");
ret = -EOPNOTSUPP;
goto out_teardown_irq;
}
}
- /* Configure CPU tagging */
- dsa_switch_for_each_cpu_port(cpu_dp, ds) {
- cpu->mask |= BIT(cpu_dp->index);
+ /* Start with all ports blocked, including unused ports */
+ dsa_switch_for_each_port(dp, ds) {
+ struct rtl8365mb_port *p = &mb->ports[dp->index];
- if (cpu->trap_port == RTL8365MB_MAX_NUM_PORTS)
- cpu->trap_port = cpu_dp->index;
- }
- cpu->enable = cpu->mask > 0;
+ /* Set the initial STP state of all ports to DISABLED, otherwise
+ * ports will still forward frames to the CPU despite being
+ * administratively down by default.
+ */
+ rtl8365mb_port_stp_state_set(ds, dp->index, BR_STATE_DISABLED);
- if (!cpu->enable) {
- dev_err(priv->dev, "no CPU port defined\n");
- ret = -EINVAL;
- goto out_teardown_irq;
- }
+ /* Start with all port completely isolated */
+ ret = rtl8365mb_port_set_isolation(priv, dp->index, 0);
+ if (ret)
+ goto out_teardown_irq;
- ret = rtl8365mb_cpu_config(priv);
- if (ret)
- goto out_teardown_irq;
+ /* Disable learning */
+ ret = rtl8365mb_port_set_learning(priv, dp->index, false);
+ if (ret)
+ goto out_teardown_irq;
+
+ /* Set up per-port private data */
+ p->priv = priv;
+ p->index = dp->index;
- /* Configure ports */
- for (i = 0; i < priv->num_ports; i++) {
- struct rtl8365mb_port *p = &mb->ports[i];
+ /* Collect CPU ports. If we support cascade switches, it should
+ * also include the upstream DSA ports.
+ */
+ if (!dsa_port_is_cpu(dp))
+ continue;
+
+ upports_mask |= BIT(dp->index);
+ }
- if (dsa_is_unused_port(ds, i))
+ /* Configure user ports */
+ dsa_switch_for_each_port(dp, ds) {
+ if (!dsa_port_is_user(dp))
continue;
/* Forward only to the CPU */
- ret = rtl8365mb_port_set_isolation(priv, i, cpu->mask);
+ ret = rtl8365mb_port_set_isolation(priv, dp->index,
+ upports_mask);
if (ret)
goto out_teardown_irq;
- /* Disable learning */
- ret = rtl8365mb_port_set_learning(priv, i, false);
+ /* If we support cascade switches, it should also include the
+ * downstream DSA ports.
+ */
+ downports_mask |= BIT(dp->index);
+ }
+
+ /* Configure CPU tagging */
+ /* If we support cascade switches, it should also include the upstream
+ * DSA ports.
+ */
+ dsa_switch_for_each_cpu_port(dp, ds) {
+ /* Use the first CPU port as trap_port */
+ if (cpu->trap_port == RTL8365MB_MAX_NUM_PORTS)
+ cpu->trap_port = dp->index;
+
+ /* Forward to all user ports */
+ ret = rtl8365mb_port_set_isolation(priv, dp->index,
+ downports_mask);
if (ret)
goto out_teardown_irq;
+ }
- /* Set the initial STP state of all ports to DISABLED, otherwise
- * ports will still forward frames to the CPU despite being
- * administratively down by default.
- */
- rtl8365mb_port_stp_state_set(ds, i, BR_STATE_DISABLED);
+ cpu->mask = upports_mask;
+ cpu->enable = cpu->mask > 0;
- /* Set up per-port private data */
- p->priv = priv;
- p->index = i;
+ if (!cpu->enable) {
+ dev_err(priv->dev, "no CPU port defined\n");
+ ret = -EINVAL;
+ goto out_teardown_irq;
}
+ ret = rtl8365mb_cpu_config(priv);
+ if (ret)
+ goto out_teardown_irq;
+
ret = rtl8365mb_port_change_mtu(ds, cpu->trap_port, ETH_DATA_LEN);
if (ret)
goto out_teardown_irq;
@@ -0,0 +1,394 @@
From fbafdd3b224a03b7b335de144f44a600de937586 Mon Sep 17 00:00:00 2001
From: Alvin Šipraga <alsi@bang-olufsen.dk>
Date: Sat, 6 Jun 2026 05:29:29 -0300
Subject: net: dsa: realtek: rtl8365mb: add table lookup interface
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Add a generic table lookup interface to centralize access to
the RTL8365MB internal tables.
This interface abstracts the low-level table access logic and
will be used by subsequent commits to implement FDB and VLAN
operations.
Signed-off-by: Alvin Šipraga <alsi@bang-olufsen.dk>
Reviewed-by: Linus Walleij <linusw@kernel.org>
Reviewed-by: Mieczyslaw Nalewaj <namiltd@yahoo.com>
Co-developed-by: Luiz Angelo Daros de Luca <luizluca@gmail.com>
Signed-off-by: Luiz Angelo Daros de Luca <luizluca@gmail.com>
Link: https://patch.msgid.link/20260606-realtek_forward-v13-5-b9e409687cbe@gmail.com
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
---
drivers/net/dsa/realtek/Makefile | 1 +
drivers/net/dsa/realtek/rtl8365mb_table.c | 214 ++++++++++++++++++++++++++++++
drivers/net/dsa/realtek/rtl8365mb_table.h | 138 +++++++++++++++++++
3 files changed, 353 insertions(+)
--- a/drivers/net/dsa/realtek/Makefile
+++ b/drivers/net/dsa/realtek/Makefile
@@ -17,4 +17,5 @@ rtl8366-objs += rtl8366rb-leds.o
endif
obj-$(CONFIG_NET_DSA_REALTEK_RTL8365MB) += rtl8365mb.o
rtl8365mb-objs := rtl8365mb_main.o \
+ rtl8365mb_table.o \
# end of rtl8365mb-objs
--- /dev/null
+++ b/drivers/net/dsa/realtek/rtl8365mb_table.c
@@ -0,0 +1,214 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Look-up table query interface for the rtl8365mb switch family
+ *
+ * Copyright (C) 2022 Alvin Šipraga <alsi@bang-olufsen.dk>
+ */
+
+#include "rtl8365mb_table.h"
+#include <linux/regmap.h>
+
+/* Table access control register */
+#define RTL8365MB_TABLE_CTRL_REG 0x0500
+/* Should be one of rtl8365mb_table enum members */
+#define RTL8365MB_TABLE_CTRL_TABLE_MASK GENMASK(2, 0)
+/* Should be one of rtl8365mb_table_op enum members */
+#define RTL8365MB_TABLE_CTRL_OP_MASK GENMASK(3, 3)
+/* Should be one of rtl8365mb_table_l2_method enum members */
+#define RTL8365MB_TABLE_CTRL_METHOD_MASK GENMASK(6, 4)
+#define RTL8365MB_TABLE_CTRL_PORT_MASK GENMASK(11, 8)
+
+/* Table access address register */
+#define RTL8365MB_TABLE_ACCESS_ADDR_REG 0x0501
+#define RTL8365MB_TABLE_ADDR_MASK GENMASK(12, 0)
+
+/* Table status register */
+#define RTL8365MB_TABLE_STATUS_REG 0x0502
+#define RTL8365MB_TABLE_STATUS_ADDRESS_MASK GENMASK(10, 0)
+/* set for L3, unset for L2 */
+#define RTL8365MB_TABLE_STATUS_ADDR_TYPE_MASK GENMASK(11, 11)
+#define RTL8365MB_TABLE_STATUS_HIT_STATUS_MASK GENMASK(12, 12)
+#define RTL8365MB_TABLE_STATUS_BUSY_FLAG_MASK GENMASK(13, 13)
+#define RTL8365MB_TABLE_STATUS_ADDRESS_EXT_MASK GENMASK(14, 14)
+
+/* Table read/write registers */
+#define RTL8365MB_TABLE_WRITE_BASE 0x0510
+#define RTL8365MB_TABLE_WRITE_REG(_x) \
+ (RTL8365MB_TABLE_WRITE_BASE + (_x))
+#define RTL8365MB_TABLE_READ_BASE 0x0520
+#define RTL8365MB_TABLE_READ_REG(_x) \
+ (RTL8365MB_TABLE_READ_BASE + (_x))
+#define RTL8365MB_TABLE_10TH_DATA_MASK GENMASK(3, 0)
+#define RTL8365MB_TABLE_WRITE_10TH_REG \
+ RTL8365MB_TABLE_WRITE_REG(RTL8365MB_TABLE_ENTRY_MAX_SIZE - 1)
+
+static int rtl8365mb_table_poll_busy(struct realtek_priv *priv)
+{
+ u32 val;
+
+ return regmap_read_poll_timeout(priv->map_nolock,
+ RTL8365MB_TABLE_STATUS_REG, val,
+ !FIELD_GET(RTL8365MB_TABLE_STATUS_BUSY_FLAG_MASK, val),
+ 10, 10000);
+}
+
+int rtl8365mb_table_query(struct realtek_priv *priv,
+ enum rtl8365mb_table table,
+ enum rtl8365mb_table_op op, u16 *addr,
+ enum rtl8365mb_table_l2_method method,
+ u16 port, u16 *data, size_t size)
+{
+ bool addr_as_input = true;
+ bool write_data = false;
+ int ret = 0;
+ u32 cmd;
+ u32 val;
+ u32 hit;
+
+ /* Prepare target table and operation (read or write) */
+ cmd = 0;
+ cmd |= FIELD_PREP(RTL8365MB_TABLE_CTRL_TABLE_MASK, table);
+ cmd |= FIELD_PREP(RTL8365MB_TABLE_CTRL_OP_MASK, op);
+ if (op == RTL8365MB_TABLE_OP_READ && table == RTL8365MB_TABLE_L2) {
+ cmd |= FIELD_PREP(RTL8365MB_TABLE_CTRL_METHOD_MASK, method);
+ switch (method) {
+ case RTL8365MB_TABLE_L2_METHOD_MAC:
+ /*
+ * Method MAC requires as input the same L2 table format
+ * you'll get as result. However, it might only use mac
+ * address and FID/VID fields.
+ */
+ write_data = true;
+
+ /* METHOD_MAC does not use addr as input, but may return
+ * the matched index.
+ */
+ addr_as_input = false;
+
+ break;
+ case RTL8365MB_TABLE_L2_METHOD_ADDR:
+ case RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT:
+ case RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT_UC:
+ case RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT_MC:
+ break;
+ case RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT_UC_PORT:
+ cmd |= FIELD_PREP(RTL8365MB_TABLE_CTRL_PORT_MASK, port);
+ break;
+ default:
+ return -EINVAL;
+ }
+ } else if (op == RTL8365MB_TABLE_OP_WRITE) {
+ write_data = true;
+
+ /* Writing to L2 does not use addr as input, as the table index
+ * is derived from key fields.
+ */
+ if (table == RTL8365MB_TABLE_L2)
+ addr_as_input = false;
+ }
+
+ /* To prevent concurrent access to the look-up tables, take the regmap
+ * lock manually and access via the map_nolock regmap.
+ */
+ mutex_lock(&priv->map_lock);
+
+ /* Protect from a busy table access (i.e. previous access timeouts) */
+ ret = rtl8365mb_table_poll_busy(priv);
+ if (ret)
+ goto out;
+
+ /* Write entry data if writing to the table (or L2_METHOD_MAC) */
+ if (write_data) {
+ /* bulk write data up to 9th word */
+ ret = regmap_bulk_write(priv->map_nolock,
+ RTL8365MB_TABLE_WRITE_BASE,
+ data,
+ min_t(size_t, size,
+ RTL8365MB_TABLE_ENTRY_MAX_SIZE -
+ 1));
+ if (ret)
+ goto out;
+
+ /* 10th register uses only 4 least significant bits */
+ if (size == RTL8365MB_TABLE_ENTRY_MAX_SIZE) {
+ val = FIELD_PREP(RTL8365MB_TABLE_10TH_DATA_MASK,
+ data[size - 1]);
+ ret = regmap_update_bits(priv->map_nolock,
+ RTL8365MB_TABLE_WRITE_10TH_REG,
+ RTL8365MB_TABLE_10TH_DATA_MASK,
+ val);
+ }
+
+ if (ret)
+ goto out;
+ }
+
+ /* Write address (if needed) */
+ if (addr_as_input) {
+ ret = regmap_write(priv->map_nolock,
+ RTL8365MB_TABLE_ACCESS_ADDR_REG,
+ FIELD_PREP(RTL8365MB_TABLE_ADDR_MASK,
+ *addr));
+ if (ret)
+ goto out;
+ }
+
+ /* Execute */
+ ret = regmap_write(priv->map_nolock, RTL8365MB_TABLE_CTRL_REG, cmd);
+ if (ret)
+ goto out;
+
+ /* Poll for completion */
+ ret = rtl8365mb_table_poll_busy(priv);
+ if (ret)
+ goto out;
+
+ /* For both reads and writes to the L2 table, check status */
+ if (table == RTL8365MB_TABLE_L2) {
+ ret = regmap_read(priv->map_nolock, RTL8365MB_TABLE_STATUS_REG,
+ &val);
+ if (ret)
+ goto out;
+
+ /* Did the query find an entry? */
+ hit = FIELD_GET(RTL8365MB_TABLE_STATUS_HIT_STATUS_MASK, val);
+ if (!hit) {
+ ret = -ENOENT;
+ goto out;
+ }
+
+ /* If so, extract the address */
+ *addr = 0;
+ *addr |= FIELD_GET(RTL8365MB_TABLE_STATUS_ADDRESS_MASK, val);
+ *addr |= FIELD_GET(RTL8365MB_TABLE_STATUS_ADDRESS_EXT_MASK, val)
+ << 11;
+ /* only set if it is a L3 address */
+ *addr |= FIELD_GET(RTL8365MB_TABLE_STATUS_ADDR_TYPE_MASK, val)
+ << 12;
+ }
+
+ /* Finally, get the table entry if we were reading */
+ if (op == RTL8365MB_TABLE_OP_READ) {
+ ret = regmap_bulk_read(priv->map_nolock,
+ RTL8365MB_TABLE_READ_BASE,
+ data, size);
+ if (ret)
+ goto out;
+
+ /* For the biggest table entries, the uppermost table
+ * entry register has space for only one nibble. Mask
+ * out the remainder bits. Empirically I saw nothing
+ * wrong with omitting this mask, but it may prevent
+ * unwanted behaviour. FYI.
+ */
+ if (size == RTL8365MB_TABLE_ENTRY_MAX_SIZE) {
+ val = FIELD_GET(RTL8365MB_TABLE_10TH_DATA_MASK,
+ data[size - 1]);
+ data[size - 1] = val;
+ }
+ }
+
+out:
+ mutex_unlock(&priv->map_lock);
+
+ return ret;
+}
--- /dev/null
+++ b/drivers/net/dsa/realtek/rtl8365mb_table.h
@@ -0,0 +1,138 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/* Look-up table query interface for the rtl8365mb switch family
+ *
+ * Copyright (C) 2022 Alvin Šipraga <alsi@bang-olufsen.dk>
+ */
+
+#ifndef _REALTEK_RTL8365MB_TABLE_H
+#define _REALTEK_RTL8365MB_TABLE_H
+
+#include <linux/if_ether.h>
+#include <linux/types.h>
+
+#include "realtek.h"
+
+#define RTL8365MB_TABLE_ENTRY_MAX_SIZE 10
+
+/*
+ * enum rtl8365mb_table - available switch tables
+ * @RTL8365MB_TABLE_ACL_RULE: ACL rules
+ * @RTL8365MB_TABLE_ACL_ACTION: ACL actions
+ * @RTL8365MB_TABLE_CVLAN: VLAN4k configurations
+ * @RTL8365MB_TABLE_L2: filtering database (2K hash table)
+ * @RTL8365MB_TABLE_IGMP_GROUP: IGMP group database (readonly)
+ *
+ * NOTE: Don't change the enum values. They must concur with the field
+ * described by @RTL8365MB_TABLE_CTRL_TABLE_MASK.
+ */
+enum rtl8365mb_table {
+ RTL8365MB_TABLE_ACL_RULE = 1,
+ RTL8365MB_TABLE_ACL_ACTION = 2,
+ RTL8365MB_TABLE_CVLAN = 3,
+ RTL8365MB_TABLE_L2 = 4,
+ RTL8365MB_TABLE_IGMP_GROUP = 5,
+};
+
+/*
+ * enum rtl8365mb_table_op - table query operation
+ * @RTL8365MB_TABLE_OP_READ: read an entry from the target table
+ * @RTL8365MB_TABLE_OP_WRITE: write an entry to the target table
+ *
+ * NOTE: Don't change the enum values. They must concur with the field
+ * described by @RTL8365MB_TABLE_CTRL_OP_MASK.
+ */
+enum rtl8365mb_table_op {
+ RTL8365MB_TABLE_OP_READ = 0,
+ RTL8365MB_TABLE_OP_WRITE = 1,
+};
+
+/*
+ * enum rtl8365mb_table_l2_method - look-up method for read queries of L2 table
+ * @RTL8365MB_TABLE_L2_METHOD_MAC: look-up by source MAC address and FID (or
+ * VID)
+ * @RTL8365MB_TABLE_L2_METHOD_ADDR: look-up by entry address
+ * @RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT: look-up next entry starting from the
+ * supplied address
+ * @RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT_UC: same as ADDR_NEXT but search only
+ * unicast addresses
+ * @RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT_MC: same as ADDR_NEXT but search only
+ * multicast addresses
+ * @RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT_UC_PORT: same as ADDR_NEXT_UC but
+ * search only entries with matching source port
+ *
+ * NOTE: Don't change the enum values. They must concur with the field
+ * described by @RTL8365MB_TABLE_CTRL_METHOD_MASK
+ */
+enum rtl8365mb_table_l2_method {
+ RTL8365MB_TABLE_L2_METHOD_MAC = 0,
+ RTL8365MB_TABLE_L2_METHOD_ADDR = 1,
+ RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT = 2,
+ RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT_UC = 3,
+ RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT_MC = 4,
+ /*
+ * RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT_MC_L3 = 5,
+ * RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT_MC_L2L3 = 6,
+ */
+ RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT_UC_PORT = 7,
+};
+
+/*
+ * rtl8365mb_table_query() - read from or write to a switch table
+ * @priv: driver context
+ * @table: target table, see &enum rtl8365mb_table
+ * @op: read or write operation, see &enum rtl8365mb_table_op
+ * @addr: table address. For indexed tables, this selects the entry to access.
+ * For L2 read queries, it is ignored as input for MAC-based lookup
+ * methods and used as input for address-based lookup methods. On
+ * successful L2 queries, it is updated with the matched entry address.
+ * @method: L2 table lookup method, see &enum rtl8365mb_table_l2_method.
+ * Ignored for non-L2 tables.
+ * @port: for L2 read queries using method
+ * %RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT_UC_PORT, restrict the search
+ * to entries associated with this source port. Ignored otherwise.
+ * @data: data buffer used to read from or write to the table. For L2 MAC
+ * lookups, this buffer provides the lookup key and receives the
+ * matched entry contents on success.
+ * @size: size of @data in 16-bit words. The caller must ensure that @size
+ * matches the target table's entry size and does not exceed
+ * RTL8365MB_TABLE_ENTRY_MAX_SIZE.
+ *
+ * This function provides unified access to the internal tables of the switch.
+ * All tables except the L2 table are simple indexed tables, where @addr
+ * selects the entry and @op determines whether the access is a read or a
+ * write operation.
+ *
+ * The content of @data is used as input when writing to tables or when
+ * specifying the lookup key for L2 MAC searches, and as output for all
+ * successful read operations. It remains unchanged during write operations or
+ * failed read operations that return %-ENOENT. For other errors during read
+ * operations, it is undefined.
+ *
+ * The L2 table is a hash table and supports multiple lookup methods. For
+ * %RTL8365MB_TABLE_L2_METHOD_MAC, an entry is searched based on the MAC
+ * address and FID/VID fields provided in @data, using the same format as
+ * an L2 table entry. Address-based methods either read a specific entry
+ * (%RTL8365MB_TABLE_L2_METHOD_ADDR) or iterate over valid entries starting
+ * from @addr (%RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT and variants). When using
+ * %RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT_UC_PORT, only entries associated with
+ * the specified @port are considered.
+ *
+ * On successful L2 operations, @addr is updated with the matched table address
+ * or allocated entry address. If no matching entry is found, or if an L2 write
+ * operation fails (e.g., due to a full table during addition or a missing entry
+ * during deletion), %-ENOENT is returned and @addr remains unchanged. It is the
+ * caller's responsibility to map the returned error to the appropriate
+ * semantic error.
+ *
+ * @size must match the size of the target table entry, expressed in 16-bit
+ * words.
+ *
+ * Return: 0 on success, or a negative error code on failure.
+ */
+int rtl8365mb_table_query(struct realtek_priv *priv,
+ enum rtl8365mb_table table,
+ enum rtl8365mb_table_op op, u16 *addr,
+ enum rtl8365mb_table_l2_method method,
+ u16 port, u16 *data, size_t size);
+
+#endif /* _REALTEK_RTL8365MB_TABLE_H */
@@ -0,0 +1,335 @@
From 183bd68b1fe1d5ad584355a7449eea32da79334a Mon Sep 17 00:00:00 2001
From: Alvin Šipraga <alsi@bang-olufsen.dk>
Date: Sat, 6 Jun 2026 05:29:32 -0300
Subject: net: dsa: realtek: rtl8365mb: add port_bridge_{join,leave}
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Implement hardware offloading of bridge functionality. This is achieved
by using the per-port isolation registers, which contain a forwarding
port mask. The switch will refuse to forward packets ingressed on a
given port to a port which is not in its forwarding mask.
For each bridge that is offloaded, use the DSA-provided bridge number
for the Extended Filtering ID (EFID). When using Independent VLAN
Learning (IVL), the forwarding database is keyed with the tuple
{VID, MAC, EFID}. There are 8 EFIDs available (0~7), but we reserve the
default EFID 0 for standalone ports where learning is disabled. This
fits nicely because DSA indexes the bridge number starting from 1.
Because of the limited number of EFIDs, we have to set the
max_num_bridges property of our switch to 7: we can't offload more than
that or we will fail to offer IVL as at least two bridges would end up
having to share an EFID.
All ports start isolated, forwarding exclusively to CPU ports, and
with VLAN transparent, ignoring VLAN membership. Once a member in a
bridge, the port isolation is expanded to include the bridge members.
When that bridge enables VLAN filtering, the VLAN transparent feature is
disabled, letting the switch filter based on VLAN setup.
Signed-off-by: Alvin Šipraga <alsi@bang-olufsen.dk>
Reviewed-by: Linus Walleij <linusw@kernel.org>
Reviewed-by: Mieczyslaw Nalewaj <namiltd@yahoo.com>
Co-developed-by: Luiz Angelo Daros de Luca <luizluca@gmail.com>
Signed-off-by: Luiz Angelo Daros de Luca <luizluca@gmail.com>
Link: https://patch.msgid.link/20260606-realtek_forward-v13-8-b9e409687cbe@gmail.com
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
---
drivers/net/dsa/realtek/realtek.h | 7 ++
drivers/net/dsa/realtek/rtl8365mb_main.c | 47 ++++++++-
drivers/net/dsa/realtek/rtl83xx.c | 169 +++++++++++++++++++++++++++++++
drivers/net/dsa/realtek/rtl83xx.h | 7 ++
4 files changed, 229 insertions(+), 1 deletion(-)
--- a/drivers/net/dsa/realtek/realtek.h
+++ b/drivers/net/dsa/realtek/realtek.h
@@ -127,6 +127,13 @@ struct realtek_ops {
int (*enable_vlan)(struct realtek_priv *priv, bool enable);
int (*enable_vlan4k)(struct realtek_priv *priv, bool enable);
int (*enable_port)(struct realtek_priv *priv, int port, bool enable);
+ int (*port_add_isolation)(struct realtek_priv *priv, int port,
+ u32 mask);
+ int (*port_remove_isolation)(struct realtek_priv *priv, int port,
+ u32 mask);
+ int (*port_set_efid)(struct realtek_priv *priv, int port, u32 efid);
+ int (*port_set_learning)(struct realtek_priv *priv, int port,
+ bool enable);
int (*l2_add_uc)(struct realtek_priv *priv, int port,
const unsigned char addr[ETH_ALEN],
u16 efid, u16 vid);
--- a/drivers/net/dsa/realtek/rtl8365mb_main.c
+++ b/drivers/net/dsa/realtek/rtl8365mb_main.c
@@ -1568,10 +1568,44 @@ static int rtl8365mb_port_set_learning(s
enable ? RTL8365MB_LEARN_LIMIT_MAX : 0);
}
+static int rtl8365mb_port_set_efid(struct realtek_priv *priv, int port,
+ u32 efid)
+{
+ return regmap_update_bits(priv->map, RTL8365MB_PORT_EFID_REG(port),
+ RTL8365MB_PORT_EFID_MASK(port),
+ efid << RTL8365MB_PORT_EFID_OFFSET(port));
+}
+
+/* Port isolation manipulation functions.
+ *
+ * The port isolation register controls the forwarding mask of a given
+ * port. The switch will not forward packets ingressed on a given port
+ * to ports which are not enabled in its forwarding mask.
+ *
+ * The port forwarding mask has the highest priority in forwarding
+ * decisions. The only exception to this rule is when the switch
+ * receives a packet on its CPU port with ALLOW=0. In that case the TX
+ * field of the CPU tag will override the forwarding port mask.
+ */
static int rtl8365mb_port_set_isolation(struct realtek_priv *priv, int port,
u32 mask)
{
- return regmap_write(priv->map, RTL8365MB_PORT_ISOLATION_REG(port), mask);
+ return regmap_write(priv->map, RTL8365MB_PORT_ISOLATION_REG(port),
+ mask);
+}
+
+static int rtl8365mb_port_add_isolation(struct realtek_priv *priv, int port,
+ u32 mask)
+{
+ return regmap_update_bits(priv->map, RTL8365MB_PORT_ISOLATION_REG(port),
+ mask, mask);
+}
+
+static int rtl8365mb_port_remove_isolation(struct realtek_priv *priv, int port,
+ u32 mask)
+{
+ return regmap_update_bits(priv->map, RTL8365MB_PORT_ISOLATION_REG(port),
+ mask, 0);
}
static int rtl8365mb_mib_counter_read(struct realtek_priv *priv, int port,
@@ -2378,6 +2412,11 @@ static int rtl8365mb_setup(struct dsa_sw
if (ret)
goto out_teardown_irq;
+ /* Set the default EFID 0 for standalone mode */
+ ret = rtl8365mb_port_set_efid(priv, dp->index, 0);
+ if (ret)
+ goto out_teardown_irq;
+
/* Disable learning */
ret = rtl8365mb_port_set_learning(priv, dp->index, false);
if (ret)
@@ -2567,6 +2606,8 @@ static const struct dsa_switch_ops rtl83
.setup = rtl8365mb_setup,
.teardown = rtl8365mb_teardown,
.phylink_get_caps = rtl8365mb_phylink_get_caps,
+ .port_bridge_join = rtl83xx_port_bridge_join,
+ .port_bridge_leave = rtl83xx_port_bridge_leave,
.port_stp_state_set = rtl8365mb_port_stp_state_set,
.port_fast_age = rtl83xx_port_fast_age,
.port_fdb_add = rtl83xx_port_fdb_add,
@@ -2590,6 +2631,10 @@ static const struct dsa_switch_ops rtl83
static const struct realtek_ops rtl8365mb_ops = {
.detect = rtl8365mb_detect,
+ .port_add_isolation = rtl8365mb_port_add_isolation,
+ .port_remove_isolation = rtl8365mb_port_remove_isolation,
+ .port_set_efid = rtl8365mb_port_set_efid,
+ .port_set_learning = rtl8365mb_port_set_learning,
.l2_add_uc = rtl8365mb_l2_add_uc,
.l2_del_uc = rtl8365mb_l2_del_uc,
.l2_get_next_uc = rtl8365mb_l2_get_next_uc,
--- a/drivers/net/dsa/realtek/rtl83xx.c
+++ b/drivers/net/dsa/realtek/rtl83xx.c
@@ -329,6 +329,175 @@ void rtl83xx_reset_deassert(struct realt
}
/**
+ * rtl83xx_port_bridge_join() - join a port to a bridge
+ * @ds: DSA switch instance
+ * @port: port index
+ * @bridge: bridge being joined
+ * @tx_forward_offload: if the switch can offload TX forwarding
+ * @extack: netlink extended ack for reporting errors
+ *
+ * This function handles joining a port to a bridge. It updates the port
+ * isolation masks and EFID.
+ *
+ * Context: Can sleep.
+ * Return: 0 on success, negative value for failure.
+ */
+int rtl83xx_port_bridge_join(struct dsa_switch *ds, int port,
+ struct dsa_bridge bridge,
+ bool *tx_forward_offload,
+ struct netlink_ext_ack *extack)
+{
+ struct realtek_priv *priv = ds->priv;
+ struct dsa_port *dp;
+ u32 mask = 0;
+ int ret;
+
+ if (!priv->ops->port_add_isolation)
+ return -EOPNOTSUPP;
+
+ if (!priv->ops->port_set_learning)
+ return -EOPNOTSUPP;
+
+ dev_dbg(priv->dev, "bridge %d join port %d\n", bridge.num, port);
+
+ /* Add this port to the isolation group of every other port
+ * offloading this bridge.
+ */
+ dsa_switch_for_each_user_port(dp, ds) {
+ /* Handle this port after */
+ if (dp->index == port)
+ continue;
+
+ /* Skip ports that are not in this bridge */
+ if (!dsa_port_offloads_bridge(dp, &bridge))
+ continue;
+
+ ret = priv->ops->port_add_isolation(priv, dp->index, BIT(port));
+ if (ret)
+ goto undo_isolation;
+
+ mask |= BIT(dp->index);
+ }
+
+ /* If we support cascade switches, it should also include the
+ * downstream DSA ports to the isolation group.
+ */
+
+ /* Add those ports to the isolation group of this port */
+ ret = priv->ops->port_add_isolation(priv, port, mask);
+ if (ret)
+ goto undo_isolation;
+
+ /* Use the bridge number as the EFID for this port */
+ if (priv->ops->port_set_efid) {
+ ret = priv->ops->port_set_efid(priv, port, bridge.num);
+ if (ret)
+ goto undo_self_isolation;
+ }
+
+ ret = priv->ops->port_set_learning(priv, port, true);
+ if (ret)
+ goto undo_efid;
+
+ return 0;
+
+undo_efid:
+ if (priv->ops->port_set_efid)
+ priv->ops->port_set_efid(priv, port, 0);
+
+undo_self_isolation:
+ priv->ops->port_remove_isolation(priv, port, mask);
+
+undo_isolation:
+ dsa_switch_for_each_port(dp, ds) {
+ if (mask & BIT(dp->index))
+ priv->ops->port_remove_isolation(priv, dp->index,
+ BIT(port));
+ }
+
+ return ret;
+}
+EXPORT_SYMBOL_NS_GPL(rtl83xx_port_bridge_join, "REALTEK_DSA");
+
+/**
+ * rtl83xx_port_bridge_leave() - leave a bridge
+ * @ds: DSA switch instance
+ * @port: port index
+ * @bridge: bridge being left
+ *
+ * This function handles removing a port from a bridge. It updates the port
+ * isolation masks and EFID.
+ *
+ * Context: Can sleep.
+ * Return: nothing
+ */
+void rtl83xx_port_bridge_leave(struct dsa_switch *ds, int port,
+ struct dsa_bridge bridge)
+{
+ struct realtek_priv *priv = ds->priv;
+ struct dsa_port *dp;
+ u32 mask = 0;
+ int ret;
+
+ if (!priv->ops->port_remove_isolation)
+ return;
+
+ if (!priv->ops->port_set_learning)
+ return;
+
+ dev_dbg(priv->dev, "bridge %d leave port %d\n", bridge.num, port);
+
+ /* Remove this port from the isolation group of every other
+ * port offloading this bridge.
+ */
+ dsa_switch_for_each_user_port(dp, ds) {
+ /* Handle this port after */
+ if (dp->index == port)
+ continue;
+
+ /* Skip ports that are not in this bridge */
+ if (!dsa_port_offloads_bridge(dp, &bridge))
+ continue;
+
+ ret = priv->ops->port_remove_isolation(priv, dp->index,
+ BIT(port));
+ if (ret)
+ dev_err(priv->dev,
+ "failed to isolate port %d from port %d: %pe\n",
+ port, dp->index, ERR_PTR(ret));
+
+ mask |= BIT(dp->index);
+ }
+
+ /* If we support cascade switches, it should also exclude the
+ * downstream DSA ports from the isolation group.
+ */
+
+ ret = priv->ops->port_set_learning(priv, port, false);
+ if (ret)
+ dev_err(priv->dev,
+ "failed to disable learning on port %d: %pe\n",
+ port, ERR_PTR(ret));
+
+ /* Remove those ports from the isolation group of this port */
+ ret = priv->ops->port_remove_isolation(priv, port, mask);
+ if (ret)
+ dev_err(priv->dev,
+ "failed to remove isolation mask from port %d: %pe\n",
+ port, ERR_PTR(ret));
+
+ /* Revert to the default EFID 0 for standalone mode */
+ if (priv->ops->port_set_efid) {
+ ret = priv->ops->port_set_efid(priv, port, 0);
+ if (ret)
+ dev_err(priv->dev,
+ "failed to clear EFID on port %d: %pe\n",
+ port, ERR_PTR(ret));
+ }
+}
+EXPORT_SYMBOL_NS_GPL(rtl83xx_port_bridge_leave, "REALTEK_DSA");
+
+/**
* rtl83xx_port_fast_age() - flush dynamic FDB entries learned on a port
* @ds: DSA switch instance
* @port: port index
--- a/drivers/net/dsa/realtek/rtl83xx.h
+++ b/drivers/net/dsa/realtek/rtl83xx.h
@@ -21,6 +21,13 @@ void rtl83xx_remove(struct realtek_priv
void rtl83xx_reset_assert(struct realtek_priv *priv);
void rtl83xx_reset_deassert(struct realtek_priv *priv);
+int rtl83xx_port_bridge_join(struct dsa_switch *ds, int port,
+ struct dsa_bridge bridge,
+ bool *tx_forward_offload,
+ struct netlink_ext_ack *extack);
+void rtl83xx_port_bridge_leave(struct dsa_switch *ds, int port,
+ struct dsa_bridge bridge);
+
void rtl83xx_port_fast_age(struct dsa_switch *ds, int port);
int rtl83xx_port_fdb_add(struct dsa_switch *ds, int port,
const unsigned char *addr, u16 vid,
@@ -0,0 +1,279 @@
From 660a9e399ab02c0cb86d277ed6b0c9d10c350fdd Mon Sep 17 00:00:00 2001
From: Luiz Angelo Daros de Luca <luizluca@gmail.com>
Date: Sat, 6 Jun 2026 05:29:33 -0300
Subject: net: dsa: realtek: rtl8365mb: add bridge port flags
Implement support for bridge port flags to control learning and flooding
behavior. This patch maps hardware functionalities to the following
bridge flags:
- BR_LEARNING
- BR_FLOOD
- BR_MCAST_FLOOD
- BR_BCAST_FLOOD
By default, all flooding types are enabled during port setup to ensure
standard bridge behavior.
Reviewed-by: Linus Walleij <linusw@kernel.org>
Reviewed-by: Mieczyslaw Nalewaj <namiltd@yahoo.com>
Signed-off-by: Luiz Angelo Daros de Luca <luizluca@gmail.com>
Link: https://patch.msgid.link/20260606-realtek_forward-v13-9-b9e409687cbe@gmail.com
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
---
drivers/net/dsa/realtek/realtek.h | 6 ++
drivers/net/dsa/realtek/rtl8365mb_main.c | 68 +++++++++++++++++++++
drivers/net/dsa/realtek/rtl83xx.c | 101 +++++++++++++++++++++++++++++++
drivers/net/dsa/realtek/rtl83xx.h | 4 ++
4 files changed, 179 insertions(+)
--- a/drivers/net/dsa/realtek/realtek.h
+++ b/drivers/net/dsa/realtek/realtek.h
@@ -134,6 +134,12 @@ struct realtek_ops {
int (*port_set_efid)(struct realtek_priv *priv, int port, u32 efid);
int (*port_set_learning)(struct realtek_priv *priv, int port,
bool enable);
+ int (*port_set_ucast_flood)(struct realtek_priv *priv, int port,
+ bool enable);
+ int (*port_set_mcast_flood)(struct realtek_priv *priv, int port,
+ bool enable);
+ int (*port_set_bcast_flood)(struct realtek_priv *priv, int port,
+ bool enable);
int (*l2_add_uc)(struct realtek_priv *priv, int port,
const unsigned char addr[ETH_ALEN],
u16 efid, u16 vid);
--- a/drivers/net/dsa/realtek/rtl8365mb_main.c
+++ b/drivers/net/dsa/realtek/rtl8365mb_main.c
@@ -307,6 +307,21 @@
#define RTL8365MB_MSTI_CTRL_PORT_STATE_MASK(_physport) \
(0x3 << RTL8365MB_MSTI_CTRL_PORT_STATE_OFFSET((_physport)))
+/* Unknown unicast DA flooding port mask */
+#define RTL8365MB_UNKNOWN_UNICAST_FLOODING_PMASK_REG 0x0890
+#define RTL8365MB_UNKNOWN_UNICAST_FLOODING_PMASK_MASK 0x07FF
+
+/* Unknown multicast DA flooding port mask */
+#define RTL8365MB_UNKNOWN_MULTICAST_FLOODING_PMASK_REG 0x0891
+#define RTL8365MB_UNKNOWN_MULTICAST_FLOODING_PMASK_MASK 0x07FF
+
+/* Broadcast flooding port mask */
+#define RTL8365MB_UNKNOWN_BROADCAST_FLOODING_PMASK_REG 0x0892
+#define RTL8365MB_UNKNOWN_BROADCAST_FLOODING_PMASK_MASK 0x07FF
+
+#define RTL8365MB_SUPPORTED_BRIDGE_FLAGS \
+ (BR_LEARNING | BR_FLOOD | BR_MCAST_FLOOD | BR_BCAST_FLOOD)
+
/* Miscellaneous port configuration register, incl. VLAN egress mode */
#define RTL8365MB_PORT_MISC_CFG_REG_BASE 0x000E
#define RTL8365MB_PORT_MISC_CFG_REG(_p) \
@@ -1568,6 +1583,49 @@ static int rtl8365mb_port_set_learning(s
enable ? RTL8365MB_LEARN_LIMIT_MAX : 0);
}
+static int rtl8365mb_port_set_ucast_flood(struct realtek_priv *priv, int port,
+ bool enable)
+{
+ /* Frames with unknown unicast DA will be flooded to a programmable
+ * port mask that by default includes all ports. Add or remove
+ * the specified port from this port mask accordingly.
+ */
+ return regmap_update_bits(priv->map,
+ RTL8365MB_UNKNOWN_UNICAST_FLOODING_PMASK_REG,
+ BIT(port), enable ? BIT(port) : 0);
+}
+
+static int rtl8365mb_port_set_mcast_flood(struct realtek_priv *priv, int port,
+ bool enable)
+{
+ return regmap_update_bits(priv->map,
+ RTL8365MB_UNKNOWN_MULTICAST_FLOODING_PMASK_REG,
+ BIT(port), enable ? BIT(port) : 0);
+}
+
+static int rtl8365mb_port_set_bcast_flood(struct realtek_priv *priv, int port,
+ bool enable)
+{
+ return regmap_update_bits(priv->map,
+ RTL8365MB_UNKNOWN_BROADCAST_FLOODING_PMASK_REG,
+ BIT(port), enable ? BIT(port) : 0);
+}
+
+static int rtl8365mb_port_pre_bridge_flags(struct dsa_switch *ds, int port,
+ struct switchdev_brport_flags flags,
+ struct netlink_ext_ack *extack)
+{
+ struct realtek_priv *priv = ds->priv;
+
+ dev_dbg(priv->dev, "pre_bridge_flags port:%d flags:%lx supported:%lx\n",
+ port, flags.mask, RTL8365MB_SUPPORTED_BRIDGE_FLAGS);
+
+ if (flags.mask & ~RTL8365MB_SUPPORTED_BRIDGE_FLAGS)
+ return -EINVAL;
+
+ return 0;
+}
+
static int rtl8365mb_port_set_efid(struct realtek_priv *priv, int port,
u32 efid)
{
@@ -2422,6 +2480,11 @@ static int rtl8365mb_setup(struct dsa_sw
if (ret)
goto out_teardown_irq;
+ /* Enable all types of flooding */
+ ret = rtl83xx_setup_port_flood_control(priv, dp->index);
+ if (ret)
+ goto out_teardown_irq;
+
/* Set up per-port private data */
p->priv = priv;
p->index = dp->index;
@@ -2608,6 +2671,8 @@ static const struct dsa_switch_ops rtl83
.phylink_get_caps = rtl8365mb_phylink_get_caps,
.port_bridge_join = rtl83xx_port_bridge_join,
.port_bridge_leave = rtl83xx_port_bridge_leave,
+ .port_pre_bridge_flags = rtl8365mb_port_pre_bridge_flags,
+ .port_bridge_flags = rtl83xx_port_bridge_flags,
.port_stp_state_set = rtl8365mb_port_stp_state_set,
.port_fast_age = rtl83xx_port_fast_age,
.port_fdb_add = rtl83xx_port_fdb_add,
@@ -2635,6 +2700,9 @@ static const struct realtek_ops rtl8365m
.port_remove_isolation = rtl8365mb_port_remove_isolation,
.port_set_efid = rtl8365mb_port_set_efid,
.port_set_learning = rtl8365mb_port_set_learning,
+ .port_set_ucast_flood = rtl8365mb_port_set_ucast_flood,
+ .port_set_mcast_flood = rtl8365mb_port_set_mcast_flood,
+ .port_set_bcast_flood = rtl8365mb_port_set_bcast_flood,
.l2_add_uc = rtl8365mb_l2_add_uc,
.l2_del_uc = rtl8365mb_l2_del_uc,
.l2_get_next_uc = rtl8365mb_l2_get_next_uc,
--- a/drivers/net/dsa/realtek/rtl83xx.c
+++ b/drivers/net/dsa/realtek/rtl83xx.c
@@ -3,6 +3,7 @@
#include <linux/module.h>
#include <linux/regmap.h>
#include <linux/of_mdio.h>
+#include <linux/if_bridge.h>
#include <linux/etherdevice.h>
#include "realtek.h"
@@ -787,6 +788,106 @@ int rtl83xx_port_mdb_del(struct dsa_swit
}
EXPORT_SYMBOL_NS_GPL(rtl83xx_port_mdb_del, "REALTEK_DSA");
+/**
+ * rtl83xx_port_bridge_flags() - set port bridge flags
+ * @ds: DSA switch instance
+ * @port: port index
+ * @flags: bridge port flags
+ * @extack: netlink extended ack for reporting errors
+ *
+ * This function handles setting bridge port flags like learning and flooding.
+ *
+ * Context: Can sleep.
+ * Return: 0 on success, negative value for failure.
+ */
+int rtl83xx_port_bridge_flags(struct dsa_switch *ds, int port,
+ struct switchdev_brport_flags flags,
+ struct netlink_ext_ack *extack)
+{
+ struct realtek_priv *priv = ds->priv;
+ bool enable;
+ int ret;
+
+ if (flags.mask & BR_LEARNING) {
+ if (!priv->ops->port_set_learning)
+ return -EOPNOTSUPP;
+
+ enable = !!(flags.val & BR_LEARNING);
+ ret = priv->ops->port_set_learning(priv, port, enable);
+ if (ret)
+ return ret;
+ }
+
+ if (flags.mask & BR_FLOOD) {
+ if (!priv->ops->port_set_ucast_flood)
+ return -EOPNOTSUPP;
+
+ enable = !!(flags.val & BR_FLOOD);
+ ret = priv->ops->port_set_ucast_flood(priv, port, enable);
+ if (ret)
+ return ret;
+ }
+
+ if (flags.mask & BR_MCAST_FLOOD) {
+ if (!priv->ops->port_set_mcast_flood)
+ return -EOPNOTSUPP;
+
+ enable = !!(flags.val & BR_MCAST_FLOOD);
+ ret = priv->ops->port_set_mcast_flood(priv, port, enable);
+ if (ret)
+ return ret;
+ }
+
+ if (flags.mask & BR_BCAST_FLOOD) {
+ if (!priv->ops->port_set_bcast_flood)
+ return -EOPNOTSUPP;
+
+ enable = !!(flags.val & BR_BCAST_FLOOD);
+ ret = priv->ops->port_set_bcast_flood(priv, port, enable);
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+EXPORT_SYMBOL_NS_GPL(rtl83xx_port_bridge_flags, "REALTEK_DSA");
+
+/**
+ * rtl83xx_setup_port_flood_control() - setup default flood control for a port
+ * @priv: realtek_priv pointer
+ * @port: port index
+ *
+ * This function enables flooding for a given port.
+ *
+ * Context: Can sleep.
+ * Return: 0 on success, negative value for failure.
+ */
+int rtl83xx_setup_port_flood_control(struct realtek_priv *priv, int port)
+{
+ int ret;
+
+ if (priv->ops->port_set_ucast_flood) {
+ ret = priv->ops->port_set_ucast_flood(priv, port, true);
+ if (ret)
+ return ret;
+ }
+
+ if (priv->ops->port_set_mcast_flood) {
+ ret = priv->ops->port_set_mcast_flood(priv, port, true);
+ if (ret)
+ return ret;
+ }
+
+ if (priv->ops->port_set_bcast_flood) {
+ ret = priv->ops->port_set_bcast_flood(priv, port, true);
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+EXPORT_SYMBOL_NS_GPL(rtl83xx_setup_port_flood_control, "REALTEK_DSA");
+
MODULE_AUTHOR("Luiz Angelo Daros de Luca <luizluca@gmail.com>");
MODULE_AUTHOR("Linus Walleij <linus.walleij@linaro.org>");
MODULE_DESCRIPTION("Realtek DSA switches common module");
--- a/drivers/net/dsa/realtek/rtl83xx.h
+++ b/drivers/net/dsa/realtek/rtl83xx.h
@@ -27,6 +27,10 @@ int rtl83xx_port_bridge_join(struct dsa_
struct netlink_ext_ack *extack);
void rtl83xx_port_bridge_leave(struct dsa_switch *ds, int port,
struct dsa_bridge bridge);
+int rtl83xx_port_bridge_flags(struct dsa_switch *ds, int port,
+ struct switchdev_brport_flags flags,
+ struct netlink_ext_ack *extack);
+int rtl83xx_setup_port_flood_control(struct realtek_priv *priv, int port);
void rtl83xx_port_fast_age(struct dsa_switch *ds, int port);
int rtl83xx_port_fdb_add(struct dsa_switch *ds, int port,