]> git.ipfire.org Git - thirdparty/openwrt.git/commitdiff
kernel: backport rtl8365mb DSA driver improvements 23738/head
authorLuiz Angelo Daros de Luca <luizluca@gmail.com>
Sat, 16 May 2026 16:32:04 +0000 (18:32 +0200)
committerRobert Marko <robimarko@gmail.com>
Thu, 11 Jun 2026 12:49:43 +0000 (14:49 +0200)
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>
22 files changed:
target/linux/generic/backport-6.12/940-01-v7.1-net-dsa-tag_rtl8_4-update-format-description.patch [new file with mode: 0644]
target/linux/generic/backport-6.12/940-02-v7.1-net-dsa-tag_rtl8_4-set-KEEP-flag.patch [new file with mode: 0644]
target/linux/generic/backport-6.12/942-01-v7.2-net-dsa-realtek-rtl8365mb-use-ERR_PTR.patch [new file with mode: 0644]
target/linux/generic/backport-6.12/942-02-v7.2-net-dsa-realtek-rtl8365mb-reject-unsupported-topolog.patch [new file with mode: 0644]
target/linux/generic/backport-6.12/942-03-v7.2-net-dsa-realtek-rtl8365mb-use-dsa-helpers-for-port-i.patch [new file with mode: 0644]
target/linux/generic/backport-6.12/942-04-v7.2-net-dsa-realtek-rtl8365mb-prepare-for-multiple-sourc.patch [new file with mode: 0644]
target/linux/generic/backport-6.12/942-05-v7.2-net-dsa-realtek-rtl8365mb-add-table-lookup-interface.patch [new file with mode: 0644]
target/linux/generic/backport-6.12/942-06-v7.2-net-dsa-realtek-rtl8365mb-add-VLAN-support.patch [new file with mode: 0644]
target/linux/generic/backport-6.12/942-07-v7.2-net-dsa-realtek-rtl8365mb-add-FDB-support.patch [new file with mode: 0644]
target/linux/generic/backport-6.12/942-08-v7.2-net-dsa-realtek-rtl8365mb-add-port_bridge_-join-leav.patch [new file with mode: 0644]
target/linux/generic/backport-6.12/942-09-v7.2-net-dsa-realtek-rtl8365mb-add-bridge-port-flags.patch [new file with mode: 0644]
target/linux/generic/backport-6.18/940-01-v7.1-net-dsa-tag_rtl8_4-update-format-description.patch [new file with mode: 0644]
target/linux/generic/backport-6.18/940-02-v7.1-net-dsa-tag_rtl8_4-set-KEEP-flag.patch [new file with mode: 0644]
target/linux/generic/backport-6.18/942-01-7.2-net-dsa-realtek-rtl8365mb-use-ERR_PTR.patch [new file with mode: 0644]
target/linux/generic/backport-6.18/942-02-7.2-net-dsa-realtek-rtl8365mb-reject-unsupported-topolog.patch [new file with mode: 0644]
target/linux/generic/backport-6.18/942-03-7.2-net-dsa-realtek-rtl8365mb-use-dsa-helpers-for-port-i.patch [new file with mode: 0644]
target/linux/generic/backport-6.18/942-04-7.2-net-dsa-realtek-rtl8365mb-prepare-for-multiple-sourc.patch [new file with mode: 0644]
target/linux/generic/backport-6.18/942-05-7.2-net-dsa-realtek-rtl8365mb-add-table-lookup-interface.patch [new file with mode: 0644]
target/linux/generic/backport-6.18/942-06-7.2-net-dsa-realtek-rtl8365mb-add-VLAN-support.patch [new file with mode: 0644]
target/linux/generic/backport-6.18/942-07-7.2-net-dsa-realtek-rtl8365mb-add-FDB-support.patch [new file with mode: 0644]
target/linux/generic/backport-6.18/942-08-7.2-net-dsa-realtek-rtl8365mb-add-port_bridge_-join-leav.patch [new file with mode: 0644]
target/linux/generic/backport-6.18/942-09-7.2-net-dsa-realtek-rtl8365mb-add-bridge-port-flags.patch [new file with mode: 0644]

diff --git a/target/linux/generic/backport-6.12/940-01-v7.1-net-dsa-tag_rtl8_4-update-format-description.patch b/target/linux/generic/backport-6.12/940-01-v7.1-net-dsa-tag_rtl8_4-update-format-description.patch
new file mode 100644 (file)
index 0000000..d4c129a
--- /dev/null
@@ -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 */
diff --git a/target/linux/generic/backport-6.12/940-02-v7.1-net-dsa-tag_rtl8_4-set-KEEP-flag.patch b/target/linux/generic/backport-6.12/940-02-v7.1-net-dsa-tag_rtl8_4-set-KEEP-flag.patch
new file mode 100644 (file)
index 0000000..7a9ea34
--- /dev/null
@@ -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)));
diff --git a/target/linux/generic/backport-6.12/942-01-v7.2-net-dsa-realtek-rtl8365mb-use-ERR_PTR.patch b/target/linux/generic/backport-6.12/942-01-v7.2-net-dsa-realtek-rtl8365mb-use-ERR_PTR.patch
new file mode 100644 (file)
index 0000000..8a790f5
--- /dev/null
@@ -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;
+       }
diff --git a/target/linux/generic/backport-6.12/942-02-v7.2-net-dsa-realtek-rtl8365mb-reject-unsupported-topolog.patch b/target/linux/generic/backport-6.12/942-02-v7.2-net-dsa-realtek-rtl8365mb-reject-unsupported-topolog.patch
new file mode 100644 (file)
index 0000000..8a907f8
--- /dev/null
@@ -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;
diff --git a/target/linux/generic/backport-6.12/942-03-v7.2-net-dsa-realtek-rtl8365mb-use-dsa-helpers-for-port-i.patch b/target/linux/generic/backport-6.12/942-03-v7.2-net-dsa-realtek-rtl8365mb-use-dsa-helpers-for-port-i.patch
new file mode 100644 (file)
index 0000000..7a625de
--- /dev/null
@@ -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;
diff --git a/target/linux/generic/backport-6.12/942-04-v7.2-net-dsa-realtek-rtl8365mb-prepare-for-multiple-sourc.patch b/target/linux/generic/backport-6.12/942-04-v7.2-net-dsa-realtek-rtl8365mb-prepare-for-multiple-sourc.patch
new file mode 100644 (file)
index 0000000..f3eb40d
--- /dev/null
@@ -0,0 +1,4631 @@
+From 0e692c27fe84db294866fe726f9944f7e125b817 Mon Sep 17 00:00:00 2001
+From: Alvin Šipraga <alsi@bang-olufsen.dk>
+Date: Sat, 6 Jun 2026 05:29:28 -0300
+Subject: net: dsa: realtek: rtl8365mb: prepare for multiple source files
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+Rename rtl8365mb.c to rtl8365mb_main.c in preparation for subsequent
+commits which add additional source files to the driver.
+
+The trailing backslash in the Makefile is deliberate. It allows for new
+files to be added without clobbering git history.
+
+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-4-b9e409687cbe@gmail.com
+Signed-off-by: Jakub Kicinski <kuba@kernel.org>
+---
+ drivers/net/dsa/realtek/Makefile         |    2 +
+ drivers/net/dsa/realtek/rtl8365mb.c      | 2296 ------------------------------
+ drivers/net/dsa/realtek/rtl8365mb_main.c | 2296 ++++++++++++++++++++++++++++++
+ 3 files changed, 2298 insertions(+), 2296 deletions(-)
+ delete mode 100644 drivers/net/dsa/realtek/rtl8365mb.c
+ create mode 100644 drivers/net/dsa/realtek/rtl8365mb_main.c
+
+--- a/drivers/net/dsa/realtek/Makefile
++++ b/drivers/net/dsa/realtek/Makefile
+@@ -16,3 +16,5 @@ ifdef CONFIG_NET_DSA_REALTEK_RTL8366RB_L
+ rtl8366-objs                          += rtl8366rb-leds.o
+ endif
+ obj-$(CONFIG_NET_DSA_REALTEK_RTL8365MB) += rtl8365mb.o
++rtl8365mb-objs := rtl8365mb_main.o \
++# end of rtl8365mb-objs
+--- a/drivers/net/dsa/realtek/rtl8365mb.c
++++ /dev/null
+@@ -1,2294 +0,0 @@
+-// SPDX-License-Identifier: GPL-2.0
+-/* Realtek SMI subdriver for the Realtek RTL8365MB-VC ethernet switch.
+- *
+- * Copyright (C) 2021 Alvin Šipraga <alsi@bang-olufsen.dk>
+- * Copyright (C) 2021 Michael Rasmussen <mir@bang-olufsen.dk>
+- *
+- * The RTL8365MB-VC is a 4+1 port 10/100/1000M switch controller. It includes 4
+- * integrated PHYs for the user facing ports, and an extension interface which
+- * can be connected to the CPU - or another PHY - via either MII, RMII, or
+- * RGMII. The switch is configured via the Realtek Simple Management Interface
+- * (SMI), which uses the MDIO/MDC lines.
+- *
+- * Below is a simplified block diagram of the chip and its relevant interfaces.
+- *
+- *                          .-----------------------------------.
+- *                          |                                   |
+- *         UTP <---------------> Giga PHY <-> PCS <-> P0 GMAC   |
+- *         UTP <---------------> Giga PHY <-> PCS <-> P1 GMAC   |
+- *         UTP <---------------> Giga PHY <-> PCS <-> P2 GMAC   |
+- *         UTP <---------------> Giga PHY <-> PCS <-> P3 GMAC   |
+- *                          |                                   |
+- *     CPU/PHY <-MII/RMII/RGMII--->  Extension  <---> Extension |
+- *                          |       interface 1        GMAC 1   |
+- *                          |                                   |
+- *     SMI driver/ <-MDC/SCL---> Management    ~~~~~~~~~~~~~~   |
+- *        EEPROM   <-MDIO/SDA--> interface     ~REALTEK ~~~~~   |
+- *                          |                  ~RTL8365MB ~~~   |
+- *                          |                  ~GXXXC TAIWAN~   |
+- *        GPIO <--------------> Reset          ~~~~~~~~~~~~~~   |
+- *                          |                                   |
+- *      Interrupt  <----------> Link UP/DOWN events             |
+- *      controller          |                                   |
+- *                          '-----------------------------------'
+- *
+- * The driver uses DSA to integrate the 4 user and 1 extension ports into the
+- * kernel. Netdevices are created for the user ports, as are PHY devices for
+- * their integrated PHYs. The device tree firmware should also specify the link
+- * partner of the extension port - either via a fixed-link or other phy-handle.
+- * See the device tree bindings for more detailed information. Note that the
+- * driver has only been tested with a fixed-link, but in principle it should not
+- * matter.
+- *
+- * NOTE: Currently, only the RGMII interface is implemented in this driver.
+- *
+- * The interrupt line is asserted on link UP/DOWN events. The driver creates a
+- * custom irqchip to handle this interrupt and demultiplex the events by reading
+- * the status registers via SMI. Interrupts are then propagated to the relevant
+- * PHY device.
+- *
+- * The EEPROM contains initial register values which the chip will read over I2C
+- * upon hardware reset. It is also possible to omit the EEPROM. In both cases,
+- * the driver will manually reprogram some registers using jam tables to reach
+- * an initial state defined by the vendor driver.
+- *
+- * This Linux driver is written based on an OS-agnostic vendor driver from
+- * Realtek. The reference GPL-licensed sources can be found in the OpenWrt
+- * source tree under the name rtl8367c. The vendor driver claims to support a
+- * number of similar switch controllers from Realtek, but the only hardware we
+- * have is the RTL8365MB-VC. Moreover, there does not seem to be any chip under
+- * the name RTL8367C. Although one wishes that the 'C' stood for some kind of
+- * common hardware revision, there exist examples of chips with the suffix -VC
+- * which are explicitly not supported by the rtl8367c driver and which instead
+- * require the rtl8367d vendor driver. With all this uncertainty, the driver has
+- * been modestly named rtl8365mb. Future implementors may wish to rename things
+- * accordingly.
+- *
+- * In the same family of chips, some carry up to 8 user ports and up to 2
+- * extension ports. Where possible this driver tries to make things generic, but
+- * more work must be done to support these configurations. According to
+- * documentation from Realtek, the family should include the following chips:
+- *
+- *  - RTL8363NB
+- *  - RTL8363NB-VB
+- *  - RTL8363SC
+- *  - RTL8363SC-VB
+- *  - RTL8364NB
+- *  - RTL8364NB-VB
+- *  - RTL8365MB-VC
+- *  - RTL8366SC
+- *  - RTL8367RB-VB
+- *  - RTL8367SB
+- *  - RTL8367S
+- *  - RTL8370MB
+- *  - RTL8310SR
+- *
+- * Some of the register logic for these additional chips has been skipped over
+- * while implementing this driver. It is therefore not possible to assume that
+- * things will work out-of-the-box for other chips, and a careful review of the
+- * vendor driver may be needed to expand support. The RTL8365MB-VC seems to be
+- * one of the simpler chips.
+- */
+-
+-#include <linux/bitfield.h>
+-#include <linux/bitops.h>
+-#include <linux/interrupt.h>
+-#include <linux/irqdomain.h>
+-#include <linux/mutex.h>
+-#include <linux/of_irq.h>
+-#include <linux/regmap.h>
+-#include <linux/if_bridge.h>
+-#include <linux/if_vlan.h>
+-
+-#include "realtek.h"
+-#include "realtek-smi.h"
+-#include "realtek-mdio.h"
+-#include "rtl83xx.h"
+-
+-/* Family-specific data and limits */
+-#define RTL8365MB_PHYADDRMAX          7
+-#define RTL8365MB_NUM_PHYREGS         32
+-#define RTL8365MB_PHYREGMAX           (RTL8365MB_NUM_PHYREGS - 1)
+-#define RTL8365MB_MAX_NUM_PORTS               11
+-#define RTL8365MB_MAX_NUM_EXTINTS     3
+-#define RTL8365MB_LEARN_LIMIT_MAX     2112
+-
+-/* Chip identification registers */
+-#define RTL8365MB_CHIP_ID_REG         0x1300
+-
+-#define RTL8365MB_CHIP_VER_REG                0x1301
+-
+-#define RTL8365MB_MAGIC_REG           0x13C2
+-#define   RTL8365MB_MAGIC_VALUE               0x0249
+-
+-/* Chip reset register */
+-#define RTL8365MB_CHIP_RESET_REG      0x1322
+-#define RTL8365MB_CHIP_RESET_SW_MASK  0x0002
+-#define RTL8365MB_CHIP_RESET_HW_MASK  0x0001
+-
+-/* Interrupt polarity register */
+-#define RTL8365MB_INTR_POLARITY_REG   0x1100
+-#define   RTL8365MB_INTR_POLARITY_MASK        0x0001
+-#define   RTL8365MB_INTR_POLARITY_HIGH        0
+-#define   RTL8365MB_INTR_POLARITY_LOW 1
+-
+-/* Interrupt control/status register - enable/check specific interrupt types */
+-#define RTL8365MB_INTR_CTRL_REG                       0x1101
+-#define RTL8365MB_INTR_STATUS_REG             0x1102
+-#define   RTL8365MB_INTR_SLIENT_START_2_MASK  0x1000
+-#define   RTL8365MB_INTR_SLIENT_START_MASK    0x0800
+-#define   RTL8365MB_INTR_ACL_ACTION_MASK      0x0200
+-#define   RTL8365MB_INTR_CABLE_DIAG_FIN_MASK  0x0100
+-#define   RTL8365MB_INTR_INTERRUPT_8051_MASK  0x0080
+-#define   RTL8365MB_INTR_LOOP_DETECTION_MASK  0x0040
+-#define   RTL8365MB_INTR_GREEN_TIMER_MASK     0x0020
+-#define   RTL8365MB_INTR_SPECIAL_CONGEST_MASK 0x0010
+-#define   RTL8365MB_INTR_SPEED_CHANGE_MASK    0x0008
+-#define   RTL8365MB_INTR_LEARN_OVER_MASK      0x0004
+-#define   RTL8365MB_INTR_METER_EXCEEDED_MASK  0x0002
+-#define   RTL8365MB_INTR_LINK_CHANGE_MASK     0x0001
+-#define   RTL8365MB_INTR_ALL_MASK                      \
+-              (RTL8365MB_INTR_SLIENT_START_2_MASK |  \
+-               RTL8365MB_INTR_SLIENT_START_MASK |    \
+-               RTL8365MB_INTR_ACL_ACTION_MASK |      \
+-               RTL8365MB_INTR_CABLE_DIAG_FIN_MASK |  \
+-               RTL8365MB_INTR_INTERRUPT_8051_MASK |  \
+-               RTL8365MB_INTR_LOOP_DETECTION_MASK |  \
+-               RTL8365MB_INTR_GREEN_TIMER_MASK |     \
+-               RTL8365MB_INTR_SPECIAL_CONGEST_MASK | \
+-               RTL8365MB_INTR_SPEED_CHANGE_MASK |    \
+-               RTL8365MB_INTR_LEARN_OVER_MASK |      \
+-               RTL8365MB_INTR_METER_EXCEEDED_MASK |  \
+-               RTL8365MB_INTR_LINK_CHANGE_MASK)
+-
+-/* Per-port interrupt type status registers */
+-#define RTL8365MB_PORT_LINKDOWN_IND_REG               0x1106
+-#define   RTL8365MB_PORT_LINKDOWN_IND_MASK    0x07FF
+-
+-#define RTL8365MB_PORT_LINKUP_IND_REG         0x1107
+-#define   RTL8365MB_PORT_LINKUP_IND_MASK      0x07FF
+-
+-/* PHY indirect access registers */
+-#define RTL8365MB_INDIRECT_ACCESS_CTRL_REG                    0x1F00
+-#define   RTL8365MB_INDIRECT_ACCESS_CTRL_RW_MASK              0x0002
+-#define   RTL8365MB_INDIRECT_ACCESS_CTRL_RW_READ              0
+-#define   RTL8365MB_INDIRECT_ACCESS_CTRL_RW_WRITE             1
+-#define   RTL8365MB_INDIRECT_ACCESS_CTRL_CMD_MASK             0x0001
+-#define   RTL8365MB_INDIRECT_ACCESS_CTRL_CMD_VALUE            1
+-#define RTL8365MB_INDIRECT_ACCESS_STATUS_REG                  0x1F01
+-#define RTL8365MB_INDIRECT_ACCESS_ADDRESS_REG                 0x1F02
+-#define   RTL8365MB_INDIRECT_ACCESS_ADDRESS_OCPADR_5_1_MASK   GENMASK(4, 0)
+-#define   RTL8365MB_INDIRECT_ACCESS_ADDRESS_PHYNUM_MASK               GENMASK(7, 5)
+-#define   RTL8365MB_INDIRECT_ACCESS_ADDRESS_OCPADR_9_6_MASK   GENMASK(11, 8)
+-#define   RTL8365MB_PHY_BASE                                  0x2000
+-#define RTL8365MB_INDIRECT_ACCESS_WRITE_DATA_REG              0x1F03
+-#define RTL8365MB_INDIRECT_ACCESS_READ_DATA_REG                       0x1F04
+-
+-/* PHY OCP address prefix register */
+-#define RTL8365MB_GPHY_OCP_MSB_0_REG                  0x1D15
+-#define   RTL8365MB_GPHY_OCP_MSB_0_CFG_CPU_OCPADR_MASK        0x0FC0
+-#define RTL8365MB_PHY_OCP_ADDR_PREFIX_MASK            0xFC00
+-
+-/* The PHY OCP addresses of PHY registers 0~31 start here */
+-#define RTL8365MB_PHY_OCP_ADDR_PHYREG_BASE            0xA400
+-
+-/* External interface port mode values - used in DIGITAL_INTERFACE_SELECT */
+-#define RTL8365MB_EXT_PORT_MODE_DISABLE               0
+-#define RTL8365MB_EXT_PORT_MODE_RGMII         1
+-#define RTL8365MB_EXT_PORT_MODE_MII_MAC               2
+-#define RTL8365MB_EXT_PORT_MODE_MII_PHY               3
+-#define RTL8365MB_EXT_PORT_MODE_TMII_MAC      4
+-#define RTL8365MB_EXT_PORT_MODE_TMII_PHY      5
+-#define RTL8365MB_EXT_PORT_MODE_GMII          6
+-#define RTL8365MB_EXT_PORT_MODE_RMII_MAC      7
+-#define RTL8365MB_EXT_PORT_MODE_RMII_PHY      8
+-#define RTL8365MB_EXT_PORT_MODE_SGMII         9
+-#define RTL8365MB_EXT_PORT_MODE_HSGMII                10
+-#define RTL8365MB_EXT_PORT_MODE_1000X_100FX   11
+-#define RTL8365MB_EXT_PORT_MODE_1000X         12
+-#define RTL8365MB_EXT_PORT_MODE_100FX         13
+-
+-/* External interface mode configuration registers 0~1 */
+-#define RTL8365MB_DIGITAL_INTERFACE_SELECT_REG0               0x1305 /* EXT0,EXT1 */
+-#define RTL8365MB_DIGITAL_INTERFACE_SELECT_REG1               0x13C3 /* EXT2 */
+-#define RTL8365MB_DIGITAL_INTERFACE_SELECT_REG(_extint) \
+-              ((_extint) <= 1 ? RTL8365MB_DIGITAL_INTERFACE_SELECT_REG0 : \
+-               (_extint) == 2 ? RTL8365MB_DIGITAL_INTERFACE_SELECT_REG1 : \
+-               0x0)
+-#define   RTL8365MB_DIGITAL_INTERFACE_SELECT_MODE_MASK(_extint) \
+-              (0xF << (((_extint) % 2) * 4))
+-#define   RTL8365MB_DIGITAL_INTERFACE_SELECT_MODE_OFFSET(_extint) \
+-              (((_extint) % 2) * 4)
+-
+-/* External interface RGMII TX/RX delay configuration registers 0~2 */
+-#define RTL8365MB_EXT_RGMXF_REG0              0x1306 /* EXT0 */
+-#define RTL8365MB_EXT_RGMXF_REG1              0x1307 /* EXT1 */
+-#define RTL8365MB_EXT_RGMXF_REG2              0x13C5 /* EXT2 */
+-#define RTL8365MB_EXT_RGMXF_REG(_extint) \
+-              ((_extint) == 0 ? RTL8365MB_EXT_RGMXF_REG0 : \
+-               (_extint) == 1 ? RTL8365MB_EXT_RGMXF_REG1 : \
+-               (_extint) == 2 ? RTL8365MB_EXT_RGMXF_REG2 : \
+-               0x0)
+-#define   RTL8365MB_EXT_RGMXF_RXDELAY_MASK    0x0007
+-#define   RTL8365MB_EXT_RGMXF_TXDELAY_MASK    0x0008
+-
+-/* External interface port speed values - used in DIGITAL_INTERFACE_FORCE */
+-#define RTL8365MB_PORT_SPEED_10M      0
+-#define RTL8365MB_PORT_SPEED_100M     1
+-#define RTL8365MB_PORT_SPEED_1000M    2
+-
+-/* External interface force configuration registers 0~2 */
+-#define RTL8365MB_DIGITAL_INTERFACE_FORCE_REG0                0x1310 /* EXT0 */
+-#define RTL8365MB_DIGITAL_INTERFACE_FORCE_REG1                0x1311 /* EXT1 */
+-#define RTL8365MB_DIGITAL_INTERFACE_FORCE_REG2                0x13C4 /* EXT2 */
+-#define RTL8365MB_DIGITAL_INTERFACE_FORCE_REG(_extint) \
+-              ((_extint) == 0 ? RTL8365MB_DIGITAL_INTERFACE_FORCE_REG0 : \
+-               (_extint) == 1 ? RTL8365MB_DIGITAL_INTERFACE_FORCE_REG1 : \
+-               (_extint) == 2 ? RTL8365MB_DIGITAL_INTERFACE_FORCE_REG2 : \
+-               0x0)
+-#define   RTL8365MB_DIGITAL_INTERFACE_FORCE_EN_MASK           0x1000
+-#define   RTL8365MB_DIGITAL_INTERFACE_FORCE_NWAY_MASK         0x0080
+-#define   RTL8365MB_DIGITAL_INTERFACE_FORCE_TXPAUSE_MASK      0x0040
+-#define   RTL8365MB_DIGITAL_INTERFACE_FORCE_RXPAUSE_MASK      0x0020
+-#define   RTL8365MB_DIGITAL_INTERFACE_FORCE_LINK_MASK         0x0010
+-#define   RTL8365MB_DIGITAL_INTERFACE_FORCE_DUPLEX_MASK               0x0004
+-#define   RTL8365MB_DIGITAL_INTERFACE_FORCE_SPEED_MASK                0x0003
+-
+-/* CPU port mask register - controls which ports are treated as CPU ports */
+-#define RTL8365MB_CPU_PORT_MASK_REG   0x1219
+-#define   RTL8365MB_CPU_PORT_MASK_MASK        0x07FF
+-
+-/* CPU control register */
+-#define RTL8365MB_CPU_CTRL_REG                        0x121A
+-#define   RTL8365MB_CPU_CTRL_TRAP_PORT_EXT_MASK       0x0400
+-#define   RTL8365MB_CPU_CTRL_TAG_FORMAT_MASK  0x0200
+-#define   RTL8365MB_CPU_CTRL_RXBYTECOUNT_MASK 0x0080
+-#define   RTL8365MB_CPU_CTRL_TAG_POSITION_MASK        0x0040
+-#define   RTL8365MB_CPU_CTRL_TRAP_PORT_MASK   0x0038
+-#define   RTL8365MB_CPU_CTRL_INSERTMODE_MASK  0x0006
+-#define   RTL8365MB_CPU_CTRL_EN_MASK          0x0001
+-
+-/* Maximum packet length register */
+-#define RTL8365MB_CFG0_MAX_LEN_REG    0x088C
+-#define   RTL8365MB_CFG0_MAX_LEN_MASK 0x3FFF
+-#define RTL8365MB_CFG0_MAX_LEN_MAX    0x3FFF
+-
+-/* Port learning limit registers */
+-#define RTL8365MB_LUT_PORT_LEARN_LIMIT_BASE           0x0A20
+-#define RTL8365MB_LUT_PORT_LEARN_LIMIT_REG(_physport) \
+-              (RTL8365MB_LUT_PORT_LEARN_LIMIT_BASE + (_physport))
+-
+-/* Port isolation (forwarding mask) registers */
+-#define RTL8365MB_PORT_ISOLATION_REG_BASE             0x08A2
+-#define RTL8365MB_PORT_ISOLATION_REG(_physport) \
+-              (RTL8365MB_PORT_ISOLATION_REG_BASE + (_physport))
+-#define   RTL8365MB_PORT_ISOLATION_MASK                       0x07FF
+-
+-/* MSTP port state registers - indexed by tree instance */
+-#define RTL8365MB_MSTI_CTRL_BASE                      0x0A00
+-#define RTL8365MB_MSTI_CTRL_REG(_msti, _physport) \
+-              (RTL8365MB_MSTI_CTRL_BASE + ((_msti) << 1) + ((_physport) >> 3))
+-#define   RTL8365MB_MSTI_CTRL_PORT_STATE_OFFSET(_physport) ((_physport) << 1)
+-#define   RTL8365MB_MSTI_CTRL_PORT_STATE_MASK(_physport) \
+-              (0x3 << RTL8365MB_MSTI_CTRL_PORT_STATE_OFFSET((_physport)))
+-
+-/* MIB counter value registers */
+-#define RTL8365MB_MIB_COUNTER_BASE    0x1000
+-#define RTL8365MB_MIB_COUNTER_REG(_x) (RTL8365MB_MIB_COUNTER_BASE + (_x))
+-
+-/* MIB counter address register */
+-#define RTL8365MB_MIB_ADDRESS_REG             0x1004
+-#define   RTL8365MB_MIB_ADDRESS_PORT_OFFSET   0x007C
+-#define   RTL8365MB_MIB_ADDRESS(_p, _x) \
+-              (((RTL8365MB_MIB_ADDRESS_PORT_OFFSET) * (_p) + (_x)) >> 2)
+-
+-#define RTL8365MB_MIB_CTRL0_REG                       0x1005
+-#define   RTL8365MB_MIB_CTRL0_RESET_MASK      0x0002
+-#define   RTL8365MB_MIB_CTRL0_BUSY_MASK               0x0001
+-
+-/* The DSA callback .get_stats64 runs in atomic context, so we are not allowed
+- * to block. On the other hand, accessing MIB counters absolutely requires us to
+- * block. The solution is thus to schedule work which polls the MIB counters
+- * asynchronously and updates some private data, which the callback can then
+- * fetch atomically. Three seconds should be a good enough polling interval.
+- */
+-#define RTL8365MB_STATS_INTERVAL_JIFFIES      (3 * HZ)
+-
+-enum rtl8365mb_mib_counter_index {
+-      RTL8365MB_MIB_ifInOctets,
+-      RTL8365MB_MIB_dot3StatsFCSErrors,
+-      RTL8365MB_MIB_dot3StatsSymbolErrors,
+-      RTL8365MB_MIB_dot3InPauseFrames,
+-      RTL8365MB_MIB_dot3ControlInUnknownOpcodes,
+-      RTL8365MB_MIB_etherStatsFragments,
+-      RTL8365MB_MIB_etherStatsJabbers,
+-      RTL8365MB_MIB_ifInUcastPkts,
+-      RTL8365MB_MIB_etherStatsDropEvents,
+-      RTL8365MB_MIB_ifInMulticastPkts,
+-      RTL8365MB_MIB_ifInBroadcastPkts,
+-      RTL8365MB_MIB_inMldChecksumError,
+-      RTL8365MB_MIB_inIgmpChecksumError,
+-      RTL8365MB_MIB_inMldSpecificQuery,
+-      RTL8365MB_MIB_inMldGeneralQuery,
+-      RTL8365MB_MIB_inIgmpSpecificQuery,
+-      RTL8365MB_MIB_inIgmpGeneralQuery,
+-      RTL8365MB_MIB_inMldLeaves,
+-      RTL8365MB_MIB_inIgmpLeaves,
+-      RTL8365MB_MIB_etherStatsOctets,
+-      RTL8365MB_MIB_etherStatsUnderSizePkts,
+-      RTL8365MB_MIB_etherOversizeStats,
+-      RTL8365MB_MIB_etherStatsPkts64Octets,
+-      RTL8365MB_MIB_etherStatsPkts65to127Octets,
+-      RTL8365MB_MIB_etherStatsPkts128to255Octets,
+-      RTL8365MB_MIB_etherStatsPkts256to511Octets,
+-      RTL8365MB_MIB_etherStatsPkts512to1023Octets,
+-      RTL8365MB_MIB_etherStatsPkts1024to1518Octets,
+-      RTL8365MB_MIB_ifOutOctets,
+-      RTL8365MB_MIB_dot3StatsSingleCollisionFrames,
+-      RTL8365MB_MIB_dot3StatsMultipleCollisionFrames,
+-      RTL8365MB_MIB_dot3StatsDeferredTransmissions,
+-      RTL8365MB_MIB_dot3StatsLateCollisions,
+-      RTL8365MB_MIB_etherStatsCollisions,
+-      RTL8365MB_MIB_dot3StatsExcessiveCollisions,
+-      RTL8365MB_MIB_dot3OutPauseFrames,
+-      RTL8365MB_MIB_ifOutDiscards,
+-      RTL8365MB_MIB_dot1dTpPortInDiscards,
+-      RTL8365MB_MIB_ifOutUcastPkts,
+-      RTL8365MB_MIB_ifOutMulticastPkts,
+-      RTL8365MB_MIB_ifOutBroadcastPkts,
+-      RTL8365MB_MIB_outOampduPkts,
+-      RTL8365MB_MIB_inOampduPkts,
+-      RTL8365MB_MIB_inIgmpJoinsSuccess,
+-      RTL8365MB_MIB_inIgmpJoinsFail,
+-      RTL8365MB_MIB_inMldJoinsSuccess,
+-      RTL8365MB_MIB_inMldJoinsFail,
+-      RTL8365MB_MIB_inReportSuppressionDrop,
+-      RTL8365MB_MIB_inLeaveSuppressionDrop,
+-      RTL8365MB_MIB_outIgmpReports,
+-      RTL8365MB_MIB_outIgmpLeaves,
+-      RTL8365MB_MIB_outIgmpGeneralQuery,
+-      RTL8365MB_MIB_outIgmpSpecificQuery,
+-      RTL8365MB_MIB_outMldReports,
+-      RTL8365MB_MIB_outMldLeaves,
+-      RTL8365MB_MIB_outMldGeneralQuery,
+-      RTL8365MB_MIB_outMldSpecificQuery,
+-      RTL8365MB_MIB_inKnownMulticastPkts,
+-      RTL8365MB_MIB_END,
+-};
+-
+-struct rtl8365mb_mib_counter {
+-      u32 offset;
+-      u32 length;
+-      const char *name;
+-};
+-
+-#define RTL8365MB_MAKE_MIB_COUNTER(_offset, _length, _name) \
+-              [RTL8365MB_MIB_ ## _name] = { _offset, _length, #_name }
+-
+-static struct rtl8365mb_mib_counter rtl8365mb_mib_counters[] = {
+-      RTL8365MB_MAKE_MIB_COUNTER(0, 4, ifInOctets),
+-      RTL8365MB_MAKE_MIB_COUNTER(4, 2, dot3StatsFCSErrors),
+-      RTL8365MB_MAKE_MIB_COUNTER(6, 2, dot3StatsSymbolErrors),
+-      RTL8365MB_MAKE_MIB_COUNTER(8, 2, dot3InPauseFrames),
+-      RTL8365MB_MAKE_MIB_COUNTER(10, 2, dot3ControlInUnknownOpcodes),
+-      RTL8365MB_MAKE_MIB_COUNTER(12, 2, etherStatsFragments),
+-      RTL8365MB_MAKE_MIB_COUNTER(14, 2, etherStatsJabbers),
+-      RTL8365MB_MAKE_MIB_COUNTER(16, 2, ifInUcastPkts),
+-      RTL8365MB_MAKE_MIB_COUNTER(18, 2, etherStatsDropEvents),
+-      RTL8365MB_MAKE_MIB_COUNTER(20, 2, ifInMulticastPkts),
+-      RTL8365MB_MAKE_MIB_COUNTER(22, 2, ifInBroadcastPkts),
+-      RTL8365MB_MAKE_MIB_COUNTER(24, 2, inMldChecksumError),
+-      RTL8365MB_MAKE_MIB_COUNTER(26, 2, inIgmpChecksumError),
+-      RTL8365MB_MAKE_MIB_COUNTER(28, 2, inMldSpecificQuery),
+-      RTL8365MB_MAKE_MIB_COUNTER(30, 2, inMldGeneralQuery),
+-      RTL8365MB_MAKE_MIB_COUNTER(32, 2, inIgmpSpecificQuery),
+-      RTL8365MB_MAKE_MIB_COUNTER(34, 2, inIgmpGeneralQuery),
+-      RTL8365MB_MAKE_MIB_COUNTER(36, 2, inMldLeaves),
+-      RTL8365MB_MAKE_MIB_COUNTER(38, 2, inIgmpLeaves),
+-      RTL8365MB_MAKE_MIB_COUNTER(40, 4, etherStatsOctets),
+-      RTL8365MB_MAKE_MIB_COUNTER(44, 2, etherStatsUnderSizePkts),
+-      RTL8365MB_MAKE_MIB_COUNTER(46, 2, etherOversizeStats),
+-      RTL8365MB_MAKE_MIB_COUNTER(48, 2, etherStatsPkts64Octets),
+-      RTL8365MB_MAKE_MIB_COUNTER(50, 2, etherStatsPkts65to127Octets),
+-      RTL8365MB_MAKE_MIB_COUNTER(52, 2, etherStatsPkts128to255Octets),
+-      RTL8365MB_MAKE_MIB_COUNTER(54, 2, etherStatsPkts256to511Octets),
+-      RTL8365MB_MAKE_MIB_COUNTER(56, 2, etherStatsPkts512to1023Octets),
+-      RTL8365MB_MAKE_MIB_COUNTER(58, 2, etherStatsPkts1024to1518Octets),
+-      RTL8365MB_MAKE_MIB_COUNTER(60, 4, ifOutOctets),
+-      RTL8365MB_MAKE_MIB_COUNTER(64, 2, dot3StatsSingleCollisionFrames),
+-      RTL8365MB_MAKE_MIB_COUNTER(66, 2, dot3StatsMultipleCollisionFrames),
+-      RTL8365MB_MAKE_MIB_COUNTER(68, 2, dot3StatsDeferredTransmissions),
+-      RTL8365MB_MAKE_MIB_COUNTER(70, 2, dot3StatsLateCollisions),
+-      RTL8365MB_MAKE_MIB_COUNTER(72, 2, etherStatsCollisions),
+-      RTL8365MB_MAKE_MIB_COUNTER(74, 2, dot3StatsExcessiveCollisions),
+-      RTL8365MB_MAKE_MIB_COUNTER(76, 2, dot3OutPauseFrames),
+-      RTL8365MB_MAKE_MIB_COUNTER(78, 2, ifOutDiscards),
+-      RTL8365MB_MAKE_MIB_COUNTER(80, 2, dot1dTpPortInDiscards),
+-      RTL8365MB_MAKE_MIB_COUNTER(82, 2, ifOutUcastPkts),
+-      RTL8365MB_MAKE_MIB_COUNTER(84, 2, ifOutMulticastPkts),
+-      RTL8365MB_MAKE_MIB_COUNTER(86, 2, ifOutBroadcastPkts),
+-      RTL8365MB_MAKE_MIB_COUNTER(88, 2, outOampduPkts),
+-      RTL8365MB_MAKE_MIB_COUNTER(90, 2, inOampduPkts),
+-      RTL8365MB_MAKE_MIB_COUNTER(92, 4, inIgmpJoinsSuccess),
+-      RTL8365MB_MAKE_MIB_COUNTER(96, 2, inIgmpJoinsFail),
+-      RTL8365MB_MAKE_MIB_COUNTER(98, 2, inMldJoinsSuccess),
+-      RTL8365MB_MAKE_MIB_COUNTER(100, 2, inMldJoinsFail),
+-      RTL8365MB_MAKE_MIB_COUNTER(102, 2, inReportSuppressionDrop),
+-      RTL8365MB_MAKE_MIB_COUNTER(104, 2, inLeaveSuppressionDrop),
+-      RTL8365MB_MAKE_MIB_COUNTER(106, 2, outIgmpReports),
+-      RTL8365MB_MAKE_MIB_COUNTER(108, 2, outIgmpLeaves),
+-      RTL8365MB_MAKE_MIB_COUNTER(110, 2, outIgmpGeneralQuery),
+-      RTL8365MB_MAKE_MIB_COUNTER(112, 2, outIgmpSpecificQuery),
+-      RTL8365MB_MAKE_MIB_COUNTER(114, 2, outMldReports),
+-      RTL8365MB_MAKE_MIB_COUNTER(116, 2, outMldLeaves),
+-      RTL8365MB_MAKE_MIB_COUNTER(118, 2, outMldGeneralQuery),
+-      RTL8365MB_MAKE_MIB_COUNTER(120, 2, outMldSpecificQuery),
+-      RTL8365MB_MAKE_MIB_COUNTER(122, 2, inKnownMulticastPkts),
+-};
+-
+-static_assert(ARRAY_SIZE(rtl8365mb_mib_counters) == RTL8365MB_MIB_END);
+-
+-struct rtl8365mb_jam_tbl_entry {
+-      u16 reg;
+-      u16 val;
+-};
+-
+-/* Lifted from the vendor driver sources */
+-static const struct rtl8365mb_jam_tbl_entry rtl8365mb_init_jam_8365mb_vc[] = {
+-      { 0x13EB, 0x15BB }, { 0x1303, 0x06D6 }, { 0x1304, 0x0700 },
+-      { 0x13E2, 0x003F }, { 0x13F9, 0x0090 }, { 0x121E, 0x03CA },
+-      { 0x1233, 0x0352 }, { 0x1237, 0x00A0 }, { 0x123A, 0x0030 },
+-      { 0x1239, 0x0084 }, { 0x0301, 0x1000 }, { 0x1349, 0x001F },
+-      { 0x18E0, 0x4004 }, { 0x122B, 0x241C }, { 0x1305, 0xC000 },
+-      { 0x13F0, 0x0000 },
+-};
+-
+-static const struct rtl8365mb_jam_tbl_entry rtl8365mb_init_jam_common[] = {
+-      { 0x1200, 0x7FCB }, { 0x0884, 0x0003 }, { 0x06EB, 0x0001 },
+-      { 0x03Fa, 0x0007 }, { 0x08C8, 0x00C0 }, { 0x0A30, 0x020E },
+-      { 0x0800, 0x0000 }, { 0x0802, 0x0000 }, { 0x09DA, 0x0013 },
+-      { 0x1D32, 0x0002 },
+-};
+-
+-enum rtl8365mb_phy_interface_mode {
+-      RTL8365MB_PHY_INTERFACE_MODE_INVAL = 0,
+-      RTL8365MB_PHY_INTERFACE_MODE_INTERNAL = BIT(0),
+-      RTL8365MB_PHY_INTERFACE_MODE_MII = BIT(1),
+-      RTL8365MB_PHY_INTERFACE_MODE_TMII = BIT(2),
+-      RTL8365MB_PHY_INTERFACE_MODE_RMII = BIT(3),
+-      RTL8365MB_PHY_INTERFACE_MODE_RGMII = BIT(4),
+-      RTL8365MB_PHY_INTERFACE_MODE_SGMII = BIT(5),
+-      RTL8365MB_PHY_INTERFACE_MODE_HSGMII = BIT(6),
+-};
+-
+-/**
+- * struct rtl8365mb_extint - external interface info
+- * @port: the port with an external interface
+- * @id: the external interface ID, which is either 0, 1, or 2
+- * @supported_interfaces: a bitmask of supported PHY interface modes
+- *
+- * Represents a mapping: port -> { id, supported_interfaces }. To be embedded
+- * in &struct rtl8365mb_chip_info for every port with an external interface.
+- */
+-struct rtl8365mb_extint {
+-      int port;
+-      int id;
+-      unsigned int supported_interfaces;
+-};
+-
+-/**
+- * struct rtl8365mb_chip_info - static chip-specific info
+- * @name: human-readable chip name
+- * @chip_id: chip identifier
+- * @chip_ver: chip silicon revision
+- * @extints: available external interfaces
+- * @jam_table: chip-specific initialization jam table
+- * @jam_size: size of the chip's jam table
+- *
+- * These data are specific to a given chip in the family of switches supported
+- * by this driver. When adding support for another chip in the family, a new
+- * chip info should be added to the rtl8365mb_chip_infos array.
+- */
+-struct rtl8365mb_chip_info {
+-      const char *name;
+-      u32 chip_id;
+-      u32 chip_ver;
+-      const struct rtl8365mb_extint extints[RTL8365MB_MAX_NUM_EXTINTS];
+-      const struct rtl8365mb_jam_tbl_entry *jam_table;
+-      size_t jam_size;
+-};
+-
+-/* Chip info for each supported switch in the family */
+-#define PHY_INTF(_mode) (RTL8365MB_PHY_INTERFACE_MODE_ ## _mode)
+-static const struct rtl8365mb_chip_info rtl8365mb_chip_infos[] = {
+-      {
+-              .name = "RTL8365MB-VC",
+-              .chip_id = 0x6367,
+-              .chip_ver = 0x0040,
+-              .extints = {
+-                      { 6, 1, PHY_INTF(MII) | PHY_INTF(TMII) |
+-                              PHY_INTF(RMII) | PHY_INTF(RGMII) },
+-              },
+-              .jam_table = rtl8365mb_init_jam_8365mb_vc,
+-              .jam_size = ARRAY_SIZE(rtl8365mb_init_jam_8365mb_vc),
+-      },
+-      {
+-              .name = "RTL8367S",
+-              .chip_id = 0x6367,
+-              .chip_ver = 0x00A0,
+-              .extints = {
+-                      { 6, 1, PHY_INTF(SGMII) | PHY_INTF(HSGMII) },
+-                      { 7, 2, PHY_INTF(MII) | PHY_INTF(TMII) |
+-                              PHY_INTF(RMII) | PHY_INTF(RGMII) },
+-              },
+-              .jam_table = rtl8365mb_init_jam_8365mb_vc,
+-              .jam_size = ARRAY_SIZE(rtl8365mb_init_jam_8365mb_vc),
+-      },
+-      {
+-              .name = "RTL8367SB",
+-              .chip_id = 0x6367,
+-              .chip_ver = 0x0010,
+-              .extints = {
+-                      { 6, 1, PHY_INTF(MII) | PHY_INTF(TMII) |
+-                              PHY_INTF(RMII) | PHY_INTF(RGMII) |
+-                              PHY_INTF(SGMII) | PHY_INTF(HSGMII) },
+-                      { 7, 2, PHY_INTF(MII) | PHY_INTF(TMII) |
+-                              PHY_INTF(RMII) | PHY_INTF(RGMII) },
+-              },
+-              .jam_table = rtl8365mb_init_jam_8365mb_vc,
+-              .jam_size = ARRAY_SIZE(rtl8365mb_init_jam_8365mb_vc),
+-      },
+-      {
+-              .name = "RTL8367RB-VB",
+-              .chip_id = 0x6367,
+-              .chip_ver = 0x0020,
+-              .extints = {
+-                      { 6, 1, PHY_INTF(MII) | PHY_INTF(TMII) |
+-                              PHY_INTF(RMII) | PHY_INTF(RGMII) },
+-                      { 7, 2, PHY_INTF(MII) | PHY_INTF(TMII) |
+-                              PHY_INTF(RMII) | PHY_INTF(RGMII) },
+-              },
+-              .jam_table = rtl8365mb_init_jam_8365mb_vc,
+-              .jam_size = ARRAY_SIZE(rtl8365mb_init_jam_8365mb_vc),
+-      },
+-};
+-
+-enum rtl8365mb_stp_state {
+-      RTL8365MB_STP_STATE_DISABLED = 0,
+-      RTL8365MB_STP_STATE_BLOCKING = 1,
+-      RTL8365MB_STP_STATE_LEARNING = 2,
+-      RTL8365MB_STP_STATE_FORWARDING = 3,
+-};
+-
+-enum rtl8365mb_cpu_insert {
+-      RTL8365MB_CPU_INSERT_TO_ALL = 0,
+-      RTL8365MB_CPU_INSERT_TO_TRAPPING = 1,
+-      RTL8365MB_CPU_INSERT_TO_NONE = 2,
+-};
+-
+-enum rtl8365mb_cpu_position {
+-      RTL8365MB_CPU_POS_AFTER_SA = 0,
+-      RTL8365MB_CPU_POS_BEFORE_CRC = 1,
+-};
+-
+-enum rtl8365mb_cpu_format {
+-      RTL8365MB_CPU_FORMAT_8BYTES = 0,
+-      RTL8365MB_CPU_FORMAT_4BYTES = 1,
+-};
+-
+-enum rtl8365mb_cpu_rxlen {
+-      RTL8365MB_CPU_RXLEN_72BYTES = 0,
+-      RTL8365MB_CPU_RXLEN_64BYTES = 1,
+-};
+-
+-/**
+- * struct rtl8365mb_cpu - CPU port configuration
+- * @enable: enable/disable hardware insertion of CPU tag in switch->CPU frames
+- * @mask: port mask of ports that parse should parse CPU tags
+- * @trap_port: forward trapped frames to this port
+- * @insert: CPU tag insertion mode in switch->CPU frames
+- * @position: position of CPU tag in frame
+- * @rx_length: minimum CPU RX length
+- * @format: CPU tag format
+- *
+- * Represents the CPU tagging and CPU port configuration of the switch. These
+- * settings are configurable at runtime.
+- */
+-struct rtl8365mb_cpu {
+-      bool enable;
+-      u32 mask;
+-      u32 trap_port;
+-      enum rtl8365mb_cpu_insert insert;
+-      enum rtl8365mb_cpu_position position;
+-      enum rtl8365mb_cpu_rxlen rx_length;
+-      enum rtl8365mb_cpu_format format;
+-};
+-
+-/**
+- * struct rtl8365mb_port - private per-port data
+- * @priv: pointer to parent realtek_priv data
+- * @index: DSA port index, same as dsa_port::index
+- * @stats: link statistics populated by rtl8365mb_stats_poll, ready for atomic
+- *         access via rtl8365mb_get_stats64
+- * @stats_lock: protect the stats structure during read/update
+- * @mib_work: delayed work for polling MIB counters
+- */
+-struct rtl8365mb_port {
+-      struct realtek_priv *priv;
+-      unsigned int index;
+-      struct rtnl_link_stats64 stats;
+-      spinlock_t stats_lock;
+-      struct delayed_work mib_work;
+-};
+-
+-/**
+- * struct rtl8365mb - driver private data
+- * @priv: pointer to parent realtek_priv data
+- * @irq: registered IRQ or zero
+- * @chip_info: chip-specific info about the attached switch
+- * @cpu: CPU tagging and CPU port configuration for this chip
+- * @mib_lock: prevent concurrent reads of MIB counters
+- * @ports: per-port data
+- *
+- * Private data for this driver.
+- */
+-struct rtl8365mb {
+-      struct realtek_priv *priv;
+-      int irq;
+-      const struct rtl8365mb_chip_info *chip_info;
+-      struct rtl8365mb_cpu cpu;
+-      struct mutex mib_lock;
+-      struct rtl8365mb_port ports[RTL8365MB_MAX_NUM_PORTS];
+-};
+-
+-static int rtl8365mb_phy_poll_busy(struct realtek_priv *priv)
+-{
+-      u32 val;
+-
+-      return regmap_read_poll_timeout(priv->map_nolock,
+-                                      RTL8365MB_INDIRECT_ACCESS_STATUS_REG,
+-                                      val, !val, 10, 100);
+-}
+-
+-static int rtl8365mb_phy_ocp_prepare(struct realtek_priv *priv, int phy,
+-                                   u32 ocp_addr)
+-{
+-      u32 val;
+-      int ret;
+-
+-      /* Set OCP prefix */
+-      val = FIELD_GET(RTL8365MB_PHY_OCP_ADDR_PREFIX_MASK, ocp_addr);
+-      ret = regmap_update_bits(
+-              priv->map_nolock, RTL8365MB_GPHY_OCP_MSB_0_REG,
+-              RTL8365MB_GPHY_OCP_MSB_0_CFG_CPU_OCPADR_MASK,
+-              FIELD_PREP(RTL8365MB_GPHY_OCP_MSB_0_CFG_CPU_OCPADR_MASK, val));
+-      if (ret)
+-              return ret;
+-
+-      /* Set PHY register address */
+-      val = RTL8365MB_PHY_BASE;
+-      val |= FIELD_PREP(RTL8365MB_INDIRECT_ACCESS_ADDRESS_PHYNUM_MASK, phy);
+-      val |= FIELD_PREP(RTL8365MB_INDIRECT_ACCESS_ADDRESS_OCPADR_5_1_MASK,
+-                        ocp_addr >> 1);
+-      val |= FIELD_PREP(RTL8365MB_INDIRECT_ACCESS_ADDRESS_OCPADR_9_6_MASK,
+-                        ocp_addr >> 6);
+-      ret = regmap_write(priv->map_nolock,
+-                         RTL8365MB_INDIRECT_ACCESS_ADDRESS_REG, val);
+-      if (ret)
+-              return ret;
+-
+-      return 0;
+-}
+-
+-static int rtl8365mb_phy_ocp_read(struct realtek_priv *priv, int phy,
+-                                u32 ocp_addr, u16 *data)
+-{
+-      u32 val;
+-      int ret;
+-
+-      rtl83xx_lock(priv);
+-
+-      ret = rtl8365mb_phy_poll_busy(priv);
+-      if (ret)
+-              goto out;
+-
+-      ret = rtl8365mb_phy_ocp_prepare(priv, phy, ocp_addr);
+-      if (ret)
+-              goto out;
+-
+-      /* Execute read operation */
+-      val = FIELD_PREP(RTL8365MB_INDIRECT_ACCESS_CTRL_CMD_MASK,
+-                       RTL8365MB_INDIRECT_ACCESS_CTRL_CMD_VALUE) |
+-            FIELD_PREP(RTL8365MB_INDIRECT_ACCESS_CTRL_RW_MASK,
+-                       RTL8365MB_INDIRECT_ACCESS_CTRL_RW_READ);
+-      ret = regmap_write(priv->map_nolock, RTL8365MB_INDIRECT_ACCESS_CTRL_REG,
+-                         val);
+-      if (ret)
+-              goto out;
+-
+-      ret = rtl8365mb_phy_poll_busy(priv);
+-      if (ret)
+-              goto out;
+-
+-      /* Get PHY register data */
+-      ret = regmap_read(priv->map_nolock,
+-                        RTL8365MB_INDIRECT_ACCESS_READ_DATA_REG, &val);
+-      if (ret)
+-              goto out;
+-
+-      *data = val & 0xFFFF;
+-
+-out:
+-      rtl83xx_unlock(priv);
+-
+-      return ret;
+-}
+-
+-static int rtl8365mb_phy_ocp_write(struct realtek_priv *priv, int phy,
+-                                 u32 ocp_addr, u16 data)
+-{
+-      u32 val;
+-      int ret;
+-
+-      rtl83xx_lock(priv);
+-
+-      ret = rtl8365mb_phy_poll_busy(priv);
+-      if (ret)
+-              goto out;
+-
+-      ret = rtl8365mb_phy_ocp_prepare(priv, phy, ocp_addr);
+-      if (ret)
+-              goto out;
+-
+-      /* Set PHY register data */
+-      ret = regmap_write(priv->map_nolock,
+-                         RTL8365MB_INDIRECT_ACCESS_WRITE_DATA_REG, data);
+-      if (ret)
+-              goto out;
+-
+-      /* Execute write operation */
+-      val = FIELD_PREP(RTL8365MB_INDIRECT_ACCESS_CTRL_CMD_MASK,
+-                       RTL8365MB_INDIRECT_ACCESS_CTRL_CMD_VALUE) |
+-            FIELD_PREP(RTL8365MB_INDIRECT_ACCESS_CTRL_RW_MASK,
+-                       RTL8365MB_INDIRECT_ACCESS_CTRL_RW_WRITE);
+-      ret = regmap_write(priv->map_nolock, RTL8365MB_INDIRECT_ACCESS_CTRL_REG,
+-                         val);
+-      if (ret)
+-              goto out;
+-
+-      ret = rtl8365mb_phy_poll_busy(priv);
+-      if (ret)
+-              goto out;
+-
+-out:
+-      rtl83xx_unlock(priv);
+-
+-      return ret;
+-}
+-
+-static int rtl8365mb_phy_read(struct realtek_priv *priv, int phy, int regnum)
+-{
+-      u32 ocp_addr;
+-      u16 val;
+-      int ret;
+-
+-      if (phy > RTL8365MB_PHYADDRMAX)
+-              return -EINVAL;
+-
+-      if (regnum > RTL8365MB_PHYREGMAX)
+-              return -EINVAL;
+-
+-      ocp_addr = RTL8365MB_PHY_OCP_ADDR_PHYREG_BASE + regnum * 2;
+-
+-      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 %pe\n", phy,
+-                      regnum, ocp_addr, ERR_PTR(ret));
+-              return ret;
+-      }
+-
+-      dev_dbg(priv->dev, "read PHY%d register 0x%02x @ %04x, val <- %04x\n",
+-              phy, regnum, ocp_addr, val);
+-
+-      return val;
+-}
+-
+-static int rtl8365mb_phy_write(struct realtek_priv *priv, int phy, int regnum,
+-                             u16 val)
+-{
+-      u32 ocp_addr;
+-      int ret;
+-
+-      if (phy > RTL8365MB_PHYADDRMAX)
+-              return -EINVAL;
+-
+-      if (regnum > RTL8365MB_PHYREGMAX)
+-              return -EINVAL;
+-
+-      ocp_addr = RTL8365MB_PHY_OCP_ADDR_PHYREG_BASE + regnum * 2;
+-
+-      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 %pe\n", phy,
+-                      regnum, ocp_addr, ERR_PTR(ret));
+-              return ret;
+-      }
+-
+-      dev_dbg(priv->dev, "write PHY%d register 0x%02x @ %04x, val -> %04x\n",
+-              phy, regnum, ocp_addr, val);
+-
+-      return 0;
+-}
+-
+-static const struct rtl8365mb_extint *
+-rtl8365mb_get_port_extint(struct realtek_priv *priv, int port)
+-{
+-      struct rtl8365mb *mb = priv->chip_data;
+-      int i;
+-
+-      for (i = 0; i < RTL8365MB_MAX_NUM_EXTINTS; i++) {
+-              const struct rtl8365mb_extint *extint =
+-                      &mb->chip_info->extints[i];
+-
+-              if (!extint->supported_interfaces)
+-                      continue;
+-
+-              if (extint->port == port)
+-                      return extint;
+-      }
+-
+-      return NULL;
+-}
+-
+-static enum dsa_tag_protocol
+-rtl8365mb_get_tag_protocol(struct dsa_switch *ds, int port,
+-                         enum dsa_tag_protocol mp)
+-{
+-      struct realtek_priv *priv = ds->priv;
+-      struct rtl8365mb_cpu *cpu;
+-      struct rtl8365mb *mb;
+-
+-      mb = priv->chip_data;
+-      cpu = &mb->cpu;
+-
+-      if (cpu->position == RTL8365MB_CPU_POS_BEFORE_CRC)
+-              return DSA_TAG_PROTO_RTL8_4T;
+-
+-      return DSA_TAG_PROTO_RTL8_4;
+-}
+-
+-static int rtl8365mb_ext_config_rgmii(struct realtek_priv *priv, int port,
+-                                    phy_interface_t interface)
+-{
+-      const struct rtl8365mb_extint *extint =
+-              rtl8365mb_get_port_extint(priv, port);
+-      struct dsa_switch *ds = &priv->ds;
+-      struct device_node *dn;
+-      struct dsa_port *dp;
+-      int tx_delay = 0;
+-      int rx_delay = 0;
+-      u32 val;
+-      int ret;
+-
+-      if (!extint)
+-              return -ENODEV;
+-
+-      dp = dsa_to_port(ds, port);
+-      dn = dp->dn;
+-
+-      /* Set the RGMII TX/RX delay
+-       *
+-       * The Realtek vendor driver indicates the following possible
+-       * configuration settings:
+-       *
+-       *   TX delay:
+-       *     0 = no delay, 1 = 2 ns delay
+-       *   RX delay:
+-       *     0 = no delay, 7 = maximum delay
+-       *     Each step is approximately 0.3 ns, so the maximum delay is about
+-       *     2.1 ns.
+-       *
+-       * The vendor driver also states that this must be configured *before*
+-       * forcing the external interface into a particular mode, which is done
+-       * in the rtl8365mb_phylink_mac_link_{up,down} functions.
+-       *
+-       * Only configure an RGMII TX (resp. RX) delay if the
+-       * tx-internal-delay-ps (resp. rx-internal-delay-ps) OF property is
+-       * specified. We ignore the detail of the RGMII interface mode
+-       * (RGMII_{RXID, TXID, etc.}), as this is considered to be a PHY-only
+-       * property.
+-       */
+-      if (!of_property_read_u32(dn, "tx-internal-delay-ps", &val)) {
+-              val = val / 1000; /* convert to ns */
+-
+-              if (val == 0 || val == 2)
+-                      tx_delay = val / 2;
+-              else
+-                      dev_warn(priv->dev,
+-                               "RGMII TX delay must be 0 or 2 ns\n");
+-      }
+-
+-      if (!of_property_read_u32(dn, "rx-internal-delay-ps", &val)) {
+-              val = DIV_ROUND_CLOSEST(val, 300); /* convert to 0.3 ns step */
+-
+-              if (val <= 7)
+-                      rx_delay = val;
+-              else
+-                      dev_warn(priv->dev,
+-                               "RGMII RX delay must be 0 to 2.1 ns\n");
+-      }
+-
+-      ret = regmap_update_bits(
+-              priv->map, RTL8365MB_EXT_RGMXF_REG(extint->id),
+-              RTL8365MB_EXT_RGMXF_TXDELAY_MASK |
+-                      RTL8365MB_EXT_RGMXF_RXDELAY_MASK,
+-              FIELD_PREP(RTL8365MB_EXT_RGMXF_TXDELAY_MASK, tx_delay) |
+-                      FIELD_PREP(RTL8365MB_EXT_RGMXF_RXDELAY_MASK, rx_delay));
+-      if (ret)
+-              return ret;
+-
+-      ret = regmap_update_bits(
+-              priv->map, RTL8365MB_DIGITAL_INTERFACE_SELECT_REG(extint->id),
+-              RTL8365MB_DIGITAL_INTERFACE_SELECT_MODE_MASK(extint->id),
+-              RTL8365MB_EXT_PORT_MODE_RGMII
+-                      << RTL8365MB_DIGITAL_INTERFACE_SELECT_MODE_OFFSET(
+-                                 extint->id));
+-      if (ret)
+-              return ret;
+-
+-      return 0;
+-}
+-
+-static int rtl8365mb_ext_config_forcemode(struct realtek_priv *priv, int port,
+-                                        bool link, int speed, int duplex,
+-                                        bool tx_pause, bool rx_pause)
+-{
+-      const struct rtl8365mb_extint *extint =
+-              rtl8365mb_get_port_extint(priv, port);
+-      u32 r_tx_pause;
+-      u32 r_rx_pause;
+-      u32 r_duplex;
+-      u32 r_speed;
+-      u32 r_link;
+-      int val;
+-      int ret;
+-
+-      if (!extint)
+-              return -ENODEV;
+-
+-      if (link) {
+-              /* Force the link up with the desired configuration */
+-              r_link = 1;
+-              r_rx_pause = rx_pause ? 1 : 0;
+-              r_tx_pause = tx_pause ? 1 : 0;
+-
+-              if (speed == SPEED_1000) {
+-                      r_speed = RTL8365MB_PORT_SPEED_1000M;
+-              } else if (speed == SPEED_100) {
+-                      r_speed = RTL8365MB_PORT_SPEED_100M;
+-              } else if (speed == SPEED_10) {
+-                      r_speed = RTL8365MB_PORT_SPEED_10M;
+-              } else {
+-                      dev_err(priv->dev, "unsupported port speed %s\n",
+-                              phy_speed_to_str(speed));
+-                      return -EINVAL;
+-              }
+-
+-              if (duplex == DUPLEX_FULL) {
+-                      r_duplex = 1;
+-              } else if (duplex == DUPLEX_HALF) {
+-                      r_duplex = 0;
+-              } else {
+-                      dev_err(priv->dev, "unsupported duplex %s\n",
+-                              phy_duplex_to_str(duplex));
+-                      return -EINVAL;
+-              }
+-      } else {
+-              /* Force the link down and reset any programmed configuration */
+-              r_link = 0;
+-              r_tx_pause = 0;
+-              r_rx_pause = 0;
+-              r_speed = 0;
+-              r_duplex = 0;
+-      }
+-
+-      val = FIELD_PREP(RTL8365MB_DIGITAL_INTERFACE_FORCE_EN_MASK, 1) |
+-            FIELD_PREP(RTL8365MB_DIGITAL_INTERFACE_FORCE_TXPAUSE_MASK,
+-                       r_tx_pause) |
+-            FIELD_PREP(RTL8365MB_DIGITAL_INTERFACE_FORCE_RXPAUSE_MASK,
+-                       r_rx_pause) |
+-            FIELD_PREP(RTL8365MB_DIGITAL_INTERFACE_FORCE_LINK_MASK, r_link) |
+-            FIELD_PREP(RTL8365MB_DIGITAL_INTERFACE_FORCE_DUPLEX_MASK,
+-                       r_duplex) |
+-            FIELD_PREP(RTL8365MB_DIGITAL_INTERFACE_FORCE_SPEED_MASK, r_speed);
+-      ret = regmap_write(priv->map,
+-                         RTL8365MB_DIGITAL_INTERFACE_FORCE_REG(extint->id),
+-                         val);
+-      if (ret)
+-              return ret;
+-
+-      return 0;
+-}
+-
+-static void rtl8365mb_phylink_get_caps(struct dsa_switch *ds, int port,
+-                                     struct phylink_config *config)
+-{
+-      const struct rtl8365mb_extint *extint =
+-              rtl8365mb_get_port_extint(ds->priv, port);
+-
+-      config->mac_capabilities = MAC_SYM_PAUSE | MAC_ASYM_PAUSE |
+-                                 MAC_10 | MAC_100 | MAC_1000FD;
+-
+-      if (!extint) {
+-              __set_bit(PHY_INTERFACE_MODE_INTERNAL,
+-                        config->supported_interfaces);
+-
+-              /* GMII is the default interface mode for phylib, so
+-               * we have to support it for ports with integrated PHY.
+-               */
+-              __set_bit(PHY_INTERFACE_MODE_GMII,
+-                        config->supported_interfaces);
+-              return;
+-      }
+-
+-      /* Populate according to the modes supported by _this driver_,
+-       * not necessarily the modes supported by the hardware, some of
+-       * which remain unimplemented.
+-       */
+-
+-      if (extint->supported_interfaces & RTL8365MB_PHY_INTERFACE_MODE_RGMII)
+-              phy_interface_set_rgmii(config->supported_interfaces);
+-}
+-
+-static void rtl8365mb_phylink_mac_config(struct phylink_config *config,
+-                                       unsigned int mode,
+-                                       const struct phylink_link_state *state)
+-{
+-      struct dsa_port *dp = dsa_phylink_to_port(config);
+-      struct realtek_priv *priv = dp->ds->priv;
+-      u8 port = dp->index;
+-      int ret;
+-
+-      if (mode != MLO_AN_PHY && mode != MLO_AN_FIXED) {
+-              dev_err(priv->dev,
+-                      "port %d supports only conventional PHY or fixed-link\n",
+-                      port);
+-              return;
+-      }
+-
+-      if (phy_interface_mode_is_rgmii(state->interface)) {
+-              ret = rtl8365mb_ext_config_rgmii(priv, port, state->interface);
+-              if (ret)
+-                      dev_err(priv->dev,
+-                              "failed to configure RGMII mode on port %d: %pe\n",
+-                              port, ERR_PTR(ret));
+-              return;
+-      }
+-
+-      /* TODO: Implement MII and RMII modes, which the RTL8365MB-VC also
+-       * supports
+-       */
+-}
+-
+-static void rtl8365mb_phylink_mac_link_down(struct phylink_config *config,
+-                                          unsigned int mode,
+-                                          phy_interface_t interface)
+-{
+-      struct dsa_port *dp = dsa_phylink_to_port(config);
+-      struct realtek_priv *priv = dp->ds->priv;
+-      struct rtl8365mb_port *p;
+-      struct rtl8365mb *mb;
+-      u8 port = dp->index;
+-      int ret;
+-
+-      mb = priv->chip_data;
+-      p = &mb->ports[port];
+-      cancel_delayed_work_sync(&p->mib_work);
+-
+-      if (phy_interface_mode_is_rgmii(interface)) {
+-              ret = rtl8365mb_ext_config_forcemode(priv, port, false, 0, 0,
+-                                                   false, false);
+-              if (ret)
+-                      dev_err(priv->dev,
+-                              "failed to reset forced mode on port %d: %pe\n",
+-                              port, ERR_PTR(ret));
+-
+-              return;
+-      }
+-}
+-
+-static void rtl8365mb_phylink_mac_link_up(struct phylink_config *config,
+-                                        struct phy_device *phydev,
+-                                        unsigned int mode,
+-                                        phy_interface_t interface,
+-                                        int speed, int duplex, bool tx_pause,
+-                                        bool rx_pause)
+-{
+-      struct dsa_port *dp = dsa_phylink_to_port(config);
+-      struct realtek_priv *priv = dp->ds->priv;
+-      struct rtl8365mb_port *p;
+-      struct rtl8365mb *mb;
+-      u8 port = dp->index;
+-      int ret;
+-
+-      mb = priv->chip_data;
+-      p = &mb->ports[port];
+-      schedule_delayed_work(&p->mib_work, 0);
+-
+-      if (phy_interface_mode_is_rgmii(interface)) {
+-              ret = rtl8365mb_ext_config_forcemode(priv, port, true, speed,
+-                                                   duplex, tx_pause,
+-                                                   rx_pause);
+-              if (ret)
+-                      dev_err(priv->dev,
+-                              "failed to force mode on port %d: %pe\n", port,
+-                              ERR_PTR(ret));
+-
+-              return;
+-      }
+-}
+-
+-static int rtl8365mb_port_change_mtu(struct dsa_switch *ds, int port,
+-                                   int new_mtu)
+-{
+-      struct realtek_priv *priv = ds->priv;
+-      int frame_size;
+-
+-      /* When a new MTU is set, DSA always sets the CPU port's MTU to the
+-       * largest MTU of the user ports. Because the switch only has a global
+-       * RX length register, only allowing CPU port here is enough.
+-       */
+-      if (!dsa_is_cpu_port(ds, port))
+-              return 0;
+-
+-      frame_size = new_mtu + VLAN_ETH_HLEN + ETH_FCS_LEN;
+-
+-      dev_dbg(priv->dev, "changing mtu to %d (frame size: %d)\n",
+-              new_mtu, frame_size);
+-
+-      return regmap_update_bits(priv->map, RTL8365MB_CFG0_MAX_LEN_REG,
+-                                RTL8365MB_CFG0_MAX_LEN_MASK,
+-                                FIELD_PREP(RTL8365MB_CFG0_MAX_LEN_MASK,
+-                                           frame_size));
+-}
+-
+-static int rtl8365mb_port_max_mtu(struct dsa_switch *ds, int port)
+-{
+-      return RTL8365MB_CFG0_MAX_LEN_MAX - VLAN_ETH_HLEN - ETH_FCS_LEN;
+-}
+-
+-static void rtl8365mb_port_stp_state_set(struct dsa_switch *ds, int port,
+-                                       u8 state)
+-{
+-      struct realtek_priv *priv = ds->priv;
+-      enum rtl8365mb_stp_state val;
+-      int msti = 0;
+-
+-      switch (state) {
+-      case BR_STATE_DISABLED:
+-              val = RTL8365MB_STP_STATE_DISABLED;
+-              break;
+-      case BR_STATE_BLOCKING:
+-      case BR_STATE_LISTENING:
+-              val = RTL8365MB_STP_STATE_BLOCKING;
+-              break;
+-      case BR_STATE_LEARNING:
+-              val = RTL8365MB_STP_STATE_LEARNING;
+-              break;
+-      case BR_STATE_FORWARDING:
+-              val = RTL8365MB_STP_STATE_FORWARDING;
+-              break;
+-      default:
+-              dev_err(priv->dev, "invalid STP state: %u\n", state);
+-              return;
+-      }
+-
+-      regmap_update_bits(priv->map, RTL8365MB_MSTI_CTRL_REG(msti, port),
+-                         RTL8365MB_MSTI_CTRL_PORT_STATE_MASK(port),
+-                         val << RTL8365MB_MSTI_CTRL_PORT_STATE_OFFSET(port));
+-}
+-
+-static int rtl8365mb_port_set_learning(struct realtek_priv *priv, int port,
+-                                     bool enable)
+-{
+-      /* Enable/disable learning by limiting the number of L2 addresses the
+-       * port can learn. Realtek documentation states that a limit of zero
+-       * disables learning. When enabling learning, set it to the chip's
+-       * maximum.
+-       */
+-      return regmap_write(priv->map, RTL8365MB_LUT_PORT_LEARN_LIMIT_REG(port),
+-                          enable ? RTL8365MB_LEARN_LIMIT_MAX : 0);
+-}
+-
+-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);
+-}
+-
+-static int rtl8365mb_mib_counter_read(struct realtek_priv *priv, int port,
+-                                    u32 offset, u32 length, u64 *mibvalue)
+-{
+-      u64 tmpvalue = 0;
+-      u32 val;
+-      int ret;
+-      int i;
+-
+-      /* The MIB address is an SRAM address. We request a particular address
+-       * and then poll the control register before reading the value from some
+-       * counter registers.
+-       */
+-      ret = regmap_write(priv->map, RTL8365MB_MIB_ADDRESS_REG,
+-                         RTL8365MB_MIB_ADDRESS(port, offset));
+-      if (ret)
+-              return ret;
+-
+-      /* Poll for completion */
+-      ret = regmap_read_poll_timeout(priv->map, RTL8365MB_MIB_CTRL0_REG, val,
+-                                     !(val & RTL8365MB_MIB_CTRL0_BUSY_MASK),
+-                                     10, 100);
+-      if (ret)
+-              return ret;
+-
+-      /* Presumably this indicates a MIB counter read failure */
+-      if (val & RTL8365MB_MIB_CTRL0_RESET_MASK)
+-              return -EIO;
+-
+-      /* There are four MIB counter registers each holding a 16 bit word of a
+-       * MIB counter. Depending on the offset, we should read from the upper
+-       * two or lower two registers. In case the MIB counter is 4 words, we
+-       * read from all four registers.
+-       */
+-      if (length == 4)
+-              offset = 3;
+-      else
+-              offset = (offset + 1) % 4;
+-
+-      /* Read the MIB counter 16 bits at a time */
+-      for (i = 0; i < length; i++) {
+-              ret = regmap_read(priv->map,
+-                                RTL8365MB_MIB_COUNTER_REG(offset - i), &val);
+-              if (ret)
+-                      return ret;
+-
+-              tmpvalue = ((tmpvalue) << 16) | (val & 0xFFFF);
+-      }
+-
+-      /* Only commit the result if no error occurred */
+-      *mibvalue = tmpvalue;
+-
+-      return 0;
+-}
+-
+-static void rtl8365mb_get_ethtool_stats(struct dsa_switch *ds, int port, u64 *data)
+-{
+-      struct realtek_priv *priv = ds->priv;
+-      struct rtl8365mb *mb;
+-      int ret;
+-      int i;
+-
+-      mb = priv->chip_data;
+-
+-      mutex_lock(&mb->mib_lock);
+-      for (i = 0; i < RTL8365MB_MIB_END; i++) {
+-              struct rtl8365mb_mib_counter *mib = &rtl8365mb_mib_counters[i];
+-
+-              ret = rtl8365mb_mib_counter_read(priv, port, mib->offset,
+-                                               mib->length, &data[i]);
+-              if (ret) {
+-                      dev_err(priv->dev,
+-                              "failed to read port %d counters: %pe\n", port,
+-                              ERR_PTR(ret));
+-                      break;
+-              }
+-      }
+-      mutex_unlock(&mb->mib_lock);
+-}
+-
+-static void rtl8365mb_get_strings(struct dsa_switch *ds, int port, u32 stringset, u8 *data)
+-{
+-      int i;
+-
+-      if (stringset != ETH_SS_STATS)
+-              return;
+-
+-      for (i = 0; i < RTL8365MB_MIB_END; i++) {
+-              struct rtl8365mb_mib_counter *mib = &rtl8365mb_mib_counters[i];
+-              ethtool_puts(&data, mib->name);
+-      }
+-}
+-
+-static int rtl8365mb_get_sset_count(struct dsa_switch *ds, int port, int sset)
+-{
+-      if (sset != ETH_SS_STATS)
+-              return -EOPNOTSUPP;
+-
+-      return RTL8365MB_MIB_END;
+-}
+-
+-static void rtl8365mb_get_phy_stats(struct dsa_switch *ds, int port,
+-                                  struct ethtool_eth_phy_stats *phy_stats)
+-{
+-      struct realtek_priv *priv = ds->priv;
+-      struct rtl8365mb_mib_counter *mib;
+-      struct rtl8365mb *mb;
+-
+-      mb = priv->chip_data;
+-      mib = &rtl8365mb_mib_counters[RTL8365MB_MIB_dot3StatsSymbolErrors];
+-
+-      mutex_lock(&mb->mib_lock);
+-      rtl8365mb_mib_counter_read(priv, port, mib->offset, mib->length,
+-                                 &phy_stats->SymbolErrorDuringCarrier);
+-      mutex_unlock(&mb->mib_lock);
+-}
+-
+-static void rtl8365mb_get_mac_stats(struct dsa_switch *ds, int port,
+-                                  struct ethtool_eth_mac_stats *mac_stats)
+-{
+-      u64 cnt[RTL8365MB_MIB_END] = {
+-              [RTL8365MB_MIB_ifOutOctets] = 1,
+-              [RTL8365MB_MIB_ifOutUcastPkts] = 1,
+-              [RTL8365MB_MIB_ifOutMulticastPkts] = 1,
+-              [RTL8365MB_MIB_ifOutBroadcastPkts] = 1,
+-              [RTL8365MB_MIB_dot3OutPauseFrames] = 1,
+-              [RTL8365MB_MIB_ifOutDiscards] = 1,
+-              [RTL8365MB_MIB_ifInOctets] = 1,
+-              [RTL8365MB_MIB_ifInUcastPkts] = 1,
+-              [RTL8365MB_MIB_ifInMulticastPkts] = 1,
+-              [RTL8365MB_MIB_ifInBroadcastPkts] = 1,
+-              [RTL8365MB_MIB_dot3InPauseFrames] = 1,
+-              [RTL8365MB_MIB_dot3StatsSingleCollisionFrames] = 1,
+-              [RTL8365MB_MIB_dot3StatsMultipleCollisionFrames] = 1,
+-              [RTL8365MB_MIB_dot3StatsFCSErrors] = 1,
+-              [RTL8365MB_MIB_dot3StatsDeferredTransmissions] = 1,
+-              [RTL8365MB_MIB_dot3StatsLateCollisions] = 1,
+-              [RTL8365MB_MIB_dot3StatsExcessiveCollisions] = 1,
+-
+-      };
+-      struct realtek_priv *priv = ds->priv;
+-      struct rtl8365mb *mb;
+-      int ret;
+-      int i;
+-
+-      mb = priv->chip_data;
+-
+-      mutex_lock(&mb->mib_lock);
+-      for (i = 0; i < RTL8365MB_MIB_END; i++) {
+-              struct rtl8365mb_mib_counter *mib = &rtl8365mb_mib_counters[i];
+-
+-              /* Only fetch required MIB counters (marked = 1 above) */
+-              if (!cnt[i])
+-                      continue;
+-
+-              ret = rtl8365mb_mib_counter_read(priv, port, mib->offset,
+-                                               mib->length, &cnt[i]);
+-              if (ret)
+-                      break;
+-      }
+-      mutex_unlock(&mb->mib_lock);
+-
+-      /* The RTL8365MB-VC exposes MIB objects, which we have to translate into
+-       * IEEE 802.3 Managed Objects. This is not always completely faithful,
+-       * but we try out best. See RFC 3635 for a detailed treatment of the
+-       * subject.
+-       */
+-
+-      mac_stats->FramesTransmittedOK = cnt[RTL8365MB_MIB_ifOutUcastPkts] +
+-                                       cnt[RTL8365MB_MIB_ifOutMulticastPkts] +
+-                                       cnt[RTL8365MB_MIB_ifOutBroadcastPkts] +
+-                                       cnt[RTL8365MB_MIB_dot3OutPauseFrames] -
+-                                       cnt[RTL8365MB_MIB_ifOutDiscards];
+-      mac_stats->SingleCollisionFrames =
+-              cnt[RTL8365MB_MIB_dot3StatsSingleCollisionFrames];
+-      mac_stats->MultipleCollisionFrames =
+-              cnt[RTL8365MB_MIB_dot3StatsMultipleCollisionFrames];
+-      mac_stats->FramesReceivedOK = cnt[RTL8365MB_MIB_ifInUcastPkts] +
+-                                    cnt[RTL8365MB_MIB_ifInMulticastPkts] +
+-                                    cnt[RTL8365MB_MIB_ifInBroadcastPkts] +
+-                                    cnt[RTL8365MB_MIB_dot3InPauseFrames];
+-      mac_stats->FrameCheckSequenceErrors =
+-              cnt[RTL8365MB_MIB_dot3StatsFCSErrors];
+-      mac_stats->OctetsTransmittedOK = cnt[RTL8365MB_MIB_ifOutOctets] -
+-                                       18 * mac_stats->FramesTransmittedOK;
+-      mac_stats->FramesWithDeferredXmissions =
+-              cnt[RTL8365MB_MIB_dot3StatsDeferredTransmissions];
+-      mac_stats->LateCollisions = cnt[RTL8365MB_MIB_dot3StatsLateCollisions];
+-      mac_stats->FramesAbortedDueToXSColls =
+-              cnt[RTL8365MB_MIB_dot3StatsExcessiveCollisions];
+-      mac_stats->OctetsReceivedOK = cnt[RTL8365MB_MIB_ifInOctets] -
+-                                    18 * mac_stats->FramesReceivedOK;
+-      mac_stats->MulticastFramesXmittedOK =
+-              cnt[RTL8365MB_MIB_ifOutMulticastPkts];
+-      mac_stats->BroadcastFramesXmittedOK =
+-              cnt[RTL8365MB_MIB_ifOutBroadcastPkts];
+-      mac_stats->MulticastFramesReceivedOK =
+-              cnt[RTL8365MB_MIB_ifInMulticastPkts];
+-      mac_stats->BroadcastFramesReceivedOK =
+-              cnt[RTL8365MB_MIB_ifInBroadcastPkts];
+-}
+-
+-static void rtl8365mb_get_ctrl_stats(struct dsa_switch *ds, int port,
+-                                   struct ethtool_eth_ctrl_stats *ctrl_stats)
+-{
+-      struct realtek_priv *priv = ds->priv;
+-      struct rtl8365mb_mib_counter *mib;
+-      struct rtl8365mb *mb;
+-
+-      mb = priv->chip_data;
+-      mib = &rtl8365mb_mib_counters[RTL8365MB_MIB_dot3ControlInUnknownOpcodes];
+-
+-      mutex_lock(&mb->mib_lock);
+-      rtl8365mb_mib_counter_read(priv, port, mib->offset, mib->length,
+-                                 &ctrl_stats->UnsupportedOpcodesReceived);
+-      mutex_unlock(&mb->mib_lock);
+-}
+-
+-static void rtl8365mb_stats_update(struct realtek_priv *priv, int port)
+-{
+-      u64 cnt[RTL8365MB_MIB_END] = {
+-              [RTL8365MB_MIB_ifOutOctets] = 1,
+-              [RTL8365MB_MIB_ifOutUcastPkts] = 1,
+-              [RTL8365MB_MIB_ifOutMulticastPkts] = 1,
+-              [RTL8365MB_MIB_ifOutBroadcastPkts] = 1,
+-              [RTL8365MB_MIB_ifOutDiscards] = 1,
+-              [RTL8365MB_MIB_ifInOctets] = 1,
+-              [RTL8365MB_MIB_ifInUcastPkts] = 1,
+-              [RTL8365MB_MIB_ifInMulticastPkts] = 1,
+-              [RTL8365MB_MIB_ifInBroadcastPkts] = 1,
+-              [RTL8365MB_MIB_etherStatsDropEvents] = 1,
+-              [RTL8365MB_MIB_etherStatsCollisions] = 1,
+-              [RTL8365MB_MIB_etherStatsFragments] = 1,
+-              [RTL8365MB_MIB_etherStatsJabbers] = 1,
+-              [RTL8365MB_MIB_dot3StatsFCSErrors] = 1,
+-              [RTL8365MB_MIB_dot3StatsLateCollisions] = 1,
+-      };
+-      struct rtl8365mb *mb = priv->chip_data;
+-      struct rtnl_link_stats64 *stats;
+-      int ret;
+-      int i;
+-
+-      stats = &mb->ports[port].stats;
+-
+-      mutex_lock(&mb->mib_lock);
+-      for (i = 0; i < RTL8365MB_MIB_END; i++) {
+-              struct rtl8365mb_mib_counter *c = &rtl8365mb_mib_counters[i];
+-
+-              /* Only fetch required MIB counters (marked = 1 above) */
+-              if (!cnt[i])
+-                      continue;
+-
+-              ret = rtl8365mb_mib_counter_read(priv, port, c->offset,
+-                                               c->length, &cnt[i]);
+-              if (ret)
+-                      break;
+-      }
+-      mutex_unlock(&mb->mib_lock);
+-
+-      /* Don't update statistics if there was an error reading the counters */
+-      if (ret)
+-              return;
+-
+-      spin_lock(&mb->ports[port].stats_lock);
+-
+-      stats->rx_packets = cnt[RTL8365MB_MIB_ifInUcastPkts] +
+-                          cnt[RTL8365MB_MIB_ifInMulticastPkts] +
+-                          cnt[RTL8365MB_MIB_ifInBroadcastPkts];
+-
+-      stats->tx_packets = cnt[RTL8365MB_MIB_ifOutUcastPkts] +
+-                          cnt[RTL8365MB_MIB_ifOutMulticastPkts] +
+-                          cnt[RTL8365MB_MIB_ifOutBroadcastPkts];
+-
+-      /* if{In,Out}Octets includes FCS - remove it */
+-      stats->rx_bytes = cnt[RTL8365MB_MIB_ifInOctets] - 4 * stats->rx_packets;
+-      stats->tx_bytes =
+-              cnt[RTL8365MB_MIB_ifOutOctets] - 4 * stats->tx_packets;
+-
+-      stats->rx_dropped = cnt[RTL8365MB_MIB_etherStatsDropEvents];
+-      stats->tx_dropped = cnt[RTL8365MB_MIB_ifOutDiscards];
+-
+-      stats->multicast = cnt[RTL8365MB_MIB_ifInMulticastPkts];
+-      stats->collisions = cnt[RTL8365MB_MIB_etherStatsCollisions];
+-
+-      stats->rx_length_errors = cnt[RTL8365MB_MIB_etherStatsFragments] +
+-                                cnt[RTL8365MB_MIB_etherStatsJabbers];
+-      stats->rx_crc_errors = cnt[RTL8365MB_MIB_dot3StatsFCSErrors];
+-      stats->rx_errors = stats->rx_length_errors + stats->rx_crc_errors;
+-
+-      stats->tx_aborted_errors = cnt[RTL8365MB_MIB_ifOutDiscards];
+-      stats->tx_window_errors = cnt[RTL8365MB_MIB_dot3StatsLateCollisions];
+-      stats->tx_errors = stats->tx_aborted_errors + stats->tx_window_errors;
+-
+-      spin_unlock(&mb->ports[port].stats_lock);
+-}
+-
+-static void rtl8365mb_stats_poll(struct work_struct *work)
+-{
+-      struct rtl8365mb_port *p = container_of(to_delayed_work(work),
+-                                              struct rtl8365mb_port,
+-                                              mib_work);
+-      struct realtek_priv *priv = p->priv;
+-
+-      rtl8365mb_stats_update(priv, p->index);
+-
+-      schedule_delayed_work(&p->mib_work, RTL8365MB_STATS_INTERVAL_JIFFIES);
+-}
+-
+-static void rtl8365mb_get_stats64(struct dsa_switch *ds, int port,
+-                                struct rtnl_link_stats64 *s)
+-{
+-      struct realtek_priv *priv = ds->priv;
+-      struct rtl8365mb_port *p;
+-      struct rtl8365mb *mb;
+-
+-      mb = priv->chip_data;
+-      p = &mb->ports[port];
+-
+-      spin_lock(&p->stats_lock);
+-      memcpy(s, &p->stats, sizeof(*s));
+-      spin_unlock(&p->stats_lock);
+-}
+-
+-static void rtl8365mb_stats_setup(struct realtek_priv *priv)
+-{
+-      struct rtl8365mb *mb = priv->chip_data;
+-      struct dsa_switch *ds = &priv->ds;
+-      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);
+-
+-      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);
+-
+-              /* This work polls the MIB counters and keeps the stats64 data
+-               * up-to-date.
+-               */
+-              INIT_DELAYED_WORK(&p->mib_work, rtl8365mb_stats_poll);
+-      }
+-}
+-
+-static void rtl8365mb_stats_teardown(struct realtek_priv *priv)
+-{
+-      struct rtl8365mb *mb = priv->chip_data;
+-      struct dsa_switch *ds = &priv->ds;
+-      struct dsa_port *dp;
+-
+-      dsa_switch_for_each_available_port(dp, ds) {
+-              struct rtl8365mb_port *p = &mb->ports[dp->index];
+-
+-              cancel_delayed_work_sync(&p->mib_work);
+-      }
+-}
+-
+-static int rtl8365mb_get_and_clear_status_reg(struct realtek_priv *priv, u32 reg,
+-                                            u32 *val)
+-{
+-      int ret;
+-
+-      ret = regmap_read(priv->map, reg, val);
+-      if (ret)
+-              return ret;
+-
+-      return regmap_write(priv->map, reg, *val);
+-}
+-
+-static irqreturn_t rtl8365mb_irq(int irq, void *data)
+-{
+-      struct realtek_priv *priv = data;
+-      unsigned long line_changes = 0;
+-      u32 stat;
+-      int line;
+-      int ret;
+-
+-      ret = rtl8365mb_get_and_clear_status_reg(priv, RTL8365MB_INTR_STATUS_REG,
+-                                               &stat);
+-      if (ret)
+-              goto out_error;
+-
+-      if (stat & RTL8365MB_INTR_LINK_CHANGE_MASK) {
+-              u32 linkdown_ind;
+-              u32 linkup_ind;
+-              u32 val;
+-
+-              ret = rtl8365mb_get_and_clear_status_reg(
+-                      priv, RTL8365MB_PORT_LINKUP_IND_REG, &val);
+-              if (ret)
+-                      goto out_error;
+-
+-              linkup_ind = FIELD_GET(RTL8365MB_PORT_LINKUP_IND_MASK, val);
+-
+-              ret = rtl8365mb_get_and_clear_status_reg(
+-                      priv, RTL8365MB_PORT_LINKDOWN_IND_REG, &val);
+-              if (ret)
+-                      goto out_error;
+-
+-              linkdown_ind = FIELD_GET(RTL8365MB_PORT_LINKDOWN_IND_MASK, val);
+-
+-              line_changes = linkup_ind | linkdown_ind;
+-      }
+-
+-      if (!line_changes)
+-              goto out_none;
+-
+-      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);
+-      }
+-
+-      return IRQ_HANDLED;
+-
+-out_error:
+-      dev_err(priv->dev, "failed to read interrupt status: %pe\n",
+-              ERR_PTR(ret));
+-
+-out_none:
+-      return IRQ_NONE;
+-}
+-
+-static struct irq_chip rtl8365mb_irq_chip = {
+-      .name = "rtl8365mb",
+-      /* The hardware doesn't support masking IRQs on a per-port basis */
+-};
+-
+-static int rtl8365mb_irq_map(struct irq_domain *domain, unsigned int irq,
+-                           irq_hw_number_t hwirq)
+-{
+-      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;
+-}
+-
+-static void rtl8365mb_irq_unmap(struct irq_domain *d, unsigned int irq)
+-{
+-      irq_set_nested_thread(irq, 0);
+-      irq_set_chip_and_handler(irq, NULL, NULL);
+-      irq_set_chip_data(irq, NULL);
+-}
+-
+-static const struct irq_domain_ops rtl8365mb_irqdomain_ops = {
+-      .map = rtl8365mb_irq_map,
+-      .unmap = rtl8365mb_irq_unmap,
+-      .xlate = irq_domain_xlate_onecell,
+-};
+-
+-static int rtl8365mb_set_irq_enable(struct realtek_priv *priv, bool enable)
+-{
+-      return regmap_update_bits(priv->map, RTL8365MB_INTR_CTRL_REG,
+-                                RTL8365MB_INTR_LINK_CHANGE_MASK,
+-                                FIELD_PREP(RTL8365MB_INTR_LINK_CHANGE_MASK,
+-                                           enable ? 1 : 0));
+-}
+-
+-static int rtl8365mb_irq_enable(struct realtek_priv *priv)
+-{
+-      return rtl8365mb_set_irq_enable(priv, true);
+-}
+-
+-static int rtl8365mb_irq_disable(struct realtek_priv *priv)
+-{
+-      return rtl8365mb_set_irq_enable(priv, false);
+-}
+-
+-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;
+-
+-      intc = of_get_child_by_name(priv->dev->of_node, "interrupt-controller");
+-      if (!intc) {
+-              dev_err(priv->dev, "missing child interrupt-controller node\n");
+-              return -EINVAL;
+-      }
+-
+-      /* rtl8365mb IRQs cascade off this one */
+-      irq = of_irq_get(intc, 0);
+-      if (irq <= 0) {
+-              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;
+-      }
+-
+-      /* 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) {
+-              dev_err(priv->dev, "failed to add irq domain\n");
+-              ret = -ENOMEM;
+-              goto out_put_node;
+-      }
+-
+-      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");
+-                      ret = -EINVAL;
+-                      goto out_remove_irqdomain;
+-              }
+-
+-              irq_set_parent(virq, irq);
+-      }
+-
+-      /* Configure chip interrupt signal polarity */
+-      irq_trig = irq_get_trigger_type(irq);
+-      switch (irq_trig) {
+-      case IRQF_TRIGGER_RISING:
+-      case IRQF_TRIGGER_HIGH:
+-              val = RTL8365MB_INTR_POLARITY_HIGH;
+-              break;
+-      case IRQF_TRIGGER_FALLING:
+-      case IRQF_TRIGGER_LOW:
+-              val = RTL8365MB_INTR_POLARITY_LOW;
+-              break;
+-      default:
+-              dev_err(priv->dev, "unsupported irq trigger type %u\n",
+-                      irq_trig);
+-              ret = -EINVAL;
+-              goto out_remove_irqdomain;
+-      }
+-
+-      ret = regmap_update_bits(priv->map, RTL8365MB_INTR_POLARITY_REG,
+-                               RTL8365MB_INTR_POLARITY_MASK,
+-                               FIELD_PREP(RTL8365MB_INTR_POLARITY_MASK, val));
+-      if (ret)
+-              goto out_remove_irqdomain;
+-
+-      /* Disable the interrupt in case the chip has it enabled on reset */
+-      ret = rtl8365mb_irq_disable(priv);
+-      if (ret)
+-              goto out_remove_irqdomain;
+-
+-      /* Clear the interrupt status register */
+-      ret = regmap_write(priv->map, RTL8365MB_INTR_STATUS_REG,
+-                         RTL8365MB_INTR_ALL_MASK);
+-      if (ret)
+-              goto out_remove_irqdomain;
+-
+-      ret = request_threaded_irq(irq, NULL, rtl8365mb_irq, IRQF_ONESHOT,
+-                                 "rtl8365mb", priv);
+-      if (ret) {
+-              dev_err(priv->dev, "failed to request irq: %pe\n",
+-                      ERR_PTR(ret));
+-              goto out_remove_irqdomain;
+-      }
+-
+-      ret = rtl8365mb_irq_enable(priv);
+-      if (ret)
+-              goto out_free_irq;
+-
+-      of_node_put(intc);
+-
+-      return 0;
+-
+-out_free_irq:
+-      free_irq(mb->irq, priv);
+-
+-out_remove_irqdomain:
+-      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;
+-}
+-
+-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;
+-
+-      if (mb->irq) {
+-              free_irq(mb->irq, priv);
+-              mb->irq = 0;
+-      }
+-
+-      if (priv->irqdomain) {
+-              /* 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);
+-              priv->irqdomain = NULL;
+-      }
+-}
+-
+-static int rtl8365mb_cpu_config(struct realtek_priv *priv)
+-{
+-      struct rtl8365mb *mb = priv->chip_data;
+-      struct rtl8365mb_cpu *cpu = &mb->cpu;
+-      u32 val;
+-      int ret;
+-
+-      ret = regmap_update_bits(priv->map, RTL8365MB_CPU_PORT_MASK_REG,
+-                               RTL8365MB_CPU_PORT_MASK_MASK,
+-                               FIELD_PREP(RTL8365MB_CPU_PORT_MASK_MASK,
+-                                          cpu->mask));
+-      if (ret)
+-              return ret;
+-
+-      val = FIELD_PREP(RTL8365MB_CPU_CTRL_EN_MASK, cpu->enable ? 1 : 0) |
+-            FIELD_PREP(RTL8365MB_CPU_CTRL_INSERTMODE_MASK, cpu->insert) |
+-            FIELD_PREP(RTL8365MB_CPU_CTRL_TAG_POSITION_MASK, cpu->position) |
+-            FIELD_PREP(RTL8365MB_CPU_CTRL_RXBYTECOUNT_MASK, cpu->rx_length) |
+-            FIELD_PREP(RTL8365MB_CPU_CTRL_TAG_FORMAT_MASK, cpu->format) |
+-            FIELD_PREP(RTL8365MB_CPU_CTRL_TRAP_PORT_MASK, cpu->trap_port & 0x7) |
+-            FIELD_PREP(RTL8365MB_CPU_CTRL_TRAP_PORT_EXT_MASK,
+-                       cpu->trap_port >> 3 & 0x1);
+-      ret = regmap_write(priv->map, RTL8365MB_CPU_CTRL_REG, val);
+-      if (ret)
+-              return ret;
+-
+-      return 0;
+-}
+-
+-static int rtl8365mb_change_tag_protocol(struct dsa_switch *ds,
+-                                       enum dsa_tag_protocol proto)
+-{
+-      struct realtek_priv *priv = ds->priv;
+-      struct rtl8365mb_cpu *cpu;
+-      struct rtl8365mb *mb;
+-
+-      mb = priv->chip_data;
+-      cpu = &mb->cpu;
+-
+-      switch (proto) {
+-      case DSA_TAG_PROTO_RTL8_4:
+-              cpu->format = RTL8365MB_CPU_FORMAT_8BYTES;
+-              cpu->position = RTL8365MB_CPU_POS_AFTER_SA;
+-              break;
+-      case DSA_TAG_PROTO_RTL8_4T:
+-              cpu->format = RTL8365MB_CPU_FORMAT_8BYTES;
+-              cpu->position = RTL8365MB_CPU_POS_BEFORE_CRC;
+-              break;
+-      /* The switch also supports a 4-byte format, similar to rtl4a but with
+-       * the same 0x04 8-bit version and probably 8-bit port source/dest.
+-       * There is no public doc about it. Not supported yet and it will probably
+-       * never be.
+-       */
+-      default:
+-              return -EPROTONOSUPPORT;
+-      }
+-
+-      return rtl8365mb_cpu_config(priv);
+-}
+-
+-static int rtl8365mb_switch_init(struct realtek_priv *priv)
+-{
+-      struct rtl8365mb *mb = priv->chip_data;
+-      const struct rtl8365mb_chip_info *ci;
+-      int ret;
+-      int i;
+-
+-      ci = mb->chip_info;
+-
+-      /* Do any chip-specific init jam before getting to the common stuff */
+-      if (ci->jam_table) {
+-              for (i = 0; i < ci->jam_size; i++) {
+-                      ret = regmap_write(priv->map, ci->jam_table[i].reg,
+-                                         ci->jam_table[i].val);
+-                      if (ret)
+-                              return ret;
+-              }
+-      }
+-
+-      /* Common init jam */
+-      for (i = 0; i < ARRAY_SIZE(rtl8365mb_init_jam_common); i++) {
+-              ret = regmap_write(priv->map, rtl8365mb_init_jam_common[i].reg,
+-                                 rtl8365mb_init_jam_common[i].val);
+-              if (ret)
+-                      return ret;
+-      }
+-
+-      return 0;
+-}
+-
+-static int rtl8365mb_reset_chip(struct realtek_priv *priv)
+-{
+-      u32 val;
+-
+-      priv->write_reg_noack(priv, RTL8365MB_CHIP_RESET_REG,
+-                            FIELD_PREP(RTL8365MB_CHIP_RESET_HW_MASK, 1));
+-
+-      /* Realtek documentation says the chip needs 1 second to reset. Sleep
+-       * for 100 ms before accessing any registers to prevent ACK timeouts.
+-       */
+-      msleep(100);
+-      return regmap_read_poll_timeout(priv->map, RTL8365MB_CHIP_RESET_REG, val,
+-                                      !(val & RTL8365MB_CHIP_RESET_HW_MASK),
+-                                      20000, 1e6);
+-}
+-
+-static int rtl8365mb_setup(struct dsa_switch *ds)
+-{
+-      struct realtek_priv *priv = ds->priv;
+-      struct rtl8365mb_cpu *cpu;
+-      u32 downports_mask = 0;
+-      u32 upports_mask = 0;
+-      struct rtl8365mb *mb;
+-      struct dsa_port *dp;
+-      int ret;
+-
+-      mb = priv->chip_data;
+-      cpu = &mb->cpu;
+-
+-      ret = rtl8365mb_reset_chip(priv);
+-      if (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: %pe\n",
+-                      ERR_PTR(ret));
+-              goto out_error;
+-      }
+-
+-      /* Set up cascading IRQs */
+-      ret = rtl8365mb_irq_setup(priv);
+-      if (ret == -EPROBE_DEFER)
+-              return ret;
+-      else if (ret)
+-              dev_info(priv->dev, "no interrupt support\n");
+-
+-      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_port_is_dsa(dp)) {
+-                      dev_err(priv->dev, "Cascading (DSA link) not supported\n");
+-                      ret = -EOPNOTSUPP;
+-                      goto out_teardown_irq;
+-              }
+-      }
+-
+-      /* Start with all ports blocked, including unused ports */
+-      dsa_switch_for_each_port(dp, ds) {
+-              struct rtl8365mb_port *p = &mb->ports[dp->index];
+-
+-              /* 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);
+-
+-              /* Start with all port completely isolated */
+-              ret = rtl8365mb_port_set_isolation(priv, dp->index, 0);
+-              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;
+-
+-              /* 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);
+-      }
+-
+-      /* 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, dp->index,
+-                                                 upports_mask);
+-              if (ret)
+-                      goto out_teardown_irq;
+-
+-              /* 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;
+-      }
+-
+-      cpu->mask = upports_mask;
+-      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;
+-
+-      ret = rtl8365mb_port_change_mtu(ds, cpu->trap_port, ETH_DATA_LEN);
+-      if (ret)
+-              goto out_teardown_irq;
+-
+-      ret = rtl83xx_setup_user_mdio(ds);
+-      if (ret) {
+-              dev_err(priv->dev, "could not set up MDIO bus\n");
+-              goto out_teardown_irq;
+-      }
+-
+-      /* Start statistics counter polling */
+-      rtl8365mb_stats_setup(priv);
+-
+-      return 0;
+-
+-out_teardown_irq:
+-      rtl8365mb_irq_teardown(priv);
+-
+-out_error:
+-      return ret;
+-}
+-
+-static void rtl8365mb_teardown(struct dsa_switch *ds)
+-{
+-      struct realtek_priv *priv = ds->priv;
+-
+-      rtl8365mb_stats_teardown(priv);
+-      rtl8365mb_irq_teardown(priv);
+-}
+-
+-static int rtl8365mb_get_chip_id_and_ver(struct regmap *map, u32 *id, u32 *ver)
+-{
+-      int ret;
+-
+-      /* For some reason we have to write a magic value to an arbitrary
+-       * register whenever accessing the chip ID/version registers.
+-       */
+-      ret = regmap_write(map, RTL8365MB_MAGIC_REG, RTL8365MB_MAGIC_VALUE);
+-      if (ret)
+-              return ret;
+-
+-      ret = regmap_read(map, RTL8365MB_CHIP_ID_REG, id);
+-      if (ret)
+-              return ret;
+-
+-      ret = regmap_read(map, RTL8365MB_CHIP_VER_REG, ver);
+-      if (ret)
+-              return ret;
+-
+-      /* Reset magic register */
+-      ret = regmap_write(map, RTL8365MB_MAGIC_REG, 0);
+-      if (ret)
+-              return ret;
+-
+-      return 0;
+-}
+-
+-static int rtl8365mb_detect(struct realtek_priv *priv)
+-{
+-      struct rtl8365mb *mb = priv->chip_data;
+-      u32 chip_id;
+-      u32 chip_ver;
+-      int ret;
+-      int i;
+-
+-      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: %pe\n",
+-                      ERR_PTR(ret));
+-              return ret;
+-      }
+-
+-      for (i = 0; i < ARRAY_SIZE(rtl8365mb_chip_infos); i++) {
+-              const struct rtl8365mb_chip_info *ci = &rtl8365mb_chip_infos[i];
+-
+-              if (ci->chip_id == chip_id && ci->chip_ver == chip_ver) {
+-                      mb->chip_info = ci;
+-                      break;
+-              }
+-      }
+-
+-      if (!mb->chip_info) {
+-              dev_err(priv->dev,
+-                      "unrecognized switch (id=0x%04x, ver=0x%04x)", chip_id,
+-                      chip_ver);
+-              return -ENODEV;
+-      }
+-
+-      dev_info(priv->dev, "found an %s switch\n", mb->chip_info->name);
+-
+-      priv->num_ports = RTL8365MB_MAX_NUM_PORTS;
+-      mb->priv = priv;
+-      mb->cpu.trap_port = RTL8365MB_MAX_NUM_PORTS;
+-      mb->cpu.insert = RTL8365MB_CPU_INSERT_TO_ALL;
+-      mb->cpu.position = RTL8365MB_CPU_POS_AFTER_SA;
+-      mb->cpu.rx_length = RTL8365MB_CPU_RXLEN_64BYTES;
+-      mb->cpu.format = RTL8365MB_CPU_FORMAT_8BYTES;
+-
+-      return 0;
+-}
+-
+-static const struct phylink_mac_ops rtl8365mb_phylink_mac_ops = {
+-      .mac_config = rtl8365mb_phylink_mac_config,
+-      .mac_link_down = rtl8365mb_phylink_mac_link_down,
+-      .mac_link_up = rtl8365mb_phylink_mac_link_up,
+-};
+-
+-static const struct dsa_switch_ops rtl8365mb_switch_ops = {
+-      .get_tag_protocol = rtl8365mb_get_tag_protocol,
+-      .change_tag_protocol = rtl8365mb_change_tag_protocol,
+-      .setup = rtl8365mb_setup,
+-      .teardown = rtl8365mb_teardown,
+-      .phylink_get_caps = rtl8365mb_phylink_get_caps,
+-      .port_stp_state_set = rtl8365mb_port_stp_state_set,
+-      .get_strings = rtl8365mb_get_strings,
+-      .get_ethtool_stats = rtl8365mb_get_ethtool_stats,
+-      .get_sset_count = rtl8365mb_get_sset_count,
+-      .get_eth_phy_stats = rtl8365mb_get_phy_stats,
+-      .get_eth_mac_stats = rtl8365mb_get_mac_stats,
+-      .get_eth_ctrl_stats = rtl8365mb_get_ctrl_stats,
+-      .get_stats64 = rtl8365mb_get_stats64,
+-      .port_change_mtu = rtl8365mb_port_change_mtu,
+-      .port_max_mtu = rtl8365mb_port_max_mtu,
+-};
+-
+-static const struct realtek_ops rtl8365mb_ops = {
+-      .detect = rtl8365mb_detect,
+-      .phy_read = rtl8365mb_phy_read,
+-      .phy_write = rtl8365mb_phy_write,
+-};
+-
+-const struct realtek_variant rtl8365mb_variant = {
+-      .ds_ops = &rtl8365mb_switch_ops,
+-      .ops = &rtl8365mb_ops,
+-      .phylink_mac_ops = &rtl8365mb_phylink_mac_ops,
+-      .clk_delay = 10,
+-      .cmd_read = 0xb9,
+-      .cmd_write = 0xb8,
+-      .chip_data_sz = sizeof(struct rtl8365mb),
+-};
+-
+-static const struct of_device_id rtl8365mb_of_match[] = {
+-      { .compatible = "realtek,rtl8365mb", .data = &rtl8365mb_variant, },
+-      { /* sentinel */ },
+-};
+-MODULE_DEVICE_TABLE(of, rtl8365mb_of_match);
+-
+-static struct platform_driver rtl8365mb_smi_driver = {
+-      .driver = {
+-              .name = "rtl8365mb-smi",
+-              .of_match_table = rtl8365mb_of_match,
+-      },
+-      .probe  = realtek_smi_probe,
+-      .remove_new = realtek_smi_remove,
+-      .shutdown = realtek_smi_shutdown,
+-};
+-
+-static struct mdio_driver rtl8365mb_mdio_driver = {
+-      .mdiodrv.driver = {
+-              .name = "rtl8365mb-mdio",
+-              .of_match_table = rtl8365mb_of_match,
+-      },
+-      .probe  = realtek_mdio_probe,
+-      .remove = realtek_mdio_remove,
+-      .shutdown = realtek_mdio_shutdown,
+-};
+-
+-static int rtl8365mb_init(void)
+-{
+-      int ret;
+-
+-      ret = realtek_mdio_driver_register(&rtl8365mb_mdio_driver);
+-      if (ret)
+-              return ret;
+-
+-      ret = realtek_smi_driver_register(&rtl8365mb_smi_driver);
+-      if (ret) {
+-              realtek_mdio_driver_unregister(&rtl8365mb_mdio_driver);
+-              return ret;
+-      }
+-
+-      return 0;
+-}
+-module_init(rtl8365mb_init);
+-
+-static void __exit rtl8365mb_exit(void)
+-{
+-      realtek_smi_driver_unregister(&rtl8365mb_smi_driver);
+-      realtek_mdio_driver_unregister(&rtl8365mb_mdio_driver);
+-}
+-module_exit(rtl8365mb_exit);
+-
+-MODULE_AUTHOR("Alvin Šipraga <alsi@bang-olufsen.dk>");
+-MODULE_DESCRIPTION("Driver for RTL8365MB-VC ethernet switch");
+-MODULE_LICENSE("GPL");
+-MODULE_IMPORT_NS(REALTEK_DSA);
+--- /dev/null
++++ b/drivers/net/dsa/realtek/rtl8365mb_main.c
+@@ -0,0 +1,2294 @@
++// SPDX-License-Identifier: GPL-2.0
++/* Realtek SMI subdriver for the Realtek RTL8365MB-VC ethernet switch.
++ *
++ * Copyright (C) 2021 Alvin Šipraga <alsi@bang-olufsen.dk>
++ * Copyright (C) 2021 Michael Rasmussen <mir@bang-olufsen.dk>
++ *
++ * The RTL8365MB-VC is a 4+1 port 10/100/1000M switch controller. It includes 4
++ * integrated PHYs for the user facing ports, and an extension interface which
++ * can be connected to the CPU - or another PHY - via either MII, RMII, or
++ * RGMII. The switch is configured via the Realtek Simple Management Interface
++ * (SMI), which uses the MDIO/MDC lines.
++ *
++ * Below is a simplified block diagram of the chip and its relevant interfaces.
++ *
++ *                          .-----------------------------------.
++ *                          |                                   |
++ *         UTP <---------------> Giga PHY <-> PCS <-> P0 GMAC   |
++ *         UTP <---------------> Giga PHY <-> PCS <-> P1 GMAC   |
++ *         UTP <---------------> Giga PHY <-> PCS <-> P2 GMAC   |
++ *         UTP <---------------> Giga PHY <-> PCS <-> P3 GMAC   |
++ *                          |                                   |
++ *     CPU/PHY <-MII/RMII/RGMII--->  Extension  <---> Extension |
++ *                          |       interface 1        GMAC 1   |
++ *                          |                                   |
++ *     SMI driver/ <-MDC/SCL---> Management    ~~~~~~~~~~~~~~   |
++ *        EEPROM   <-MDIO/SDA--> interface     ~REALTEK ~~~~~   |
++ *                          |                  ~RTL8365MB ~~~   |
++ *                          |                  ~GXXXC TAIWAN~   |
++ *        GPIO <--------------> Reset          ~~~~~~~~~~~~~~   |
++ *                          |                                   |
++ *      Interrupt  <----------> Link UP/DOWN events             |
++ *      controller          |                                   |
++ *                          '-----------------------------------'
++ *
++ * The driver uses DSA to integrate the 4 user and 1 extension ports into the
++ * kernel. Netdevices are created for the user ports, as are PHY devices for
++ * their integrated PHYs. The device tree firmware should also specify the link
++ * partner of the extension port - either via a fixed-link or other phy-handle.
++ * See the device tree bindings for more detailed information. Note that the
++ * driver has only been tested with a fixed-link, but in principle it should not
++ * matter.
++ *
++ * NOTE: Currently, only the RGMII interface is implemented in this driver.
++ *
++ * The interrupt line is asserted on link UP/DOWN events. The driver creates a
++ * custom irqchip to handle this interrupt and demultiplex the events by reading
++ * the status registers via SMI. Interrupts are then propagated to the relevant
++ * PHY device.
++ *
++ * The EEPROM contains initial register values which the chip will read over I2C
++ * upon hardware reset. It is also possible to omit the EEPROM. In both cases,
++ * the driver will manually reprogram some registers using jam tables to reach
++ * an initial state defined by the vendor driver.
++ *
++ * This Linux driver is written based on an OS-agnostic vendor driver from
++ * Realtek. The reference GPL-licensed sources can be found in the OpenWrt
++ * source tree under the name rtl8367c. The vendor driver claims to support a
++ * number of similar switch controllers from Realtek, but the only hardware we
++ * have is the RTL8365MB-VC. Moreover, there does not seem to be any chip under
++ * the name RTL8367C. Although one wishes that the 'C' stood for some kind of
++ * common hardware revision, there exist examples of chips with the suffix -VC
++ * which are explicitly not supported by the rtl8367c driver and which instead
++ * require the rtl8367d vendor driver. With all this uncertainty, the driver has
++ * been modestly named rtl8365mb. Future implementors may wish to rename things
++ * accordingly.
++ *
++ * In the same family of chips, some carry up to 8 user ports and up to 2
++ * extension ports. Where possible this driver tries to make things generic, but
++ * more work must be done to support these configurations. According to
++ * documentation from Realtek, the family should include the following chips:
++ *
++ *  - RTL8363NB
++ *  - RTL8363NB-VB
++ *  - RTL8363SC
++ *  - RTL8363SC-VB
++ *  - RTL8364NB
++ *  - RTL8364NB-VB
++ *  - RTL8365MB-VC
++ *  - RTL8366SC
++ *  - RTL8367RB-VB
++ *  - RTL8367SB
++ *  - RTL8367S
++ *  - RTL8370MB
++ *  - RTL8310SR
++ *
++ * Some of the register logic for these additional chips has been skipped over
++ * while implementing this driver. It is therefore not possible to assume that
++ * things will work out-of-the-box for other chips, and a careful review of the
++ * vendor driver may be needed to expand support. The RTL8365MB-VC seems to be
++ * one of the simpler chips.
++ */
++
++#include <linux/bitfield.h>
++#include <linux/bitops.h>
++#include <linux/interrupt.h>
++#include <linux/irqdomain.h>
++#include <linux/mutex.h>
++#include <linux/of_irq.h>
++#include <linux/regmap.h>
++#include <linux/if_bridge.h>
++#include <linux/if_vlan.h>
++
++#include "realtek.h"
++#include "realtek-smi.h"
++#include "realtek-mdio.h"
++#include "rtl83xx.h"
++
++/* Family-specific data and limits */
++#define RTL8365MB_PHYADDRMAX          7
++#define RTL8365MB_NUM_PHYREGS         32
++#define RTL8365MB_PHYREGMAX           (RTL8365MB_NUM_PHYREGS - 1)
++#define RTL8365MB_MAX_NUM_PORTS               11
++#define RTL8365MB_MAX_NUM_EXTINTS     3
++#define RTL8365MB_LEARN_LIMIT_MAX     2112
++
++/* Chip identification registers */
++#define RTL8365MB_CHIP_ID_REG         0x1300
++
++#define RTL8365MB_CHIP_VER_REG                0x1301
++
++#define RTL8365MB_MAGIC_REG           0x13C2
++#define   RTL8365MB_MAGIC_VALUE               0x0249
++
++/* Chip reset register */
++#define RTL8365MB_CHIP_RESET_REG      0x1322
++#define RTL8365MB_CHIP_RESET_SW_MASK  0x0002
++#define RTL8365MB_CHIP_RESET_HW_MASK  0x0001
++
++/* Interrupt polarity register */
++#define RTL8365MB_INTR_POLARITY_REG   0x1100
++#define   RTL8365MB_INTR_POLARITY_MASK        0x0001
++#define   RTL8365MB_INTR_POLARITY_HIGH        0
++#define   RTL8365MB_INTR_POLARITY_LOW 1
++
++/* Interrupt control/status register - enable/check specific interrupt types */
++#define RTL8365MB_INTR_CTRL_REG                       0x1101
++#define RTL8365MB_INTR_STATUS_REG             0x1102
++#define   RTL8365MB_INTR_SLIENT_START_2_MASK  0x1000
++#define   RTL8365MB_INTR_SLIENT_START_MASK    0x0800
++#define   RTL8365MB_INTR_ACL_ACTION_MASK      0x0200
++#define   RTL8365MB_INTR_CABLE_DIAG_FIN_MASK  0x0100
++#define   RTL8365MB_INTR_INTERRUPT_8051_MASK  0x0080
++#define   RTL8365MB_INTR_LOOP_DETECTION_MASK  0x0040
++#define   RTL8365MB_INTR_GREEN_TIMER_MASK     0x0020
++#define   RTL8365MB_INTR_SPECIAL_CONGEST_MASK 0x0010
++#define   RTL8365MB_INTR_SPEED_CHANGE_MASK    0x0008
++#define   RTL8365MB_INTR_LEARN_OVER_MASK      0x0004
++#define   RTL8365MB_INTR_METER_EXCEEDED_MASK  0x0002
++#define   RTL8365MB_INTR_LINK_CHANGE_MASK     0x0001
++#define   RTL8365MB_INTR_ALL_MASK                      \
++              (RTL8365MB_INTR_SLIENT_START_2_MASK |  \
++               RTL8365MB_INTR_SLIENT_START_MASK |    \
++               RTL8365MB_INTR_ACL_ACTION_MASK |      \
++               RTL8365MB_INTR_CABLE_DIAG_FIN_MASK |  \
++               RTL8365MB_INTR_INTERRUPT_8051_MASK |  \
++               RTL8365MB_INTR_LOOP_DETECTION_MASK |  \
++               RTL8365MB_INTR_GREEN_TIMER_MASK |     \
++               RTL8365MB_INTR_SPECIAL_CONGEST_MASK | \
++               RTL8365MB_INTR_SPEED_CHANGE_MASK |    \
++               RTL8365MB_INTR_LEARN_OVER_MASK |      \
++               RTL8365MB_INTR_METER_EXCEEDED_MASK |  \
++               RTL8365MB_INTR_LINK_CHANGE_MASK)
++
++/* Per-port interrupt type status registers */
++#define RTL8365MB_PORT_LINKDOWN_IND_REG               0x1106
++#define   RTL8365MB_PORT_LINKDOWN_IND_MASK    0x07FF
++
++#define RTL8365MB_PORT_LINKUP_IND_REG         0x1107
++#define   RTL8365MB_PORT_LINKUP_IND_MASK      0x07FF
++
++/* PHY indirect access registers */
++#define RTL8365MB_INDIRECT_ACCESS_CTRL_REG                    0x1F00
++#define   RTL8365MB_INDIRECT_ACCESS_CTRL_RW_MASK              0x0002
++#define   RTL8365MB_INDIRECT_ACCESS_CTRL_RW_READ              0
++#define   RTL8365MB_INDIRECT_ACCESS_CTRL_RW_WRITE             1
++#define   RTL8365MB_INDIRECT_ACCESS_CTRL_CMD_MASK             0x0001
++#define   RTL8365MB_INDIRECT_ACCESS_CTRL_CMD_VALUE            1
++#define RTL8365MB_INDIRECT_ACCESS_STATUS_REG                  0x1F01
++#define RTL8365MB_INDIRECT_ACCESS_ADDRESS_REG                 0x1F02
++#define   RTL8365MB_INDIRECT_ACCESS_ADDRESS_OCPADR_5_1_MASK   GENMASK(4, 0)
++#define   RTL8365MB_INDIRECT_ACCESS_ADDRESS_PHYNUM_MASK               GENMASK(7, 5)
++#define   RTL8365MB_INDIRECT_ACCESS_ADDRESS_OCPADR_9_6_MASK   GENMASK(11, 8)
++#define   RTL8365MB_PHY_BASE                                  0x2000
++#define RTL8365MB_INDIRECT_ACCESS_WRITE_DATA_REG              0x1F03
++#define RTL8365MB_INDIRECT_ACCESS_READ_DATA_REG                       0x1F04
++
++/* PHY OCP address prefix register */
++#define RTL8365MB_GPHY_OCP_MSB_0_REG                  0x1D15
++#define   RTL8365MB_GPHY_OCP_MSB_0_CFG_CPU_OCPADR_MASK        0x0FC0
++#define RTL8365MB_PHY_OCP_ADDR_PREFIX_MASK            0xFC00
++
++/* The PHY OCP addresses of PHY registers 0~31 start here */
++#define RTL8365MB_PHY_OCP_ADDR_PHYREG_BASE            0xA400
++
++/* External interface port mode values - used in DIGITAL_INTERFACE_SELECT */
++#define RTL8365MB_EXT_PORT_MODE_DISABLE               0
++#define RTL8365MB_EXT_PORT_MODE_RGMII         1
++#define RTL8365MB_EXT_PORT_MODE_MII_MAC               2
++#define RTL8365MB_EXT_PORT_MODE_MII_PHY               3
++#define RTL8365MB_EXT_PORT_MODE_TMII_MAC      4
++#define RTL8365MB_EXT_PORT_MODE_TMII_PHY      5
++#define RTL8365MB_EXT_PORT_MODE_GMII          6
++#define RTL8365MB_EXT_PORT_MODE_RMII_MAC      7
++#define RTL8365MB_EXT_PORT_MODE_RMII_PHY      8
++#define RTL8365MB_EXT_PORT_MODE_SGMII         9
++#define RTL8365MB_EXT_PORT_MODE_HSGMII                10
++#define RTL8365MB_EXT_PORT_MODE_1000X_100FX   11
++#define RTL8365MB_EXT_PORT_MODE_1000X         12
++#define RTL8365MB_EXT_PORT_MODE_100FX         13
++
++/* External interface mode configuration registers 0~1 */
++#define RTL8365MB_DIGITAL_INTERFACE_SELECT_REG0               0x1305 /* EXT0,EXT1 */
++#define RTL8365MB_DIGITAL_INTERFACE_SELECT_REG1               0x13C3 /* EXT2 */
++#define RTL8365MB_DIGITAL_INTERFACE_SELECT_REG(_extint) \
++              ((_extint) <= 1 ? RTL8365MB_DIGITAL_INTERFACE_SELECT_REG0 : \
++               (_extint) == 2 ? RTL8365MB_DIGITAL_INTERFACE_SELECT_REG1 : \
++               0x0)
++#define   RTL8365MB_DIGITAL_INTERFACE_SELECT_MODE_MASK(_extint) \
++              (0xF << (((_extint) % 2) * 4))
++#define   RTL8365MB_DIGITAL_INTERFACE_SELECT_MODE_OFFSET(_extint) \
++              (((_extint) % 2) * 4)
++
++/* External interface RGMII TX/RX delay configuration registers 0~2 */
++#define RTL8365MB_EXT_RGMXF_REG0              0x1306 /* EXT0 */
++#define RTL8365MB_EXT_RGMXF_REG1              0x1307 /* EXT1 */
++#define RTL8365MB_EXT_RGMXF_REG2              0x13C5 /* EXT2 */
++#define RTL8365MB_EXT_RGMXF_REG(_extint) \
++              ((_extint) == 0 ? RTL8365MB_EXT_RGMXF_REG0 : \
++               (_extint) == 1 ? RTL8365MB_EXT_RGMXF_REG1 : \
++               (_extint) == 2 ? RTL8365MB_EXT_RGMXF_REG2 : \
++               0x0)
++#define   RTL8365MB_EXT_RGMXF_RXDELAY_MASK    0x0007
++#define   RTL8365MB_EXT_RGMXF_TXDELAY_MASK    0x0008
++
++/* External interface port speed values - used in DIGITAL_INTERFACE_FORCE */
++#define RTL8365MB_PORT_SPEED_10M      0
++#define RTL8365MB_PORT_SPEED_100M     1
++#define RTL8365MB_PORT_SPEED_1000M    2
++
++/* External interface force configuration registers 0~2 */
++#define RTL8365MB_DIGITAL_INTERFACE_FORCE_REG0                0x1310 /* EXT0 */
++#define RTL8365MB_DIGITAL_INTERFACE_FORCE_REG1                0x1311 /* EXT1 */
++#define RTL8365MB_DIGITAL_INTERFACE_FORCE_REG2                0x13C4 /* EXT2 */
++#define RTL8365MB_DIGITAL_INTERFACE_FORCE_REG(_extint) \
++              ((_extint) == 0 ? RTL8365MB_DIGITAL_INTERFACE_FORCE_REG0 : \
++               (_extint) == 1 ? RTL8365MB_DIGITAL_INTERFACE_FORCE_REG1 : \
++               (_extint) == 2 ? RTL8365MB_DIGITAL_INTERFACE_FORCE_REG2 : \
++               0x0)
++#define   RTL8365MB_DIGITAL_INTERFACE_FORCE_EN_MASK           0x1000
++#define   RTL8365MB_DIGITAL_INTERFACE_FORCE_NWAY_MASK         0x0080
++#define   RTL8365MB_DIGITAL_INTERFACE_FORCE_TXPAUSE_MASK      0x0040
++#define   RTL8365MB_DIGITAL_INTERFACE_FORCE_RXPAUSE_MASK      0x0020
++#define   RTL8365MB_DIGITAL_INTERFACE_FORCE_LINK_MASK         0x0010
++#define   RTL8365MB_DIGITAL_INTERFACE_FORCE_DUPLEX_MASK               0x0004
++#define   RTL8365MB_DIGITAL_INTERFACE_FORCE_SPEED_MASK                0x0003
++
++/* CPU port mask register - controls which ports are treated as CPU ports */
++#define RTL8365MB_CPU_PORT_MASK_REG   0x1219
++#define   RTL8365MB_CPU_PORT_MASK_MASK        0x07FF
++
++/* CPU control register */
++#define RTL8365MB_CPU_CTRL_REG                        0x121A
++#define   RTL8365MB_CPU_CTRL_TRAP_PORT_EXT_MASK       0x0400
++#define   RTL8365MB_CPU_CTRL_TAG_FORMAT_MASK  0x0200
++#define   RTL8365MB_CPU_CTRL_RXBYTECOUNT_MASK 0x0080
++#define   RTL8365MB_CPU_CTRL_TAG_POSITION_MASK        0x0040
++#define   RTL8365MB_CPU_CTRL_TRAP_PORT_MASK   0x0038
++#define   RTL8365MB_CPU_CTRL_INSERTMODE_MASK  0x0006
++#define   RTL8365MB_CPU_CTRL_EN_MASK          0x0001
++
++/* Maximum packet length register */
++#define RTL8365MB_CFG0_MAX_LEN_REG    0x088C
++#define   RTL8365MB_CFG0_MAX_LEN_MASK 0x3FFF
++#define RTL8365MB_CFG0_MAX_LEN_MAX    0x3FFF
++
++/* Port learning limit registers */
++#define RTL8365MB_LUT_PORT_LEARN_LIMIT_BASE           0x0A20
++#define RTL8365MB_LUT_PORT_LEARN_LIMIT_REG(_physport) \
++              (RTL8365MB_LUT_PORT_LEARN_LIMIT_BASE + (_physport))
++
++/* Port isolation (forwarding mask) registers */
++#define RTL8365MB_PORT_ISOLATION_REG_BASE             0x08A2
++#define RTL8365MB_PORT_ISOLATION_REG(_physport) \
++              (RTL8365MB_PORT_ISOLATION_REG_BASE + (_physport))
++#define   RTL8365MB_PORT_ISOLATION_MASK                       0x07FF
++
++/* MSTP port state registers - indexed by tree instance */
++#define RTL8365MB_MSTI_CTRL_BASE                      0x0A00
++#define RTL8365MB_MSTI_CTRL_REG(_msti, _physport) \
++              (RTL8365MB_MSTI_CTRL_BASE + ((_msti) << 1) + ((_physport) >> 3))
++#define   RTL8365MB_MSTI_CTRL_PORT_STATE_OFFSET(_physport) ((_physport) << 1)
++#define   RTL8365MB_MSTI_CTRL_PORT_STATE_MASK(_physport) \
++              (0x3 << RTL8365MB_MSTI_CTRL_PORT_STATE_OFFSET((_physport)))
++
++/* MIB counter value registers */
++#define RTL8365MB_MIB_COUNTER_BASE    0x1000
++#define RTL8365MB_MIB_COUNTER_REG(_x) (RTL8365MB_MIB_COUNTER_BASE + (_x))
++
++/* MIB counter address register */
++#define RTL8365MB_MIB_ADDRESS_REG             0x1004
++#define   RTL8365MB_MIB_ADDRESS_PORT_OFFSET   0x007C
++#define   RTL8365MB_MIB_ADDRESS(_p, _x) \
++              (((RTL8365MB_MIB_ADDRESS_PORT_OFFSET) * (_p) + (_x)) >> 2)
++
++#define RTL8365MB_MIB_CTRL0_REG                       0x1005
++#define   RTL8365MB_MIB_CTRL0_RESET_MASK      0x0002
++#define   RTL8365MB_MIB_CTRL0_BUSY_MASK               0x0001
++
++/* The DSA callback .get_stats64 runs in atomic context, so we are not allowed
++ * to block. On the other hand, accessing MIB counters absolutely requires us to
++ * block. The solution is thus to schedule work which polls the MIB counters
++ * asynchronously and updates some private data, which the callback can then
++ * fetch atomically. Three seconds should be a good enough polling interval.
++ */
++#define RTL8365MB_STATS_INTERVAL_JIFFIES      (3 * HZ)
++
++enum rtl8365mb_mib_counter_index {
++      RTL8365MB_MIB_ifInOctets,
++      RTL8365MB_MIB_dot3StatsFCSErrors,
++      RTL8365MB_MIB_dot3StatsSymbolErrors,
++      RTL8365MB_MIB_dot3InPauseFrames,
++      RTL8365MB_MIB_dot3ControlInUnknownOpcodes,
++      RTL8365MB_MIB_etherStatsFragments,
++      RTL8365MB_MIB_etherStatsJabbers,
++      RTL8365MB_MIB_ifInUcastPkts,
++      RTL8365MB_MIB_etherStatsDropEvents,
++      RTL8365MB_MIB_ifInMulticastPkts,
++      RTL8365MB_MIB_ifInBroadcastPkts,
++      RTL8365MB_MIB_inMldChecksumError,
++      RTL8365MB_MIB_inIgmpChecksumError,
++      RTL8365MB_MIB_inMldSpecificQuery,
++      RTL8365MB_MIB_inMldGeneralQuery,
++      RTL8365MB_MIB_inIgmpSpecificQuery,
++      RTL8365MB_MIB_inIgmpGeneralQuery,
++      RTL8365MB_MIB_inMldLeaves,
++      RTL8365MB_MIB_inIgmpLeaves,
++      RTL8365MB_MIB_etherStatsOctets,
++      RTL8365MB_MIB_etherStatsUnderSizePkts,
++      RTL8365MB_MIB_etherOversizeStats,
++      RTL8365MB_MIB_etherStatsPkts64Octets,
++      RTL8365MB_MIB_etherStatsPkts65to127Octets,
++      RTL8365MB_MIB_etherStatsPkts128to255Octets,
++      RTL8365MB_MIB_etherStatsPkts256to511Octets,
++      RTL8365MB_MIB_etherStatsPkts512to1023Octets,
++      RTL8365MB_MIB_etherStatsPkts1024to1518Octets,
++      RTL8365MB_MIB_ifOutOctets,
++      RTL8365MB_MIB_dot3StatsSingleCollisionFrames,
++      RTL8365MB_MIB_dot3StatsMultipleCollisionFrames,
++      RTL8365MB_MIB_dot3StatsDeferredTransmissions,
++      RTL8365MB_MIB_dot3StatsLateCollisions,
++      RTL8365MB_MIB_etherStatsCollisions,
++      RTL8365MB_MIB_dot3StatsExcessiveCollisions,
++      RTL8365MB_MIB_dot3OutPauseFrames,
++      RTL8365MB_MIB_ifOutDiscards,
++      RTL8365MB_MIB_dot1dTpPortInDiscards,
++      RTL8365MB_MIB_ifOutUcastPkts,
++      RTL8365MB_MIB_ifOutMulticastPkts,
++      RTL8365MB_MIB_ifOutBroadcastPkts,
++      RTL8365MB_MIB_outOampduPkts,
++      RTL8365MB_MIB_inOampduPkts,
++      RTL8365MB_MIB_inIgmpJoinsSuccess,
++      RTL8365MB_MIB_inIgmpJoinsFail,
++      RTL8365MB_MIB_inMldJoinsSuccess,
++      RTL8365MB_MIB_inMldJoinsFail,
++      RTL8365MB_MIB_inReportSuppressionDrop,
++      RTL8365MB_MIB_inLeaveSuppressionDrop,
++      RTL8365MB_MIB_outIgmpReports,
++      RTL8365MB_MIB_outIgmpLeaves,
++      RTL8365MB_MIB_outIgmpGeneralQuery,
++      RTL8365MB_MIB_outIgmpSpecificQuery,
++      RTL8365MB_MIB_outMldReports,
++      RTL8365MB_MIB_outMldLeaves,
++      RTL8365MB_MIB_outMldGeneralQuery,
++      RTL8365MB_MIB_outMldSpecificQuery,
++      RTL8365MB_MIB_inKnownMulticastPkts,
++      RTL8365MB_MIB_END,
++};
++
++struct rtl8365mb_mib_counter {
++      u32 offset;
++      u32 length;
++      const char *name;
++};
++
++#define RTL8365MB_MAKE_MIB_COUNTER(_offset, _length, _name) \
++              [RTL8365MB_MIB_ ## _name] = { _offset, _length, #_name }
++
++static struct rtl8365mb_mib_counter rtl8365mb_mib_counters[] = {
++      RTL8365MB_MAKE_MIB_COUNTER(0, 4, ifInOctets),
++      RTL8365MB_MAKE_MIB_COUNTER(4, 2, dot3StatsFCSErrors),
++      RTL8365MB_MAKE_MIB_COUNTER(6, 2, dot3StatsSymbolErrors),
++      RTL8365MB_MAKE_MIB_COUNTER(8, 2, dot3InPauseFrames),
++      RTL8365MB_MAKE_MIB_COUNTER(10, 2, dot3ControlInUnknownOpcodes),
++      RTL8365MB_MAKE_MIB_COUNTER(12, 2, etherStatsFragments),
++      RTL8365MB_MAKE_MIB_COUNTER(14, 2, etherStatsJabbers),
++      RTL8365MB_MAKE_MIB_COUNTER(16, 2, ifInUcastPkts),
++      RTL8365MB_MAKE_MIB_COUNTER(18, 2, etherStatsDropEvents),
++      RTL8365MB_MAKE_MIB_COUNTER(20, 2, ifInMulticastPkts),
++      RTL8365MB_MAKE_MIB_COUNTER(22, 2, ifInBroadcastPkts),
++      RTL8365MB_MAKE_MIB_COUNTER(24, 2, inMldChecksumError),
++      RTL8365MB_MAKE_MIB_COUNTER(26, 2, inIgmpChecksumError),
++      RTL8365MB_MAKE_MIB_COUNTER(28, 2, inMldSpecificQuery),
++      RTL8365MB_MAKE_MIB_COUNTER(30, 2, inMldGeneralQuery),
++      RTL8365MB_MAKE_MIB_COUNTER(32, 2, inIgmpSpecificQuery),
++      RTL8365MB_MAKE_MIB_COUNTER(34, 2, inIgmpGeneralQuery),
++      RTL8365MB_MAKE_MIB_COUNTER(36, 2, inMldLeaves),
++      RTL8365MB_MAKE_MIB_COUNTER(38, 2, inIgmpLeaves),
++      RTL8365MB_MAKE_MIB_COUNTER(40, 4, etherStatsOctets),
++      RTL8365MB_MAKE_MIB_COUNTER(44, 2, etherStatsUnderSizePkts),
++      RTL8365MB_MAKE_MIB_COUNTER(46, 2, etherOversizeStats),
++      RTL8365MB_MAKE_MIB_COUNTER(48, 2, etherStatsPkts64Octets),
++      RTL8365MB_MAKE_MIB_COUNTER(50, 2, etherStatsPkts65to127Octets),
++      RTL8365MB_MAKE_MIB_COUNTER(52, 2, etherStatsPkts128to255Octets),
++      RTL8365MB_MAKE_MIB_COUNTER(54, 2, etherStatsPkts256to511Octets),
++      RTL8365MB_MAKE_MIB_COUNTER(56, 2, etherStatsPkts512to1023Octets),
++      RTL8365MB_MAKE_MIB_COUNTER(58, 2, etherStatsPkts1024to1518Octets),
++      RTL8365MB_MAKE_MIB_COUNTER(60, 4, ifOutOctets),
++      RTL8365MB_MAKE_MIB_COUNTER(64, 2, dot3StatsSingleCollisionFrames),
++      RTL8365MB_MAKE_MIB_COUNTER(66, 2, dot3StatsMultipleCollisionFrames),
++      RTL8365MB_MAKE_MIB_COUNTER(68, 2, dot3StatsDeferredTransmissions),
++      RTL8365MB_MAKE_MIB_COUNTER(70, 2, dot3StatsLateCollisions),
++      RTL8365MB_MAKE_MIB_COUNTER(72, 2, etherStatsCollisions),
++      RTL8365MB_MAKE_MIB_COUNTER(74, 2, dot3StatsExcessiveCollisions),
++      RTL8365MB_MAKE_MIB_COUNTER(76, 2, dot3OutPauseFrames),
++      RTL8365MB_MAKE_MIB_COUNTER(78, 2, ifOutDiscards),
++      RTL8365MB_MAKE_MIB_COUNTER(80, 2, dot1dTpPortInDiscards),
++      RTL8365MB_MAKE_MIB_COUNTER(82, 2, ifOutUcastPkts),
++      RTL8365MB_MAKE_MIB_COUNTER(84, 2, ifOutMulticastPkts),
++      RTL8365MB_MAKE_MIB_COUNTER(86, 2, ifOutBroadcastPkts),
++      RTL8365MB_MAKE_MIB_COUNTER(88, 2, outOampduPkts),
++      RTL8365MB_MAKE_MIB_COUNTER(90, 2, inOampduPkts),
++      RTL8365MB_MAKE_MIB_COUNTER(92, 4, inIgmpJoinsSuccess),
++      RTL8365MB_MAKE_MIB_COUNTER(96, 2, inIgmpJoinsFail),
++      RTL8365MB_MAKE_MIB_COUNTER(98, 2, inMldJoinsSuccess),
++      RTL8365MB_MAKE_MIB_COUNTER(100, 2, inMldJoinsFail),
++      RTL8365MB_MAKE_MIB_COUNTER(102, 2, inReportSuppressionDrop),
++      RTL8365MB_MAKE_MIB_COUNTER(104, 2, inLeaveSuppressionDrop),
++      RTL8365MB_MAKE_MIB_COUNTER(106, 2, outIgmpReports),
++      RTL8365MB_MAKE_MIB_COUNTER(108, 2, outIgmpLeaves),
++      RTL8365MB_MAKE_MIB_COUNTER(110, 2, outIgmpGeneralQuery),
++      RTL8365MB_MAKE_MIB_COUNTER(112, 2, outIgmpSpecificQuery),
++      RTL8365MB_MAKE_MIB_COUNTER(114, 2, outMldReports),
++      RTL8365MB_MAKE_MIB_COUNTER(116, 2, outMldLeaves),
++      RTL8365MB_MAKE_MIB_COUNTER(118, 2, outMldGeneralQuery),
++      RTL8365MB_MAKE_MIB_COUNTER(120, 2, outMldSpecificQuery),
++      RTL8365MB_MAKE_MIB_COUNTER(122, 2, inKnownMulticastPkts),
++};
++
++static_assert(ARRAY_SIZE(rtl8365mb_mib_counters) == RTL8365MB_MIB_END);
++
++struct rtl8365mb_jam_tbl_entry {
++      u16 reg;
++      u16 val;
++};
++
++/* Lifted from the vendor driver sources */
++static const struct rtl8365mb_jam_tbl_entry rtl8365mb_init_jam_8365mb_vc[] = {
++      { 0x13EB, 0x15BB }, { 0x1303, 0x06D6 }, { 0x1304, 0x0700 },
++      { 0x13E2, 0x003F }, { 0x13F9, 0x0090 }, { 0x121E, 0x03CA },
++      { 0x1233, 0x0352 }, { 0x1237, 0x00A0 }, { 0x123A, 0x0030 },
++      { 0x1239, 0x0084 }, { 0x0301, 0x1000 }, { 0x1349, 0x001F },
++      { 0x18E0, 0x4004 }, { 0x122B, 0x241C }, { 0x1305, 0xC000 },
++      { 0x13F0, 0x0000 },
++};
++
++static const struct rtl8365mb_jam_tbl_entry rtl8365mb_init_jam_common[] = {
++      { 0x1200, 0x7FCB }, { 0x0884, 0x0003 }, { 0x06EB, 0x0001 },
++      { 0x03Fa, 0x0007 }, { 0x08C8, 0x00C0 }, { 0x0A30, 0x020E },
++      { 0x0800, 0x0000 }, { 0x0802, 0x0000 }, { 0x09DA, 0x0013 },
++      { 0x1D32, 0x0002 },
++};
++
++enum rtl8365mb_phy_interface_mode {
++      RTL8365MB_PHY_INTERFACE_MODE_INVAL = 0,
++      RTL8365MB_PHY_INTERFACE_MODE_INTERNAL = BIT(0),
++      RTL8365MB_PHY_INTERFACE_MODE_MII = BIT(1),
++      RTL8365MB_PHY_INTERFACE_MODE_TMII = BIT(2),
++      RTL8365MB_PHY_INTERFACE_MODE_RMII = BIT(3),
++      RTL8365MB_PHY_INTERFACE_MODE_RGMII = BIT(4),
++      RTL8365MB_PHY_INTERFACE_MODE_SGMII = BIT(5),
++      RTL8365MB_PHY_INTERFACE_MODE_HSGMII = BIT(6),
++};
++
++/**
++ * struct rtl8365mb_extint - external interface info
++ * @port: the port with an external interface
++ * @id: the external interface ID, which is either 0, 1, or 2
++ * @supported_interfaces: a bitmask of supported PHY interface modes
++ *
++ * Represents a mapping: port -> { id, supported_interfaces }. To be embedded
++ * in &struct rtl8365mb_chip_info for every port with an external interface.
++ */
++struct rtl8365mb_extint {
++      int port;
++      int id;
++      unsigned int supported_interfaces;
++};
++
++/**
++ * struct rtl8365mb_chip_info - static chip-specific info
++ * @name: human-readable chip name
++ * @chip_id: chip identifier
++ * @chip_ver: chip silicon revision
++ * @extints: available external interfaces
++ * @jam_table: chip-specific initialization jam table
++ * @jam_size: size of the chip's jam table
++ *
++ * These data are specific to a given chip in the family of switches supported
++ * by this driver. When adding support for another chip in the family, a new
++ * chip info should be added to the rtl8365mb_chip_infos array.
++ */
++struct rtl8365mb_chip_info {
++      const char *name;
++      u32 chip_id;
++      u32 chip_ver;
++      const struct rtl8365mb_extint extints[RTL8365MB_MAX_NUM_EXTINTS];
++      const struct rtl8365mb_jam_tbl_entry *jam_table;
++      size_t jam_size;
++};
++
++/* Chip info for each supported switch in the family */
++#define PHY_INTF(_mode) (RTL8365MB_PHY_INTERFACE_MODE_ ## _mode)
++static const struct rtl8365mb_chip_info rtl8365mb_chip_infos[] = {
++      {
++              .name = "RTL8365MB-VC",
++              .chip_id = 0x6367,
++              .chip_ver = 0x0040,
++              .extints = {
++                      { 6, 1, PHY_INTF(MII) | PHY_INTF(TMII) |
++                              PHY_INTF(RMII) | PHY_INTF(RGMII) },
++              },
++              .jam_table = rtl8365mb_init_jam_8365mb_vc,
++              .jam_size = ARRAY_SIZE(rtl8365mb_init_jam_8365mb_vc),
++      },
++      {
++              .name = "RTL8367S",
++              .chip_id = 0x6367,
++              .chip_ver = 0x00A0,
++              .extints = {
++                      { 6, 1, PHY_INTF(SGMII) | PHY_INTF(HSGMII) },
++                      { 7, 2, PHY_INTF(MII) | PHY_INTF(TMII) |
++                              PHY_INTF(RMII) | PHY_INTF(RGMII) },
++              },
++              .jam_table = rtl8365mb_init_jam_8365mb_vc,
++              .jam_size = ARRAY_SIZE(rtl8365mb_init_jam_8365mb_vc),
++      },
++      {
++              .name = "RTL8367SB",
++              .chip_id = 0x6367,
++              .chip_ver = 0x0010,
++              .extints = {
++                      { 6, 1, PHY_INTF(MII) | PHY_INTF(TMII) |
++                              PHY_INTF(RMII) | PHY_INTF(RGMII) |
++                              PHY_INTF(SGMII) | PHY_INTF(HSGMII) },
++                      { 7, 2, PHY_INTF(MII) | PHY_INTF(TMII) |
++                              PHY_INTF(RMII) | PHY_INTF(RGMII) },
++              },
++              .jam_table = rtl8365mb_init_jam_8365mb_vc,
++              .jam_size = ARRAY_SIZE(rtl8365mb_init_jam_8365mb_vc),
++      },
++      {
++              .name = "RTL8367RB-VB",
++              .chip_id = 0x6367,
++              .chip_ver = 0x0020,
++              .extints = {
++                      { 6, 1, PHY_INTF(MII) | PHY_INTF(TMII) |
++                              PHY_INTF(RMII) | PHY_INTF(RGMII) },
++                      { 7, 2, PHY_INTF(MII) | PHY_INTF(TMII) |
++                              PHY_INTF(RMII) | PHY_INTF(RGMII) },
++              },
++              .jam_table = rtl8365mb_init_jam_8365mb_vc,
++              .jam_size = ARRAY_SIZE(rtl8365mb_init_jam_8365mb_vc),
++      },
++};
++
++enum rtl8365mb_stp_state {
++      RTL8365MB_STP_STATE_DISABLED = 0,
++      RTL8365MB_STP_STATE_BLOCKING = 1,
++      RTL8365MB_STP_STATE_LEARNING = 2,
++      RTL8365MB_STP_STATE_FORWARDING = 3,
++};
++
++enum rtl8365mb_cpu_insert {
++      RTL8365MB_CPU_INSERT_TO_ALL = 0,
++      RTL8365MB_CPU_INSERT_TO_TRAPPING = 1,
++      RTL8365MB_CPU_INSERT_TO_NONE = 2,
++};
++
++enum rtl8365mb_cpu_position {
++      RTL8365MB_CPU_POS_AFTER_SA = 0,
++      RTL8365MB_CPU_POS_BEFORE_CRC = 1,
++};
++
++enum rtl8365mb_cpu_format {
++      RTL8365MB_CPU_FORMAT_8BYTES = 0,
++      RTL8365MB_CPU_FORMAT_4BYTES = 1,
++};
++
++enum rtl8365mb_cpu_rxlen {
++      RTL8365MB_CPU_RXLEN_72BYTES = 0,
++      RTL8365MB_CPU_RXLEN_64BYTES = 1,
++};
++
++/**
++ * struct rtl8365mb_cpu - CPU port configuration
++ * @enable: enable/disable hardware insertion of CPU tag in switch->CPU frames
++ * @mask: port mask of ports that parse should parse CPU tags
++ * @trap_port: forward trapped frames to this port
++ * @insert: CPU tag insertion mode in switch->CPU frames
++ * @position: position of CPU tag in frame
++ * @rx_length: minimum CPU RX length
++ * @format: CPU tag format
++ *
++ * Represents the CPU tagging and CPU port configuration of the switch. These
++ * settings are configurable at runtime.
++ */
++struct rtl8365mb_cpu {
++      bool enable;
++      u32 mask;
++      u32 trap_port;
++      enum rtl8365mb_cpu_insert insert;
++      enum rtl8365mb_cpu_position position;
++      enum rtl8365mb_cpu_rxlen rx_length;
++      enum rtl8365mb_cpu_format format;
++};
++
++/**
++ * struct rtl8365mb_port - private per-port data
++ * @priv: pointer to parent realtek_priv data
++ * @index: DSA port index, same as dsa_port::index
++ * @stats: link statistics populated by rtl8365mb_stats_poll, ready for atomic
++ *         access via rtl8365mb_get_stats64
++ * @stats_lock: protect the stats structure during read/update
++ * @mib_work: delayed work for polling MIB counters
++ */
++struct rtl8365mb_port {
++      struct realtek_priv *priv;
++      unsigned int index;
++      struct rtnl_link_stats64 stats;
++      spinlock_t stats_lock;
++      struct delayed_work mib_work;
++};
++
++/**
++ * struct rtl8365mb - driver private data
++ * @priv: pointer to parent realtek_priv data
++ * @irq: registered IRQ or zero
++ * @chip_info: chip-specific info about the attached switch
++ * @cpu: CPU tagging and CPU port configuration for this chip
++ * @mib_lock: prevent concurrent reads of MIB counters
++ * @ports: per-port data
++ *
++ * Private data for this driver.
++ */
++struct rtl8365mb {
++      struct realtek_priv *priv;
++      int irq;
++      const struct rtl8365mb_chip_info *chip_info;
++      struct rtl8365mb_cpu cpu;
++      struct mutex mib_lock;
++      struct rtl8365mb_port ports[RTL8365MB_MAX_NUM_PORTS];
++};
++
++static int rtl8365mb_phy_poll_busy(struct realtek_priv *priv)
++{
++      u32 val;
++
++      return regmap_read_poll_timeout(priv->map_nolock,
++                                      RTL8365MB_INDIRECT_ACCESS_STATUS_REG,
++                                      val, !val, 10, 100);
++}
++
++static int rtl8365mb_phy_ocp_prepare(struct realtek_priv *priv, int phy,
++                                   u32 ocp_addr)
++{
++      u32 val;
++      int ret;
++
++      /* Set OCP prefix */
++      val = FIELD_GET(RTL8365MB_PHY_OCP_ADDR_PREFIX_MASK, ocp_addr);
++      ret = regmap_update_bits(
++              priv->map_nolock, RTL8365MB_GPHY_OCP_MSB_0_REG,
++              RTL8365MB_GPHY_OCP_MSB_0_CFG_CPU_OCPADR_MASK,
++              FIELD_PREP(RTL8365MB_GPHY_OCP_MSB_0_CFG_CPU_OCPADR_MASK, val));
++      if (ret)
++              return ret;
++
++      /* Set PHY register address */
++      val = RTL8365MB_PHY_BASE;
++      val |= FIELD_PREP(RTL8365MB_INDIRECT_ACCESS_ADDRESS_PHYNUM_MASK, phy);
++      val |= FIELD_PREP(RTL8365MB_INDIRECT_ACCESS_ADDRESS_OCPADR_5_1_MASK,
++                        ocp_addr >> 1);
++      val |= FIELD_PREP(RTL8365MB_INDIRECT_ACCESS_ADDRESS_OCPADR_9_6_MASK,
++                        ocp_addr >> 6);
++      ret = regmap_write(priv->map_nolock,
++                         RTL8365MB_INDIRECT_ACCESS_ADDRESS_REG, val);
++      if (ret)
++              return ret;
++
++      return 0;
++}
++
++static int rtl8365mb_phy_ocp_read(struct realtek_priv *priv, int phy,
++                                u32 ocp_addr, u16 *data)
++{
++      u32 val;
++      int ret;
++
++      rtl83xx_lock(priv);
++
++      ret = rtl8365mb_phy_poll_busy(priv);
++      if (ret)
++              goto out;
++
++      ret = rtl8365mb_phy_ocp_prepare(priv, phy, ocp_addr);
++      if (ret)
++              goto out;
++
++      /* Execute read operation */
++      val = FIELD_PREP(RTL8365MB_INDIRECT_ACCESS_CTRL_CMD_MASK,
++                       RTL8365MB_INDIRECT_ACCESS_CTRL_CMD_VALUE) |
++            FIELD_PREP(RTL8365MB_INDIRECT_ACCESS_CTRL_RW_MASK,
++                       RTL8365MB_INDIRECT_ACCESS_CTRL_RW_READ);
++      ret = regmap_write(priv->map_nolock, RTL8365MB_INDIRECT_ACCESS_CTRL_REG,
++                         val);
++      if (ret)
++              goto out;
++
++      ret = rtl8365mb_phy_poll_busy(priv);
++      if (ret)
++              goto out;
++
++      /* Get PHY register data */
++      ret = regmap_read(priv->map_nolock,
++                        RTL8365MB_INDIRECT_ACCESS_READ_DATA_REG, &val);
++      if (ret)
++              goto out;
++
++      *data = val & 0xFFFF;
++
++out:
++      rtl83xx_unlock(priv);
++
++      return ret;
++}
++
++static int rtl8365mb_phy_ocp_write(struct realtek_priv *priv, int phy,
++                                 u32 ocp_addr, u16 data)
++{
++      u32 val;
++      int ret;
++
++      rtl83xx_lock(priv);
++
++      ret = rtl8365mb_phy_poll_busy(priv);
++      if (ret)
++              goto out;
++
++      ret = rtl8365mb_phy_ocp_prepare(priv, phy, ocp_addr);
++      if (ret)
++              goto out;
++
++      /* Set PHY register data */
++      ret = regmap_write(priv->map_nolock,
++                         RTL8365MB_INDIRECT_ACCESS_WRITE_DATA_REG, data);
++      if (ret)
++              goto out;
++
++      /* Execute write operation */
++      val = FIELD_PREP(RTL8365MB_INDIRECT_ACCESS_CTRL_CMD_MASK,
++                       RTL8365MB_INDIRECT_ACCESS_CTRL_CMD_VALUE) |
++            FIELD_PREP(RTL8365MB_INDIRECT_ACCESS_CTRL_RW_MASK,
++                       RTL8365MB_INDIRECT_ACCESS_CTRL_RW_WRITE);
++      ret = regmap_write(priv->map_nolock, RTL8365MB_INDIRECT_ACCESS_CTRL_REG,
++                         val);
++      if (ret)
++              goto out;
++
++      ret = rtl8365mb_phy_poll_busy(priv);
++      if (ret)
++              goto out;
++
++out:
++      rtl83xx_unlock(priv);
++
++      return ret;
++}
++
++static int rtl8365mb_phy_read(struct realtek_priv *priv, int phy, int regnum)
++{
++      u32 ocp_addr;
++      u16 val;
++      int ret;
++
++      if (phy > RTL8365MB_PHYADDRMAX)
++              return -EINVAL;
++
++      if (regnum > RTL8365MB_PHYREGMAX)
++              return -EINVAL;
++
++      ocp_addr = RTL8365MB_PHY_OCP_ADDR_PHYREG_BASE + regnum * 2;
++
++      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 %pe\n", phy,
++                      regnum, ocp_addr, ERR_PTR(ret));
++              return ret;
++      }
++
++      dev_dbg(priv->dev, "read PHY%d register 0x%02x @ %04x, val <- %04x\n",
++              phy, regnum, ocp_addr, val);
++
++      return val;
++}
++
++static int rtl8365mb_phy_write(struct realtek_priv *priv, int phy, int regnum,
++                             u16 val)
++{
++      u32 ocp_addr;
++      int ret;
++
++      if (phy > RTL8365MB_PHYADDRMAX)
++              return -EINVAL;
++
++      if (regnum > RTL8365MB_PHYREGMAX)
++              return -EINVAL;
++
++      ocp_addr = RTL8365MB_PHY_OCP_ADDR_PHYREG_BASE + regnum * 2;
++
++      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 %pe\n", phy,
++                      regnum, ocp_addr, ERR_PTR(ret));
++              return ret;
++      }
++
++      dev_dbg(priv->dev, "write PHY%d register 0x%02x @ %04x, val -> %04x\n",
++              phy, regnum, ocp_addr, val);
++
++      return 0;
++}
++
++static const struct rtl8365mb_extint *
++rtl8365mb_get_port_extint(struct realtek_priv *priv, int port)
++{
++      struct rtl8365mb *mb = priv->chip_data;
++      int i;
++
++      for (i = 0; i < RTL8365MB_MAX_NUM_EXTINTS; i++) {
++              const struct rtl8365mb_extint *extint =
++                      &mb->chip_info->extints[i];
++
++              if (!extint->supported_interfaces)
++                      continue;
++
++              if (extint->port == port)
++                      return extint;
++      }
++
++      return NULL;
++}
++
++static enum dsa_tag_protocol
++rtl8365mb_get_tag_protocol(struct dsa_switch *ds, int port,
++                         enum dsa_tag_protocol mp)
++{
++      struct realtek_priv *priv = ds->priv;
++      struct rtl8365mb_cpu *cpu;
++      struct rtl8365mb *mb;
++
++      mb = priv->chip_data;
++      cpu = &mb->cpu;
++
++      if (cpu->position == RTL8365MB_CPU_POS_BEFORE_CRC)
++              return DSA_TAG_PROTO_RTL8_4T;
++
++      return DSA_TAG_PROTO_RTL8_4;
++}
++
++static int rtl8365mb_ext_config_rgmii(struct realtek_priv *priv, int port,
++                                    phy_interface_t interface)
++{
++      const struct rtl8365mb_extint *extint =
++              rtl8365mb_get_port_extint(priv, port);
++      struct dsa_switch *ds = &priv->ds;
++      struct device_node *dn;
++      struct dsa_port *dp;
++      int tx_delay = 0;
++      int rx_delay = 0;
++      u32 val;
++      int ret;
++
++      if (!extint)
++              return -ENODEV;
++
++      dp = dsa_to_port(ds, port);
++      dn = dp->dn;
++
++      /* Set the RGMII TX/RX delay
++       *
++       * The Realtek vendor driver indicates the following possible
++       * configuration settings:
++       *
++       *   TX delay:
++       *     0 = no delay, 1 = 2 ns delay
++       *   RX delay:
++       *     0 = no delay, 7 = maximum delay
++       *     Each step is approximately 0.3 ns, so the maximum delay is about
++       *     2.1 ns.
++       *
++       * The vendor driver also states that this must be configured *before*
++       * forcing the external interface into a particular mode, which is done
++       * in the rtl8365mb_phylink_mac_link_{up,down} functions.
++       *
++       * Only configure an RGMII TX (resp. RX) delay if the
++       * tx-internal-delay-ps (resp. rx-internal-delay-ps) OF property is
++       * specified. We ignore the detail of the RGMII interface mode
++       * (RGMII_{RXID, TXID, etc.}), as this is considered to be a PHY-only
++       * property.
++       */
++      if (!of_property_read_u32(dn, "tx-internal-delay-ps", &val)) {
++              val = val / 1000; /* convert to ns */
++
++              if (val == 0 || val == 2)
++                      tx_delay = val / 2;
++              else
++                      dev_warn(priv->dev,
++                               "RGMII TX delay must be 0 or 2 ns\n");
++      }
++
++      if (!of_property_read_u32(dn, "rx-internal-delay-ps", &val)) {
++              val = DIV_ROUND_CLOSEST(val, 300); /* convert to 0.3 ns step */
++
++              if (val <= 7)
++                      rx_delay = val;
++              else
++                      dev_warn(priv->dev,
++                               "RGMII RX delay must be 0 to 2.1 ns\n");
++      }
++
++      ret = regmap_update_bits(
++              priv->map, RTL8365MB_EXT_RGMXF_REG(extint->id),
++              RTL8365MB_EXT_RGMXF_TXDELAY_MASK |
++                      RTL8365MB_EXT_RGMXF_RXDELAY_MASK,
++              FIELD_PREP(RTL8365MB_EXT_RGMXF_TXDELAY_MASK, tx_delay) |
++                      FIELD_PREP(RTL8365MB_EXT_RGMXF_RXDELAY_MASK, rx_delay));
++      if (ret)
++              return ret;
++
++      ret = regmap_update_bits(
++              priv->map, RTL8365MB_DIGITAL_INTERFACE_SELECT_REG(extint->id),
++              RTL8365MB_DIGITAL_INTERFACE_SELECT_MODE_MASK(extint->id),
++              RTL8365MB_EXT_PORT_MODE_RGMII
++                      << RTL8365MB_DIGITAL_INTERFACE_SELECT_MODE_OFFSET(
++                                 extint->id));
++      if (ret)
++              return ret;
++
++      return 0;
++}
++
++static int rtl8365mb_ext_config_forcemode(struct realtek_priv *priv, int port,
++                                        bool link, int speed, int duplex,
++                                        bool tx_pause, bool rx_pause)
++{
++      const struct rtl8365mb_extint *extint =
++              rtl8365mb_get_port_extint(priv, port);
++      u32 r_tx_pause;
++      u32 r_rx_pause;
++      u32 r_duplex;
++      u32 r_speed;
++      u32 r_link;
++      int val;
++      int ret;
++
++      if (!extint)
++              return -ENODEV;
++
++      if (link) {
++              /* Force the link up with the desired configuration */
++              r_link = 1;
++              r_rx_pause = rx_pause ? 1 : 0;
++              r_tx_pause = tx_pause ? 1 : 0;
++
++              if (speed == SPEED_1000) {
++                      r_speed = RTL8365MB_PORT_SPEED_1000M;
++              } else if (speed == SPEED_100) {
++                      r_speed = RTL8365MB_PORT_SPEED_100M;
++              } else if (speed == SPEED_10) {
++                      r_speed = RTL8365MB_PORT_SPEED_10M;
++              } else {
++                      dev_err(priv->dev, "unsupported port speed %s\n",
++                              phy_speed_to_str(speed));
++                      return -EINVAL;
++              }
++
++              if (duplex == DUPLEX_FULL) {
++                      r_duplex = 1;
++              } else if (duplex == DUPLEX_HALF) {
++                      r_duplex = 0;
++              } else {
++                      dev_err(priv->dev, "unsupported duplex %s\n",
++                              phy_duplex_to_str(duplex));
++                      return -EINVAL;
++              }
++      } else {
++              /* Force the link down and reset any programmed configuration */
++              r_link = 0;
++              r_tx_pause = 0;
++              r_rx_pause = 0;
++              r_speed = 0;
++              r_duplex = 0;
++      }
++
++      val = FIELD_PREP(RTL8365MB_DIGITAL_INTERFACE_FORCE_EN_MASK, 1) |
++            FIELD_PREP(RTL8365MB_DIGITAL_INTERFACE_FORCE_TXPAUSE_MASK,
++                       r_tx_pause) |
++            FIELD_PREP(RTL8365MB_DIGITAL_INTERFACE_FORCE_RXPAUSE_MASK,
++                       r_rx_pause) |
++            FIELD_PREP(RTL8365MB_DIGITAL_INTERFACE_FORCE_LINK_MASK, r_link) |
++            FIELD_PREP(RTL8365MB_DIGITAL_INTERFACE_FORCE_DUPLEX_MASK,
++                       r_duplex) |
++            FIELD_PREP(RTL8365MB_DIGITAL_INTERFACE_FORCE_SPEED_MASK, r_speed);
++      ret = regmap_write(priv->map,
++                         RTL8365MB_DIGITAL_INTERFACE_FORCE_REG(extint->id),
++                         val);
++      if (ret)
++              return ret;
++
++      return 0;
++}
++
++static void rtl8365mb_phylink_get_caps(struct dsa_switch *ds, int port,
++                                     struct phylink_config *config)
++{
++      const struct rtl8365mb_extint *extint =
++              rtl8365mb_get_port_extint(ds->priv, port);
++
++      config->mac_capabilities = MAC_SYM_PAUSE | MAC_ASYM_PAUSE |
++                                 MAC_10 | MAC_100 | MAC_1000FD;
++
++      if (!extint) {
++              __set_bit(PHY_INTERFACE_MODE_INTERNAL,
++                        config->supported_interfaces);
++
++              /* GMII is the default interface mode for phylib, so
++               * we have to support it for ports with integrated PHY.
++               */
++              __set_bit(PHY_INTERFACE_MODE_GMII,
++                        config->supported_interfaces);
++              return;
++      }
++
++      /* Populate according to the modes supported by _this driver_,
++       * not necessarily the modes supported by the hardware, some of
++       * which remain unimplemented.
++       */
++
++      if (extint->supported_interfaces & RTL8365MB_PHY_INTERFACE_MODE_RGMII)
++              phy_interface_set_rgmii(config->supported_interfaces);
++}
++
++static void rtl8365mb_phylink_mac_config(struct phylink_config *config,
++                                       unsigned int mode,
++                                       const struct phylink_link_state *state)
++{
++      struct dsa_port *dp = dsa_phylink_to_port(config);
++      struct realtek_priv *priv = dp->ds->priv;
++      u8 port = dp->index;
++      int ret;
++
++      if (mode != MLO_AN_PHY && mode != MLO_AN_FIXED) {
++              dev_err(priv->dev,
++                      "port %d supports only conventional PHY or fixed-link\n",
++                      port);
++              return;
++      }
++
++      if (phy_interface_mode_is_rgmii(state->interface)) {
++              ret = rtl8365mb_ext_config_rgmii(priv, port, state->interface);
++              if (ret)
++                      dev_err(priv->dev,
++                              "failed to configure RGMII mode on port %d: %pe\n",
++                              port, ERR_PTR(ret));
++              return;
++      }
++
++      /* TODO: Implement MII and RMII modes, which the RTL8365MB-VC also
++       * supports
++       */
++}
++
++static void rtl8365mb_phylink_mac_link_down(struct phylink_config *config,
++                                          unsigned int mode,
++                                          phy_interface_t interface)
++{
++      struct dsa_port *dp = dsa_phylink_to_port(config);
++      struct realtek_priv *priv = dp->ds->priv;
++      struct rtl8365mb_port *p;
++      struct rtl8365mb *mb;
++      u8 port = dp->index;
++      int ret;
++
++      mb = priv->chip_data;
++      p = &mb->ports[port];
++      cancel_delayed_work_sync(&p->mib_work);
++
++      if (phy_interface_mode_is_rgmii(interface)) {
++              ret = rtl8365mb_ext_config_forcemode(priv, port, false, 0, 0,
++                                                   false, false);
++              if (ret)
++                      dev_err(priv->dev,
++                              "failed to reset forced mode on port %d: %pe\n",
++                              port, ERR_PTR(ret));
++
++              return;
++      }
++}
++
++static void rtl8365mb_phylink_mac_link_up(struct phylink_config *config,
++                                        struct phy_device *phydev,
++                                        unsigned int mode,
++                                        phy_interface_t interface,
++                                        int speed, int duplex, bool tx_pause,
++                                        bool rx_pause)
++{
++      struct dsa_port *dp = dsa_phylink_to_port(config);
++      struct realtek_priv *priv = dp->ds->priv;
++      struct rtl8365mb_port *p;
++      struct rtl8365mb *mb;
++      u8 port = dp->index;
++      int ret;
++
++      mb = priv->chip_data;
++      p = &mb->ports[port];
++      schedule_delayed_work(&p->mib_work, 0);
++
++      if (phy_interface_mode_is_rgmii(interface)) {
++              ret = rtl8365mb_ext_config_forcemode(priv, port, true, speed,
++                                                   duplex, tx_pause,
++                                                   rx_pause);
++              if (ret)
++                      dev_err(priv->dev,
++                              "failed to force mode on port %d: %pe\n", port,
++                              ERR_PTR(ret));
++
++              return;
++      }
++}
++
++static int rtl8365mb_port_change_mtu(struct dsa_switch *ds, int port,
++                                   int new_mtu)
++{
++      struct realtek_priv *priv = ds->priv;
++      int frame_size;
++
++      /* When a new MTU is set, DSA always sets the CPU port's MTU to the
++       * largest MTU of the user ports. Because the switch only has a global
++       * RX length register, only allowing CPU port here is enough.
++       */
++      if (!dsa_is_cpu_port(ds, port))
++              return 0;
++
++      frame_size = new_mtu + VLAN_ETH_HLEN + ETH_FCS_LEN;
++
++      dev_dbg(priv->dev, "changing mtu to %d (frame size: %d)\n",
++              new_mtu, frame_size);
++
++      return regmap_update_bits(priv->map, RTL8365MB_CFG0_MAX_LEN_REG,
++                                RTL8365MB_CFG0_MAX_LEN_MASK,
++                                FIELD_PREP(RTL8365MB_CFG0_MAX_LEN_MASK,
++                                           frame_size));
++}
++
++static int rtl8365mb_port_max_mtu(struct dsa_switch *ds, int port)
++{
++      return RTL8365MB_CFG0_MAX_LEN_MAX - VLAN_ETH_HLEN - ETH_FCS_LEN;
++}
++
++static void rtl8365mb_port_stp_state_set(struct dsa_switch *ds, int port,
++                                       u8 state)
++{
++      struct realtek_priv *priv = ds->priv;
++      enum rtl8365mb_stp_state val;
++      int msti = 0;
++
++      switch (state) {
++      case BR_STATE_DISABLED:
++              val = RTL8365MB_STP_STATE_DISABLED;
++              break;
++      case BR_STATE_BLOCKING:
++      case BR_STATE_LISTENING:
++              val = RTL8365MB_STP_STATE_BLOCKING;
++              break;
++      case BR_STATE_LEARNING:
++              val = RTL8365MB_STP_STATE_LEARNING;
++              break;
++      case BR_STATE_FORWARDING:
++              val = RTL8365MB_STP_STATE_FORWARDING;
++              break;
++      default:
++              dev_err(priv->dev, "invalid STP state: %u\n", state);
++              return;
++      }
++
++      regmap_update_bits(priv->map, RTL8365MB_MSTI_CTRL_REG(msti, port),
++                         RTL8365MB_MSTI_CTRL_PORT_STATE_MASK(port),
++                         val << RTL8365MB_MSTI_CTRL_PORT_STATE_OFFSET(port));
++}
++
++static int rtl8365mb_port_set_learning(struct realtek_priv *priv, int port,
++                                     bool enable)
++{
++      /* Enable/disable learning by limiting the number of L2 addresses the
++       * port can learn. Realtek documentation states that a limit of zero
++       * disables learning. When enabling learning, set it to the chip's
++       * maximum.
++       */
++      return regmap_write(priv->map, RTL8365MB_LUT_PORT_LEARN_LIMIT_REG(port),
++                          enable ? RTL8365MB_LEARN_LIMIT_MAX : 0);
++}
++
++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);
++}
++
++static int rtl8365mb_mib_counter_read(struct realtek_priv *priv, int port,
++                                    u32 offset, u32 length, u64 *mibvalue)
++{
++      u64 tmpvalue = 0;
++      u32 val;
++      int ret;
++      int i;
++
++      /* The MIB address is an SRAM address. We request a particular address
++       * and then poll the control register before reading the value from some
++       * counter registers.
++       */
++      ret = regmap_write(priv->map, RTL8365MB_MIB_ADDRESS_REG,
++                         RTL8365MB_MIB_ADDRESS(port, offset));
++      if (ret)
++              return ret;
++
++      /* Poll for completion */
++      ret = regmap_read_poll_timeout(priv->map, RTL8365MB_MIB_CTRL0_REG, val,
++                                     !(val & RTL8365MB_MIB_CTRL0_BUSY_MASK),
++                                     10, 100);
++      if (ret)
++              return ret;
++
++      /* Presumably this indicates a MIB counter read failure */
++      if (val & RTL8365MB_MIB_CTRL0_RESET_MASK)
++              return -EIO;
++
++      /* There are four MIB counter registers each holding a 16 bit word of a
++       * MIB counter. Depending on the offset, we should read from the upper
++       * two or lower two registers. In case the MIB counter is 4 words, we
++       * read from all four registers.
++       */
++      if (length == 4)
++              offset = 3;
++      else
++              offset = (offset + 1) % 4;
++
++      /* Read the MIB counter 16 bits at a time */
++      for (i = 0; i < length; i++) {
++              ret = regmap_read(priv->map,
++                                RTL8365MB_MIB_COUNTER_REG(offset - i), &val);
++              if (ret)
++                      return ret;
++
++              tmpvalue = ((tmpvalue) << 16) | (val & 0xFFFF);
++      }
++
++      /* Only commit the result if no error occurred */
++      *mibvalue = tmpvalue;
++
++      return 0;
++}
++
++static void rtl8365mb_get_ethtool_stats(struct dsa_switch *ds, int port, u64 *data)
++{
++      struct realtek_priv *priv = ds->priv;
++      struct rtl8365mb *mb;
++      int ret;
++      int i;
++
++      mb = priv->chip_data;
++
++      mutex_lock(&mb->mib_lock);
++      for (i = 0; i < RTL8365MB_MIB_END; i++) {
++              struct rtl8365mb_mib_counter *mib = &rtl8365mb_mib_counters[i];
++
++              ret = rtl8365mb_mib_counter_read(priv, port, mib->offset,
++                                               mib->length, &data[i]);
++              if (ret) {
++                      dev_err(priv->dev,
++                              "failed to read port %d counters: %pe\n", port,
++                              ERR_PTR(ret));
++                      break;
++              }
++      }
++      mutex_unlock(&mb->mib_lock);
++}
++
++static void rtl8365mb_get_strings(struct dsa_switch *ds, int port, u32 stringset, u8 *data)
++{
++      int i;
++
++      if (stringset != ETH_SS_STATS)
++              return;
++
++      for (i = 0; i < RTL8365MB_MIB_END; i++) {
++              struct rtl8365mb_mib_counter *mib = &rtl8365mb_mib_counters[i];
++              ethtool_puts(&data, mib->name);
++      }
++}
++
++static int rtl8365mb_get_sset_count(struct dsa_switch *ds, int port, int sset)
++{
++      if (sset != ETH_SS_STATS)
++              return -EOPNOTSUPP;
++
++      return RTL8365MB_MIB_END;
++}
++
++static void rtl8365mb_get_phy_stats(struct dsa_switch *ds, int port,
++                                  struct ethtool_eth_phy_stats *phy_stats)
++{
++      struct realtek_priv *priv = ds->priv;
++      struct rtl8365mb_mib_counter *mib;
++      struct rtl8365mb *mb;
++
++      mb = priv->chip_data;
++      mib = &rtl8365mb_mib_counters[RTL8365MB_MIB_dot3StatsSymbolErrors];
++
++      mutex_lock(&mb->mib_lock);
++      rtl8365mb_mib_counter_read(priv, port, mib->offset, mib->length,
++                                 &phy_stats->SymbolErrorDuringCarrier);
++      mutex_unlock(&mb->mib_lock);
++}
++
++static void rtl8365mb_get_mac_stats(struct dsa_switch *ds, int port,
++                                  struct ethtool_eth_mac_stats *mac_stats)
++{
++      u64 cnt[RTL8365MB_MIB_END] = {
++              [RTL8365MB_MIB_ifOutOctets] = 1,
++              [RTL8365MB_MIB_ifOutUcastPkts] = 1,
++              [RTL8365MB_MIB_ifOutMulticastPkts] = 1,
++              [RTL8365MB_MIB_ifOutBroadcastPkts] = 1,
++              [RTL8365MB_MIB_dot3OutPauseFrames] = 1,
++              [RTL8365MB_MIB_ifOutDiscards] = 1,
++              [RTL8365MB_MIB_ifInOctets] = 1,
++              [RTL8365MB_MIB_ifInUcastPkts] = 1,
++              [RTL8365MB_MIB_ifInMulticastPkts] = 1,
++              [RTL8365MB_MIB_ifInBroadcastPkts] = 1,
++              [RTL8365MB_MIB_dot3InPauseFrames] = 1,
++              [RTL8365MB_MIB_dot3StatsSingleCollisionFrames] = 1,
++              [RTL8365MB_MIB_dot3StatsMultipleCollisionFrames] = 1,
++              [RTL8365MB_MIB_dot3StatsFCSErrors] = 1,
++              [RTL8365MB_MIB_dot3StatsDeferredTransmissions] = 1,
++              [RTL8365MB_MIB_dot3StatsLateCollisions] = 1,
++              [RTL8365MB_MIB_dot3StatsExcessiveCollisions] = 1,
++
++      };
++      struct realtek_priv *priv = ds->priv;
++      struct rtl8365mb *mb;
++      int ret;
++      int i;
++
++      mb = priv->chip_data;
++
++      mutex_lock(&mb->mib_lock);
++      for (i = 0; i < RTL8365MB_MIB_END; i++) {
++              struct rtl8365mb_mib_counter *mib = &rtl8365mb_mib_counters[i];
++
++              /* Only fetch required MIB counters (marked = 1 above) */
++              if (!cnt[i])
++                      continue;
++
++              ret = rtl8365mb_mib_counter_read(priv, port, mib->offset,
++                                               mib->length, &cnt[i]);
++              if (ret)
++                      break;
++      }
++      mutex_unlock(&mb->mib_lock);
++
++      /* The RTL8365MB-VC exposes MIB objects, which we have to translate into
++       * IEEE 802.3 Managed Objects. This is not always completely faithful,
++       * but we try out best. See RFC 3635 for a detailed treatment of the
++       * subject.
++       */
++
++      mac_stats->FramesTransmittedOK = cnt[RTL8365MB_MIB_ifOutUcastPkts] +
++                                       cnt[RTL8365MB_MIB_ifOutMulticastPkts] +
++                                       cnt[RTL8365MB_MIB_ifOutBroadcastPkts] +
++                                       cnt[RTL8365MB_MIB_dot3OutPauseFrames] -
++                                       cnt[RTL8365MB_MIB_ifOutDiscards];
++      mac_stats->SingleCollisionFrames =
++              cnt[RTL8365MB_MIB_dot3StatsSingleCollisionFrames];
++      mac_stats->MultipleCollisionFrames =
++              cnt[RTL8365MB_MIB_dot3StatsMultipleCollisionFrames];
++      mac_stats->FramesReceivedOK = cnt[RTL8365MB_MIB_ifInUcastPkts] +
++                                    cnt[RTL8365MB_MIB_ifInMulticastPkts] +
++                                    cnt[RTL8365MB_MIB_ifInBroadcastPkts] +
++                                    cnt[RTL8365MB_MIB_dot3InPauseFrames];
++      mac_stats->FrameCheckSequenceErrors =
++              cnt[RTL8365MB_MIB_dot3StatsFCSErrors];
++      mac_stats->OctetsTransmittedOK = cnt[RTL8365MB_MIB_ifOutOctets] -
++                                       18 * mac_stats->FramesTransmittedOK;
++      mac_stats->FramesWithDeferredXmissions =
++              cnt[RTL8365MB_MIB_dot3StatsDeferredTransmissions];
++      mac_stats->LateCollisions = cnt[RTL8365MB_MIB_dot3StatsLateCollisions];
++      mac_stats->FramesAbortedDueToXSColls =
++              cnt[RTL8365MB_MIB_dot3StatsExcessiveCollisions];
++      mac_stats->OctetsReceivedOK = cnt[RTL8365MB_MIB_ifInOctets] -
++                                    18 * mac_stats->FramesReceivedOK;
++      mac_stats->MulticastFramesXmittedOK =
++              cnt[RTL8365MB_MIB_ifOutMulticastPkts];
++      mac_stats->BroadcastFramesXmittedOK =
++              cnt[RTL8365MB_MIB_ifOutBroadcastPkts];
++      mac_stats->MulticastFramesReceivedOK =
++              cnt[RTL8365MB_MIB_ifInMulticastPkts];
++      mac_stats->BroadcastFramesReceivedOK =
++              cnt[RTL8365MB_MIB_ifInBroadcastPkts];
++}
++
++static void rtl8365mb_get_ctrl_stats(struct dsa_switch *ds, int port,
++                                   struct ethtool_eth_ctrl_stats *ctrl_stats)
++{
++      struct realtek_priv *priv = ds->priv;
++      struct rtl8365mb_mib_counter *mib;
++      struct rtl8365mb *mb;
++
++      mb = priv->chip_data;
++      mib = &rtl8365mb_mib_counters[RTL8365MB_MIB_dot3ControlInUnknownOpcodes];
++
++      mutex_lock(&mb->mib_lock);
++      rtl8365mb_mib_counter_read(priv, port, mib->offset, mib->length,
++                                 &ctrl_stats->UnsupportedOpcodesReceived);
++      mutex_unlock(&mb->mib_lock);
++}
++
++static void rtl8365mb_stats_update(struct realtek_priv *priv, int port)
++{
++      u64 cnt[RTL8365MB_MIB_END] = {
++              [RTL8365MB_MIB_ifOutOctets] = 1,
++              [RTL8365MB_MIB_ifOutUcastPkts] = 1,
++              [RTL8365MB_MIB_ifOutMulticastPkts] = 1,
++              [RTL8365MB_MIB_ifOutBroadcastPkts] = 1,
++              [RTL8365MB_MIB_ifOutDiscards] = 1,
++              [RTL8365MB_MIB_ifInOctets] = 1,
++              [RTL8365MB_MIB_ifInUcastPkts] = 1,
++              [RTL8365MB_MIB_ifInMulticastPkts] = 1,
++              [RTL8365MB_MIB_ifInBroadcastPkts] = 1,
++              [RTL8365MB_MIB_etherStatsDropEvents] = 1,
++              [RTL8365MB_MIB_etherStatsCollisions] = 1,
++              [RTL8365MB_MIB_etherStatsFragments] = 1,
++              [RTL8365MB_MIB_etherStatsJabbers] = 1,
++              [RTL8365MB_MIB_dot3StatsFCSErrors] = 1,
++              [RTL8365MB_MIB_dot3StatsLateCollisions] = 1,
++      };
++      struct rtl8365mb *mb = priv->chip_data;
++      struct rtnl_link_stats64 *stats;
++      int ret;
++      int i;
++
++      stats = &mb->ports[port].stats;
++
++      mutex_lock(&mb->mib_lock);
++      for (i = 0; i < RTL8365MB_MIB_END; i++) {
++              struct rtl8365mb_mib_counter *c = &rtl8365mb_mib_counters[i];
++
++              /* Only fetch required MIB counters (marked = 1 above) */
++              if (!cnt[i])
++                      continue;
++
++              ret = rtl8365mb_mib_counter_read(priv, port, c->offset,
++                                               c->length, &cnt[i]);
++              if (ret)
++                      break;
++      }
++      mutex_unlock(&mb->mib_lock);
++
++      /* Don't update statistics if there was an error reading the counters */
++      if (ret)
++              return;
++
++      spin_lock(&mb->ports[port].stats_lock);
++
++      stats->rx_packets = cnt[RTL8365MB_MIB_ifInUcastPkts] +
++                          cnt[RTL8365MB_MIB_ifInMulticastPkts] +
++                          cnt[RTL8365MB_MIB_ifInBroadcastPkts];
++
++      stats->tx_packets = cnt[RTL8365MB_MIB_ifOutUcastPkts] +
++                          cnt[RTL8365MB_MIB_ifOutMulticastPkts] +
++                          cnt[RTL8365MB_MIB_ifOutBroadcastPkts];
++
++      /* if{In,Out}Octets includes FCS - remove it */
++      stats->rx_bytes = cnt[RTL8365MB_MIB_ifInOctets] - 4 * stats->rx_packets;
++      stats->tx_bytes =
++              cnt[RTL8365MB_MIB_ifOutOctets] - 4 * stats->tx_packets;
++
++      stats->rx_dropped = cnt[RTL8365MB_MIB_etherStatsDropEvents];
++      stats->tx_dropped = cnt[RTL8365MB_MIB_ifOutDiscards];
++
++      stats->multicast = cnt[RTL8365MB_MIB_ifInMulticastPkts];
++      stats->collisions = cnt[RTL8365MB_MIB_etherStatsCollisions];
++
++      stats->rx_length_errors = cnt[RTL8365MB_MIB_etherStatsFragments] +
++                                cnt[RTL8365MB_MIB_etherStatsJabbers];
++      stats->rx_crc_errors = cnt[RTL8365MB_MIB_dot3StatsFCSErrors];
++      stats->rx_errors = stats->rx_length_errors + stats->rx_crc_errors;
++
++      stats->tx_aborted_errors = cnt[RTL8365MB_MIB_ifOutDiscards];
++      stats->tx_window_errors = cnt[RTL8365MB_MIB_dot3StatsLateCollisions];
++      stats->tx_errors = stats->tx_aborted_errors + stats->tx_window_errors;
++
++      spin_unlock(&mb->ports[port].stats_lock);
++}
++
++static void rtl8365mb_stats_poll(struct work_struct *work)
++{
++      struct rtl8365mb_port *p = container_of(to_delayed_work(work),
++                                              struct rtl8365mb_port,
++                                              mib_work);
++      struct realtek_priv *priv = p->priv;
++
++      rtl8365mb_stats_update(priv, p->index);
++
++      schedule_delayed_work(&p->mib_work, RTL8365MB_STATS_INTERVAL_JIFFIES);
++}
++
++static void rtl8365mb_get_stats64(struct dsa_switch *ds, int port,
++                                struct rtnl_link_stats64 *s)
++{
++      struct realtek_priv *priv = ds->priv;
++      struct rtl8365mb_port *p;
++      struct rtl8365mb *mb;
++
++      mb = priv->chip_data;
++      p = &mb->ports[port];
++
++      spin_lock(&p->stats_lock);
++      memcpy(s, &p->stats, sizeof(*s));
++      spin_unlock(&p->stats_lock);
++}
++
++static void rtl8365mb_stats_setup(struct realtek_priv *priv)
++{
++      struct rtl8365mb *mb = priv->chip_data;
++      struct dsa_switch *ds = &priv->ds;
++      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);
++
++      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);
++
++              /* This work polls the MIB counters and keeps the stats64 data
++               * up-to-date.
++               */
++              INIT_DELAYED_WORK(&p->mib_work, rtl8365mb_stats_poll);
++      }
++}
++
++static void rtl8365mb_stats_teardown(struct realtek_priv *priv)
++{
++      struct rtl8365mb *mb = priv->chip_data;
++      struct dsa_switch *ds = &priv->ds;
++      struct dsa_port *dp;
++
++      dsa_switch_for_each_available_port(dp, ds) {
++              struct rtl8365mb_port *p = &mb->ports[dp->index];
++
++              cancel_delayed_work_sync(&p->mib_work);
++      }
++}
++
++static int rtl8365mb_get_and_clear_status_reg(struct realtek_priv *priv, u32 reg,
++                                            u32 *val)
++{
++      int ret;
++
++      ret = regmap_read(priv->map, reg, val);
++      if (ret)
++              return ret;
++
++      return regmap_write(priv->map, reg, *val);
++}
++
++static irqreturn_t rtl8365mb_irq(int irq, void *data)
++{
++      struct realtek_priv *priv = data;
++      unsigned long line_changes = 0;
++      u32 stat;
++      int line;
++      int ret;
++
++      ret = rtl8365mb_get_and_clear_status_reg(priv, RTL8365MB_INTR_STATUS_REG,
++                                               &stat);
++      if (ret)
++              goto out_error;
++
++      if (stat & RTL8365MB_INTR_LINK_CHANGE_MASK) {
++              u32 linkdown_ind;
++              u32 linkup_ind;
++              u32 val;
++
++              ret = rtl8365mb_get_and_clear_status_reg(
++                      priv, RTL8365MB_PORT_LINKUP_IND_REG, &val);
++              if (ret)
++                      goto out_error;
++
++              linkup_ind = FIELD_GET(RTL8365MB_PORT_LINKUP_IND_MASK, val);
++
++              ret = rtl8365mb_get_and_clear_status_reg(
++                      priv, RTL8365MB_PORT_LINKDOWN_IND_REG, &val);
++              if (ret)
++                      goto out_error;
++
++              linkdown_ind = FIELD_GET(RTL8365MB_PORT_LINKDOWN_IND_MASK, val);
++
++              line_changes = linkup_ind | linkdown_ind;
++      }
++
++      if (!line_changes)
++              goto out_none;
++
++      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);
++      }
++
++      return IRQ_HANDLED;
++
++out_error:
++      dev_err(priv->dev, "failed to read interrupt status: %pe\n",
++              ERR_PTR(ret));
++
++out_none:
++      return IRQ_NONE;
++}
++
++static struct irq_chip rtl8365mb_irq_chip = {
++      .name = "rtl8365mb",
++      /* The hardware doesn't support masking IRQs on a per-port basis */
++};
++
++static int rtl8365mb_irq_map(struct irq_domain *domain, unsigned int irq,
++                           irq_hw_number_t hwirq)
++{
++      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;
++}
++
++static void rtl8365mb_irq_unmap(struct irq_domain *d, unsigned int irq)
++{
++      irq_set_nested_thread(irq, 0);
++      irq_set_chip_and_handler(irq, NULL, NULL);
++      irq_set_chip_data(irq, NULL);
++}
++
++static const struct irq_domain_ops rtl8365mb_irqdomain_ops = {
++      .map = rtl8365mb_irq_map,
++      .unmap = rtl8365mb_irq_unmap,
++      .xlate = irq_domain_xlate_onecell,
++};
++
++static int rtl8365mb_set_irq_enable(struct realtek_priv *priv, bool enable)
++{
++      return regmap_update_bits(priv->map, RTL8365MB_INTR_CTRL_REG,
++                                RTL8365MB_INTR_LINK_CHANGE_MASK,
++                                FIELD_PREP(RTL8365MB_INTR_LINK_CHANGE_MASK,
++                                           enable ? 1 : 0));
++}
++
++static int rtl8365mb_irq_enable(struct realtek_priv *priv)
++{
++      return rtl8365mb_set_irq_enable(priv, true);
++}
++
++static int rtl8365mb_irq_disable(struct realtek_priv *priv)
++{
++      return rtl8365mb_set_irq_enable(priv, false);
++}
++
++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;
++
++      intc = of_get_child_by_name(priv->dev->of_node, "interrupt-controller");
++      if (!intc) {
++              dev_err(priv->dev, "missing child interrupt-controller node\n");
++              return -EINVAL;
++      }
++
++      /* rtl8365mb IRQs cascade off this one */
++      irq = of_irq_get(intc, 0);
++      if (irq <= 0) {
++              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;
++      }
++
++      /* 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) {
++              dev_err(priv->dev, "failed to add irq domain\n");
++              ret = -ENOMEM;
++              goto out_put_node;
++      }
++
++      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");
++                      ret = -EINVAL;
++                      goto out_remove_irqdomain;
++              }
++
++              irq_set_parent(virq, irq);
++      }
++
++      /* Configure chip interrupt signal polarity */
++      irq_trig = irq_get_trigger_type(irq);
++      switch (irq_trig) {
++      case IRQF_TRIGGER_RISING:
++      case IRQF_TRIGGER_HIGH:
++              val = RTL8365MB_INTR_POLARITY_HIGH;
++              break;
++      case IRQF_TRIGGER_FALLING:
++      case IRQF_TRIGGER_LOW:
++              val = RTL8365MB_INTR_POLARITY_LOW;
++              break;
++      default:
++              dev_err(priv->dev, "unsupported irq trigger type %u\n",
++                      irq_trig);
++              ret = -EINVAL;
++              goto out_remove_irqdomain;
++      }
++
++      ret = regmap_update_bits(priv->map, RTL8365MB_INTR_POLARITY_REG,
++                               RTL8365MB_INTR_POLARITY_MASK,
++                               FIELD_PREP(RTL8365MB_INTR_POLARITY_MASK, val));
++      if (ret)
++              goto out_remove_irqdomain;
++
++      /* Disable the interrupt in case the chip has it enabled on reset */
++      ret = rtl8365mb_irq_disable(priv);
++      if (ret)
++              goto out_remove_irqdomain;
++
++      /* Clear the interrupt status register */
++      ret = regmap_write(priv->map, RTL8365MB_INTR_STATUS_REG,
++                         RTL8365MB_INTR_ALL_MASK);
++      if (ret)
++              goto out_remove_irqdomain;
++
++      ret = request_threaded_irq(irq, NULL, rtl8365mb_irq, IRQF_ONESHOT,
++                                 "rtl8365mb", priv);
++      if (ret) {
++              dev_err(priv->dev, "failed to request irq: %pe\n",
++                      ERR_PTR(ret));
++              goto out_remove_irqdomain;
++      }
++
++      ret = rtl8365mb_irq_enable(priv);
++      if (ret)
++              goto out_free_irq;
++
++      of_node_put(intc);
++
++      return 0;
++
++out_free_irq:
++      free_irq(mb->irq, priv);
++
++out_remove_irqdomain:
++      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;
++}
++
++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;
++
++      if (mb->irq) {
++              free_irq(mb->irq, priv);
++              mb->irq = 0;
++      }
++
++      if (priv->irqdomain) {
++              /* 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);
++              priv->irqdomain = NULL;
++      }
++}
++
++static int rtl8365mb_cpu_config(struct realtek_priv *priv)
++{
++      struct rtl8365mb *mb = priv->chip_data;
++      struct rtl8365mb_cpu *cpu = &mb->cpu;
++      u32 val;
++      int ret;
++
++      ret = regmap_update_bits(priv->map, RTL8365MB_CPU_PORT_MASK_REG,
++                               RTL8365MB_CPU_PORT_MASK_MASK,
++                               FIELD_PREP(RTL8365MB_CPU_PORT_MASK_MASK,
++                                          cpu->mask));
++      if (ret)
++              return ret;
++
++      val = FIELD_PREP(RTL8365MB_CPU_CTRL_EN_MASK, cpu->enable ? 1 : 0) |
++            FIELD_PREP(RTL8365MB_CPU_CTRL_INSERTMODE_MASK, cpu->insert) |
++            FIELD_PREP(RTL8365MB_CPU_CTRL_TAG_POSITION_MASK, cpu->position) |
++            FIELD_PREP(RTL8365MB_CPU_CTRL_RXBYTECOUNT_MASK, cpu->rx_length) |
++            FIELD_PREP(RTL8365MB_CPU_CTRL_TAG_FORMAT_MASK, cpu->format) |
++            FIELD_PREP(RTL8365MB_CPU_CTRL_TRAP_PORT_MASK, cpu->trap_port & 0x7) |
++            FIELD_PREP(RTL8365MB_CPU_CTRL_TRAP_PORT_EXT_MASK,
++                       cpu->trap_port >> 3 & 0x1);
++      ret = regmap_write(priv->map, RTL8365MB_CPU_CTRL_REG, val);
++      if (ret)
++              return ret;
++
++      return 0;
++}
++
++static int rtl8365mb_change_tag_protocol(struct dsa_switch *ds,
++                                       enum dsa_tag_protocol proto)
++{
++      struct realtek_priv *priv = ds->priv;
++      struct rtl8365mb_cpu *cpu;
++      struct rtl8365mb *mb;
++
++      mb = priv->chip_data;
++      cpu = &mb->cpu;
++
++      switch (proto) {
++      case DSA_TAG_PROTO_RTL8_4:
++              cpu->format = RTL8365MB_CPU_FORMAT_8BYTES;
++              cpu->position = RTL8365MB_CPU_POS_AFTER_SA;
++              break;
++      case DSA_TAG_PROTO_RTL8_4T:
++              cpu->format = RTL8365MB_CPU_FORMAT_8BYTES;
++              cpu->position = RTL8365MB_CPU_POS_BEFORE_CRC;
++              break;
++      /* The switch also supports a 4-byte format, similar to rtl4a but with
++       * the same 0x04 8-bit version and probably 8-bit port source/dest.
++       * There is no public doc about it. Not supported yet and it will probably
++       * never be.
++       */
++      default:
++              return -EPROTONOSUPPORT;
++      }
++
++      return rtl8365mb_cpu_config(priv);
++}
++
++static int rtl8365mb_switch_init(struct realtek_priv *priv)
++{
++      struct rtl8365mb *mb = priv->chip_data;
++      const struct rtl8365mb_chip_info *ci;
++      int ret;
++      int i;
++
++      ci = mb->chip_info;
++
++      /* Do any chip-specific init jam before getting to the common stuff */
++      if (ci->jam_table) {
++              for (i = 0; i < ci->jam_size; i++) {
++                      ret = regmap_write(priv->map, ci->jam_table[i].reg,
++                                         ci->jam_table[i].val);
++                      if (ret)
++                              return ret;
++              }
++      }
++
++      /* Common init jam */
++      for (i = 0; i < ARRAY_SIZE(rtl8365mb_init_jam_common); i++) {
++              ret = regmap_write(priv->map, rtl8365mb_init_jam_common[i].reg,
++                                 rtl8365mb_init_jam_common[i].val);
++              if (ret)
++                      return ret;
++      }
++
++      return 0;
++}
++
++static int rtl8365mb_reset_chip(struct realtek_priv *priv)
++{
++      u32 val;
++
++      priv->write_reg_noack(priv, RTL8365MB_CHIP_RESET_REG,
++                            FIELD_PREP(RTL8365MB_CHIP_RESET_HW_MASK, 1));
++
++      /* Realtek documentation says the chip needs 1 second to reset. Sleep
++       * for 100 ms before accessing any registers to prevent ACK timeouts.
++       */
++      msleep(100);
++      return regmap_read_poll_timeout(priv->map, RTL8365MB_CHIP_RESET_REG, val,
++                                      !(val & RTL8365MB_CHIP_RESET_HW_MASK),
++                                      20000, 1e6);
++}
++
++static int rtl8365mb_setup(struct dsa_switch *ds)
++{
++      struct realtek_priv *priv = ds->priv;
++      struct rtl8365mb_cpu *cpu;
++      u32 downports_mask = 0;
++      u32 upports_mask = 0;
++      struct rtl8365mb *mb;
++      struct dsa_port *dp;
++      int ret;
++
++      mb = priv->chip_data;
++      cpu = &mb->cpu;
++
++      ret = rtl8365mb_reset_chip(priv);
++      if (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: %pe\n",
++                      ERR_PTR(ret));
++              goto out_error;
++      }
++
++      /* Set up cascading IRQs */
++      ret = rtl8365mb_irq_setup(priv);
++      if (ret == -EPROBE_DEFER)
++              return ret;
++      else if (ret)
++              dev_info(priv->dev, "no interrupt support\n");
++
++      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_port_is_dsa(dp)) {
++                      dev_err(priv->dev, "Cascading (DSA link) not supported\n");
++                      ret = -EOPNOTSUPP;
++                      goto out_teardown_irq;
++              }
++      }
++
++      /* Start with all ports blocked, including unused ports */
++      dsa_switch_for_each_port(dp, ds) {
++              struct rtl8365mb_port *p = &mb->ports[dp->index];
++
++              /* 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);
++
++              /* Start with all port completely isolated */
++              ret = rtl8365mb_port_set_isolation(priv, dp->index, 0);
++              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;
++
++              /* 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);
++      }
++
++      /* 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, dp->index,
++                                                 upports_mask);
++              if (ret)
++                      goto out_teardown_irq;
++
++              /* 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;
++      }
++
++      cpu->mask = upports_mask;
++      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;
++
++      ret = rtl8365mb_port_change_mtu(ds, cpu->trap_port, ETH_DATA_LEN);
++      if (ret)
++              goto out_teardown_irq;
++
++      ret = rtl83xx_setup_user_mdio(ds);
++      if (ret) {
++              dev_err(priv->dev, "could not set up MDIO bus\n");
++              goto out_teardown_irq;
++      }
++
++      /* Start statistics counter polling */
++      rtl8365mb_stats_setup(priv);
++
++      return 0;
++
++out_teardown_irq:
++      rtl8365mb_irq_teardown(priv);
++
++out_error:
++      return ret;
++}
++
++static void rtl8365mb_teardown(struct dsa_switch *ds)
++{
++      struct realtek_priv *priv = ds->priv;
++
++      rtl8365mb_stats_teardown(priv);
++      rtl8365mb_irq_teardown(priv);
++}
++
++static int rtl8365mb_get_chip_id_and_ver(struct regmap *map, u32 *id, u32 *ver)
++{
++      int ret;
++
++      /* For some reason we have to write a magic value to an arbitrary
++       * register whenever accessing the chip ID/version registers.
++       */
++      ret = regmap_write(map, RTL8365MB_MAGIC_REG, RTL8365MB_MAGIC_VALUE);
++      if (ret)
++              return ret;
++
++      ret = regmap_read(map, RTL8365MB_CHIP_ID_REG, id);
++      if (ret)
++              return ret;
++
++      ret = regmap_read(map, RTL8365MB_CHIP_VER_REG, ver);
++      if (ret)
++              return ret;
++
++      /* Reset magic register */
++      ret = regmap_write(map, RTL8365MB_MAGIC_REG, 0);
++      if (ret)
++              return ret;
++
++      return 0;
++}
++
++static int rtl8365mb_detect(struct realtek_priv *priv)
++{
++      struct rtl8365mb *mb = priv->chip_data;
++      u32 chip_id;
++      u32 chip_ver;
++      int ret;
++      int i;
++
++      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: %pe\n",
++                      ERR_PTR(ret));
++              return ret;
++      }
++
++      for (i = 0; i < ARRAY_SIZE(rtl8365mb_chip_infos); i++) {
++              const struct rtl8365mb_chip_info *ci = &rtl8365mb_chip_infos[i];
++
++              if (ci->chip_id == chip_id && ci->chip_ver == chip_ver) {
++                      mb->chip_info = ci;
++                      break;
++              }
++      }
++
++      if (!mb->chip_info) {
++              dev_err(priv->dev,
++                      "unrecognized switch (id=0x%04x, ver=0x%04x)", chip_id,
++                      chip_ver);
++              return -ENODEV;
++      }
++
++      dev_info(priv->dev, "found an %s switch\n", mb->chip_info->name);
++
++      priv->num_ports = RTL8365MB_MAX_NUM_PORTS;
++      mb->priv = priv;
++      mb->cpu.trap_port = RTL8365MB_MAX_NUM_PORTS;
++      mb->cpu.insert = RTL8365MB_CPU_INSERT_TO_ALL;
++      mb->cpu.position = RTL8365MB_CPU_POS_AFTER_SA;
++      mb->cpu.rx_length = RTL8365MB_CPU_RXLEN_64BYTES;
++      mb->cpu.format = RTL8365MB_CPU_FORMAT_8BYTES;
++
++      return 0;
++}
++
++static const struct phylink_mac_ops rtl8365mb_phylink_mac_ops = {
++      .mac_config = rtl8365mb_phylink_mac_config,
++      .mac_link_down = rtl8365mb_phylink_mac_link_down,
++      .mac_link_up = rtl8365mb_phylink_mac_link_up,
++};
++
++static const struct dsa_switch_ops rtl8365mb_switch_ops = {
++      .get_tag_protocol = rtl8365mb_get_tag_protocol,
++      .change_tag_protocol = rtl8365mb_change_tag_protocol,
++      .setup = rtl8365mb_setup,
++      .teardown = rtl8365mb_teardown,
++      .phylink_get_caps = rtl8365mb_phylink_get_caps,
++      .port_stp_state_set = rtl8365mb_port_stp_state_set,
++      .get_strings = rtl8365mb_get_strings,
++      .get_ethtool_stats = rtl8365mb_get_ethtool_stats,
++      .get_sset_count = rtl8365mb_get_sset_count,
++      .get_eth_phy_stats = rtl8365mb_get_phy_stats,
++      .get_eth_mac_stats = rtl8365mb_get_mac_stats,
++      .get_eth_ctrl_stats = rtl8365mb_get_ctrl_stats,
++      .get_stats64 = rtl8365mb_get_stats64,
++      .port_change_mtu = rtl8365mb_port_change_mtu,
++      .port_max_mtu = rtl8365mb_port_max_mtu,
++};
++
++static const struct realtek_ops rtl8365mb_ops = {
++      .detect = rtl8365mb_detect,
++      .phy_read = rtl8365mb_phy_read,
++      .phy_write = rtl8365mb_phy_write,
++};
++
++const struct realtek_variant rtl8365mb_variant = {
++      .ds_ops = &rtl8365mb_switch_ops,
++      .ops = &rtl8365mb_ops,
++      .phylink_mac_ops = &rtl8365mb_phylink_mac_ops,
++      .clk_delay = 10,
++      .cmd_read = 0xb9,
++      .cmd_write = 0xb8,
++      .chip_data_sz = sizeof(struct rtl8365mb),
++};
++
++static const struct of_device_id rtl8365mb_of_match[] = {
++      { .compatible = "realtek,rtl8365mb", .data = &rtl8365mb_variant, },
++      { /* sentinel */ },
++};
++MODULE_DEVICE_TABLE(of, rtl8365mb_of_match);
++
++static struct platform_driver rtl8365mb_smi_driver = {
++      .driver = {
++              .name = "rtl8365mb-smi",
++              .of_match_table = rtl8365mb_of_match,
++      },
++      .probe  = realtek_smi_probe,
++      .remove_new = realtek_smi_remove,
++      .shutdown = realtek_smi_shutdown,
++};
++
++static struct mdio_driver rtl8365mb_mdio_driver = {
++      .mdiodrv.driver = {
++              .name = "rtl8365mb-mdio",
++              .of_match_table = rtl8365mb_of_match,
++      },
++      .probe  = realtek_mdio_probe,
++      .remove = realtek_mdio_remove,
++      .shutdown = realtek_mdio_shutdown,
++};
++
++static int rtl8365mb_init(void)
++{
++      int ret;
++
++      ret = realtek_mdio_driver_register(&rtl8365mb_mdio_driver);
++      if (ret)
++              return ret;
++
++      ret = realtek_smi_driver_register(&rtl8365mb_smi_driver);
++      if (ret) {
++              realtek_mdio_driver_unregister(&rtl8365mb_mdio_driver);
++              return ret;
++      }
++
++      return 0;
++}
++module_init(rtl8365mb_init);
++
++static void __exit rtl8365mb_exit(void)
++{
++      realtek_smi_driver_unregister(&rtl8365mb_smi_driver);
++      realtek_mdio_driver_unregister(&rtl8365mb_mdio_driver);
++}
++module_exit(rtl8365mb_exit);
++
++MODULE_AUTHOR("Alvin Šipraga <alsi@bang-olufsen.dk>");
++MODULE_DESCRIPTION("Driver for RTL8365MB-VC ethernet switch");
++MODULE_LICENSE("GPL");
++MODULE_IMPORT_NS(REALTEK_DSA);
diff --git a/target/linux/generic/backport-6.12/942-05-v7.2-net-dsa-realtek-rtl8365mb-add-table-lookup-interface.patch b/target/linux/generic/backport-6.12/942-05-v7.2-net-dsa-realtek-rtl8365mb-add-table-lookup-interface.patch
new file mode 100644 (file)
index 0000000..1239c79
--- /dev/null
@@ -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 */
diff --git a/target/linux/generic/backport-6.12/942-06-v7.2-net-dsa-realtek-rtl8365mb-add-VLAN-support.patch b/target/linux/generic/backport-6.12/942-06-v7.2-net-dsa-realtek-rtl8365mb-add-VLAN-support.patch
new file mode 100644 (file)
index 0000000..3d3e114
--- /dev/null
@@ -0,0 +1,1461 @@
+From 9da2c8672f77108a1f09232320f22225ab53dde9 Mon Sep 17 00:00:00 2001
+From: Alvin Šipraga <alsi@bang-olufsen.dk>
+Date: Sat, 6 Jun 2026 05:29:30 -0300
+Subject: net: dsa: realtek: rtl8365mb: add VLAN support
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+Realtek RTL8365MB switches (a.k.a. RTL8367C family) use two different
+structures for VLANs:
+
+- VLAN4K: A full table with 4096 entries defining port membership and
+  tagging.
+- VLANMC: A smaller table with 32 entries used primarily for PVID
+  assignment.
+
+In this hardware, a port's PVID must point to an index in the VLANMC
+table rather than a VID directly. Since the VLANMC table is limited to
+32 entries, the driver implements a dynamic allocation scheme to
+maximize resource usage:
+
+- VLAN4K is treated by the driver as the source of truth for membership.
+- A VLANMC entry is only allocated when a port is configured to use a
+  specific VID as its PVID.
+- VLANMC entries are deleted when no longer needed as a PVID by any port.
+
+Although VLANMC has a members field, the switch only checks membership
+in the VLAN4K table. This driver will use VLANMC members field as way to
+track which ports are using that entry as PVID.
+
+VLANMC index 0, although a valid entry, is reserved in this driver as a
+neutral PVID value for ports not using a specific PVID.
+
+In the subsequent RTL8367D switch family, VLANMC table was
+removed and PVID assignment was delegated to a dedicated set of
+registers.
+
+The use of FIELD_PREP for reconstructing LO/HI values was suggested by
+Yury Norov.
+
+Fix for vlan_setup and vlan_filtering was suggested by Abdulkader
+Alrezej.
+
+Suggested-by: Yury Norov <ynorov@nvidia.com>
+Suggested-by: Abdulkader Alrezej <abdulkader.alrezej@gmail.com>
+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-6-b9e409687cbe@gmail.com
+Signed-off-by: Jakub Kicinski <kuba@kernel.org>
+---
+ drivers/net/dsa/realtek/Makefile         |   1 +
+ drivers/net/dsa/realtek/realtek.h        |   5 +
+ drivers/net/dsa/realtek/rtl8365mb_main.c | 342 +++++++++++
+ drivers/net/dsa/realtek/rtl8365mb_vlan.c | 944 +++++++++++++++++++++++++++++++
+ drivers/net/dsa/realtek/rtl8365mb_vlan.h |  39 ++
+ drivers/net/dsa/realtek/rtl83xx.c        |   1 +
+ 6 files changed, 1332 insertions(+)
+
+--- a/drivers/net/dsa/realtek/Makefile
++++ b/drivers/net/dsa/realtek/Makefile
+@@ -18,4 +18,5 @@ endif
+ obj-$(CONFIG_NET_DSA_REALTEK_RTL8365MB) += rtl8365mb.o
+ rtl8365mb-objs := rtl8365mb_main.o \
+                 rtl8365mb_table.o \
++                rtl8365mb_vlan.o \
+ # end of rtl8365mb-objs
+--- a/drivers/net/dsa/realtek/realtek.h
++++ b/drivers/net/dsa/realtek/realtek.h
+@@ -57,6 +57,11 @@ struct realtek_priv {
+       struct regmap           *map;
+       struct regmap           *map_nolock;
+       struct mutex            map_lock;
++      /* vlan_lock protects against concurrent Read-Modify-Write operations
++       * on the global VLAN 4K and VLANMC tables, such as when adding or
++       * deleting port VLAN memberships and PVID configurations.
++       */
++      struct mutex            vlan_lock;
+       struct mii_bus          *user_mii_bus;
+       struct mii_bus          *bus;
+       int                     mdio_addr;
+--- a/drivers/net/dsa/realtek/rtl8365mb_main.c
++++ b/drivers/net/dsa/realtek/rtl8365mb_main.c
+@@ -104,6 +104,7 @@
+ #include "realtek-smi.h"
+ #include "realtek-mdio.h"
+ #include "rtl83xx.h"
++#include "rtl8365mb_vlan.h"
+ /* Family-specific data and limits */
+ #define RTL8365MB_PHYADDRMAX          7
+@@ -292,6 +293,57 @@
+ #define   RTL8365MB_MSTI_CTRL_PORT_STATE_MASK(_physport) \
+               (0x3 << RTL8365MB_MSTI_CTRL_PORT_STATE_OFFSET((_physport)))
++/* Miscellaneous port configuration register, incl. VLAN egress mode */
++#define RTL8365MB_PORT_MISC_CFG_REG_BASE                      0x000E
++#define RTL8365MB_PORT_MISC_CFG_REG(_p) \
++              (RTL8365MB_PORT_MISC_CFG_REG_BASE + ((_p) << 5))
++#define   RTL8365MB_PORT_MISC_CFG_SMALL_TAG_IPG_MASK          0x8000
++#define   RTL8365MB_PORT_MISC_CFG_TX_ITFSP_MODE_MASK          0x4000
++#define   RTL8365MB_PORT_MISC_CFG_FLOWCTRL_INDEP_MASK         0x2000
++#define   RTL8365MB_PORT_MISC_CFG_DOT1Q_REMARK_ENABLE_MASK    0x1000
++#define   RTL8365MB_PORT_MISC_CFG_INGRESSBW_FLOWCTRL_MASK     0x0800
++#define   RTL8365MB_PORT_MISC_CFG_INGRESSBW_IFG_MASK          0x0400
++#define   RTL8365MB_PORT_MISC_CFG_RX_SPC_MASK                 0x0200
++#define   RTL8365MB_PORT_MISC_CFG_CRC_SKIP_MASK                       0x0100
++#define   RTL8365MB_PORT_MISC_CFG_PKTGEN_TX_FIRST_MASK                0x0080
++#define   RTL8365MB_PORT_MISC_CFG_MAC_LOOPBACK_MASK           0x0040
++/* See &rtl8365mb_vlan_egress_mode */
++#define   RTL8365MB_PORT_MISC_CFG_VLAN_EGRESS_MODE_MASK               0x0030
++#define   RTL8365MB_PORT_MISC_CFG_CONGESTION_SUSTAIN_TIME_MASK        0x000F
++
++/**
++ * enum rtl8365mb_vlan_egress_mode - port VLAN egress mode
++ * @RTL8365MB_VLAN_EGRESS_MODE_ORIGINAL: follow untag mask in VLAN4k table entry
++ * @RTL8365MB_VLAN_EGRESS_MODE_KEEP: the VLAN tag format of egressed packets
++ * will remain the same as their ingressed format, but the priority and VID
++ * fields may be altered
++ * @RTL8365MB_VLAN_EGRESS_MODE_PRI_TAG: always egress with priority tag
++ * @RTL8365MB_VLAN_EGRESS_MODE_REAL_KEEP: the VLAN tag format of egressed
++ * packets will remain the same as their ingressed format, and neither the
++ * priority nor VID fields can be altered
++ */
++enum rtl8365mb_vlan_egress_mode {
++      RTL8365MB_VLAN_EGRESS_MODE_ORIGINAL = 0,
++      RTL8365MB_VLAN_EGRESS_MODE_KEEP = 1,
++      RTL8365MB_VLAN_EGRESS_MODE_PRI_TAG = 2,
++      RTL8365MB_VLAN_EGRESS_MODE_REAL_KEEP = 3,
++};
++
++/* VLAN control register */
++#define RTL8365MB_VLAN_CTRL_REG                       0x07A8
++#define   RTL8365MB_VLAN_CTRL_EN_MASK         0x0001
++
++/* VLAN ingress filter register */
++#define RTL8365MB_VLAN_INGRESS_REG                            0x07A9
++#define   RTL8365MB_VLAN_INGRESS_MASK                         GENMASK(10, 0)
++#define   RTL8365MB_VLAN_INGRESS_FILTER_PORT_EN_OFFSET(_p)    (_p)
++#define   RTL8365MB_VLAN_INGRESS_FILTER_PORT_EN_MASK(_p)      BIT(_p)
++
++/* VLAN "transparent" setting registers */
++#define RTL8365MB_VLAN_EGRESS_TRANSPARENT_REG_BASE    0x09D0
++#define RTL8365MB_VLAN_EGRESS_TRANSPARENT_REG(_p) \
++              (RTL8365MB_VLAN_EGRESS_TRANSPARENT_REG_BASE + (_p))
++
+ /* MIB counter value registers */
+ #define RTL8365MB_MIB_COUNTER_BASE    0x1000
+ #define RTL8365MB_MIB_COUNTER_REG(_x) (RTL8365MB_MIB_COUNTER_BASE + (_x))
+@@ -1210,6 +1262,286 @@ static void rtl8365mb_port_stp_state_set
+                          val << RTL8365MB_MSTI_CTRL_PORT_STATE_OFFSET(port));
+ }
++static int rtl8365mb_port_set_transparent(struct realtek_priv *priv,
++                                        int igr_port, int egr_port,
++                                        bool enable)
++{
++      dev_dbg(priv->dev, "%s transparent VLAN from %d to %d\n",
++              enable ? "Enable" : "Disable", igr_port, egr_port);
++
++      /* "Transparent" between the two ports means that packets forwarded by
++       * igr_port and egressed on egr_port will not be filtered by the usual
++       * VLAN membership settings.
++       */
++      return regmap_update_bits(priv->map,
++                      RTL8365MB_VLAN_EGRESS_TRANSPARENT_REG(egr_port),
++                      BIT(igr_port), enable ? BIT(igr_port) : 0);
++}
++
++static int rtl8365mb_port_set_ingress_filtering(struct realtek_priv *priv,
++                                              int port, bool enable)
++{
++      /* Ingress filtering enabled: Discard VLAN-tagged frames if the port is
++       * not a member of the VLAN with which the packet is associated.
++       * Untagged packets will also be discarded unless the port has a PVID
++       * programmed. Priority-tagged frames are treated as untagged frames.
++       *
++       * Ingress filtering disabled: Accept all tagged and untagged frames.
++       */
++      return regmap_update_bits(priv->map, RTL8365MB_VLAN_INGRESS_REG,
++                      RTL8365MB_VLAN_INGRESS_FILTER_PORT_EN_MASK(port),
++                      enable ?
++                      RTL8365MB_VLAN_INGRESS_FILTER_PORT_EN_MASK(port) :
++                      0);
++}
++
++static int
++rtl8365mb_port_set_vlan_egress_mode(struct realtek_priv *priv, int port,
++                                  enum rtl8365mb_vlan_egress_mode mode)
++{
++      u32 val;
++
++      val = FIELD_PREP(RTL8365MB_PORT_MISC_CFG_VLAN_EGRESS_MODE_MASK, mode);
++      return regmap_update_bits(priv->map,
++                      RTL8365MB_PORT_MISC_CFG_REG(port),
++                      RTL8365MB_PORT_MISC_CFG_VLAN_EGRESS_MODE_MASK, val);
++}
++
++static int rtl8365mb_port_vlan_filtering(struct dsa_switch *ds, int port,
++                                       bool vlan_filtering,
++                                       struct netlink_ext_ack *extack)
++{
++      enum rtl8365mb_frame_ingress accepted_frame, prev_accepted_frame;
++      enum rtl8365mb_vlan_egress_mode mode;
++      struct realtek_priv *priv = ds->priv;
++      u32 configured_ports = 0;
++      struct dsa_port *dp;
++      u16 pvid_vid;
++      int ret;
++
++      dev_dbg(priv->dev, "port %d: %s VLAN filtering\n", port,
++              vlan_filtering ? "enable" : "disable");
++
++      ret = rtl8365mb_vlan_port_get_framefilter(priv, port,
++                                                &prev_accepted_frame);
++      if (ret) {
++              NL_SET_ERR_MSG_MOD(extack,
++                                 "Failed to get current framefilter");
++              return ret;
++      }
++
++      /* While filtering, only accepts untagged frames if PVID is enabled */
++      if (vlan_filtering) {
++              ret = rtl8365mb_vlan_port_get_pvid(priv, port, &pvid_vid);
++              if (ret)
++                      return ret;
++
++              if (pvid_vid)
++                      accepted_frame = RTL8365MB_FRAME_TYPE_ANY_FRAME;
++              else
++                      accepted_frame = RTL8365MB_FRAME_TYPE_TAGGED_ONLY;
++      } else {
++              accepted_frame = RTL8365MB_FRAME_TYPE_ANY_FRAME;
++      }
++
++      /* When vlan filter is enable/disabled in a bridge, this function is
++       * called for all member ports. We need to enable/disable ingress
++       * VLAN membership check.
++       */
++      ret = rtl8365mb_port_set_ingress_filtering(priv, port, vlan_filtering);
++      if (ret)
++              return ret;
++
++      /* However, we also enable/disable egress filtering because the switch
++       * still consider the egress interface VLAN membership to forward the
++       * traffic. We enable/disable that check disabling/enabling transparent
++       * VLAN between the ingress port and all other available ports.
++       */
++      dsa_switch_for_each_available_port(dp, ds) {
++              /* port isolation will still keep traffic inside the bridge */
++              ret = rtl8365mb_port_set_transparent(priv, port, dp->index,
++                                                   !vlan_filtering);
++              if (ret)
++                      goto undo_transparent;
++
++              configured_ports |= BIT(dp->index);
++      }
++
++      if (accepted_frame != prev_accepted_frame) {
++              ret = rtl8365mb_vlan_port_set_framefilter(priv, port,
++                                                        accepted_frame);
++              if (ret) {
++                      NL_SET_ERR_MSG_MOD(extack,
++                                         "Failed to set port framefilter");
++                      goto undo_transparent;
++              }
++      }
++
++      /* When VLAN filtering is disabled, preserve frames exactly as received.
++       * Otherwise, the VLAN egress pipeline may still alter tag state
++       * according to VLAN membership and untag configuration.
++       */
++      if (vlan_filtering)
++              mode = RTL8365MB_VLAN_EGRESS_MODE_ORIGINAL;
++      else
++              mode = RTL8365MB_VLAN_EGRESS_MODE_REAL_KEEP;
++
++      ret = rtl8365mb_port_set_vlan_egress_mode(priv, port, mode);
++      if (ret)
++              goto undo_set_framefilter;
++
++      return ret;
++
++undo_set_framefilter:
++      if (prev_accepted_frame != accepted_frame)
++              rtl8365mb_vlan_port_set_framefilter(priv, port,
++                                                  prev_accepted_frame);
++undo_transparent:
++      /* The DSA core guarantees this callback is only invoked on an actual
++       * state transition, ensuring the previous hardware state was the
++       * opposite (!vlan_filtering). It is also called during setup but, in
++       * that case, any failure here aborts the entire switch initialization.
++       *
++       * VLAN_INGRESS and VLAN_EGRESS_TRANSPARENT states are directly derived
++       * from vlan_filtering. That way, we can simply undo it without
++       * checking the current HW state as we do with VLAN_EGRESS_MODE.
++       */
++      dsa_switch_for_each_port(dp, ds) {
++              if (configured_ports & BIT(dp->index))
++                      rtl8365mb_port_set_transparent(priv, port, dp->index,
++                                                     vlan_filtering);
++      }
++
++      rtl8365mb_port_set_ingress_filtering(priv, port, !vlan_filtering);
++
++      return ret;
++}
++
++static int rtl8365mb_port_vlan_add(struct dsa_switch *ds, int port,
++                                 const struct switchdev_obj_port_vlan *vlan,
++                                 struct netlink_ext_ack *extack)
++{
++      bool untagged = !!(vlan->flags & BRIDGE_VLAN_INFO_UNTAGGED);
++      bool pvid = !!(vlan->flags & BRIDGE_VLAN_INFO_PVID);
++      u16 pvid_vid;
++      struct realtek_priv *priv = ds->priv;
++      int ret;
++
++      dev_dbg(priv->dev, "add VLAN %d on port %d, %s, %s\n",
++              vlan->vid, port, untagged ? "untagged" : "tagged",
++              pvid ? "PVID" : "no PVID");
++
++      /* VID == 0 is reserved in this driver */
++      if (vlan->vid == 0) {
++              NL_SET_ERR_MSG_MOD(extack,
++                                 "VLAN 0 is reserved by this driver");
++              return -EOPNOTSUPP;
++      }
++
++      mutex_lock(&priv->vlan_lock);
++
++      ret = rtl8365mb_vlan_port_get_pvid(priv, port, &pvid_vid);
++      if (ret)
++              goto out_unlock;
++
++      /* Set PVID if needed */
++      if (pvid) {
++              ret = rtl8365mb_vlan_pvid_port_set(ds, port, vlan->vid,
++                                                 extack);
++              if (ret)
++                      goto out_unlock;
++      } else {
++              /* or try to unset it if not */
++              ret = rtl8365mb_vlan_pvid_port_clear(ds, port, vlan->vid);
++              if (ret)
++                      goto out_unlock;
++      }
++
++      /* add port to vlan4k. It knows nothing about PVID */
++      ret = rtl8365mb_vlan_4k_port_add(ds, port, vlan, extack);
++      if (ret)
++              goto undo_set_pvid;
++
++      ret = 0;
++      goto out_unlock;
++
++undo_set_pvid:
++      /* undo the pvid definition */
++      if (pvid != (pvid_vid == vlan->vid)) {
++              if (pvid_vid)
++                      (void)rtl8365mb_vlan_pvid_port_set(ds, port, pvid_vid,
++                                                         NULL);
++              else
++                      (void)rtl8365mb_vlan_pvid_port_clear(ds, port,
++                                                           vlan->vid);
++      }
++out_unlock:
++      mutex_unlock(&priv->vlan_lock);
++      return ret;
++}
++
++static int rtl8365mb_port_vlan_del(struct dsa_switch *ds, int port,
++                                 const struct switchdev_obj_port_vlan *vlan)
++{
++      bool untagged = !!(vlan->flags & BRIDGE_VLAN_INFO_UNTAGGED);
++      bool pvid = !!(vlan->flags & BRIDGE_VLAN_INFO_PVID);
++      struct realtek_priv *priv = ds->priv;
++      int ret;
++
++      dev_dbg(priv->dev, "del VLAN %d on port %d, %s, %s\n",
++              vlan->vid, port, untagged ? "untagged" : "tagged",
++              pvid ? "PVID" : "no PVID");
++
++      /* VID == 0 is reserved in this driver */
++      if (vlan->vid == 0)
++              return -EOPNOTSUPP;
++
++      mutex_lock(&priv->vlan_lock);
++      ret = rtl8365mb_vlan_pvid_port_clear(ds, port, vlan->vid);
++      if (ret)
++              goto out_unlock;
++
++      ret = rtl8365mb_vlan_4k_port_del(ds, port, vlan);
++      /* There is little incentive to try to undo the removal of PVID (if it
++       * was really in use) as an error here might indicate the ASIC stopped
++       * to answer.
++       */
++
++out_unlock:
++      mutex_unlock(&priv->vlan_lock);
++      return ret;
++}
++
++/* VLAN support is always enabled in the switch.
++ *
++ * Standalone forwarding relies on transparent VLAN mode combined with per-port
++ * isolation masks restricting egress to CPU ports only.
++ *
++ */
++static int rtl8365mb_vlan_setup(struct dsa_switch *ds)
++{
++      struct realtek_priv *priv = ds->priv;
++      struct dsa_port *dp;
++      int ret;
++
++      dsa_switch_for_each_available_port(dp, ds) {
++              /* Disable vlan-filtering for all ports */
++              ret = rtl8365mb_port_vlan_filtering(ds, dp->index, false, NULL);
++              if (ret) {
++                      dev_err(priv->dev,
++                              "Failed to disable vlan filtering on port %d\n",
++                              dp->index);
++                      return ret;
++              }
++      }
++
++      /* VLAN is always enabled. */
++      ret = regmap_update_bits(priv->map, RTL8365MB_VLAN_CTRL_REG,
++                               RTL8365MB_VLAN_CTRL_EN_MASK,
++                               FIELD_PREP(RTL8365MB_VLAN_CTRL_EN_MASK, 1));
++      return ret;
++}
++
+ static int rtl8365mb_port_set_learning(struct realtek_priv *priv, int port,
+                                      bool enable)
+ {
+@@ -2100,6 +2432,13 @@ static int rtl8365mb_setup(struct dsa_sw
+       if (ret)
+               goto out_teardown_irq;
++      ds->configure_vlan_while_not_filtering = true;
++
++      /* Set up VLAN */
++      ret = rtl8365mb_vlan_setup(ds);
++      if (ret)
++              goto out_teardown_irq;
++
+       ret = rtl83xx_setup_user_mdio(ds);
+       if (ret) {
+               dev_err(priv->dev, "could not set up MDIO bus\n");
+@@ -2210,6 +2549,9 @@ static const struct dsa_switch_ops rtl83
+       .teardown = rtl8365mb_teardown,
+       .phylink_get_caps = rtl8365mb_phylink_get_caps,
+       .port_stp_state_set = rtl8365mb_port_stp_state_set,
++      .port_vlan_add = rtl8365mb_port_vlan_add,
++      .port_vlan_del = rtl8365mb_port_vlan_del,
++      .port_vlan_filtering = rtl8365mb_port_vlan_filtering,
+       .get_strings = rtl8365mb_get_strings,
+       .get_ethtool_stats = rtl8365mb_get_ethtool_stats,
+       .get_sset_count = rtl8365mb_get_sset_count,
+--- /dev/null
++++ b/drivers/net/dsa/realtek/rtl8365mb_vlan.c
+@@ -0,0 +1,944 @@
++// SPDX-License-Identifier: GPL-2.0
++/* VLAN configuration interface for the rtl8365mb switch family
++ *
++ * Copyright (C) 2022 Alvin Šipraga <alsi@bang-olufsen.dk>
++ *
++ * VLAN configuration takes place in two separate domains of the switch: the
++ * VLAN4k table and the VLAN membership configuration (MC) database. While the
++ * VLAN4k table is exhaustive and can be fully populated with 4096 VLAN
++ * configurations, the same does not hold for the VLAN membership configuration
++ * database, which is limited to 32 entries.
++ *
++ * The switch will normally only use the VLAN4k table when making forwarding
++ * decisions. The VLAN membership configuration database is a vestigial ASIC
++ * design and is only used for a few specific features in the rtl8365mb
++ * family. This means that the limit of 32 entries should not hinder us in
++ * programming a huge number of VLANs into the switch.
++ *
++ * One necessary use of the VLAN membership configuration database is for the
++ * programming of a port-based VLAN ID (PVID). The PVID is programmed on a
++ * per-port basis via register field, which refers to a specific VLAN membership
++ * configuration via an index 0~31. In order to maintain coherent behaviour on a
++ * port with a PVID, it is necessary to keep the VLAN configuration synchronized
++ * between the VLAN4k table and the VLAN membership configuration database.
++ *
++ * Since VLAN membership configs are a scarce resource, it will only be used
++ * when strictly needed (i.e. a VLAN with members using PVID). Otherwise, the
++ * VLAN4k will be enough.
++ *
++ * With some exceptions, the entries in both the VLAN4k table and the VLAN
++ * membership configuration database offer the same configuration options. The
++ * differences are as follows:
++ *
++ * 1. VLAN4k entries can specify whether to use Independent or Shared VLAN
++ *    Learning (IVL or SVL respectively). VLAN membership config entries
++ *    cannot. This underscores the fact that VLAN membership configs are not
++ *    involved in the learning process of the ASIC.
++ *
++ * 2. VLAN membership config entries use an "enhanced VLAN ID" (efid), which has
++ *    a range 0~8191 compared with the standard 0~4095 range of the VLAN4k
++ *    table. This underscores the fact that VLAN membership configs can be used
++ *    to group ports on a layer beyond the standard VLAN configuration, which
++ *    may be useful for ACL rules which specify alternative forwarding
++ *    decisions.
++ *
++ * VLANMC index 0 is reserved as a neutral PVID, used for standalone ports.
++ *
++ */
++
++#include "rtl8365mb_vlan.h"
++#include "rtl8365mb_table.h"
++#include <linux/if_bridge.h>
++#include <linux/lockdep.h>
++#include <linux/regmap.h>
++
++/* CVLAN (i.e. VLAN4k) table entry layout, u16[3] */
++#define RTL8365MB_CVLAN_ENTRY_SIZE                    3 /* 48-bits */
++#define RTL8365MB_CVLAN_ENTRY_D0_MBR_MASK             GENMASK(7, 0)
++#define   RTL8365MB_CVLAN_MBR_LO_MASK                 GENMASK(7, 0)
++#define RTL8365MB_CVLAN_ENTRY_D0_UNTAG_MASK           GENMASK(15, 8)
++#define   RTL8365MB_CVLAN_UNTAG_LO_MASK                       GENMASK(7, 0)
++#define RTL8365MB_CVLAN_ENTRY_D1_FID_MASK             GENMASK(3, 0)
++#define RTL8365MB_CVLAN_ENTRY_D1_VBPEN_MASK           GENMASK(4, 4)
++#define RTL8365MB_CVLAN_ENTRY_D1_VBPRI_MASK           GENMASK(7, 5)
++#define RTL8365MB_CVLAN_ENTRY_D1_ENVLANPOL_MASK               GENMASK(8, 8)
++#define RTL8365MB_CVLAN_ENTRY_D1_METERIDX_MASK                GENMASK(13, 9)
++#define   RTL8365MB_CVLAN_METERIDX_LO_MASK            GENMASK(4, 0)
++#define RTL8365MB_CVLAN_ENTRY_D1_IVL_SVL_MASK         GENMASK(14, 14)
++/* extends RTL8365MB_CVLAN_ENTRY_D0_MBR_MASK */
++#define RTL8365MB_CVLAN_ENTRY_D2_MBR_EXT_MASK         GENMASK(2, 0)
++#define   RTL8365MB_CVLAN_MBR_HI_MASK                 GENMASK(10, 8)
++/* extends RTL8365MB_CVLAN_ENTRY_D0_UNTAG_MASK */
++#define RTL8365MB_CVLAN_ENTRY_D2_UNTAG_EXT_MASK               GENMASK(5, 3)
++#define   RTL8365MB_CVLAN_UNTAG_HI_MASK                       GENMASK(10, 8)
++/* extends RTL8365MB_CVLAN_ENTRY_D1_METERIDX_MASK */
++#define RTL8365MB_CVLAN_ENTRY_D2_METERIDX_EXT_MASK    GENMASK(6, 6)
++#define   RTL8365MB_CVLAN_METERIDX_HI_MASK            GENMASK(5, 5)
++
++/* VLAN member configuration registers 0~31, u16[3] */
++#define RTL8365MB_VLAN_MC_BASE                                0x0728
++#define RTL8365MB_VLAN_MC_ENTRY_SIZE                  4 /* 64-bit */
++#define RTL8365MB_VLAN_MC_REG(index) \
++              (RTL8365MB_VLAN_MC_BASE + \
++               (RTL8365MB_VLAN_MC_ENTRY_SIZE * (index)))
++#define   RTL8365MB_VLAN_MC_D0_MBR_MASK                       GENMASK(10, 0)
++#define   RTL8365MB_VLAN_MC_D1_FID_MASK                       GENMASK(3, 0)
++
++#define   RTL8365MB_VLAN_MC_D2_VBPEN_MASK             GENMASK(0, 0)
++#define   RTL8365MB_VLAN_MC_D2_VBPRI_MASK             GENMASK(3, 1)
++#define   RTL8365MB_VLAN_MC_D2_ENVLANPOL_MASK         GENMASK(4, 4)
++#define   RTL8365MB_VLAN_MC_D2_METERIDX_MASK          GENMASK(10, 5)
++#define   RTL8365MB_VLAN_MC_D3_EVID_MASK              GENMASK(12, 0)
++
++/* Some limits for VLAN4k/VLAN membership config entries */
++#define RTL8365MB_PRIORITYMAX 7
++#define RTL8365MB_FIDMAX      15
++#define RTL8365MB_METERMAX    63
++#define RTL8365MB_VLAN_MCMAX  31
++
++/* RTL8367S supports 4k vlans (vid<=4095) and 32 enhanced vlans
++ * for VIDs up to 8191
++ */
++#define RTL8365MB_MAX_4K_VID  0x0FFF /* 4095 */
++#define RTL8365MB_MAX_MC_VID  0x1FFF /* 8191 */
++
++ /* Port-based VID registers 0~5 - each one holds an MC index for two ports */
++#define RTL8365MB_VLAN_PVID_CTRL_BASE                 0x0700
++#define RTL8365MB_VLAN_PVID_CTRL_REG(_p) \
++              (RTL8365MB_VLAN_PVID_CTRL_BASE + ((_p) >> 1))
++#define   RTL8365MB_VLAN_PVID_CTRL_PORT0_MCIDX_MASK   0x001F
++#define   RTL8365MB_VLAN_PVID_CTRL_PORT1_MCIDX_MASK   0x1F00
++#define   RTL8365MB_VLAN_PVID_CTRL_PORT_MCIDX_OFFSET(_p) \
++              (((_p) & 1) << 3)
++#define   RTL8365MB_VLAN_PVID_CTRL_PORT_MCIDX_MASK(_p) \
++              (0x1F << RTL8365MB_VLAN_PVID_CTRL_PORT_MCIDX_OFFSET(_p))
++
++/* Frame type filtering registers */
++#define RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_BASE 0x07aa
++#define RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_REG(port) \
++              (RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_BASE + ((port) >> 3))
++/* required as FIELD_PREP cannot use non-constant masks */
++#define RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_MASK(port) \
++              (0x3 << RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_OFFSET(port))
++#define RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_OFFSET(port) \
++              (((port) & 0x7) << 1)
++
++/*
++ * struct rtl8365mb_vlan4k - VLAN4k table entry
++ * @vid: VLAN ID (0~4095)
++ * @member: port mask of ports in this VLAN
++ * @untag: port mask of ports which untag on egress
++ * @fid: filter ID - only used with SVL (unused)
++ * @priority: priority classification (unused)
++ * @priority_en: enable priority (unused)
++ * @policing_en: enable policing (unused)
++ * @ivl_en: enable IVL instead of default SVL
++ * @meteridx: metering index (unused)
++ *
++ * This structure is used to get/set entries in the VLAN4k table. The
++ * VLAN4k table dictates the VLAN configuration for the switch for the
++ * vast majority of features.
++ */
++struct rtl8365mb_vlan4k {
++      u16 vid;
++      u16 member;
++      u16 untag;
++      u8 fid : 4;
++      u8 priority : 3;
++      u8 priority_en : 1;
++      u8 policing_en : 1;
++      u8 ivl_en : 1;
++      u8 meteridx : 6;
++};
++
++/*
++ * struct rtl8365mb_vlanmc - VLAN membership config
++ * @evid: Enhanced VLAN ID (0~8191)
++ * @member: port mask of ports in this VLAN
++ * @fid: filter ID - only used with SVL (unused)
++ * @priority: priority classification (unused)
++ * @priority_en: enable priority (unused)
++ * @policing_en: enable policing (unused)
++ * @meteridx: metering index (unused)
++ *
++ * This structure is used to get/set entries in the VLAN membership
++ * configuration database. This feature is largely vestigial, but
++ * still needed for at least the following features:
++ *   - PVID configuration
++ *   - ACL configuration
++ *   - selection of VLAN by the CPU tag when VSEL=1, although the switch
++ *     can also select VLAN based on the VLAN tag if VSEL=0
++ *
++ * This is a low-level structure and it is recommended to interface with
++ * the VLAN membership config database via &struct rtl8365mb_vlanmc_entry.
++ */
++struct rtl8365mb_vlanmc {
++      u16 evid;
++      u16 member;
++      u8 fid : 4;
++      u8 priority : 3;
++      u8 priority_en : 1;
++      u8 policing_en : 1;
++      u8 meteridx : 6;
++};
++
++static int rtl8365mb_vlan_4k_read(struct realtek_priv *priv, u16 vid,
++                                struct rtl8365mb_vlan4k *vlan4k)
++{
++      u16 data[RTL8365MB_CVLAN_ENTRY_SIZE];
++      int val;
++      int ret;
++
++      ret = rtl8365mb_table_query(priv, RTL8365MB_TABLE_CVLAN,
++                                  RTL8365MB_TABLE_OP_READ, &vid, 0, 0,
++                                  data, ARRAY_SIZE(data));
++      if (ret)
++              return ret;
++
++      /* Unpack table entry */
++      memset(vlan4k, 0, sizeof(*vlan4k));
++      vlan4k->vid = vid;
++
++      val = FIELD_GET(RTL8365MB_CVLAN_ENTRY_D0_MBR_MASK, data[0]);
++      vlan4k->member = FIELD_PREP(RTL8365MB_CVLAN_MBR_LO_MASK, val);
++      val = FIELD_GET(RTL8365MB_CVLAN_ENTRY_D2_MBR_EXT_MASK, data[2]);
++      vlan4k->member |= FIELD_PREP(RTL8365MB_CVLAN_MBR_HI_MASK, val);
++
++      val = FIELD_GET(RTL8365MB_CVLAN_ENTRY_D0_UNTAG_MASK, data[0]);
++      vlan4k->untag = FIELD_PREP(RTL8365MB_CVLAN_UNTAG_LO_MASK, val);
++      val = FIELD_GET(RTL8365MB_CVLAN_ENTRY_D2_UNTAG_EXT_MASK, data[2]);
++      vlan4k->untag |= FIELD_PREP(RTL8365MB_CVLAN_UNTAG_HI_MASK, val);
++
++      vlan4k->fid = FIELD_GET(RTL8365MB_CVLAN_ENTRY_D1_FID_MASK, data[1]);
++      vlan4k->priority_en =
++              FIELD_GET(RTL8365MB_CVLAN_ENTRY_D1_VBPEN_MASK, data[1]);
++      vlan4k->priority =
++              FIELD_GET(RTL8365MB_CVLAN_ENTRY_D1_VBPRI_MASK, data[1]);
++      vlan4k->policing_en =
++              FIELD_GET(RTL8365MB_CVLAN_ENTRY_D1_ENVLANPOL_MASK, data[1]);
++
++      val = FIELD_GET(RTL8365MB_CVLAN_ENTRY_D1_METERIDX_MASK, data[1]);
++      val = FIELD_PREP(RTL8365MB_CVLAN_METERIDX_LO_MASK, val);
++      vlan4k->meteridx = val;
++      val = FIELD_GET(RTL8365MB_CVLAN_ENTRY_D2_METERIDX_EXT_MASK, data[2]);
++      val = FIELD_PREP(RTL8365MB_CVLAN_METERIDX_HI_MASK, val);
++      vlan4k->meteridx |= val;
++
++      vlan4k->ivl_en =
++              FIELD_GET(RTL8365MB_CVLAN_ENTRY_D1_IVL_SVL_MASK, data[1]);
++
++      return 0;
++}
++
++static int rtl8365mb_vlan_4k_write(struct realtek_priv *priv,
++                                 const struct rtl8365mb_vlan4k *vlan4k)
++{
++      u16 data[RTL8365MB_CVLAN_ENTRY_SIZE] = { 0 };
++      u16 vid;
++      int val;
++
++      /* Pack table entry value */
++      val = FIELD_GET(RTL8365MB_CVLAN_MBR_LO_MASK, vlan4k->member);
++      data[0] |= FIELD_PREP(RTL8365MB_CVLAN_ENTRY_D0_MBR_MASK, val);
++
++      val = FIELD_GET(RTL8365MB_CVLAN_UNTAG_LO_MASK, vlan4k->untag);
++      data[0] |= FIELD_PREP(RTL8365MB_CVLAN_ENTRY_D0_UNTAG_MASK, val);
++
++      data[1] |= FIELD_PREP(RTL8365MB_CVLAN_ENTRY_D1_FID_MASK, vlan4k->fid);
++      data[1] |= FIELD_PREP(RTL8365MB_CVLAN_ENTRY_D1_VBPEN_MASK,
++                            vlan4k->priority_en);
++      data[1] |= FIELD_PREP(RTL8365MB_CVLAN_ENTRY_D1_VBPRI_MASK,
++                            vlan4k->priority);
++      data[1] |= FIELD_PREP(RTL8365MB_CVLAN_ENTRY_D1_ENVLANPOL_MASK,
++                            vlan4k->policing_en);
++
++      /* FIELD_* does not play nice with struct bitfield. */
++      val = vlan4k->meteridx;
++      val = FIELD_GET(RTL8365MB_CVLAN_METERIDX_LO_MASK, val);
++      data[1] |= FIELD_PREP(RTL8365MB_CVLAN_ENTRY_D1_METERIDX_MASK, val);
++
++      data[1] |= FIELD_PREP(RTL8365MB_CVLAN_ENTRY_D1_IVL_SVL_MASK,
++                            vlan4k->ivl_en);
++
++      val = FIELD_GET(RTL8365MB_CVLAN_MBR_HI_MASK, vlan4k->member);
++      data[2] |= FIELD_PREP(RTL8365MB_CVLAN_ENTRY_D2_MBR_EXT_MASK, val);
++
++      val = FIELD_GET(RTL8365MB_CVLAN_UNTAG_HI_MASK, vlan4k->untag);
++      data[2] |= FIELD_PREP(RTL8365MB_CVLAN_ENTRY_D2_UNTAG_EXT_MASK, val);
++
++      val = vlan4k->meteridx;
++      val = FIELD_GET(RTL8365MB_CVLAN_METERIDX_HI_MASK, val);
++      data[2] |= FIELD_PREP(RTL8365MB_CVLAN_ENTRY_D2_METERIDX_EXT_MASK, val);
++
++      vid = vlan4k->vid;
++      return rtl8365mb_table_query(priv, RTL8365MB_TABLE_CVLAN,
++                                   RTL8365MB_TABLE_OP_WRITE, &vid, 0, 0,
++                                   data, ARRAY_SIZE(data));
++}
++
++static int
++rtl8365mb_vlan_4k_port_set(struct dsa_switch *ds, int port,
++                         const struct switchdev_obj_port_vlan *vlan,
++                         struct netlink_ext_ack *extack,
++                         bool include)
++{
++      struct realtek_priv *priv = ds->priv;
++      struct rtl8365mb_vlan4k vlan4k = {0};
++      int ret;
++
++      dev_dbg(priv->dev, "%s VLAN %d 4K on port %d\n",
++              include ? "add" : "del",
++              vlan->vid, port);
++
++      if (vlan->vid > RTL8365MB_MAX_4K_VID) {
++              NL_SET_ERR_MSG_MOD(extack, "VLAN ID greater than "
++                                 __stringify(RTL8365MB_MAX_4K_VID));
++              return -EINVAL;
++      }
++
++      ret = rtl8365mb_vlan_4k_read(priv, vlan->vid, &vlan4k);
++      if (ret) {
++              dev_err(priv->dev, "Failed to read VLAN 4k table\n");
++              return ret;
++      }
++
++      if (include)
++              vlan4k.member |= BIT(port);
++      else
++              vlan4k.member &= ~BIT(port);
++
++      if (include && (vlan->flags & BRIDGE_VLAN_INFO_UNTAGGED))
++              vlan4k.untag |= BIT(port);
++      else
++              vlan4k.untag &= ~BIT(port);
++      vlan4k.ivl_en = true; /* always use Independent VLAN Learning */
++
++      ret = rtl8365mb_vlan_4k_write(priv, &vlan4k);
++      if (ret) {
++              dev_err(priv->dev, "Failed to write VLAN 4k table\n");
++              return ret;
++      }
++
++      return 0;
++}
++
++/*
++ * rtl8365mb_vlan_4k_port_add() - Add a port to a VLAN 4K table entry
++ * @ds: dsa switch instance
++ * @port: port index
++ * @vlan: switchdev VLAN object containing the target VID and flags
++ * @extack: netlink extended ACK for error reporting
++ *
++ * Adds the specified port to the hardware VLAN 4K membership table.
++ *
++ * Context: Can sleep. Must be called with &priv->vlan_lock held.
++ * Takes and releases &priv->map_lock.
++ * Return: 0 on success, or a negative error code on failure.
++ */
++int rtl8365mb_vlan_4k_port_add(struct dsa_switch *ds, int port,
++                             const struct switchdev_obj_port_vlan *vlan,
++                             struct netlink_ext_ack *extack)
++{
++      struct realtek_priv *priv = ds->priv;
++
++      lockdep_assert_held(&priv->vlan_lock);
++
++      return rtl8365mb_vlan_4k_port_set(ds, port, vlan, extack, true);
++}
++
++/*
++ * rtl8365mb_vlan_4k_port_del() - Remove a port from a VLAN 4K table entry
++ * @ds: dsa switch instance
++ * @port: port index
++ * @vlan: switchdev VLAN object containing the target VID
++ *
++ * Removes the specified port from the hardware VLAN 4K membership table.
++ *
++ * Context: Can sleep. Must be called with &priv->vlan_lock held.
++ * Takes and releases &priv->map_lock.
++ * Return: 0 on success, or a negative error code on failure.
++ */
++int rtl8365mb_vlan_4k_port_del(struct dsa_switch *ds, int port,
++                             const struct switchdev_obj_port_vlan *vlan)
++{
++      struct realtek_priv *priv = ds->priv;
++
++      lockdep_assert_held(&priv->vlan_lock);
++
++      return rtl8365mb_vlan_4k_port_set(ds, port, vlan, NULL, false);
++}
++
++static int rtl8365mb_vlan_mc_read(struct realtek_priv *priv, u32 index,
++                                struct rtl8365mb_vlanmc *vlanmc)
++{
++      u16 data[RTL8365MB_VLAN_MC_ENTRY_SIZE];
++      int ret;
++
++      ret = regmap_bulk_read(priv->map, RTL8365MB_VLAN_MC_REG(index), &data,
++                             RTL8365MB_VLAN_MC_ENTRY_SIZE);
++      if (ret)
++              return ret;
++
++      vlanmc->member = FIELD_GET(RTL8365MB_VLAN_MC_D0_MBR_MASK, data[0]);
++      vlanmc->fid = FIELD_GET(RTL8365MB_VLAN_MC_D1_FID_MASK, data[1]);
++      vlanmc->meteridx = FIELD_GET(RTL8365MB_VLAN_MC_D2_METERIDX_MASK,
++                                   data[2]);
++      vlanmc->policing_en = FIELD_GET(RTL8365MB_VLAN_MC_D2_ENVLANPOL_MASK,
++                                      data[2]);
++      vlanmc->priority = FIELD_GET(RTL8365MB_VLAN_MC_D2_VBPRI_MASK, data[2]);
++      vlanmc->priority_en = FIELD_GET(RTL8365MB_VLAN_MC_D2_VBPEN_MASK,
++                                      data[2]);
++      vlanmc->evid = FIELD_GET(RTL8365MB_VLAN_MC_D3_EVID_MASK, data[3]);
++
++      return 0;
++}
++
++static int rtl8365mb_vlan_mc_write(struct realtek_priv *priv, u32 index,
++                                 const struct rtl8365mb_vlanmc *vlanmc)
++{
++      u16 data[RTL8365MB_VLAN_MC_ENTRY_SIZE] = { 0 };
++      int ret;
++
++      data[0] |= FIELD_PREP(RTL8365MB_VLAN_MC_D0_MBR_MASK, vlanmc->member);
++      data[1] |= FIELD_PREP(RTL8365MB_VLAN_MC_D1_FID_MASK, vlanmc->fid);
++      data[2] |= FIELD_PREP(RTL8365MB_VLAN_MC_D2_METERIDX_MASK,
++                            vlanmc->meteridx);
++      data[2] |= FIELD_PREP(RTL8365MB_VLAN_MC_D2_ENVLANPOL_MASK,
++                            vlanmc->policing_en);
++      data[2] |=
++              FIELD_PREP(RTL8365MB_VLAN_MC_D2_VBPRI_MASK, vlanmc->priority);
++      data[2] |= FIELD_PREP(RTL8365MB_VLAN_MC_D2_VBPEN_MASK,
++                            vlanmc->priority_en);
++      data[3] |= FIELD_PREP(RTL8365MB_VLAN_MC_D3_EVID_MASK, vlanmc->evid);
++
++      ret = regmap_bulk_write(priv->map, RTL8365MB_VLAN_MC_REG(index), &data,
++                              RTL8365MB_VLAN_MC_ENTRY_SIZE);
++
++      return ret;
++}
++
++static int rtl8365mb_vlan_mc_erase(struct realtek_priv *priv, u32 index)
++{
++      u16 data[RTL8365MB_VLAN_MC_ENTRY_SIZE] = { 0 };
++      int ret;
++
++      ret = regmap_bulk_write(priv->map, RTL8365MB_VLAN_MC_REG(index), &data,
++                              RTL8365MB_VLAN_MC_ENTRY_SIZE);
++
++      return ret;
++}
++
++/*
++ * rtl8365mb_vlan_mc_find() - find VLANMC index by VID or the first free index
++ *
++ * @priv: realtek_priv pointer
++ * @vid: VLAN ID
++ * @index: found index
++ * @first_free: found free index
++ *
++ * If a VLAN MC entry using @vid was found, @index will return the matched index
++ * and @first_free is undefined. If not found, @index will return 0 and
++ * @first_free will return the first found free index in VLAN MC or 0 if the
++ * table is full.
++ *
++ * Although 0 is a valid VLAN MC index, it is reserved for ports without PVID,
++ * including standalone, non-member ports. It uses VID == 0.
++ *
++ * Both @index and @first_free will be in the * 1..@RTL8365MB_VLAN_MCMAX range.
++ *
++ * Return: Returns 0 on success, a negative error on failure.
++ */
++static int rtl8365mb_vlan_mc_find(struct realtek_priv *priv, u16 vid,
++                                u8 *index, u8 *first_free)
++{
++      u32 vlan_entry_d3;
++      u8 vlanmc_idx;
++      u16 evid;
++      int ret;
++
++      *index = 0;
++      *first_free = 0;
++
++      /* look for existing entry or an empty one */
++      /* By design, VlanMC[0] is reserved as a neutral PVID value for
++       * standalone ports. It always has EVID == 0. That way, we assume that
++       * all entries after index 0 with VID == 0 are empty.
++       **/
++      for (vlanmc_idx = 1; vlanmc_idx <= RTL8365MB_VLAN_MCMAX; vlanmc_idx++) {
++              /* just read the 4th word, where the evid is */
++              ret = regmap_read(priv->map,
++                                RTL8365MB_VLAN_MC_REG(vlanmc_idx) + 3,
++                                &vlan_entry_d3);
++              if (ret)
++                      return ret;
++
++              evid = FIELD_GET(RTL8365MB_VLAN_MC_D3_EVID_MASK, vlan_entry_d3);
++
++              if (evid == vid) {
++                      *index = vlanmc_idx;
++                      return 0;
++              }
++
++              if (evid == 0x0 && *first_free < 1)
++                      *first_free = vlanmc_idx;
++      }
++      return 0;
++}
++
++static int rtl8365mb_vlan_port_get_pvid_idx(struct realtek_priv *priv,
++                                          int port, u8 *vlanmc_idx)
++{
++      u32 data;
++      int ret;
++
++      ret = regmap_read(priv->map, RTL8365MB_VLAN_PVID_CTRL_REG(port), &data);
++      if (ret)
++              return ret;
++
++      *vlanmc_idx = (data & RTL8365MB_VLAN_PVID_CTRL_PORT_MCIDX_MASK(port))
++                    >> RTL8365MB_VLAN_PVID_CTRL_PORT_MCIDX_OFFSET(port);
++
++      return 0;
++}
++
++/*
++ * rtl8365mb_vlan_mc_port_set() - include or exclude a port from VlanMC
++ * @ds: dsa switch
++ * @port: the port number
++ * @vid: the vlan VID to include/exclude @port
++ * @pvid: inform if vid is used as pvid in @port
++ * @extack: optional extack to return errors
++ * @include: whether to include or exclude @port
++ *
++ * This function is used to include/exclude ports to the VlanMC table.
++ *
++ * VlanMC stands for VLAN membership config and it is used exclusively for
++ * PVID. If @vlan members are not using PVID, this function will either
++ * remove or not create a new VlanMC entry.
++ *
++ * VlanMC members are used as a reference port map, cleaning the entry once
++ * no port is using it.
++ *
++ * Port PVID and accepted frame type are updated as well.
++ *
++ * Context: Can sleep. Must be called with &priv->vlan_lock held.
++ * Takes and releases &priv->map_lock.
++ * Return: Returns 0 on success, a negative error on failure.
++ */
++static
++int rtl8365mb_vlan_mc_port_set(struct dsa_switch *ds, int port,
++                             u16 vid, bool pvid,
++                             struct netlink_ext_ack *extack,
++                             bool include)
++{
++      struct realtek_priv *priv = ds->priv;
++      struct rtl8365mb_vlanmc vlanmc = {0};
++      u8 first_unused = 0;
++      u8 vlanmc_idx = 0;
++      int ret;
++
++      dev_dbg(priv->dev, "%s VLAN %d MC on port %d\n",
++              include ? "add" : "del",
++              vid, port);
++
++      if (vid > RTL8365MB_MAX_MC_VID) {
++              NL_SET_ERR_MSG_MOD(extack, "VLAN ID greater than "
++                                 __stringify(RTL8365MB_MAX_MC_VID));
++              return -EINVAL;
++      }
++
++      /* look for existing entry or an empty slot */
++      ret = rtl8365mb_vlan_mc_find(priv, vid, &vlanmc_idx,
++                                   &first_unused);
++      if (ret) {
++              dev_err(priv->dev, "Failed to find a VLAN MC table index\n");
++              return ret;
++      }
++
++      if (vlanmc_idx) {
++              ret = rtl8365mb_vlan_mc_read(priv, vlanmc_idx, &vlanmc);
++              if (ret) {
++                      dev_err(priv->dev, "Failed to read VLAN MC table\n");
++                      return ret;
++              }
++      } else if (include) {
++              /* for now, vlan_mc is only required for PVID. Defer allocation
++               * until at least one port uses PVID.
++               */
++              if (!pvid) {
++                      dev_dbg(priv->dev,
++                              "Not creating VlanMC for vlan %d until a port uses PVID (%d does not)\n",
++                              vid, port);
++                      return 0;
++              }
++
++              if (!first_unused) {
++                      NL_SET_ERR_MSG_MOD(extack, "All VLAN MC entries (0.."
++                                         __stringify(RTL8365MB_VLAN_MCMAX)
++                                         ") are in use.");
++                      return -ENOSPC;
++              }
++
++              vlanmc_idx = first_unused;
++              vlanmc.evid = vid;
++
++      } else /* excluding and VLANMC not found */ {
++              return 0;
++      }
++
++      dev_dbg(priv->dev,
++              "VLAN %d (idx: %d) PVID curr members: %08x\n",
++              vid, vlanmc_idx, vlanmc.member);
++
++      /* here we either have an existing VLANMC (with PVID members) or the
++       * added port is using this VLAN as PVID
++       */
++      if (include)
++              vlanmc.member |= BIT(port);
++      else
++              vlanmc.member &= ~BIT(port);
++
++      /* just like we don't need to create a VLAN_MC when there is no port
++       * using it as PVID, we can erase it when there is no more port using
++       * it as PVID.
++       */
++      if (!vlanmc.member) {
++              dev_dbg(priv->dev,
++                      "Clearing VlanMC index %d previously used by VID %d\n",
++                      vlanmc_idx, vid);
++              ret = rtl8365mb_vlan_mc_erase(priv, vlanmc_idx);
++      } else {
++              dev_dbg(priv->dev,
++                      "Saving VlanMC index %d with VID %d\n",
++                      vlanmc_idx, vid);
++              ret = rtl8365mb_vlan_mc_write(priv, vlanmc_idx, &vlanmc);
++      }
++      if (ret) {
++              dev_err(priv->dev, "Failed to write vlan MC entry\n");
++              return ret;
++      }
++
++      return 0;
++}
++
++static int rtl8365mb_vlan_port_set_pvid(struct realtek_priv *priv,
++                                      int port, u16 vlanmc_idx)
++{
++      int ret;
++      u32 val;
++
++      dev_dbg(priv->dev, "set PVID IDX %d on port %d\n", vlanmc_idx, port);
++
++      val = vlanmc_idx << RTL8365MB_VLAN_PVID_CTRL_PORT_MCIDX_OFFSET(port);
++      ret = regmap_update_bits(priv->map,
++                               RTL8365MB_VLAN_PVID_CTRL_REG(port),
++                               RTL8365MB_VLAN_PVID_CTRL_PORT_MCIDX_MASK(port),
++                               val);
++      if (ret)
++              return ret;
++
++      return 0;
++}
++
++static int rtl8365mb_vlan_get_pvid_mc(struct realtek_priv *priv,
++                                    int port, u8 *vlanmc_idx,
++                                    struct rtl8365mb_vlanmc *vlanmc)
++{
++      int ret;
++
++      ret = rtl8365mb_vlan_port_get_pvid_idx(priv, port, vlanmc_idx);
++      if (ret)
++              return ret;
++
++      memset(vlanmc, 0, sizeof(*vlanmc));
++
++      if (!*vlanmc_idx)
++              return 0;
++
++      ret = rtl8365mb_vlan_mc_read(priv, *vlanmc_idx, vlanmc);
++      if (ret)
++              return ret;
++
++      return 0;
++}
++
++/*
++ * rtl8365mb_vlan_port_get_pvid - Retrieve the port PVID
++ * @priv: realtek switch private structure
++ * @port: port index
++ * @pvid: pointer to store the retrieved VLAN ID
++ *
++ * Returns the port PVID if defined or 0 if not.
++ *
++ * Context: Can sleep. Takes and releases &priv->map_lock.
++ * Return: 0 on success or a negative error code on failure.
++ */
++int rtl8365mb_vlan_port_get_pvid(struct realtek_priv *priv, int port, u16 *pvid)
++{
++      struct rtl8365mb_vlanmc vlanmc;
++      u8 vlanmc_idx;
++      int ret;
++
++      ret = rtl8365mb_vlan_get_pvid_mc(priv, port, &vlanmc_idx, &vlanmc);
++      if (ret)
++              return ret;
++
++      *pvid = vlanmc.evid;
++      return 0;
++}
++
++/*
++ * rtl8365mb_vlan_port_get_framefilter() - Get the ingress frame filtering mode
++ * for a port
++ * @priv: realtek switch private structure
++ * @port: port index
++ * @frame_type: pointer to store the retrieved ingress frame filter type
++ *
++ * Context: Can sleep. Takes and releases &priv->map_lock.
++ * Return: 0 on success, or a negative error code on failure.
++ */
++int
++rtl8365mb_vlan_port_get_framefilter(struct realtek_priv *priv,
++                                  int port,
++                                  enum rtl8365mb_frame_ingress *frame_type)
++{
++      u32 val;
++      int ret;
++
++      /* Even if ACCEPT_FRAME_TYPE_ANY, the switch will still check if the
++       * port is a member of vlan PVID
++       */
++
++      ret = regmap_read(priv->map, RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_REG(port),
++                        &val);
++      if (ret)
++              return ret;
++
++      *frame_type = field_get(RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_MASK(port),
++                              val);
++
++      return 0;
++}
++
++/*
++ * rtl8365mb_vlan_port_set_framefilter() - Set the ingress frame filtering mode
++ * for a port
++ * @priv: realtek switch private structure
++ * @port: port index
++ * @frame_type: the ingress frame filter type to configure
++ *
++ * Context: Can sleep. Takes and releases &priv->map_lock.
++ * Return: 0 on success, or a negative error code on failure.
++ */
++int
++rtl8365mb_vlan_port_set_framefilter(struct realtek_priv *priv,
++                                  int port,
++                                  enum rtl8365mb_frame_ingress frame_type)
++{
++      u32 val;
++
++      /* Even if ACCEPT_FRAME_TYPE_ANY, the switch will still check if the
++       * port is a member of vlan PVID
++       */
++      val = frame_type << RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_OFFSET(port);
++
++      return regmap_update_bits(priv->map,
++                                RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_REG(port),
++                                RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_MASK(port),
++                                val);
++}
++
++/*
++ * rtl8365mb_vlan_pvid_port_set() - Configure a port's PVID and associated
++ * VLANMC entry
++ * @ds: dsa switch instance
++ * @port: port index
++ * @vid: target VID
++ * @extack: netlink extended ACK for error reporting
++ *
++ * Allocates or reuses a hardware VLANMC entry to map the given port to its new
++ * PVID. Gracefully unwinds and restores previous configuration if a hardware
++ * write operation fails during execution.
++ *
++ * Context: Can sleep. Must be called with &priv->vlan_lock held.
++ * Takes and releases &priv->map_lock.
++ * Return: 0 on success, or a negative error code on failure.
++ */
++int rtl8365mb_vlan_pvid_port_set(struct dsa_switch *ds, int port, u16 vid,
++                               struct netlink_ext_ack *extack)
++{
++      enum rtl8365mb_frame_ingress accepted_frame, prev_accepted_frame;
++      struct realtek_priv *priv = ds->priv;
++      struct rtl8365mb_vlanmc prev_vlanmc = {0};
++      u8 _unused_first_free_idx;
++      u8 prev_vlanmc_idx;
++      u8 vlanmc_idx;
++      int ret;
++
++      lockdep_assert_held(&priv->vlan_lock);
++
++      /* Read the old PVID exclusively to undo in case of error */
++      ret = rtl8365mb_vlan_get_pvid_mc(priv, port, &prev_vlanmc_idx,
++                                       &prev_vlanmc);
++      if (ret) {
++              dev_err(priv->dev, "Failed to read current VLAN MC\n");
++              return ret;
++      }
++
++      ret = rtl8365mb_vlan_port_get_framefilter(priv, port,
++                                                &prev_accepted_frame);
++      if (ret) {
++              dev_err(priv->dev, "Failed to get current framefilter\n");
++              return ret;
++      }
++
++      /* Find or allocate a new vlan MC and add port to members,
++       * although members are not checked by the HW in vlan MC.
++       */
++      ret = rtl8365mb_vlan_mc_port_set(ds, port, vid, true, extack, true);
++      if (ret)
++              return ret;
++
++      /* look for existing entry */
++      ret = rtl8365mb_vlan_mc_find(priv, vid, &vlanmc_idx,
++                                   &_unused_first_free_idx);
++      if (ret) {
++              dev_err(priv->dev, "Failed to find a VLAN MC table index\n");
++              goto undo_vlan_mc_port_set;
++      }
++
++      if (!vlanmc_idx) {
++              dev_err(priv->dev, "VLAN should already exist in VLAN MC\n");
++              ret = -ENOENT;
++              goto undo_vlan_mc_port_set;
++      }
++
++      ret = rtl8365mb_vlan_port_set_pvid(priv, port, vlanmc_idx);
++      if (ret) {
++              dev_err(priv->dev, "Failed to set port PVID\n");
++              goto undo_vlan_mc_port_set;
++      }
++
++      /* Changing accept frame is what enables PVID (if not enabled before) */
++      accepted_frame = RTL8365MB_FRAME_TYPE_ANY_FRAME;
++      ret = rtl8365mb_vlan_port_set_framefilter(priv, port, accepted_frame);
++      if (ret) {
++              dev_err(priv->dev, "Failed to set port frame filter\n");
++              goto undo_vlan_port_set_pvid;
++      }
++
++      /* A VLAN can be added with PVID without removing from the old
++       * PVID VLAN. Clear PVID from the old VLAN MC (if needed).
++       */
++      if (prev_vlanmc_idx && (prev_vlanmc.evid != vid)) {
++              ret = rtl8365mb_vlan_mc_port_set(ds, port, prev_vlanmc.evid,
++                                               false, NULL, false);
++              if (ret) {
++                      dev_err(priv->dev, "Failed to clear old VLAN MC\n");
++                      goto undo_set_framefilter;
++              }
++      }
++
++      return 0;
++
++undo_set_framefilter:
++      (void)rtl8365mb_vlan_port_set_framefilter(priv, port,
++                                                prev_accepted_frame);
++
++undo_vlan_port_set_pvid:
++      (void)rtl8365mb_vlan_port_set_pvid(priv, port, prev_vlanmc_idx);
++
++undo_vlan_mc_port_set:
++      if (prev_vlanmc.evid != vid)
++              (void)rtl8365mb_vlan_mc_port_set(ds, port, vid, false, NULL,
++                                               false);
++
++      return ret;
++}
++
++/*
++ * rtl8365mb_vlan_pvid_port_clear() - Remove a port's PVID configuration
++ * @ds: dsa switch instance
++ * @port: port index
++ * @vid:  VLAN VID for PVID
++ *
++ * Resets the target port's hardware PVID allocation to 0. Cleans up and frees
++ * the associated VLANMC entry if no other ports are referencing it.
++ *
++ * Context: Can sleep. Must be called with &priv->vlan_lock held.
++ * Takes and releases &priv->map_lock.
++ * Return: 0 on success, or a negative error code on failure.
++ */
++int rtl8365mb_vlan_pvid_port_clear(struct dsa_switch *ds, int port, u16 vid)
++{
++      enum rtl8365mb_frame_ingress accepted_frame, prev_accepted_frame;
++      struct realtek_priv *priv = ds->priv;
++      struct rtl8365mb_vlanmc vlanmc = {0};
++      u8 vlanmc_idx;
++      int ret;
++
++      lockdep_assert_held(&priv->vlan_lock);
++
++      ret = rtl8365mb_vlan_get_pvid_mc(priv, port, &vlanmc_idx,
++                                       &vlanmc);
++      if (ret) {
++              dev_err(priv->dev, "Failed to read current VLAN MC\n");
++              return ret;
++      }
++
++      /* Port is not using PVID. Nothing to remove. */
++      if (!vlanmc_idx)
++              return 0;
++
++      /* We are leaving a non PVID vlan, Nothing to remove. */
++      if (vlanmc.evid != vid)
++              return 0;
++
++      ret = rtl8365mb_vlan_port_get_framefilter(priv, port,
++                                                &prev_accepted_frame);
++      if (ret) {
++              dev_err(priv->dev, "Failed to get current framefilter\n");
++              return ret;
++      }
++
++      /* Changing accept frame is what really removes PVID. But only do
++       * that if we are filtering vlan
++       */
++      if (dsa_port_is_vlan_filtering(dsa_to_port(ds, port))) {
++              accepted_frame = RTL8365MB_FRAME_TYPE_TAGGED_ONLY;
++
++              ret = rtl8365mb_vlan_port_set_framefilter(priv, port,
++                                                        accepted_frame);
++              if (ret) {
++                      dev_err(priv->dev, "Failed to set port frame filter\n");
++                      return ret;
++              }
++      } else {
++              /* skip undo_set_framefilter */
++              accepted_frame = prev_accepted_frame;
++      }
++
++      ret = rtl8365mb_vlan_port_set_pvid(priv, port, 0);
++      if (ret) {
++              dev_err(priv->dev, "Failed to set port PVID to 0\n");
++              goto undo_set_framefilter;
++      }
++
++      /* Clears the VLAN MC membership and maybe VLAN MC entry if empty */
++      ret = rtl8365mb_vlan_mc_port_set(ds, port, vlanmc.evid,
++                                       false, NULL, false);
++      if (ret)
++              goto undo_port_set_pvid;
++
++      return 0;
++
++undo_port_set_pvid:
++      (void)rtl8365mb_vlan_port_set_pvid(priv, port, vlanmc_idx);
++
++undo_set_framefilter:
++      if (prev_accepted_frame != accepted_frame)
++              (void)rtl8365mb_vlan_port_set_framefilter(priv, port,
++                                                        prev_accepted_frame);
++
++      return ret;
++}
+--- /dev/null
++++ b/drivers/net/dsa/realtek/rtl8365mb_vlan.h
+@@ -0,0 +1,39 @@
++/* SPDX-License-Identifier: GPL-2.0 */
++/* VLAN configuration interface for the rtl8365mb switch family
++ *
++ * Copyright (C) 2022 Alvin Šipraga <alsi@bang-olufsen.dk>
++ *
++ */
++
++#ifndef _REALTEK_RTL8365MB_VLAN_H
++#define _REALTEK_RTL8365MB_VLAN_H
++
++#include <linux/types.h>
++
++#include "realtek.h"
++
++enum rtl8365mb_frame_ingress {
++      RTL8365MB_FRAME_TYPE_ANY_FRAME = 0,
++      RTL8365MB_FRAME_TYPE_TAGGED_ONLY,
++      RTL8365MB_FRAME_TYPE_UNTAGGED_ONLY,
++};
++
++int rtl8365mb_vlan_port_get_pvid(struct realtek_priv *priv, int port,
++                               u16 *pvid);
++int
++rtl8365mb_vlan_port_get_framefilter(struct realtek_priv *priv,
++                                  int port,
++                                  enum rtl8365mb_frame_ingress *frame_type);
++int
++rtl8365mb_vlan_port_set_framefilter(struct realtek_priv *priv,
++                                  int port,
++                                  enum rtl8365mb_frame_ingress frame_type);
++int rtl8365mb_vlan_4k_port_add(struct dsa_switch *ds, int port,
++                             const struct switchdev_obj_port_vlan *vlan,
++                             struct netlink_ext_ack *extack);
++int rtl8365mb_vlan_4k_port_del(struct dsa_switch *ds, int port,
++                             const struct switchdev_obj_port_vlan *vlan);
++int rtl8365mb_vlan_pvid_port_set(struct dsa_switch *ds, int port, u16 vid,
++                               struct netlink_ext_ack *extack);
++int rtl8365mb_vlan_pvid_port_clear(struct dsa_switch *ds, int port, u16 vid);
++#endif /* _REALTEK_RTL8365MB_VLAN_H */
+--- a/drivers/net/dsa/realtek/rtl83xx.c
++++ b/drivers/net/dsa/realtek/rtl83xx.c
+@@ -155,6 +155,7 @@ rtl83xx_probe(struct device *dev,
+               return ERR_PTR(-ENOMEM);
+       mutex_init(&priv->map_lock);
++      mutex_init(&priv->vlan_lock);
+       rc.lock_arg = priv;
+       priv->map = devm_regmap_init(dev, NULL, priv, &rc);
diff --git a/target/linux/generic/backport-6.12/942-07-v7.2-net-dsa-realtek-rtl8365mb-add-FDB-support.patch b/target/linux/generic/backport-6.12/942-07-v7.2-net-dsa-realtek-rtl8365mb-add-FDB-support.patch
new file mode 100644 (file)
index 0000000..b48f3bb
--- /dev/null
@@ -0,0 +1,1131 @@
+From 336e3e4a1ab37b6826fae27e53cd2ac43c9a96ca Mon Sep 17 00:00:00 2001
+From: Alvin Šipraga <alsi@bang-olufsen.dk>
+Date: Sat, 6 Jun 2026 05:29:31 -0300
+Subject: net: dsa: realtek: rtl8365mb: add FDB support
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+Implement support for FDB and MDB management for the RTL8365MB series
+switches.
+
+The hardware supports IVL by keying the unicast forwarding database with
+the {MAC, VID, EFID} tuple.  The Extended Filtering ID (EFID) is 3 bits
+wide, providing 8 unique filtering domains. This driver reserves EFID 0
+for standalone ports, effectively limiting the hardware offload to a
+maximum of 7 bridges. The multicast database uses a {MAC, VID} key, with
+ports from different bridges sharing the same multicast group.
+
+Introduce a mutex lock (l2_lock) to protect concurrent L2 table updates.
+
+Add support for forwarding database operations, including unicast and
+multicast entry handling as well as fast aging support.
+
+Set DSA switch flags assisted_learning_on_cpu_port and fdb_isolation.
+
+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-7-b9e409687cbe@gmail.com
+Signed-off-by: Jakub Kicinski <kuba@kernel.org>
+---
+ drivers/net/dsa/realtek/Makefile         |   1 +
+ drivers/net/dsa/realtek/realtek.h        |  28 ++
+ drivers/net/dsa/realtek/rtl8365mb_l2.c   | 576 +++++++++++++++++++++++++++++++
+ drivers/net/dsa/realtek/rtl8365mb_l2.h   |  32 ++
+ drivers/net/dsa/realtek/rtl8365mb_main.c |  33 +-
+ drivers/net/dsa/realtek/rtl83xx.c        | 292 ++++++++++++++++
+ drivers/net/dsa/realtek/rtl83xx.h        |  16 +
+ 7 files changed, 977 insertions(+), 1 deletion(-)
+
+--- a/drivers/net/dsa/realtek/Makefile
++++ b/drivers/net/dsa/realtek/Makefile
+@@ -19,4 +19,5 @@ obj-$(CONFIG_NET_DSA_REALTEK_RTL8365MB)
+ rtl8365mb-objs := rtl8365mb_main.o \
+                 rtl8365mb_table.o \
+                 rtl8365mb_vlan.o \
++                rtl8365mb_l2.o \
+ # end of rtl8365mb-objs
+--- a/drivers/net/dsa/realtek/realtek.h
++++ b/drivers/net/dsa/realtek/realtek.h
+@@ -48,6 +48,12 @@ struct rtl8366_vlan_4k {
+       u8      fid;
+ };
++struct realtek_fdb_entry {
++      u8 mac_addr[ETH_ALEN];
++      u16 vid;
++      bool is_static;
++};
++
+ struct realtek_priv {
+       struct device           *dev;
+       struct reset_control    *reset_ctl;
+@@ -62,6 +68,15 @@ struct realtek_priv {
+        * deleting port VLAN memberships and PVID configurations.
+        */
+       struct mutex            vlan_lock;
++      /* l2_lock is used to prevent concurrent modifications of L2 table
++       * entries while another function is reading it. l2_(add,del)_mc
++       * is an example that first read current table entry and then
++       * create/update it. l2_(add|del)_uc uses a single table op and,
++       * internally, it might not need this lock. However, altering FDB
++       * may still collide, as well as l2_flush, with fdb_dump iterating
++       * over FDB.
++       */
++      struct mutex            l2_lock;
+       struct mii_bus          *user_mii_bus;
+       struct mii_bus          *bus;
+       int                     mdio_addr;
+@@ -115,6 +130,19 @@ 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     (*l2_add_uc)(struct realtek_priv *priv, int port,
++                           const unsigned char addr[ETH_ALEN],
++                           u16 efid, u16 vid);
++      int     (*l2_del_uc)(struct realtek_priv *priv, int port,
++                           const unsigned char addr[ETH_ALEN],
++                           u16 efid, u16 vid);
++      int     (*l2_get_next_uc)(struct realtek_priv *priv, u16 *addr,
++                                int port, struct realtek_fdb_entry *entry);
++      int     (*l2_add_mc)(struct realtek_priv *priv, int port,
++                           const unsigned char addr[ETH_ALEN], u16 vid);
++      int     (*l2_del_mc)(struct realtek_priv *priv, int port,
++                           const unsigned char addr[ETH_ALEN], u16 vid);
++      int     (*l2_flush)(struct realtek_priv *priv, int port, u16 vid);
+       int     (*phy_read)(struct realtek_priv *priv, int phy, int regnum);
+       int     (*phy_write)(struct realtek_priv *priv, int phy, int regnum,
+                            u16 val);
+--- /dev/null
++++ b/drivers/net/dsa/realtek/rtl8365mb_l2.c
+@@ -0,0 +1,576 @@
++// SPDX-License-Identifier: GPL-2.0
++/* Forwarding and multicast database interface for the rtl8365mb switch family
++ *
++ * Copyright (C) 2022 Alvin Šipraga <alsi@bang-olufsen.dk>
++ */
++
++#include <linux/etherdevice.h>
++
++#include "rtl8365mb_l2.h"
++#include "rtl8365mb_table.h"
++#include <linux/regmap.h>
++
++#define RTL8365MB_L2_ENTRY_SIZE                       6
++
++#define RTL8365MB_L2_UC_D0_MAC5_MSK           GENMASK(7, 0)
++#define RTL8365MB_L2_UC_D0_MAC4_MSK           GENMASK(15, 8)
++#define RTL8365MB_L2_UC_D1_MAC3_MSK           GENMASK(7, 0)
++#define RTL8365MB_L2_UC_D1_MAC2_MSK           GENMASK(15, 8)
++#define RTL8365MB_L2_UC_D2_MAC1_MSK           GENMASK(7, 0)
++#define RTL8365MB_L2_UC_D2_MAC0_MSK           GENMASK(15, 8)
++#define RTL8365MB_L2_UC_D3_VID_MSK            GENMASK(11, 0)
++#define RTL8365MB_L2_UC_D3_IVL_MSK            GENMASK(13, 13)
++#define RTL8365MB_L2_UC_D3_PORT_EXT_MSK       GENMASK(15, 15)
++#define   RTL8365MB_L2_UC_PORT_HI_MSK         GENMASK(3, 3)
++#define RTL8365MB_L2_UC_D4_EFID_MSK           GENMASK(2, 0)
++#define RTL8365MB_L2_UC_D4_FID_MSK            GENMASK(6, 3)
++#define RTL8365MB_L2_UC_D4_SA_PRI_MSK         GENMASK(7, 7)
++#define RTL8365MB_L2_UC_D4_PORT_MSK           GENMASK(10, 8)
++#define   RTL8365MB_L2_UC_PORT_LO_MSK         GENMASK(2, 0)
++#define RTL8365MB_L2_UC_D4_AGE_MSK            GENMASK(13, 11)
++#define RTL8365MB_L2_UC_D4_AUTH_MSK           GENMASK(14, 14)
++#define RTL8365MB_L2_UC_D4_SA_BLOCK_MSK       GENMASK(15, 15)
++
++#define RTL8365MB_L2_UC_D5_DA_BLOCK_MSK       GENMASK(0, 0)
++#define RTL8365MB_L2_UC_D5_PRIORITY_MSK       GENMASK(3, 1)
++#define RTL8365MB_L2_UC_D5_FWD_PRI_MSK                GENMASK(4, 4)
++#define RTL8365MB_L2_UC_D5_STATIC_MSK         GENMASK(5, 5)
++
++#define RTL8365MB_L2_MC_D0_MAC5_MSK           GENMASK(7, 0)
++#define RTL8365MB_L2_MC_D0_MAC4_MSK           GENMASK(15, 8)
++#define RTL8365MB_L2_MC_D1_MAC3_MSK           GENMASK(7, 0)
++#define RTL8365MB_L2_MC_D1_MAC2_MSK           GENMASK(15, 8)
++#define RTL8365MB_L2_MC_D2_MAC1_MSK           GENMASK(7, 0)
++#define RTL8365MB_L2_MC_D2_MAC0_MSK           GENMASK(15, 8)
++#define RTL8365MB_L2_MC_D3_VID_MSK            GENMASK(11, 0)
++#define RTL8365MB_L2_MC_D3_IVL_MSK            GENMASK(13, 13)
++#define RTL8365MB_L2_MC_D3_MBR_HI1_MSK                GENMASK(15, 14)
++#define   RTL8365MB_L2_MC_MBR_HI1_MSK         GENMASK(9, 8)
++
++#define RTL8365MB_L2_MC_D4_MBR_MSK            GENMASK(7, 0)
++#define   RTL8365MB_L2_MC_MBR_LO_MSK          GENMASK(7, 0)
++#define RTL8365MB_L2_MC_D4_IGMPIDX_MSK                GENMASK(15, 8)
++
++#define RTL8365MB_L2_MC_D5_IGMP_ASIC_MSK      GENMASK(0, 0)
++#define RTL8365MB_L2_MC_D5_PRIORITY_MSK       GENMASK(3, 1)
++#define RTL8365MB_L2_MC_D5_FWD_PRI_MSK                GENMASK(4, 4)
++#define RTL8365MB_L2_MC_D5_STATIC_MSK         GENMASK(5, 5)
++#define RTL8365MB_L2_MC_D5_MBR_HI2_MSK                GENMASK(7, 7)
++#define   RTL8365MB_L2_MC_MBR_HI2_MSK         GENMASK(10, 10)
++
++/* Port flush command registers - writing a 1 to the port's MASK bit will
++ * initiate the flush procedure. Completion is signalled when the corresponding
++ * BUSY bit is 0.
++ */
++#define RTL8365MB_L2_FLUSH_PORT_REG           0x0A36
++#define   RTL8365MB_L2_FLUSH_PORT_MSK_MSK     GENMASK(7, 0)
++#define   RTL8365MB_L2_FLUSH_PORT_BUSY_MSK    GENMASK(15, 8)
++
++#define RTL8365MB_L2_FLUSH_PORT_EXT_REG               0x0A35
++#define   RTL8365MB_L2_FLUSH_PORT_EXT_MSK_MSK GENMASK(2, 0)
++#define   RTL8365MB_L2_FLUSH_PORT_EXT_BUSY_MSK        GENMASK(5, 3)
++
++#define RTL8365MB_L2_FLUSH_CTRL1_REG          0x0A37
++#define   RTL8365MB_L2_FLUSH_CTRL1_VID_MSK    GENMASK(11, 0)
++#define   RTL8365MB_L2_FLUSH_CTRL1_FID_MSK    GENMASK(15, 12)
++
++#define RTL8365MB_L2_FLUSH_CTRL2_REG          0x0A38
++#define   RTL8365MB_L2_FLUSH_CTRL2_MODE_MSK   GENMASK(1, 0)
++#define   RTL8365MB_L2_FLUSH_CTRL2_MODE_PORT  0
++#define   RTL8365MB_L2_FLUSH_CTRL2_MODE_PORT_VID 1
++#define   RTL8365MB_L2_FLUSH_CTRL2_MODE_PORT_FID 2
++#define   RTL8365MB_L2_FLUSH_CTRL2_TYPE_MSK   GENMASK(2, 2)
++#define   RTL8365MB_L2_FLUSH_CTRL2_TYPE_DYNAMIC       0
++#define   RTL8365MB_L2_FLUSH_CTRL2_TYPE_BOTH  1
++
++/* This flushes the entire LUT, reading it back it will turn 0 when the
++ * operation is complete
++ */
++#define RTL8365MB_L2_FLUSH_CTRL3_REG          0x0A39
++#define   RTL8365MB_L2_FLUSH_CTRL3_MSK                GENMASK(0, 0)
++
++struct rtl8365mb_l2_uc_key {
++      u8 mac_addr[ETH_ALEN];
++      u16 vid;
++      u16 fid;
++      bool ivl;
++      u16 efid;
++};
++
++struct rtl8365mb_l2_uc {
++      struct rtl8365mb_l2_uc_key key;
++      u8 port;
++      u8 age;
++      u8 priority;
++
++      bool sa_block;
++      bool da_block;
++      bool auth;
++      bool is_static;
++      bool sa_pri;
++      bool fwd_pri;
++};
++
++struct rtl8365mb_l2_mc_key {
++      u8 mac_addr[ETH_ALEN];
++      union {
++              u16 vid; /* IVL */
++              u16 fid; /* SVL */
++      };
++      bool ivl;
++};
++
++struct rtl8365mb_l2_mc {
++      struct rtl8365mb_l2_mc_key key;
++      u16 member;
++      u8 priority;
++      u8 igmpidx;
++
++      bool is_static;
++      bool fwd_pri;
++      bool igmp_asic;
++};
++
++static void rtl8365mb_l2_data_to_uc(const u16 *data, struct rtl8365mb_l2_uc *uc)
++{
++      u32 val;
++
++      uc->key.mac_addr[5] = FIELD_GET(RTL8365MB_L2_UC_D0_MAC5_MSK, data[0]);
++      uc->key.mac_addr[4] = FIELD_GET(RTL8365MB_L2_UC_D0_MAC4_MSK, data[0]);
++      uc->key.mac_addr[3] = FIELD_GET(RTL8365MB_L2_UC_D1_MAC3_MSK, data[1]);
++      uc->key.mac_addr[2] = FIELD_GET(RTL8365MB_L2_UC_D1_MAC2_MSK, data[1]);
++      uc->key.mac_addr[1] = FIELD_GET(RTL8365MB_L2_UC_D2_MAC1_MSK, data[2]);
++      uc->key.mac_addr[0] = FIELD_GET(RTL8365MB_L2_UC_D2_MAC0_MSK, data[2]);
++      uc->key.efid = FIELD_GET(RTL8365MB_L2_UC_D4_EFID_MSK, data[4]);
++      uc->key.vid = FIELD_GET(RTL8365MB_L2_UC_D3_VID_MSK, data[3]);
++      uc->key.ivl = FIELD_GET(RTL8365MB_L2_UC_D3_IVL_MSK, data[3]);
++      uc->key.fid = FIELD_GET(RTL8365MB_L2_UC_D4_FID_MSK, data[4]);
++      uc->age = FIELD_GET(RTL8365MB_L2_UC_D4_AGE_MSK, data[4]);
++      uc->auth = FIELD_GET(RTL8365MB_L2_UC_D4_AUTH_MSK, data[4]);
++
++      val = FIELD_GET(RTL8365MB_L2_UC_D4_PORT_MSK, data[4]);
++      uc->port = FIELD_PREP(RTL8365MB_L2_UC_PORT_LO_MSK, val);
++      val = FIELD_GET(RTL8365MB_L2_UC_D3_PORT_EXT_MSK, data[3]);
++      uc->port |= FIELD_PREP(RTL8365MB_L2_UC_PORT_HI_MSK, val);
++
++      uc->sa_pri = FIELD_GET(RTL8365MB_L2_UC_D4_SA_PRI_MSK, data[4]);
++      uc->fwd_pri = FIELD_GET(RTL8365MB_L2_UC_D5_FWD_PRI_MSK, data[5]);
++      uc->sa_block = FIELD_GET(RTL8365MB_L2_UC_D4_SA_BLOCK_MSK, data[4]);
++      uc->da_block = FIELD_GET(RTL8365MB_L2_UC_D5_DA_BLOCK_MSK, data[5]);
++      uc->priority = FIELD_GET(RTL8365MB_L2_UC_D5_PRIORITY_MSK, data[5]);
++      uc->is_static = FIELD_GET(RTL8365MB_L2_UC_D5_STATIC_MSK, data[5]);
++}
++
++static void rtl8365mb_l2_uc_to_data(const struct rtl8365mb_l2_uc *uc, u16 *data)
++{
++      u32 val;
++
++      memset(data, 0, RTL8365MB_L2_ENTRY_SIZE * 2);
++      data[0] |=
++              FIELD_PREP(RTL8365MB_L2_UC_D0_MAC5_MSK, uc->key.mac_addr[5]);
++      data[0] |=
++              FIELD_PREP(RTL8365MB_L2_UC_D0_MAC4_MSK, uc->key.mac_addr[4]);
++      data[1] |=
++              FIELD_PREP(RTL8365MB_L2_UC_D1_MAC3_MSK, uc->key.mac_addr[3]);
++      data[1] |=
++              FIELD_PREP(RTL8365MB_L2_UC_D1_MAC2_MSK, uc->key.mac_addr[2]);
++      data[2] |=
++              FIELD_PREP(RTL8365MB_L2_UC_D2_MAC1_MSK, uc->key.mac_addr[1]);
++      data[2] |=
++              FIELD_PREP(RTL8365MB_L2_UC_D2_MAC0_MSK, uc->key.mac_addr[0]);
++      data[3] |= FIELD_PREP(RTL8365MB_L2_UC_D3_VID_MSK, uc->key.vid);
++      data[3] |= FIELD_PREP(RTL8365MB_L2_UC_D3_IVL_MSK, uc->key.ivl);
++
++      val = FIELD_GET(RTL8365MB_L2_UC_PORT_HI_MSK, uc->port);
++      data[3] |= FIELD_PREP(RTL8365MB_L2_UC_D3_PORT_EXT_MSK, val);
++
++      data[4] |= FIELD_PREP(RTL8365MB_L2_UC_D4_FID_MSK, uc->key.fid);
++      data[4] |= FIELD_PREP(RTL8365MB_L2_UC_D4_EFID_MSK, uc->key.efid);
++      data[4] |= FIELD_PREP(RTL8365MB_L2_UC_D4_AGE_MSK, uc->age);
++      data[4] |= FIELD_PREP(RTL8365MB_L2_UC_D4_AUTH_MSK, uc->auth);
++
++      val = FIELD_GET(RTL8365MB_L2_UC_PORT_LO_MSK, uc->port);
++      data[4] |= FIELD_PREP(RTL8365MB_L2_UC_D4_PORT_MSK, val);
++
++      data[4] |= FIELD_PREP(RTL8365MB_L2_UC_D4_SA_PRI_MSK, uc->sa_pri);
++      data[4] |= FIELD_PREP(RTL8365MB_L2_UC_D4_SA_BLOCK_MSK, uc->sa_block);
++      data[5] |= FIELD_PREP(RTL8365MB_L2_UC_D5_FWD_PRI_MSK, uc->fwd_pri);
++      data[5] |= FIELD_PREP(RTL8365MB_L2_UC_D5_DA_BLOCK_MSK, uc->da_block);
++      data[5] |= FIELD_PREP(RTL8365MB_L2_UC_D5_PRIORITY_MSK, uc->priority);
++      data[5] |= FIELD_PREP(RTL8365MB_L2_UC_D5_STATIC_MSK, uc->is_static);
++}
++
++static void rtl8365mb_l2_data_to_mc(const u16 *data, struct rtl8365mb_l2_mc *mc)
++{
++      u32 val;
++
++      mc->key.mac_addr[5] = FIELD_GET(RTL8365MB_L2_MC_D0_MAC5_MSK, data[0]);
++      mc->key.mac_addr[4] = FIELD_GET(RTL8365MB_L2_MC_D0_MAC4_MSK, data[0]);
++      mc->key.mac_addr[3] = FIELD_GET(RTL8365MB_L2_MC_D1_MAC3_MSK, data[1]);
++      mc->key.mac_addr[2] = FIELD_GET(RTL8365MB_L2_MC_D1_MAC2_MSK, data[1]);
++      mc->key.mac_addr[1] = FIELD_GET(RTL8365MB_L2_MC_D2_MAC1_MSK, data[2]);
++      mc->key.mac_addr[0] = FIELD_GET(RTL8365MB_L2_MC_D2_MAC0_MSK, data[2]);
++      /* key.vid,key.fid shares the same memory space */
++      mc->key.vid = FIELD_GET(RTL8365MB_L2_MC_D3_VID_MSK, data[3]);
++      mc->key.ivl = FIELD_GET(RTL8365MB_L2_MC_D3_IVL_MSK, data[3]);
++      mc->priority = FIELD_GET(RTL8365MB_L2_MC_D5_PRIORITY_MSK, data[5]);
++      mc->fwd_pri = FIELD_GET(RTL8365MB_L2_MC_D5_FWD_PRI_MSK, data[5]);
++      mc->is_static = FIELD_GET(RTL8365MB_L2_MC_D5_STATIC_MSK, data[5]);
++
++      val = FIELD_GET(RTL8365MB_L2_MC_D4_MBR_MSK, data[4]);
++      mc->member = FIELD_PREP(RTL8365MB_L2_MC_MBR_LO_MSK, val);
++      val = FIELD_GET(RTL8365MB_L2_MC_D3_MBR_HI1_MSK, data[3]);
++      mc->member |= FIELD_PREP(RTL8365MB_L2_MC_MBR_HI1_MSK, val);
++      val = FIELD_GET(RTL8365MB_L2_MC_D5_MBR_HI2_MSK, data[5]);
++      mc->member |= FIELD_PREP(RTL8365MB_L2_MC_MBR_HI2_MSK, val);
++
++      mc->igmpidx = FIELD_GET(RTL8365MB_L2_MC_D4_IGMPIDX_MSK, data[4]);
++      mc->igmp_asic = FIELD_GET(RTL8365MB_L2_MC_D5_IGMP_ASIC_MSK, data[5]);
++}
++
++static void rtl8365mb_l2_mc_to_data(const struct rtl8365mb_l2_mc *mc, u16 *data)
++{
++      u32 val;
++
++      memset(data, 0, RTL8365MB_L2_ENTRY_SIZE * 2);
++      data[0] |= FIELD_PREP(RTL8365MB_L2_MC_D0_MAC5_MSK, mc->key.mac_addr[5]);
++      data[0] |= FIELD_PREP(RTL8365MB_L2_MC_D0_MAC4_MSK, mc->key.mac_addr[4]);
++      data[1] |= FIELD_PREP(RTL8365MB_L2_MC_D1_MAC3_MSK, mc->key.mac_addr[3]);
++      data[1] |= FIELD_PREP(RTL8365MB_L2_MC_D1_MAC2_MSK, mc->key.mac_addr[2]);
++      data[2] |= FIELD_PREP(RTL8365MB_L2_MC_D2_MAC1_MSK, mc->key.mac_addr[1]);
++      data[2] |= FIELD_PREP(RTL8365MB_L2_MC_D2_MAC0_MSK, mc->key.mac_addr[0]);
++      data[3] |= FIELD_PREP(RTL8365MB_L2_MC_D3_VID_MSK, mc->key.vid);
++      data[3] |= FIELD_PREP(RTL8365MB_L2_MC_D3_IVL_MSK, mc->key.ivl);
++
++      val = FIELD_GET(RTL8365MB_L2_MC_MBR_HI1_MSK, mc->member);
++      data[3] |= FIELD_PREP(RTL8365MB_L2_MC_D3_MBR_HI1_MSK, val);
++
++      val = FIELD_GET(RTL8365MB_L2_MC_MBR_LO_MSK, mc->member);
++      data[4] |= FIELD_PREP(RTL8365MB_L2_MC_D4_MBR_MSK, val);
++
++      data[4] |= FIELD_PREP(RTL8365MB_L2_MC_D4_IGMPIDX_MSK, mc->igmpidx);
++      data[5] |= FIELD_PREP(RTL8365MB_L2_MC_D5_IGMP_ASIC_MSK, mc->igmp_asic);
++      data[5] |= FIELD_PREP(RTL8365MB_L2_MC_D5_PRIORITY_MSK, mc->priority);
++      data[5] |= FIELD_PREP(RTL8365MB_L2_MC_D5_FWD_PRI_MSK, mc->fwd_pri);
++      data[5] |= FIELD_PREP(RTL8365MB_L2_MC_D5_STATIC_MSK, mc->is_static);
++
++      val = FIELD_GET(RTL8365MB_L2_MC_MBR_HI2_MSK, mc->member);
++      data[5] |= FIELD_PREP(RTL8365MB_L2_MC_D5_MBR_HI2_MSK, val);
++}
++
++/*
++ * rtl8365mb_l2_get_next_uc() - get the next Unicast L2 entry
++ * @priv: realtek_priv pointer
++ * @addr: as input, the table index to start the walk
++ *        as output, the found table index
++ * @port: restrict the walk on entries related to port
++ * @entry: returned L2 Unicast table entry
++ *
++ * This function gets the next unicast L2 table entry starting from @addr
++ * and checking exclusively entries related to @port.
++ *
++ * On success, it returns 0, updates @addr to the index of the found entry,
++ * and populates @entry. If the search reaches the end of the table and
++ * wraps around and @addr will be strictly lower than the input @addr.
++ * Callers must detect this wrap-around condition to prevent infinite loops.
++ *
++ * If the table contains no matching entries at all, it returns -ENOENT
++ * and leaves @addr and @entry unmodified.
++ *
++ * Return: Returns 0 on success, a negative error on failure.
++ **/
++int rtl8365mb_l2_get_next_uc(struct realtek_priv *priv, u16 *addr, int port,
++                           struct realtek_fdb_entry *entry)
++{
++      u16 data[RTL8365MB_L2_ENTRY_SIZE] = { 0 };
++      struct rtl8365mb_l2_uc uc;
++      int ret;
++
++      ret = rtl8365mb_table_query(priv, RTL8365MB_TABLE_L2,
++                                  RTL8365MB_TABLE_OP_READ, addr,
++                                  RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT_UC_PORT,
++                                  port, data, RTL8365MB_L2_ENTRY_SIZE);
++      if (ret)
++              return ret;
++
++      rtl8365mb_l2_data_to_uc(data, &uc);
++
++      ether_addr_copy(entry->mac_addr, uc.key.mac_addr);
++      entry->vid = uc.key.vid;
++      entry->is_static = uc.is_static;
++
++      return 0;
++}
++
++int rtl8365mb_l2_add_uc(struct realtek_priv *priv, int port,
++                      const unsigned char mac_addr[static ETH_ALEN],
++                      u16 efid, u16 vid)
++{
++      u16 data[RTL8365MB_L2_ENTRY_SIZE] = { 0 };
++      struct rtl8365mb_l2_uc uc = { 0 };
++      u16 addr;
++      int ret;
++
++      memcpy(uc.key.mac_addr, mac_addr, ETH_ALEN);
++      uc.key.efid = efid;
++      uc.key.fid = 0;
++      uc.key.ivl = true;
++      uc.key.vid = vid;
++
++      uc.port = port;
++      /* Entries programmed by DSA (including those dynamically learned by
++       * the software bridge and injected into the CPU port via assisted
++       * learning) must be static. We do not let HW decrease age behind the
++       * OS's back. As a trade-off, these will show up as permanent to users.
++       */
++      uc.is_static = true;
++      /* age greater than 0 adds/updates entries */
++      uc.age = 1;
++      rtl8365mb_l2_uc_to_data(&uc, data);
++
++      /* add the new entry or update an existing one */
++      ret = rtl8365mb_table_query(priv, RTL8365MB_TABLE_L2,
++                                  RTL8365MB_TABLE_OP_WRITE, &addr,
++                                  0, 0,
++                                  data, RTL8365MB_L2_ENTRY_SIZE);
++
++      /* Assume the missing new entry as the table is full */
++      if (ret == -ENOENT)
++              return -ENOSPC;
++
++      /* addr will hold the table index, but it is not used here */
++      return ret;
++}
++
++int rtl8365mb_l2_del_uc(struct realtek_priv *priv, int port,
++                      const unsigned char mac_addr[static ETH_ALEN],
++                      u16 efid, u16 vid)
++{
++      u16 data[RTL8365MB_L2_ENTRY_SIZE] = { 0 };
++      struct rtl8365mb_l2_uc uc = { 0 };
++      u16 addr;
++      int ret;
++
++      memcpy(uc.key.mac_addr, mac_addr, ETH_ALEN);
++      uc.key.efid = efid;
++      uc.key.fid = 0;
++      uc.key.ivl = true;
++      uc.key.vid = vid;
++      /* age 0 deletes the entry */
++      uc.age = 0;
++      rtl8365mb_l2_uc_to_data(&uc, data);
++
++      /* it looks like the switch will always add/update the entry,
++       * even when age is 0 or uc.key did not match an existing entry,
++       * just to immediately drop it because age is zero. You can still
++       * get the added/updated address from @addr
++       */
++      ret = rtl8365mb_table_query(priv, RTL8365MB_TABLE_L2,
++                                  RTL8365MB_TABLE_OP_WRITE, &addr,
++                                  0, 0,
++                                  data, RTL8365MB_L2_ENTRY_SIZE);
++
++      if (ret == -ENOENT) {
++              dev_dbg(priv->dev, "%s: %pM vid=%d efid=%d missing\n",
++                      __func__, mac_addr, vid, efid);
++              /* Silently return success */
++              return 0;
++      }
++
++      /* addr will hold the table index, but it is not used here */
++      return ret;
++}
++
++int rtl8365mb_l2_flush(struct realtek_priv *priv, int port, u16 vid)
++{
++      int mode = vid ? RTL8365MB_L2_FLUSH_CTRL2_MODE_PORT_VID :
++                       RTL8365MB_L2_FLUSH_CTRL2_MODE_PORT;
++      u32 val, mask;
++      int ret;
++
++      mutex_lock(&priv->map_lock);
++
++      /* Configure flushing mode; only flush dynamic entries */
++      ret = regmap_write(priv->map_nolock, RTL8365MB_L2_FLUSH_CTRL2_REG,
++                         FIELD_PREP(RTL8365MB_L2_FLUSH_CTRL2_MODE_MSK,
++                                    mode) |
++                         FIELD_PREP(RTL8365MB_L2_FLUSH_CTRL2_TYPE_MSK,
++                                    RTL8365MB_L2_FLUSH_CTRL2_TYPE_DYNAMIC));
++      if (ret)
++              goto out;
++
++      ret = regmap_write(priv->map_nolock, RTL8365MB_L2_FLUSH_CTRL1_REG,
++                         FIELD_PREP(RTL8365MB_L2_FLUSH_CTRL1_VID_MSK, vid));
++
++      if (ret)
++              goto out;
++      /* Now issue the flush command and wait for its completion. There are
++       * two registers for this purpose, and which one to use depends on the
++       * port number. The _EXT register is for ports 8 or higher.
++       */
++      if (port < 8) {
++              val = FIELD_PREP(RTL8365MB_L2_FLUSH_PORT_MSK_MSK,
++                               BIT(port) & 0xFF);
++              ret = regmap_write(priv->map_nolock,
++                                 RTL8365MB_L2_FLUSH_PORT_REG, val);
++              if (ret)
++                      goto out;
++
++              mask = FIELD_PREP(RTL8365MB_L2_FLUSH_PORT_BUSY_MSK,
++                                BIT(port) & 0xFF);
++              ret = regmap_read_poll_timeout(priv->map_nolock,
++                                             RTL8365MB_L2_FLUSH_PORT_REG,
++                                             val, !(val & mask), 10, 10000);
++              if (ret)
++                      goto out;
++      } else {
++              val = FIELD_PREP(RTL8365MB_L2_FLUSH_PORT_EXT_MSK_MSK,
++                               BIT(port) >> 8);
++              ret = regmap_write(priv->map_nolock,
++                                 RTL8365MB_L2_FLUSH_PORT_EXT_REG, val);
++              if (ret)
++                      goto out;
++
++              mask = FIELD_PREP(RTL8365MB_L2_FLUSH_PORT_EXT_BUSY_MSK,
++                                BIT(port) >> 8);
++              ret = regmap_read_poll_timeout(priv->map_nolock,
++                                             RTL8365MB_L2_FLUSH_PORT_EXT_REG,
++                                             val, !(val & mask), 10, 10000);
++              if (ret)
++                      goto out;
++      }
++
++out:
++      mutex_unlock(&priv->map_lock);
++
++      return ret;
++}
++
++int rtl8365mb_l2_add_mc(struct realtek_priv *priv, int port,
++                      const unsigned char mac_addr[static ETH_ALEN],
++                      u16 vid)
++{
++      u16 data[RTL8365MB_L2_ENTRY_SIZE] = { 0 };
++      struct rtl8365mb_l2_mc mc = { 0 };
++      u16 addr;
++      int ret;
++
++      memcpy(mc.key.mac_addr, mac_addr, ETH_ALEN);
++      mc.key.vid = vid;
++      mc.key.ivl = true;
++      /* Already set the port and is_static, although not used in OP_READ,
++       * data will be ready for OP_WRITE if it is a new entry.
++       */
++      mc.member |= BIT(port);
++      mc.is_static = 1;
++      rtl8365mb_l2_mc_to_data(&mc, data);
++
++      /* First look for an existing entry (to get existing port members) */
++      ret = rtl8365mb_table_query(priv, RTL8365MB_TABLE_L2,
++                                  RTL8365MB_TABLE_OP_READ, &addr,
++                                  RTL8365MB_TABLE_L2_METHOD_MAC, 0,
++                                  data, RTL8365MB_L2_ENTRY_SIZE);
++      if (!ret) {
++              /* There is already an entry... */
++              rtl8365mb_l2_data_to_mc(data, &mc);
++              dev_dbg(priv->dev,
++                      "%s: found %pM addr=%d member=0x%x igmpidx=0x%x %s\n",
++                      __func__, mac_addr, addr, mc.member, mc.igmpidx,
++                      mc.is_static ? "static" : "dynamic");
++              /* the port must be added as a member */
++              mc.member |= BIT(port);
++
++              if (!mc.is_static) {
++                      dev_dbg(priv->dev,
++                              "%s: promoting addr=%d group to static\n",
++                              __func__, addr);
++                      mc.is_static = 1;
++              }
++
++              rtl8365mb_l2_mc_to_data(&mc, data);
++      } else if (ret == -ENOENT) {
++              /* New entry, no need to update data again as it already
++               * includes the member.
++               *
++               * Multicast hardware entries do not support EFID (bridge
++               * isolation). However, traffic isolation is still maintained
++               * because the hardware applies the port isolation masks
++               * (pmasks) configured in bridge_join after the L2 lookup.
++               * Entries from different bridges will collide on the same
++               * MAC+VID slot with an OR'ed member mask, but packets will
++               * only exit through ports allowed by the source port's pmask.
++               */
++      } else {
++              return ret;
++      }
++
++      /* add the new entry or update an existing one */
++      ret = rtl8365mb_table_query(priv, RTL8365MB_TABLE_L2,
++                                  RTL8365MB_TABLE_OP_WRITE, &addr,
++                                  0, 0,
++                                  data, RTL8365MB_L2_ENTRY_SIZE);
++
++      /* Assume the missing new entry as the table is full */
++      if (ret == -ENOENT)
++              return -ENOSPC;
++
++      return ret;
++}
++
++int rtl8365mb_l2_del_mc(struct realtek_priv *priv, int port,
++                      const unsigned char mac_addr[static ETH_ALEN],
++                      u16 vid)
++{
++      u16 data[RTL8365MB_L2_ENTRY_SIZE] = { 0 };
++      struct rtl8365mb_l2_mc mc = { 0 };
++      u16 addr;
++      int ret;
++
++      memcpy(mc.key.mac_addr, mac_addr, ETH_ALEN);
++      mc.key.vid = vid;
++      mc.key.ivl = true;
++      rtl8365mb_l2_mc_to_data(&mc, data);
++
++      /* First look for an existing entry (to get existing port members) */
++      ret = rtl8365mb_table_query(priv, RTL8365MB_TABLE_L2,
++                                  RTL8365MB_TABLE_OP_READ, &addr,
++                                  RTL8365MB_TABLE_L2_METHOD_MAC, 0,
++                                  data, RTL8365MB_L2_ENTRY_SIZE);
++      if (ret == -ENOENT) {
++              dev_dbg(priv->dev, "%s: %pM vid=%d missing\n",
++                      __func__, mac_addr, vid);
++              /* Silently return success */
++              return 0;
++      }
++
++      if (ret)
++              /* Return on any other error */
++              return ret;
++
++      rtl8365mb_l2_data_to_mc(data, &mc);
++      dev_dbg(priv->dev,
++              "%s: found %pM addr=%d member=0x%x igmpidx=0x%x %s\n",
++              __func__, mac_addr, addr, mc.member, mc.igmpidx,
++              mc.is_static ? "static" : "dynamic");
++      /* the port must be removed as a member */
++      mc.member &= ~BIT(port);
++      if (!mc.member) {
++              /* Multicast entries do not have an age field. Clearing both
++               * the member portmask and is_static flags is the hardware
++               * signal to invalidate and reclaim the L2 table slot.
++               */
++              mc.is_static = 0;
++              mc.igmpidx = 0;
++              mc.priority = 0;
++              mc.fwd_pri = 0;
++              mc.igmp_asic = 0;
++      }
++      rtl8365mb_l2_mc_to_data(&mc, data);
++
++      /* update the existing entry. */
++      ret = rtl8365mb_table_query(priv, RTL8365MB_TABLE_L2,
++                                  RTL8365MB_TABLE_OP_WRITE, &addr,
++                                  0, 0,
++                                  data, RTL8365MB_L2_ENTRY_SIZE);
++      return ret;
++}
+--- /dev/null
++++ b/drivers/net/dsa/realtek/rtl8365mb_l2.h
+@@ -0,0 +1,32 @@
++/* SPDX-License-Identifier: GPL-2.0 */
++/* Forwarding and multicast database interface for the rtl8365mb switch family
++ *
++ * Copyright (C) 2022 Alvin Šipraga <alsi@bang-olufsen.dk>
++ */
++
++#ifndef _REALTEK_RTL8365MB_L2_H
++#define _REALTEK_RTL8365MB_L2_H
++
++#include <linux/if_ether.h>
++#include <linux/types.h>
++
++#include "realtek.h"
++
++int rtl8365mb_l2_get_next_uc(struct realtek_priv *priv, u16 *addr, int port,
++                           struct realtek_fdb_entry *entry);
++int rtl8365mb_l2_add_uc(struct realtek_priv *priv, int port,
++                      const unsigned char addr[static ETH_ALEN],
++                      u16 efid, u16 vid);
++int rtl8365mb_l2_del_uc(struct realtek_priv *priv, int port,
++                      const unsigned char addr[static ETH_ALEN],
++                      u16 efid, u16 vid);
++int rtl8365mb_l2_flush(struct realtek_priv *priv, int port, u16 vid);
++
++int rtl8365mb_l2_add_mc(struct realtek_priv *priv, int port,
++                      const unsigned char mac_addr[static ETH_ALEN],
++                      u16 vid);
++int rtl8365mb_l2_del_mc(struct realtek_priv *priv, int port,
++                      const unsigned char mac_addr[static ETH_ALEN],
++                      u16 vid);
++
++#endif /* _REALTEK_RTL8365MB_L2_H */
+--- a/drivers/net/dsa/realtek/rtl8365mb_main.c
++++ b/drivers/net/dsa/realtek/rtl8365mb_main.c
+@@ -104,6 +104,7 @@
+ #include "realtek-smi.h"
+ #include "realtek-mdio.h"
+ #include "rtl83xx.h"
++#include "rtl8365mb_l2.h"
+ #include "rtl8365mb_vlan.h"
+ /* Family-specific data and limits */
+@@ -111,8 +112,12 @@
+ #define RTL8365MB_NUM_PHYREGS         32
+ #define RTL8365MB_PHYREGMAX           (RTL8365MB_NUM_PHYREGS - 1)
+ #define RTL8365MB_MAX_NUM_PORTS               11
+-#define RTL8365MB_MAX_NUM_EXTINTS     3
++/* Valid for the whole family except RTL8370B, which has 4160 entries.
++ * RTL8370B is mentioned in vendor code but it might not even belong
++ * to the same RTL8367C family.
++ */
+ #define RTL8365MB_LEARN_LIMIT_MAX     2112
++#define RTL8365MB_MAX_NUM_EXTINTS     3
+ /* Chip identification registers */
+ #define RTL8365MB_CHIP_ID_REG         0x1300
+@@ -285,6 +290,15 @@
+               (RTL8365MB_PORT_ISOLATION_REG_BASE + (_physport))
+ #define   RTL8365MB_PORT_ISOLATION_MASK                       0x07FF
++/* Extended filter ID registers - used to key forwarding database with IVL */
++#define RTL8365MB_EFID_MASK                   GENMASK(2, 0)
++#define RTL8365MB_PORT_EFID_REG_BASE          0x0A32
++#define RTL8365MB_PORT_EFID_REG(_p) \
++              (RTL8365MB_PORT_EFID_REG_BASE + ((_p) >> 2))
++#define   RTL8365MB_PORT_EFID_OFFSET(_p)      (((_p) & 0x3) << 2)
++#define   RTL8365MB_PORT_EFID_MASK(_p) \
++              (RTL8365MB_EFID_MASK << RTL8365MB_PORT_EFID_OFFSET(_p))
++
+ /* MSTP port state registers - indexed by tree instance */
+ #define RTL8365MB_MSTI_CTRL_BASE                      0x0A00
+ #define RTL8365MB_MSTI_CTRL_REG(_msti, _physport) \
+@@ -2432,6 +2446,11 @@ static int rtl8365mb_setup(struct dsa_sw
+       if (ret)
+               goto out_teardown_irq;
++      ds->assisted_learning_on_cpu_port = true;
++      ds->fdb_isolation = true;
++      /* The EFID is 3 bits, but EFID 0 is reserved for standalone ports */
++      ds->max_num_bridges = FIELD_MAX(RTL8365MB_EFID_MASK);
++
+       ds->configure_vlan_while_not_filtering = true;
+       /* Set up VLAN */
+@@ -2549,6 +2568,12 @@ static const struct dsa_switch_ops rtl83
+       .teardown = rtl8365mb_teardown,
+       .phylink_get_caps = rtl8365mb_phylink_get_caps,
+       .port_stp_state_set = rtl8365mb_port_stp_state_set,
++      .port_fast_age = rtl83xx_port_fast_age,
++      .port_fdb_add = rtl83xx_port_fdb_add,
++      .port_fdb_del = rtl83xx_port_fdb_del,
++      .port_fdb_dump = rtl83xx_port_fdb_dump,
++      .port_mdb_add = rtl83xx_port_mdb_add,
++      .port_mdb_del = rtl83xx_port_mdb_del,
+       .port_vlan_add = rtl8365mb_port_vlan_add,
+       .port_vlan_del = rtl8365mb_port_vlan_del,
+       .port_vlan_filtering = rtl8365mb_port_vlan_filtering,
+@@ -2565,6 +2590,12 @@ static const struct dsa_switch_ops rtl83
+ static const struct realtek_ops rtl8365mb_ops = {
+       .detect = rtl8365mb_detect,
++      .l2_add_uc = rtl8365mb_l2_add_uc,
++      .l2_del_uc = rtl8365mb_l2_del_uc,
++      .l2_get_next_uc = rtl8365mb_l2_get_next_uc,
++      .l2_add_mc = rtl8365mb_l2_add_mc,
++      .l2_del_mc = rtl8365mb_l2_del_mc,
++      .l2_flush = rtl8365mb_l2_flush,
+       .phy_read = rtl8365mb_phy_read,
+       .phy_write = rtl8365mb_phy_write,
+ };
+--- 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/etherdevice.h>
+ #include "realtek.h"
+ #include "rtl83xx.h"
+@@ -156,6 +157,7 @@ rtl83xx_probe(struct device *dev,
+       mutex_init(&priv->map_lock);
+       mutex_init(&priv->vlan_lock);
++      mutex_init(&priv->l2_lock);
+       rc.lock_arg = priv;
+       priv->map = devm_regmap_init(dev, NULL, priv, &rc);
+@@ -326,6 +328,296 @@ void rtl83xx_reset_deassert(struct realt
+       gpiod_set_value(priv->reset, false);
+ }
++/**
++ * rtl83xx_port_fast_age() - flush dynamic FDB entries learned on a port
++ * @ds: DSA switch instance
++ * @port: port index
++ *
++ * This function requests the switch to age out dynamic FDB entries learned on
++ * @port.
++ *
++ * Context: Can sleep.
++ * Return: Nothing.
++ */
++void rtl83xx_port_fast_age(struct dsa_switch *ds, int port)
++{
++      struct realtek_priv *priv = ds->priv;
++      int ret;
++
++      if (!priv->ops->l2_flush) {
++              dev_warn_once(priv->dev, "l2_flush op not defined\n");
++              return;
++      }
++
++      dev_dbg(priv->dev, "fast_age port %d\n", port);
++
++      mutex_lock(&priv->l2_lock);
++      ret = priv->ops->l2_flush(priv, port, 0);
++      mutex_unlock(&priv->l2_lock);
++      if (ret)
++              dev_err(priv->dev, "failed to fast age on port %d: %d\n", port,
++                      ret);
++}
++EXPORT_SYMBOL_NS_GPL(rtl83xx_port_fast_age, REALTEK_DSA);
++
++/**
++ * rtl83xx_port_fdb_add() - add a static FDB entry to a port database
++ * @ds: DSA switch instance
++ * @port: port index
++ * @addr: MAC address to add
++ * @vid: VLAN ID associated with @addr
++ * @db: database where the entry should be added
++ *
++ * This function adds a static unicast FDB entry to the standalone port
++ * database or to a bridge database.
++ *
++ * Context: Can sleep.
++ * Return: 0 on success, negative value for failure.
++ */
++int rtl83xx_port_fdb_add(struct dsa_switch *ds, int port,
++                       const unsigned char *addr, u16 vid,
++                       struct dsa_db db)
++{
++      struct realtek_priv *priv = ds->priv;
++      int efid;
++      int ret;
++
++      if (is_multicast_ether_addr(addr))
++              return -EOPNOTSUPP;
++
++      if (!priv->ops->l2_add_uc)
++              return -EOPNOTSUPP;
++
++      if (db.type != DSA_DB_PORT && db.type != DSA_DB_BRIDGE)
++              return -EOPNOTSUPP;
++
++      /* Bridge ports use bridge.num as EFID, while standalone ports use
++       * EFID 0. FDB entries for the CPU port follow the bridge EFID due
++       * to assisted learning.
++       */
++      efid = db.type == DSA_DB_BRIDGE ? db.bridge.num : 0;
++
++      dev_dbg(priv->dev, "%s: port:%d addr:%pM efid:%d vid:%d dbtype:%d\n",
++              __func__, port, addr, efid, vid, db.type);
++
++      mutex_lock(&priv->l2_lock);
++      ret = priv->ops->l2_add_uc(priv, port, addr, efid, vid);
++
++      mutex_unlock(&priv->l2_lock);
++
++      if (ret)
++              dev_err(priv->dev, "fdb_add ERROR %pe\n", ERR_PTR(ret));
++      return ret;
++}
++EXPORT_SYMBOL_NS_GPL(rtl83xx_port_fdb_add, REALTEK_DSA);
++
++/**
++ * rtl83xx_port_fdb_del() - delete a static FDB entry from a port database
++ * @ds: DSA switch instance
++ * @port: port index
++ * @addr: MAC address to delete
++ * @vid: VLAN ID associated with @addr
++ * @db: database where the entry should be removed
++ *
++ * This function deletes a static unicast FDB entry from the standalone port
++ * database or from a bridge database.
++ *
++ * Context: Can sleep.
++ * Return: 0 on success, negative value for failure.
++ */
++int rtl83xx_port_fdb_del(struct dsa_switch *ds, int port,
++                       const unsigned char *addr, u16 vid,
++                       struct dsa_db db)
++{
++      struct realtek_priv *priv = ds->priv;
++      int efid;
++      int ret;
++
++      if (is_multicast_ether_addr(addr))
++              return -EOPNOTSUPP;
++
++      if (!priv->ops->l2_del_uc)
++              return -EOPNOTSUPP;
++
++      if (db.type != DSA_DB_PORT && db.type != DSA_DB_BRIDGE)
++              return -EOPNOTSUPP;
++
++      /*
++       * DSA_DB_BRIDGE ports use bridge number [1..N] as EFID, while
++       * DSA_DB_PORT use the default EFID (0), not used by any bridge.
++       */
++      efid = db.type == DSA_DB_BRIDGE ? db.bridge.num : 0;
++
++      dev_dbg(priv->dev, "%s: port:%d addr:%pM efid:%d vid:%d dbtype:%d\n",
++              __func__, port, addr, efid, vid, db.type);
++
++      mutex_lock(&priv->l2_lock);
++      ret = priv->ops->l2_del_uc(priv, port, addr, efid, vid);
++      mutex_unlock(&priv->l2_lock);
++
++      if (ret)
++              dev_err(priv->dev, "fdb_del ERROR %pe\n", ERR_PTR(ret));
++      return ret;
++}
++EXPORT_SYMBOL_NS_GPL(rtl83xx_port_fdb_del, REALTEK_DSA);
++
++/**
++ * rtl83xx_port_fdb_dump() - iterate over FDB entries associated with a port
++ * @ds: DSA switch instance
++ * @port: port index
++ * @cb: callback invoked for each entry
++ * @data: opaque pointer passed to @cb
++ *
++ * This function walks the unicast FDB entries associated with @port and calls
++ * @cb for each matching entry.
++ *
++ * Context: Can sleep.
++ * Return: 0 on success, or negative value for failure.
++ */
++int rtl83xx_port_fdb_dump(struct dsa_switch *ds, int port,
++                        dsa_fdb_dump_cb_t *cb, void *data)
++{
++      struct realtek_fdb_entry entry = { 0 };
++      struct realtek_priv *priv = ds->priv;
++      u16 start_addr, addr = 0;
++      int ret = 0;
++
++      if (!priv->ops->l2_get_next_uc)
++              return -EOPNOTSUPP;
++
++      mutex_lock(&priv->l2_lock);
++      while (true) {
++              start_addr = addr;
++
++              dev_dbg(priv->dev, "l2_get_next_uc, addr:%d, port:%d\n",
++                      addr, port);
++              ret = priv->ops->l2_get_next_uc(priv, &addr, port, &entry);
++              dev_dbg(priv->dev,
++                      "%s addr:%d mac:%pM vid:%d static:%d ret:%pe\n",
++                      __func__, addr, entry.mac_addr, entry.vid,
++                      entry.is_static, ERR_PTR(ret));
++
++              if (ret == -ENOENT) {
++                      /* If the table is empty, returns without errors. Note
++                       * that the l2_get_next_uc overflow to the first match
++                       * when it reaches the end of the table.
++                       */
++                      ret = 0;
++                      break;
++              }
++
++              if (ret)
++                      break;
++
++              /* When the addr returned is before the requested one, it
++               * indicates that we reached the end.
++               */
++              if (addr < start_addr)
++                      break;
++
++              ret = cb(entry.mac_addr, entry.vid, entry.is_static, data);
++              if (ret)
++                      break;
++
++              addr++;
++      }
++      mutex_unlock(&priv->l2_lock);
++
++      return ret;
++}
++EXPORT_SYMBOL_NS_GPL(rtl83xx_port_fdb_dump, REALTEK_DSA);
++
++/**
++ * rtl83xx_port_mdb_add() - add a multicast database entry to a port database
++ * @ds: DSA switch instance
++ * @port: port index
++ * @mdb: multicast database entry to add
++ * @db: database where the entry should be added
++ *
++ * This function adds a multicast database entry to the standalone port
++ * database or to a bridge database.
++ *
++ * Context: Can sleep.
++ * Return: 0 on success, negative value for failure.
++ */
++int rtl83xx_port_mdb_add(struct dsa_switch *ds, int port,
++                       const struct switchdev_obj_port_mdb *mdb,
++                       struct dsa_db db)
++{
++      struct realtek_priv *priv = ds->priv;
++      const unsigned char *addr = mdb->addr;
++      u16 vid = mdb->vid;
++      int efid;
++      int ret;
++
++      if (!priv->ops->l2_add_mc)
++              return -EOPNOTSUPP;
++
++      if (db.type != DSA_DB_PORT && db.type != DSA_DB_BRIDGE)
++              return -EOPNOTSUPP;
++
++      /* EFID is not used by hardware MDB entries; debugging only */
++      efid = db.type == DSA_DB_BRIDGE ? db.bridge.num : 0;
++
++      dev_dbg(priv->dev, "%s: port:%d addr:%pM efid:%d vid:%d dbtype:%d\n",
++              __func__, port, addr, efid, vid, db.type);
++
++      mutex_lock(&priv->l2_lock);
++      ret = priv->ops->l2_add_mc(priv, port, addr, vid);
++      mutex_unlock(&priv->l2_lock);
++
++      if (ret)
++              dev_err(priv->dev, "mdb_add ERROR %pe\n", ERR_PTR(ret));
++      return ret;
++}
++EXPORT_SYMBOL_NS_GPL(rtl83xx_port_mdb_add, REALTEK_DSA);
++
++/**
++ * rtl83xx_port_mdb_del() - delete a multicast database entry from a port
++ * database
++ * @ds: DSA switch instance
++ * @port: port index
++ * @mdb: multicast database entry to delete
++ * @db: database where the entry should be removed
++ *
++ * This function deletes a multicast database entry from the standalone port
++ * database or from a bridge database.
++ *
++ * Context: Can sleep.
++ * Return: 0 on success, negative value for failure.
++ */
++int rtl83xx_port_mdb_del(struct dsa_switch *ds, int port,
++                       const struct switchdev_obj_port_mdb *mdb,
++                       struct dsa_db db)
++{
++      struct realtek_priv *priv = ds->priv;
++      const unsigned char *addr = mdb->addr;
++      u16 vid = mdb->vid;
++      int efid;
++      int ret;
++
++      if (!priv->ops->l2_del_mc)
++              return -EOPNOTSUPP;
++
++      if (db.type != DSA_DB_PORT && db.type != DSA_DB_BRIDGE)
++              return -EOPNOTSUPP;
++
++      /* EFID is not used by hardware MDB entries; debugging only */
++      efid = db.type == DSA_DB_BRIDGE ? db.bridge.num : 0;
++
++      dev_dbg(priv->dev, "%s: port:%d addr:%pM efid:%d vid:%d dbtype:%d\n",
++              __func__, port, addr, efid, vid, db.type);
++
++      mutex_lock(&priv->l2_lock);
++      ret = priv->ops->l2_del_mc(priv, port, addr, vid);
++      mutex_unlock(&priv->l2_lock);
++
++      if (ret)
++              dev_err(priv->dev, "mdb_del ERROR %pe\n", ERR_PTR(ret));
++      return ret;
++}
++EXPORT_SYMBOL_NS_GPL(rtl83xx_port_mdb_del, 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
+@@ -21,4 +21,20 @@ void rtl83xx_remove(struct realtek_priv
+ void rtl83xx_reset_assert(struct realtek_priv *priv);
+ void rtl83xx_reset_deassert(struct realtek_priv *priv);
++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,
++                       struct dsa_db db);
++int rtl83xx_port_fdb_del(struct dsa_switch *ds, int port,
++                       const unsigned char *addr, u16 vid,
++                       struct dsa_db db);
++int rtl83xx_port_fdb_dump(struct dsa_switch *ds, int port,
++                        dsa_fdb_dump_cb_t *cb, void *data);
++int rtl83xx_port_mdb_add(struct dsa_switch *ds, int port,
++                       const struct switchdev_obj_port_mdb *mdb,
++                       struct dsa_db db);
++int rtl83xx_port_mdb_del(struct dsa_switch *ds, int port,
++                       const struct switchdev_obj_port_mdb *mdb,
++                       struct dsa_db db);
++
+ #endif /* _RTL83XX_H */
diff --git a/target/linux/generic/backport-6.12/942-08-v7.2-net-dsa-realtek-rtl8365mb-add-port_bridge_-join-leav.patch b/target/linux/generic/backport-6.12/942-08-v7.2-net-dsa-realtek-rtl8365mb-add-port_bridge_-join-leav.patch
new file mode 100644 (file)
index 0000000..5395f3d
--- /dev/null
@@ -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,
diff --git a/target/linux/generic/backport-6.12/942-09-v7.2-net-dsa-realtek-rtl8365mb-add-bridge-port-flags.patch b/target/linux/generic/backport-6.12/942-09-v7.2-net-dsa-realtek-rtl8365mb-add-bridge-port-flags.patch
new file mode 100644 (file)
index 0000000..c2f71e6
--- /dev/null
@@ -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,
diff --git a/target/linux/generic/backport-6.18/940-01-v7.1-net-dsa-tag_rtl8_4-update-format-description.patch b/target/linux/generic/backport-6.18/940-01-v7.1-net-dsa-tag_rtl8_4-update-format-description.patch
new file mode 100644 (file)
index 0000000..d4c129a
--- /dev/null
@@ -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 */
diff --git a/target/linux/generic/backport-6.18/940-02-v7.1-net-dsa-tag_rtl8_4-set-KEEP-flag.patch b/target/linux/generic/backport-6.18/940-02-v7.1-net-dsa-tag_rtl8_4-set-KEEP-flag.patch
new file mode 100644 (file)
index 0000000..7a9ea34
--- /dev/null
@@ -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)));
diff --git a/target/linux/generic/backport-6.18/942-01-7.2-net-dsa-realtek-rtl8365mb-use-ERR_PTR.patch b/target/linux/generic/backport-6.18/942-01-7.2-net-dsa-realtek-rtl8365mb-use-ERR_PTR.patch
new file mode 100644 (file)
index 0000000..8a790f5
--- /dev/null
@@ -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;
+       }
diff --git a/target/linux/generic/backport-6.18/942-02-7.2-net-dsa-realtek-rtl8365mb-reject-unsupported-topolog.patch b/target/linux/generic/backport-6.18/942-02-7.2-net-dsa-realtek-rtl8365mb-reject-unsupported-topolog.patch
new file mode 100644 (file)
index 0000000..8a907f8
--- /dev/null
@@ -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;
diff --git a/target/linux/generic/backport-6.18/942-03-7.2-net-dsa-realtek-rtl8365mb-use-dsa-helpers-for-port-i.patch b/target/linux/generic/backport-6.18/942-03-7.2-net-dsa-realtek-rtl8365mb-use-dsa-helpers-for-port-i.patch
new file mode 100644 (file)
index 0000000..95e2d71
--- /dev/null
@@ -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;
diff --git a/target/linux/generic/backport-6.18/942-04-7.2-net-dsa-realtek-rtl8365mb-prepare-for-multiple-sourc.patch b/target/linux/generic/backport-6.18/942-04-7.2-net-dsa-realtek-rtl8365mb-prepare-for-multiple-sourc.patch
new file mode 100644 (file)
index 0000000..55e6845
--- /dev/null
@@ -0,0 +1,4631 @@
+From 0e692c27fe84db294866fe726f9944f7e125b817 Mon Sep 17 00:00:00 2001
+From: Alvin Šipraga <alsi@bang-olufsen.dk>
+Date: Sat, 6 Jun 2026 05:29:28 -0300
+Subject: net: dsa: realtek: rtl8365mb: prepare for multiple source files
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+Rename rtl8365mb.c to rtl8365mb_main.c in preparation for subsequent
+commits which add additional source files to the driver.
+
+The trailing backslash in the Makefile is deliberate. It allows for new
+files to be added without clobbering git history.
+
+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-4-b9e409687cbe@gmail.com
+Signed-off-by: Jakub Kicinski <kuba@kernel.org>
+---
+ drivers/net/dsa/realtek/Makefile         |    2 +
+ drivers/net/dsa/realtek/rtl8365mb.c      | 2296 ------------------------------
+ drivers/net/dsa/realtek/rtl8365mb_main.c | 2296 ++++++++++++++++++++++++++++++
+ 3 files changed, 2298 insertions(+), 2296 deletions(-)
+ delete mode 100644 drivers/net/dsa/realtek/rtl8365mb.c
+ create mode 100644 drivers/net/dsa/realtek/rtl8365mb_main.c
+
+--- a/drivers/net/dsa/realtek/Makefile
++++ b/drivers/net/dsa/realtek/Makefile
+@@ -16,3 +16,5 @@ ifdef CONFIG_NET_DSA_REALTEK_RTL8366RB_L
+ rtl8366-objs                          += rtl8366rb-leds.o
+ endif
+ obj-$(CONFIG_NET_DSA_REALTEK_RTL8365MB) += rtl8365mb.o
++rtl8365mb-objs := rtl8365mb_main.o \
++# end of rtl8365mb-objs
+--- a/drivers/net/dsa/realtek/rtl8365mb.c
++++ /dev/null
+@@ -1,2294 +0,0 @@
+-// SPDX-License-Identifier: GPL-2.0
+-/* Realtek SMI subdriver for the Realtek RTL8365MB-VC ethernet switch.
+- *
+- * Copyright (C) 2021 Alvin Šipraga <alsi@bang-olufsen.dk>
+- * Copyright (C) 2021 Michael Rasmussen <mir@bang-olufsen.dk>
+- *
+- * The RTL8365MB-VC is a 4+1 port 10/100/1000M switch controller. It includes 4
+- * integrated PHYs for the user facing ports, and an extension interface which
+- * can be connected to the CPU - or another PHY - via either MII, RMII, or
+- * RGMII. The switch is configured via the Realtek Simple Management Interface
+- * (SMI), which uses the MDIO/MDC lines.
+- *
+- * Below is a simplified block diagram of the chip and its relevant interfaces.
+- *
+- *                          .-----------------------------------.
+- *                          |                                   |
+- *         UTP <---------------> Giga PHY <-> PCS <-> P0 GMAC   |
+- *         UTP <---------------> Giga PHY <-> PCS <-> P1 GMAC   |
+- *         UTP <---------------> Giga PHY <-> PCS <-> P2 GMAC   |
+- *         UTP <---------------> Giga PHY <-> PCS <-> P3 GMAC   |
+- *                          |                                   |
+- *     CPU/PHY <-MII/RMII/RGMII--->  Extension  <---> Extension |
+- *                          |       interface 1        GMAC 1   |
+- *                          |                                   |
+- *     SMI driver/ <-MDC/SCL---> Management    ~~~~~~~~~~~~~~   |
+- *        EEPROM   <-MDIO/SDA--> interface     ~REALTEK ~~~~~   |
+- *                          |                  ~RTL8365MB ~~~   |
+- *                          |                  ~GXXXC TAIWAN~   |
+- *        GPIO <--------------> Reset          ~~~~~~~~~~~~~~   |
+- *                          |                                   |
+- *      Interrupt  <----------> Link UP/DOWN events             |
+- *      controller          |                                   |
+- *                          '-----------------------------------'
+- *
+- * The driver uses DSA to integrate the 4 user and 1 extension ports into the
+- * kernel. Netdevices are created for the user ports, as are PHY devices for
+- * their integrated PHYs. The device tree firmware should also specify the link
+- * partner of the extension port - either via a fixed-link or other phy-handle.
+- * See the device tree bindings for more detailed information. Note that the
+- * driver has only been tested with a fixed-link, but in principle it should not
+- * matter.
+- *
+- * NOTE: Currently, only the RGMII interface is implemented in this driver.
+- *
+- * The interrupt line is asserted on link UP/DOWN events. The driver creates a
+- * custom irqchip to handle this interrupt and demultiplex the events by reading
+- * the status registers via SMI. Interrupts are then propagated to the relevant
+- * PHY device.
+- *
+- * The EEPROM contains initial register values which the chip will read over I2C
+- * upon hardware reset. It is also possible to omit the EEPROM. In both cases,
+- * the driver will manually reprogram some registers using jam tables to reach
+- * an initial state defined by the vendor driver.
+- *
+- * This Linux driver is written based on an OS-agnostic vendor driver from
+- * Realtek. The reference GPL-licensed sources can be found in the OpenWrt
+- * source tree under the name rtl8367c. The vendor driver claims to support a
+- * number of similar switch controllers from Realtek, but the only hardware we
+- * have is the RTL8365MB-VC. Moreover, there does not seem to be any chip under
+- * the name RTL8367C. Although one wishes that the 'C' stood for some kind of
+- * common hardware revision, there exist examples of chips with the suffix -VC
+- * which are explicitly not supported by the rtl8367c driver and which instead
+- * require the rtl8367d vendor driver. With all this uncertainty, the driver has
+- * been modestly named rtl8365mb. Future implementors may wish to rename things
+- * accordingly.
+- *
+- * In the same family of chips, some carry up to 8 user ports and up to 2
+- * extension ports. Where possible this driver tries to make things generic, but
+- * more work must be done to support these configurations. According to
+- * documentation from Realtek, the family should include the following chips:
+- *
+- *  - RTL8363NB
+- *  - RTL8363NB-VB
+- *  - RTL8363SC
+- *  - RTL8363SC-VB
+- *  - RTL8364NB
+- *  - RTL8364NB-VB
+- *  - RTL8365MB-VC
+- *  - RTL8366SC
+- *  - RTL8367RB-VB
+- *  - RTL8367SB
+- *  - RTL8367S
+- *  - RTL8370MB
+- *  - RTL8310SR
+- *
+- * Some of the register logic for these additional chips has been skipped over
+- * while implementing this driver. It is therefore not possible to assume that
+- * things will work out-of-the-box for other chips, and a careful review of the
+- * vendor driver may be needed to expand support. The RTL8365MB-VC seems to be
+- * one of the simpler chips.
+- */
+-
+-#include <linux/bitfield.h>
+-#include <linux/bitops.h>
+-#include <linux/interrupt.h>
+-#include <linux/irqdomain.h>
+-#include <linux/mutex.h>
+-#include <linux/of_irq.h>
+-#include <linux/regmap.h>
+-#include <linux/if_bridge.h>
+-#include <linux/if_vlan.h>
+-
+-#include "realtek.h"
+-#include "realtek-smi.h"
+-#include "realtek-mdio.h"
+-#include "rtl83xx.h"
+-
+-/* Family-specific data and limits */
+-#define RTL8365MB_PHYADDRMAX          7
+-#define RTL8365MB_NUM_PHYREGS         32
+-#define RTL8365MB_PHYREGMAX           (RTL8365MB_NUM_PHYREGS - 1)
+-#define RTL8365MB_MAX_NUM_PORTS               11
+-#define RTL8365MB_MAX_NUM_EXTINTS     3
+-#define RTL8365MB_LEARN_LIMIT_MAX     2112
+-
+-/* Chip identification registers */
+-#define RTL8365MB_CHIP_ID_REG         0x1300
+-
+-#define RTL8365MB_CHIP_VER_REG                0x1301
+-
+-#define RTL8365MB_MAGIC_REG           0x13C2
+-#define   RTL8365MB_MAGIC_VALUE               0x0249
+-
+-/* Chip reset register */
+-#define RTL8365MB_CHIP_RESET_REG      0x1322
+-#define RTL8365MB_CHIP_RESET_SW_MASK  0x0002
+-#define RTL8365MB_CHIP_RESET_HW_MASK  0x0001
+-
+-/* Interrupt polarity register */
+-#define RTL8365MB_INTR_POLARITY_REG   0x1100
+-#define   RTL8365MB_INTR_POLARITY_MASK        0x0001
+-#define   RTL8365MB_INTR_POLARITY_HIGH        0
+-#define   RTL8365MB_INTR_POLARITY_LOW 1
+-
+-/* Interrupt control/status register - enable/check specific interrupt types */
+-#define RTL8365MB_INTR_CTRL_REG                       0x1101
+-#define RTL8365MB_INTR_STATUS_REG             0x1102
+-#define   RTL8365MB_INTR_SLIENT_START_2_MASK  0x1000
+-#define   RTL8365MB_INTR_SLIENT_START_MASK    0x0800
+-#define   RTL8365MB_INTR_ACL_ACTION_MASK      0x0200
+-#define   RTL8365MB_INTR_CABLE_DIAG_FIN_MASK  0x0100
+-#define   RTL8365MB_INTR_INTERRUPT_8051_MASK  0x0080
+-#define   RTL8365MB_INTR_LOOP_DETECTION_MASK  0x0040
+-#define   RTL8365MB_INTR_GREEN_TIMER_MASK     0x0020
+-#define   RTL8365MB_INTR_SPECIAL_CONGEST_MASK 0x0010
+-#define   RTL8365MB_INTR_SPEED_CHANGE_MASK    0x0008
+-#define   RTL8365MB_INTR_LEARN_OVER_MASK      0x0004
+-#define   RTL8365MB_INTR_METER_EXCEEDED_MASK  0x0002
+-#define   RTL8365MB_INTR_LINK_CHANGE_MASK     0x0001
+-#define   RTL8365MB_INTR_ALL_MASK                      \
+-              (RTL8365MB_INTR_SLIENT_START_2_MASK |  \
+-               RTL8365MB_INTR_SLIENT_START_MASK |    \
+-               RTL8365MB_INTR_ACL_ACTION_MASK |      \
+-               RTL8365MB_INTR_CABLE_DIAG_FIN_MASK |  \
+-               RTL8365MB_INTR_INTERRUPT_8051_MASK |  \
+-               RTL8365MB_INTR_LOOP_DETECTION_MASK |  \
+-               RTL8365MB_INTR_GREEN_TIMER_MASK |     \
+-               RTL8365MB_INTR_SPECIAL_CONGEST_MASK | \
+-               RTL8365MB_INTR_SPEED_CHANGE_MASK |    \
+-               RTL8365MB_INTR_LEARN_OVER_MASK |      \
+-               RTL8365MB_INTR_METER_EXCEEDED_MASK |  \
+-               RTL8365MB_INTR_LINK_CHANGE_MASK)
+-
+-/* Per-port interrupt type status registers */
+-#define RTL8365MB_PORT_LINKDOWN_IND_REG               0x1106
+-#define   RTL8365MB_PORT_LINKDOWN_IND_MASK    0x07FF
+-
+-#define RTL8365MB_PORT_LINKUP_IND_REG         0x1107
+-#define   RTL8365MB_PORT_LINKUP_IND_MASK      0x07FF
+-
+-/* PHY indirect access registers */
+-#define RTL8365MB_INDIRECT_ACCESS_CTRL_REG                    0x1F00
+-#define   RTL8365MB_INDIRECT_ACCESS_CTRL_RW_MASK              0x0002
+-#define   RTL8365MB_INDIRECT_ACCESS_CTRL_RW_READ              0
+-#define   RTL8365MB_INDIRECT_ACCESS_CTRL_RW_WRITE             1
+-#define   RTL8365MB_INDIRECT_ACCESS_CTRL_CMD_MASK             0x0001
+-#define   RTL8365MB_INDIRECT_ACCESS_CTRL_CMD_VALUE            1
+-#define RTL8365MB_INDIRECT_ACCESS_STATUS_REG                  0x1F01
+-#define RTL8365MB_INDIRECT_ACCESS_ADDRESS_REG                 0x1F02
+-#define   RTL8365MB_INDIRECT_ACCESS_ADDRESS_OCPADR_5_1_MASK   GENMASK(4, 0)
+-#define   RTL8365MB_INDIRECT_ACCESS_ADDRESS_PHYNUM_MASK               GENMASK(7, 5)
+-#define   RTL8365MB_INDIRECT_ACCESS_ADDRESS_OCPADR_9_6_MASK   GENMASK(11, 8)
+-#define   RTL8365MB_PHY_BASE                                  0x2000
+-#define RTL8365MB_INDIRECT_ACCESS_WRITE_DATA_REG              0x1F03
+-#define RTL8365MB_INDIRECT_ACCESS_READ_DATA_REG                       0x1F04
+-
+-/* PHY OCP address prefix register */
+-#define RTL8365MB_GPHY_OCP_MSB_0_REG                  0x1D15
+-#define   RTL8365MB_GPHY_OCP_MSB_0_CFG_CPU_OCPADR_MASK        0x0FC0
+-#define RTL8365MB_PHY_OCP_ADDR_PREFIX_MASK            0xFC00
+-
+-/* The PHY OCP addresses of PHY registers 0~31 start here */
+-#define RTL8365MB_PHY_OCP_ADDR_PHYREG_BASE            0xA400
+-
+-/* External interface port mode values - used in DIGITAL_INTERFACE_SELECT */
+-#define RTL8365MB_EXT_PORT_MODE_DISABLE               0
+-#define RTL8365MB_EXT_PORT_MODE_RGMII         1
+-#define RTL8365MB_EXT_PORT_MODE_MII_MAC               2
+-#define RTL8365MB_EXT_PORT_MODE_MII_PHY               3
+-#define RTL8365MB_EXT_PORT_MODE_TMII_MAC      4
+-#define RTL8365MB_EXT_PORT_MODE_TMII_PHY      5
+-#define RTL8365MB_EXT_PORT_MODE_GMII          6
+-#define RTL8365MB_EXT_PORT_MODE_RMII_MAC      7
+-#define RTL8365MB_EXT_PORT_MODE_RMII_PHY      8
+-#define RTL8365MB_EXT_PORT_MODE_SGMII         9
+-#define RTL8365MB_EXT_PORT_MODE_HSGMII                10
+-#define RTL8365MB_EXT_PORT_MODE_1000X_100FX   11
+-#define RTL8365MB_EXT_PORT_MODE_1000X         12
+-#define RTL8365MB_EXT_PORT_MODE_100FX         13
+-
+-/* External interface mode configuration registers 0~1 */
+-#define RTL8365MB_DIGITAL_INTERFACE_SELECT_REG0               0x1305 /* EXT0,EXT1 */
+-#define RTL8365MB_DIGITAL_INTERFACE_SELECT_REG1               0x13C3 /* EXT2 */
+-#define RTL8365MB_DIGITAL_INTERFACE_SELECT_REG(_extint) \
+-              ((_extint) <= 1 ? RTL8365MB_DIGITAL_INTERFACE_SELECT_REG0 : \
+-               (_extint) == 2 ? RTL8365MB_DIGITAL_INTERFACE_SELECT_REG1 : \
+-               0x0)
+-#define   RTL8365MB_DIGITAL_INTERFACE_SELECT_MODE_MASK(_extint) \
+-              (0xF << (((_extint) % 2) * 4))
+-#define   RTL8365MB_DIGITAL_INTERFACE_SELECT_MODE_OFFSET(_extint) \
+-              (((_extint) % 2) * 4)
+-
+-/* External interface RGMII TX/RX delay configuration registers 0~2 */
+-#define RTL8365MB_EXT_RGMXF_REG0              0x1306 /* EXT0 */
+-#define RTL8365MB_EXT_RGMXF_REG1              0x1307 /* EXT1 */
+-#define RTL8365MB_EXT_RGMXF_REG2              0x13C5 /* EXT2 */
+-#define RTL8365MB_EXT_RGMXF_REG(_extint) \
+-              ((_extint) == 0 ? RTL8365MB_EXT_RGMXF_REG0 : \
+-               (_extint) == 1 ? RTL8365MB_EXT_RGMXF_REG1 : \
+-               (_extint) == 2 ? RTL8365MB_EXT_RGMXF_REG2 : \
+-               0x0)
+-#define   RTL8365MB_EXT_RGMXF_RXDELAY_MASK    0x0007
+-#define   RTL8365MB_EXT_RGMXF_TXDELAY_MASK    0x0008
+-
+-/* External interface port speed values - used in DIGITAL_INTERFACE_FORCE */
+-#define RTL8365MB_PORT_SPEED_10M      0
+-#define RTL8365MB_PORT_SPEED_100M     1
+-#define RTL8365MB_PORT_SPEED_1000M    2
+-
+-/* External interface force configuration registers 0~2 */
+-#define RTL8365MB_DIGITAL_INTERFACE_FORCE_REG0                0x1310 /* EXT0 */
+-#define RTL8365MB_DIGITAL_INTERFACE_FORCE_REG1                0x1311 /* EXT1 */
+-#define RTL8365MB_DIGITAL_INTERFACE_FORCE_REG2                0x13C4 /* EXT2 */
+-#define RTL8365MB_DIGITAL_INTERFACE_FORCE_REG(_extint) \
+-              ((_extint) == 0 ? RTL8365MB_DIGITAL_INTERFACE_FORCE_REG0 : \
+-               (_extint) == 1 ? RTL8365MB_DIGITAL_INTERFACE_FORCE_REG1 : \
+-               (_extint) == 2 ? RTL8365MB_DIGITAL_INTERFACE_FORCE_REG2 : \
+-               0x0)
+-#define   RTL8365MB_DIGITAL_INTERFACE_FORCE_EN_MASK           0x1000
+-#define   RTL8365MB_DIGITAL_INTERFACE_FORCE_NWAY_MASK         0x0080
+-#define   RTL8365MB_DIGITAL_INTERFACE_FORCE_TXPAUSE_MASK      0x0040
+-#define   RTL8365MB_DIGITAL_INTERFACE_FORCE_RXPAUSE_MASK      0x0020
+-#define   RTL8365MB_DIGITAL_INTERFACE_FORCE_LINK_MASK         0x0010
+-#define   RTL8365MB_DIGITAL_INTERFACE_FORCE_DUPLEX_MASK               0x0004
+-#define   RTL8365MB_DIGITAL_INTERFACE_FORCE_SPEED_MASK                0x0003
+-
+-/* CPU port mask register - controls which ports are treated as CPU ports */
+-#define RTL8365MB_CPU_PORT_MASK_REG   0x1219
+-#define   RTL8365MB_CPU_PORT_MASK_MASK        0x07FF
+-
+-/* CPU control register */
+-#define RTL8365MB_CPU_CTRL_REG                        0x121A
+-#define   RTL8365MB_CPU_CTRL_TRAP_PORT_EXT_MASK       0x0400
+-#define   RTL8365MB_CPU_CTRL_TAG_FORMAT_MASK  0x0200
+-#define   RTL8365MB_CPU_CTRL_RXBYTECOUNT_MASK 0x0080
+-#define   RTL8365MB_CPU_CTRL_TAG_POSITION_MASK        0x0040
+-#define   RTL8365MB_CPU_CTRL_TRAP_PORT_MASK   0x0038
+-#define   RTL8365MB_CPU_CTRL_INSERTMODE_MASK  0x0006
+-#define   RTL8365MB_CPU_CTRL_EN_MASK          0x0001
+-
+-/* Maximum packet length register */
+-#define RTL8365MB_CFG0_MAX_LEN_REG    0x088C
+-#define   RTL8365MB_CFG0_MAX_LEN_MASK 0x3FFF
+-#define RTL8365MB_CFG0_MAX_LEN_MAX    0x3FFF
+-
+-/* Port learning limit registers */
+-#define RTL8365MB_LUT_PORT_LEARN_LIMIT_BASE           0x0A20
+-#define RTL8365MB_LUT_PORT_LEARN_LIMIT_REG(_physport) \
+-              (RTL8365MB_LUT_PORT_LEARN_LIMIT_BASE + (_physport))
+-
+-/* Port isolation (forwarding mask) registers */
+-#define RTL8365MB_PORT_ISOLATION_REG_BASE             0x08A2
+-#define RTL8365MB_PORT_ISOLATION_REG(_physport) \
+-              (RTL8365MB_PORT_ISOLATION_REG_BASE + (_physport))
+-#define   RTL8365MB_PORT_ISOLATION_MASK                       0x07FF
+-
+-/* MSTP port state registers - indexed by tree instance */
+-#define RTL8365MB_MSTI_CTRL_BASE                      0x0A00
+-#define RTL8365MB_MSTI_CTRL_REG(_msti, _physport) \
+-              (RTL8365MB_MSTI_CTRL_BASE + ((_msti) << 1) + ((_physport) >> 3))
+-#define   RTL8365MB_MSTI_CTRL_PORT_STATE_OFFSET(_physport) ((_physport) << 1)
+-#define   RTL8365MB_MSTI_CTRL_PORT_STATE_MASK(_physport) \
+-              (0x3 << RTL8365MB_MSTI_CTRL_PORT_STATE_OFFSET((_physport)))
+-
+-/* MIB counter value registers */
+-#define RTL8365MB_MIB_COUNTER_BASE    0x1000
+-#define RTL8365MB_MIB_COUNTER_REG(_x) (RTL8365MB_MIB_COUNTER_BASE + (_x))
+-
+-/* MIB counter address register */
+-#define RTL8365MB_MIB_ADDRESS_REG             0x1004
+-#define   RTL8365MB_MIB_ADDRESS_PORT_OFFSET   0x007C
+-#define   RTL8365MB_MIB_ADDRESS(_p, _x) \
+-              (((RTL8365MB_MIB_ADDRESS_PORT_OFFSET) * (_p) + (_x)) >> 2)
+-
+-#define RTL8365MB_MIB_CTRL0_REG                       0x1005
+-#define   RTL8365MB_MIB_CTRL0_RESET_MASK      0x0002
+-#define   RTL8365MB_MIB_CTRL0_BUSY_MASK               0x0001
+-
+-/* The DSA callback .get_stats64 runs in atomic context, so we are not allowed
+- * to block. On the other hand, accessing MIB counters absolutely requires us to
+- * block. The solution is thus to schedule work which polls the MIB counters
+- * asynchronously and updates some private data, which the callback can then
+- * fetch atomically. Three seconds should be a good enough polling interval.
+- */
+-#define RTL8365MB_STATS_INTERVAL_JIFFIES      (3 * HZ)
+-
+-enum rtl8365mb_mib_counter_index {
+-      RTL8365MB_MIB_ifInOctets,
+-      RTL8365MB_MIB_dot3StatsFCSErrors,
+-      RTL8365MB_MIB_dot3StatsSymbolErrors,
+-      RTL8365MB_MIB_dot3InPauseFrames,
+-      RTL8365MB_MIB_dot3ControlInUnknownOpcodes,
+-      RTL8365MB_MIB_etherStatsFragments,
+-      RTL8365MB_MIB_etherStatsJabbers,
+-      RTL8365MB_MIB_ifInUcastPkts,
+-      RTL8365MB_MIB_etherStatsDropEvents,
+-      RTL8365MB_MIB_ifInMulticastPkts,
+-      RTL8365MB_MIB_ifInBroadcastPkts,
+-      RTL8365MB_MIB_inMldChecksumError,
+-      RTL8365MB_MIB_inIgmpChecksumError,
+-      RTL8365MB_MIB_inMldSpecificQuery,
+-      RTL8365MB_MIB_inMldGeneralQuery,
+-      RTL8365MB_MIB_inIgmpSpecificQuery,
+-      RTL8365MB_MIB_inIgmpGeneralQuery,
+-      RTL8365MB_MIB_inMldLeaves,
+-      RTL8365MB_MIB_inIgmpLeaves,
+-      RTL8365MB_MIB_etherStatsOctets,
+-      RTL8365MB_MIB_etherStatsUnderSizePkts,
+-      RTL8365MB_MIB_etherOversizeStats,
+-      RTL8365MB_MIB_etherStatsPkts64Octets,
+-      RTL8365MB_MIB_etherStatsPkts65to127Octets,
+-      RTL8365MB_MIB_etherStatsPkts128to255Octets,
+-      RTL8365MB_MIB_etherStatsPkts256to511Octets,
+-      RTL8365MB_MIB_etherStatsPkts512to1023Octets,
+-      RTL8365MB_MIB_etherStatsPkts1024to1518Octets,
+-      RTL8365MB_MIB_ifOutOctets,
+-      RTL8365MB_MIB_dot3StatsSingleCollisionFrames,
+-      RTL8365MB_MIB_dot3StatsMultipleCollisionFrames,
+-      RTL8365MB_MIB_dot3StatsDeferredTransmissions,
+-      RTL8365MB_MIB_dot3StatsLateCollisions,
+-      RTL8365MB_MIB_etherStatsCollisions,
+-      RTL8365MB_MIB_dot3StatsExcessiveCollisions,
+-      RTL8365MB_MIB_dot3OutPauseFrames,
+-      RTL8365MB_MIB_ifOutDiscards,
+-      RTL8365MB_MIB_dot1dTpPortInDiscards,
+-      RTL8365MB_MIB_ifOutUcastPkts,
+-      RTL8365MB_MIB_ifOutMulticastPkts,
+-      RTL8365MB_MIB_ifOutBroadcastPkts,
+-      RTL8365MB_MIB_outOampduPkts,
+-      RTL8365MB_MIB_inOampduPkts,
+-      RTL8365MB_MIB_inIgmpJoinsSuccess,
+-      RTL8365MB_MIB_inIgmpJoinsFail,
+-      RTL8365MB_MIB_inMldJoinsSuccess,
+-      RTL8365MB_MIB_inMldJoinsFail,
+-      RTL8365MB_MIB_inReportSuppressionDrop,
+-      RTL8365MB_MIB_inLeaveSuppressionDrop,
+-      RTL8365MB_MIB_outIgmpReports,
+-      RTL8365MB_MIB_outIgmpLeaves,
+-      RTL8365MB_MIB_outIgmpGeneralQuery,
+-      RTL8365MB_MIB_outIgmpSpecificQuery,
+-      RTL8365MB_MIB_outMldReports,
+-      RTL8365MB_MIB_outMldLeaves,
+-      RTL8365MB_MIB_outMldGeneralQuery,
+-      RTL8365MB_MIB_outMldSpecificQuery,
+-      RTL8365MB_MIB_inKnownMulticastPkts,
+-      RTL8365MB_MIB_END,
+-};
+-
+-struct rtl8365mb_mib_counter {
+-      u32 offset;
+-      u32 length;
+-      const char *name;
+-};
+-
+-#define RTL8365MB_MAKE_MIB_COUNTER(_offset, _length, _name) \
+-              [RTL8365MB_MIB_ ## _name] = { _offset, _length, #_name }
+-
+-static struct rtl8365mb_mib_counter rtl8365mb_mib_counters[] = {
+-      RTL8365MB_MAKE_MIB_COUNTER(0, 4, ifInOctets),
+-      RTL8365MB_MAKE_MIB_COUNTER(4, 2, dot3StatsFCSErrors),
+-      RTL8365MB_MAKE_MIB_COUNTER(6, 2, dot3StatsSymbolErrors),
+-      RTL8365MB_MAKE_MIB_COUNTER(8, 2, dot3InPauseFrames),
+-      RTL8365MB_MAKE_MIB_COUNTER(10, 2, dot3ControlInUnknownOpcodes),
+-      RTL8365MB_MAKE_MIB_COUNTER(12, 2, etherStatsFragments),
+-      RTL8365MB_MAKE_MIB_COUNTER(14, 2, etherStatsJabbers),
+-      RTL8365MB_MAKE_MIB_COUNTER(16, 2, ifInUcastPkts),
+-      RTL8365MB_MAKE_MIB_COUNTER(18, 2, etherStatsDropEvents),
+-      RTL8365MB_MAKE_MIB_COUNTER(20, 2, ifInMulticastPkts),
+-      RTL8365MB_MAKE_MIB_COUNTER(22, 2, ifInBroadcastPkts),
+-      RTL8365MB_MAKE_MIB_COUNTER(24, 2, inMldChecksumError),
+-      RTL8365MB_MAKE_MIB_COUNTER(26, 2, inIgmpChecksumError),
+-      RTL8365MB_MAKE_MIB_COUNTER(28, 2, inMldSpecificQuery),
+-      RTL8365MB_MAKE_MIB_COUNTER(30, 2, inMldGeneralQuery),
+-      RTL8365MB_MAKE_MIB_COUNTER(32, 2, inIgmpSpecificQuery),
+-      RTL8365MB_MAKE_MIB_COUNTER(34, 2, inIgmpGeneralQuery),
+-      RTL8365MB_MAKE_MIB_COUNTER(36, 2, inMldLeaves),
+-      RTL8365MB_MAKE_MIB_COUNTER(38, 2, inIgmpLeaves),
+-      RTL8365MB_MAKE_MIB_COUNTER(40, 4, etherStatsOctets),
+-      RTL8365MB_MAKE_MIB_COUNTER(44, 2, etherStatsUnderSizePkts),
+-      RTL8365MB_MAKE_MIB_COUNTER(46, 2, etherOversizeStats),
+-      RTL8365MB_MAKE_MIB_COUNTER(48, 2, etherStatsPkts64Octets),
+-      RTL8365MB_MAKE_MIB_COUNTER(50, 2, etherStatsPkts65to127Octets),
+-      RTL8365MB_MAKE_MIB_COUNTER(52, 2, etherStatsPkts128to255Octets),
+-      RTL8365MB_MAKE_MIB_COUNTER(54, 2, etherStatsPkts256to511Octets),
+-      RTL8365MB_MAKE_MIB_COUNTER(56, 2, etherStatsPkts512to1023Octets),
+-      RTL8365MB_MAKE_MIB_COUNTER(58, 2, etherStatsPkts1024to1518Octets),
+-      RTL8365MB_MAKE_MIB_COUNTER(60, 4, ifOutOctets),
+-      RTL8365MB_MAKE_MIB_COUNTER(64, 2, dot3StatsSingleCollisionFrames),
+-      RTL8365MB_MAKE_MIB_COUNTER(66, 2, dot3StatsMultipleCollisionFrames),
+-      RTL8365MB_MAKE_MIB_COUNTER(68, 2, dot3StatsDeferredTransmissions),
+-      RTL8365MB_MAKE_MIB_COUNTER(70, 2, dot3StatsLateCollisions),
+-      RTL8365MB_MAKE_MIB_COUNTER(72, 2, etherStatsCollisions),
+-      RTL8365MB_MAKE_MIB_COUNTER(74, 2, dot3StatsExcessiveCollisions),
+-      RTL8365MB_MAKE_MIB_COUNTER(76, 2, dot3OutPauseFrames),
+-      RTL8365MB_MAKE_MIB_COUNTER(78, 2, ifOutDiscards),
+-      RTL8365MB_MAKE_MIB_COUNTER(80, 2, dot1dTpPortInDiscards),
+-      RTL8365MB_MAKE_MIB_COUNTER(82, 2, ifOutUcastPkts),
+-      RTL8365MB_MAKE_MIB_COUNTER(84, 2, ifOutMulticastPkts),
+-      RTL8365MB_MAKE_MIB_COUNTER(86, 2, ifOutBroadcastPkts),
+-      RTL8365MB_MAKE_MIB_COUNTER(88, 2, outOampduPkts),
+-      RTL8365MB_MAKE_MIB_COUNTER(90, 2, inOampduPkts),
+-      RTL8365MB_MAKE_MIB_COUNTER(92, 4, inIgmpJoinsSuccess),
+-      RTL8365MB_MAKE_MIB_COUNTER(96, 2, inIgmpJoinsFail),
+-      RTL8365MB_MAKE_MIB_COUNTER(98, 2, inMldJoinsSuccess),
+-      RTL8365MB_MAKE_MIB_COUNTER(100, 2, inMldJoinsFail),
+-      RTL8365MB_MAKE_MIB_COUNTER(102, 2, inReportSuppressionDrop),
+-      RTL8365MB_MAKE_MIB_COUNTER(104, 2, inLeaveSuppressionDrop),
+-      RTL8365MB_MAKE_MIB_COUNTER(106, 2, outIgmpReports),
+-      RTL8365MB_MAKE_MIB_COUNTER(108, 2, outIgmpLeaves),
+-      RTL8365MB_MAKE_MIB_COUNTER(110, 2, outIgmpGeneralQuery),
+-      RTL8365MB_MAKE_MIB_COUNTER(112, 2, outIgmpSpecificQuery),
+-      RTL8365MB_MAKE_MIB_COUNTER(114, 2, outMldReports),
+-      RTL8365MB_MAKE_MIB_COUNTER(116, 2, outMldLeaves),
+-      RTL8365MB_MAKE_MIB_COUNTER(118, 2, outMldGeneralQuery),
+-      RTL8365MB_MAKE_MIB_COUNTER(120, 2, outMldSpecificQuery),
+-      RTL8365MB_MAKE_MIB_COUNTER(122, 2, inKnownMulticastPkts),
+-};
+-
+-static_assert(ARRAY_SIZE(rtl8365mb_mib_counters) == RTL8365MB_MIB_END);
+-
+-struct rtl8365mb_jam_tbl_entry {
+-      u16 reg;
+-      u16 val;
+-};
+-
+-/* Lifted from the vendor driver sources */
+-static const struct rtl8365mb_jam_tbl_entry rtl8365mb_init_jam_8365mb_vc[] = {
+-      { 0x13EB, 0x15BB }, { 0x1303, 0x06D6 }, { 0x1304, 0x0700 },
+-      { 0x13E2, 0x003F }, { 0x13F9, 0x0090 }, { 0x121E, 0x03CA },
+-      { 0x1233, 0x0352 }, { 0x1237, 0x00A0 }, { 0x123A, 0x0030 },
+-      { 0x1239, 0x0084 }, { 0x0301, 0x1000 }, { 0x1349, 0x001F },
+-      { 0x18E0, 0x4004 }, { 0x122B, 0x241C }, { 0x1305, 0xC000 },
+-      { 0x13F0, 0x0000 },
+-};
+-
+-static const struct rtl8365mb_jam_tbl_entry rtl8365mb_init_jam_common[] = {
+-      { 0x1200, 0x7FCB }, { 0x0884, 0x0003 }, { 0x06EB, 0x0001 },
+-      { 0x03Fa, 0x0007 }, { 0x08C8, 0x00C0 }, { 0x0A30, 0x020E },
+-      { 0x0800, 0x0000 }, { 0x0802, 0x0000 }, { 0x09DA, 0x0013 },
+-      { 0x1D32, 0x0002 },
+-};
+-
+-enum rtl8365mb_phy_interface_mode {
+-      RTL8365MB_PHY_INTERFACE_MODE_INVAL = 0,
+-      RTL8365MB_PHY_INTERFACE_MODE_INTERNAL = BIT(0),
+-      RTL8365MB_PHY_INTERFACE_MODE_MII = BIT(1),
+-      RTL8365MB_PHY_INTERFACE_MODE_TMII = BIT(2),
+-      RTL8365MB_PHY_INTERFACE_MODE_RMII = BIT(3),
+-      RTL8365MB_PHY_INTERFACE_MODE_RGMII = BIT(4),
+-      RTL8365MB_PHY_INTERFACE_MODE_SGMII = BIT(5),
+-      RTL8365MB_PHY_INTERFACE_MODE_HSGMII = BIT(6),
+-};
+-
+-/**
+- * struct rtl8365mb_extint - external interface info
+- * @port: the port with an external interface
+- * @id: the external interface ID, which is either 0, 1, or 2
+- * @supported_interfaces: a bitmask of supported PHY interface modes
+- *
+- * Represents a mapping: port -> { id, supported_interfaces }. To be embedded
+- * in &struct rtl8365mb_chip_info for every port with an external interface.
+- */
+-struct rtl8365mb_extint {
+-      int port;
+-      int id;
+-      unsigned int supported_interfaces;
+-};
+-
+-/**
+- * struct rtl8365mb_chip_info - static chip-specific info
+- * @name: human-readable chip name
+- * @chip_id: chip identifier
+- * @chip_ver: chip silicon revision
+- * @extints: available external interfaces
+- * @jam_table: chip-specific initialization jam table
+- * @jam_size: size of the chip's jam table
+- *
+- * These data are specific to a given chip in the family of switches supported
+- * by this driver. When adding support for another chip in the family, a new
+- * chip info should be added to the rtl8365mb_chip_infos array.
+- */
+-struct rtl8365mb_chip_info {
+-      const char *name;
+-      u32 chip_id;
+-      u32 chip_ver;
+-      const struct rtl8365mb_extint extints[RTL8365MB_MAX_NUM_EXTINTS];
+-      const struct rtl8365mb_jam_tbl_entry *jam_table;
+-      size_t jam_size;
+-};
+-
+-/* Chip info for each supported switch in the family */
+-#define PHY_INTF(_mode) (RTL8365MB_PHY_INTERFACE_MODE_ ## _mode)
+-static const struct rtl8365mb_chip_info rtl8365mb_chip_infos[] = {
+-      {
+-              .name = "RTL8365MB-VC",
+-              .chip_id = 0x6367,
+-              .chip_ver = 0x0040,
+-              .extints = {
+-                      { 6, 1, PHY_INTF(MII) | PHY_INTF(TMII) |
+-                              PHY_INTF(RMII) | PHY_INTF(RGMII) },
+-              },
+-              .jam_table = rtl8365mb_init_jam_8365mb_vc,
+-              .jam_size = ARRAY_SIZE(rtl8365mb_init_jam_8365mb_vc),
+-      },
+-      {
+-              .name = "RTL8367S",
+-              .chip_id = 0x6367,
+-              .chip_ver = 0x00A0,
+-              .extints = {
+-                      { 6, 1, PHY_INTF(SGMII) | PHY_INTF(HSGMII) },
+-                      { 7, 2, PHY_INTF(MII) | PHY_INTF(TMII) |
+-                              PHY_INTF(RMII) | PHY_INTF(RGMII) },
+-              },
+-              .jam_table = rtl8365mb_init_jam_8365mb_vc,
+-              .jam_size = ARRAY_SIZE(rtl8365mb_init_jam_8365mb_vc),
+-      },
+-      {
+-              .name = "RTL8367SB",
+-              .chip_id = 0x6367,
+-              .chip_ver = 0x0010,
+-              .extints = {
+-                      { 6, 1, PHY_INTF(MII) | PHY_INTF(TMII) |
+-                              PHY_INTF(RMII) | PHY_INTF(RGMII) |
+-                              PHY_INTF(SGMII) | PHY_INTF(HSGMII) },
+-                      { 7, 2, PHY_INTF(MII) | PHY_INTF(TMII) |
+-                              PHY_INTF(RMII) | PHY_INTF(RGMII) },
+-              },
+-              .jam_table = rtl8365mb_init_jam_8365mb_vc,
+-              .jam_size = ARRAY_SIZE(rtl8365mb_init_jam_8365mb_vc),
+-      },
+-      {
+-              .name = "RTL8367RB-VB",
+-              .chip_id = 0x6367,
+-              .chip_ver = 0x0020,
+-              .extints = {
+-                      { 6, 1, PHY_INTF(MII) | PHY_INTF(TMII) |
+-                              PHY_INTF(RMII) | PHY_INTF(RGMII) },
+-                      { 7, 2, PHY_INTF(MII) | PHY_INTF(TMII) |
+-                              PHY_INTF(RMII) | PHY_INTF(RGMII) },
+-              },
+-              .jam_table = rtl8365mb_init_jam_8365mb_vc,
+-              .jam_size = ARRAY_SIZE(rtl8365mb_init_jam_8365mb_vc),
+-      },
+-};
+-
+-enum rtl8365mb_stp_state {
+-      RTL8365MB_STP_STATE_DISABLED = 0,
+-      RTL8365MB_STP_STATE_BLOCKING = 1,
+-      RTL8365MB_STP_STATE_LEARNING = 2,
+-      RTL8365MB_STP_STATE_FORWARDING = 3,
+-};
+-
+-enum rtl8365mb_cpu_insert {
+-      RTL8365MB_CPU_INSERT_TO_ALL = 0,
+-      RTL8365MB_CPU_INSERT_TO_TRAPPING = 1,
+-      RTL8365MB_CPU_INSERT_TO_NONE = 2,
+-};
+-
+-enum rtl8365mb_cpu_position {
+-      RTL8365MB_CPU_POS_AFTER_SA = 0,
+-      RTL8365MB_CPU_POS_BEFORE_CRC = 1,
+-};
+-
+-enum rtl8365mb_cpu_format {
+-      RTL8365MB_CPU_FORMAT_8BYTES = 0,
+-      RTL8365MB_CPU_FORMAT_4BYTES = 1,
+-};
+-
+-enum rtl8365mb_cpu_rxlen {
+-      RTL8365MB_CPU_RXLEN_72BYTES = 0,
+-      RTL8365MB_CPU_RXLEN_64BYTES = 1,
+-};
+-
+-/**
+- * struct rtl8365mb_cpu - CPU port configuration
+- * @enable: enable/disable hardware insertion of CPU tag in switch->CPU frames
+- * @mask: port mask of ports that parse should parse CPU tags
+- * @trap_port: forward trapped frames to this port
+- * @insert: CPU tag insertion mode in switch->CPU frames
+- * @position: position of CPU tag in frame
+- * @rx_length: minimum CPU RX length
+- * @format: CPU tag format
+- *
+- * Represents the CPU tagging and CPU port configuration of the switch. These
+- * settings are configurable at runtime.
+- */
+-struct rtl8365mb_cpu {
+-      bool enable;
+-      u32 mask;
+-      u32 trap_port;
+-      enum rtl8365mb_cpu_insert insert;
+-      enum rtl8365mb_cpu_position position;
+-      enum rtl8365mb_cpu_rxlen rx_length;
+-      enum rtl8365mb_cpu_format format;
+-};
+-
+-/**
+- * struct rtl8365mb_port - private per-port data
+- * @priv: pointer to parent realtek_priv data
+- * @index: DSA port index, same as dsa_port::index
+- * @stats: link statistics populated by rtl8365mb_stats_poll, ready for atomic
+- *         access via rtl8365mb_get_stats64
+- * @stats_lock: protect the stats structure during read/update
+- * @mib_work: delayed work for polling MIB counters
+- */
+-struct rtl8365mb_port {
+-      struct realtek_priv *priv;
+-      unsigned int index;
+-      struct rtnl_link_stats64 stats;
+-      spinlock_t stats_lock;
+-      struct delayed_work mib_work;
+-};
+-
+-/**
+- * struct rtl8365mb - driver private data
+- * @priv: pointer to parent realtek_priv data
+- * @irq: registered IRQ or zero
+- * @chip_info: chip-specific info about the attached switch
+- * @cpu: CPU tagging and CPU port configuration for this chip
+- * @mib_lock: prevent concurrent reads of MIB counters
+- * @ports: per-port data
+- *
+- * Private data for this driver.
+- */
+-struct rtl8365mb {
+-      struct realtek_priv *priv;
+-      int irq;
+-      const struct rtl8365mb_chip_info *chip_info;
+-      struct rtl8365mb_cpu cpu;
+-      struct mutex mib_lock;
+-      struct rtl8365mb_port ports[RTL8365MB_MAX_NUM_PORTS];
+-};
+-
+-static int rtl8365mb_phy_poll_busy(struct realtek_priv *priv)
+-{
+-      u32 val;
+-
+-      return regmap_read_poll_timeout(priv->map_nolock,
+-                                      RTL8365MB_INDIRECT_ACCESS_STATUS_REG,
+-                                      val, !val, 10, 100);
+-}
+-
+-static int rtl8365mb_phy_ocp_prepare(struct realtek_priv *priv, int phy,
+-                                   u32 ocp_addr)
+-{
+-      u32 val;
+-      int ret;
+-
+-      /* Set OCP prefix */
+-      val = FIELD_GET(RTL8365MB_PHY_OCP_ADDR_PREFIX_MASK, ocp_addr);
+-      ret = regmap_update_bits(
+-              priv->map_nolock, RTL8365MB_GPHY_OCP_MSB_0_REG,
+-              RTL8365MB_GPHY_OCP_MSB_0_CFG_CPU_OCPADR_MASK,
+-              FIELD_PREP(RTL8365MB_GPHY_OCP_MSB_0_CFG_CPU_OCPADR_MASK, val));
+-      if (ret)
+-              return ret;
+-
+-      /* Set PHY register address */
+-      val = RTL8365MB_PHY_BASE;
+-      val |= FIELD_PREP(RTL8365MB_INDIRECT_ACCESS_ADDRESS_PHYNUM_MASK, phy);
+-      val |= FIELD_PREP(RTL8365MB_INDIRECT_ACCESS_ADDRESS_OCPADR_5_1_MASK,
+-                        ocp_addr >> 1);
+-      val |= FIELD_PREP(RTL8365MB_INDIRECT_ACCESS_ADDRESS_OCPADR_9_6_MASK,
+-                        ocp_addr >> 6);
+-      ret = regmap_write(priv->map_nolock,
+-                         RTL8365MB_INDIRECT_ACCESS_ADDRESS_REG, val);
+-      if (ret)
+-              return ret;
+-
+-      return 0;
+-}
+-
+-static int rtl8365mb_phy_ocp_read(struct realtek_priv *priv, int phy,
+-                                u32 ocp_addr, u16 *data)
+-{
+-      u32 val;
+-      int ret;
+-
+-      rtl83xx_lock(priv);
+-
+-      ret = rtl8365mb_phy_poll_busy(priv);
+-      if (ret)
+-              goto out;
+-
+-      ret = rtl8365mb_phy_ocp_prepare(priv, phy, ocp_addr);
+-      if (ret)
+-              goto out;
+-
+-      /* Execute read operation */
+-      val = FIELD_PREP(RTL8365MB_INDIRECT_ACCESS_CTRL_CMD_MASK,
+-                       RTL8365MB_INDIRECT_ACCESS_CTRL_CMD_VALUE) |
+-            FIELD_PREP(RTL8365MB_INDIRECT_ACCESS_CTRL_RW_MASK,
+-                       RTL8365MB_INDIRECT_ACCESS_CTRL_RW_READ);
+-      ret = regmap_write(priv->map_nolock, RTL8365MB_INDIRECT_ACCESS_CTRL_REG,
+-                         val);
+-      if (ret)
+-              goto out;
+-
+-      ret = rtl8365mb_phy_poll_busy(priv);
+-      if (ret)
+-              goto out;
+-
+-      /* Get PHY register data */
+-      ret = regmap_read(priv->map_nolock,
+-                        RTL8365MB_INDIRECT_ACCESS_READ_DATA_REG, &val);
+-      if (ret)
+-              goto out;
+-
+-      *data = val & 0xFFFF;
+-
+-out:
+-      rtl83xx_unlock(priv);
+-
+-      return ret;
+-}
+-
+-static int rtl8365mb_phy_ocp_write(struct realtek_priv *priv, int phy,
+-                                 u32 ocp_addr, u16 data)
+-{
+-      u32 val;
+-      int ret;
+-
+-      rtl83xx_lock(priv);
+-
+-      ret = rtl8365mb_phy_poll_busy(priv);
+-      if (ret)
+-              goto out;
+-
+-      ret = rtl8365mb_phy_ocp_prepare(priv, phy, ocp_addr);
+-      if (ret)
+-              goto out;
+-
+-      /* Set PHY register data */
+-      ret = regmap_write(priv->map_nolock,
+-                         RTL8365MB_INDIRECT_ACCESS_WRITE_DATA_REG, data);
+-      if (ret)
+-              goto out;
+-
+-      /* Execute write operation */
+-      val = FIELD_PREP(RTL8365MB_INDIRECT_ACCESS_CTRL_CMD_MASK,
+-                       RTL8365MB_INDIRECT_ACCESS_CTRL_CMD_VALUE) |
+-            FIELD_PREP(RTL8365MB_INDIRECT_ACCESS_CTRL_RW_MASK,
+-                       RTL8365MB_INDIRECT_ACCESS_CTRL_RW_WRITE);
+-      ret = regmap_write(priv->map_nolock, RTL8365MB_INDIRECT_ACCESS_CTRL_REG,
+-                         val);
+-      if (ret)
+-              goto out;
+-
+-      ret = rtl8365mb_phy_poll_busy(priv);
+-      if (ret)
+-              goto out;
+-
+-out:
+-      rtl83xx_unlock(priv);
+-
+-      return ret;
+-}
+-
+-static int rtl8365mb_phy_read(struct realtek_priv *priv, int phy, int regnum)
+-{
+-      u32 ocp_addr;
+-      u16 val;
+-      int ret;
+-
+-      if (phy > RTL8365MB_PHYADDRMAX)
+-              return -EINVAL;
+-
+-      if (regnum > RTL8365MB_PHYREGMAX)
+-              return -EINVAL;
+-
+-      ocp_addr = RTL8365MB_PHY_OCP_ADDR_PHYREG_BASE + regnum * 2;
+-
+-      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 %pe\n", phy,
+-                      regnum, ocp_addr, ERR_PTR(ret));
+-              return ret;
+-      }
+-
+-      dev_dbg(priv->dev, "read PHY%d register 0x%02x @ %04x, val <- %04x\n",
+-              phy, regnum, ocp_addr, val);
+-
+-      return val;
+-}
+-
+-static int rtl8365mb_phy_write(struct realtek_priv *priv, int phy, int regnum,
+-                             u16 val)
+-{
+-      u32 ocp_addr;
+-      int ret;
+-
+-      if (phy > RTL8365MB_PHYADDRMAX)
+-              return -EINVAL;
+-
+-      if (regnum > RTL8365MB_PHYREGMAX)
+-              return -EINVAL;
+-
+-      ocp_addr = RTL8365MB_PHY_OCP_ADDR_PHYREG_BASE + regnum * 2;
+-
+-      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 %pe\n", phy,
+-                      regnum, ocp_addr, ERR_PTR(ret));
+-              return ret;
+-      }
+-
+-      dev_dbg(priv->dev, "write PHY%d register 0x%02x @ %04x, val -> %04x\n",
+-              phy, regnum, ocp_addr, val);
+-
+-      return 0;
+-}
+-
+-static const struct rtl8365mb_extint *
+-rtl8365mb_get_port_extint(struct realtek_priv *priv, int port)
+-{
+-      struct rtl8365mb *mb = priv->chip_data;
+-      int i;
+-
+-      for (i = 0; i < RTL8365MB_MAX_NUM_EXTINTS; i++) {
+-              const struct rtl8365mb_extint *extint =
+-                      &mb->chip_info->extints[i];
+-
+-              if (!extint->supported_interfaces)
+-                      continue;
+-
+-              if (extint->port == port)
+-                      return extint;
+-      }
+-
+-      return NULL;
+-}
+-
+-static enum dsa_tag_protocol
+-rtl8365mb_get_tag_protocol(struct dsa_switch *ds, int port,
+-                         enum dsa_tag_protocol mp)
+-{
+-      struct realtek_priv *priv = ds->priv;
+-      struct rtl8365mb_cpu *cpu;
+-      struct rtl8365mb *mb;
+-
+-      mb = priv->chip_data;
+-      cpu = &mb->cpu;
+-
+-      if (cpu->position == RTL8365MB_CPU_POS_BEFORE_CRC)
+-              return DSA_TAG_PROTO_RTL8_4T;
+-
+-      return DSA_TAG_PROTO_RTL8_4;
+-}
+-
+-static int rtl8365mb_ext_config_rgmii(struct realtek_priv *priv, int port,
+-                                    phy_interface_t interface)
+-{
+-      const struct rtl8365mb_extint *extint =
+-              rtl8365mb_get_port_extint(priv, port);
+-      struct dsa_switch *ds = &priv->ds;
+-      struct device_node *dn;
+-      struct dsa_port *dp;
+-      int tx_delay = 0;
+-      int rx_delay = 0;
+-      u32 val;
+-      int ret;
+-
+-      if (!extint)
+-              return -ENODEV;
+-
+-      dp = dsa_to_port(ds, port);
+-      dn = dp->dn;
+-
+-      /* Set the RGMII TX/RX delay
+-       *
+-       * The Realtek vendor driver indicates the following possible
+-       * configuration settings:
+-       *
+-       *   TX delay:
+-       *     0 = no delay, 1 = 2 ns delay
+-       *   RX delay:
+-       *     0 = no delay, 7 = maximum delay
+-       *     Each step is approximately 0.3 ns, so the maximum delay is about
+-       *     2.1 ns.
+-       *
+-       * The vendor driver also states that this must be configured *before*
+-       * forcing the external interface into a particular mode, which is done
+-       * in the rtl8365mb_phylink_mac_link_{up,down} functions.
+-       *
+-       * Only configure an RGMII TX (resp. RX) delay if the
+-       * tx-internal-delay-ps (resp. rx-internal-delay-ps) OF property is
+-       * specified. We ignore the detail of the RGMII interface mode
+-       * (RGMII_{RXID, TXID, etc.}), as this is considered to be a PHY-only
+-       * property.
+-       */
+-      if (!of_property_read_u32(dn, "tx-internal-delay-ps", &val)) {
+-              val = val / 1000; /* convert to ns */
+-
+-              if (val == 0 || val == 2)
+-                      tx_delay = val / 2;
+-              else
+-                      dev_warn(priv->dev,
+-                               "RGMII TX delay must be 0 or 2 ns\n");
+-      }
+-
+-      if (!of_property_read_u32(dn, "rx-internal-delay-ps", &val)) {
+-              val = DIV_ROUND_CLOSEST(val, 300); /* convert to 0.3 ns step */
+-
+-              if (val <= 7)
+-                      rx_delay = val;
+-              else
+-                      dev_warn(priv->dev,
+-                               "RGMII RX delay must be 0 to 2.1 ns\n");
+-      }
+-
+-      ret = regmap_update_bits(
+-              priv->map, RTL8365MB_EXT_RGMXF_REG(extint->id),
+-              RTL8365MB_EXT_RGMXF_TXDELAY_MASK |
+-                      RTL8365MB_EXT_RGMXF_RXDELAY_MASK,
+-              FIELD_PREP(RTL8365MB_EXT_RGMXF_TXDELAY_MASK, tx_delay) |
+-                      FIELD_PREP(RTL8365MB_EXT_RGMXF_RXDELAY_MASK, rx_delay));
+-      if (ret)
+-              return ret;
+-
+-      ret = regmap_update_bits(
+-              priv->map, RTL8365MB_DIGITAL_INTERFACE_SELECT_REG(extint->id),
+-              RTL8365MB_DIGITAL_INTERFACE_SELECT_MODE_MASK(extint->id),
+-              RTL8365MB_EXT_PORT_MODE_RGMII
+-                      << RTL8365MB_DIGITAL_INTERFACE_SELECT_MODE_OFFSET(
+-                                 extint->id));
+-      if (ret)
+-              return ret;
+-
+-      return 0;
+-}
+-
+-static int rtl8365mb_ext_config_forcemode(struct realtek_priv *priv, int port,
+-                                        bool link, int speed, int duplex,
+-                                        bool tx_pause, bool rx_pause)
+-{
+-      const struct rtl8365mb_extint *extint =
+-              rtl8365mb_get_port_extint(priv, port);
+-      u32 r_tx_pause;
+-      u32 r_rx_pause;
+-      u32 r_duplex;
+-      u32 r_speed;
+-      u32 r_link;
+-      int val;
+-      int ret;
+-
+-      if (!extint)
+-              return -ENODEV;
+-
+-      if (link) {
+-              /* Force the link up with the desired configuration */
+-              r_link = 1;
+-              r_rx_pause = rx_pause ? 1 : 0;
+-              r_tx_pause = tx_pause ? 1 : 0;
+-
+-              if (speed == SPEED_1000) {
+-                      r_speed = RTL8365MB_PORT_SPEED_1000M;
+-              } else if (speed == SPEED_100) {
+-                      r_speed = RTL8365MB_PORT_SPEED_100M;
+-              } else if (speed == SPEED_10) {
+-                      r_speed = RTL8365MB_PORT_SPEED_10M;
+-              } else {
+-                      dev_err(priv->dev, "unsupported port speed %s\n",
+-                              phy_speed_to_str(speed));
+-                      return -EINVAL;
+-              }
+-
+-              if (duplex == DUPLEX_FULL) {
+-                      r_duplex = 1;
+-              } else if (duplex == DUPLEX_HALF) {
+-                      r_duplex = 0;
+-              } else {
+-                      dev_err(priv->dev, "unsupported duplex %s\n",
+-                              phy_duplex_to_str(duplex));
+-                      return -EINVAL;
+-              }
+-      } else {
+-              /* Force the link down and reset any programmed configuration */
+-              r_link = 0;
+-              r_tx_pause = 0;
+-              r_rx_pause = 0;
+-              r_speed = 0;
+-              r_duplex = 0;
+-      }
+-
+-      val = FIELD_PREP(RTL8365MB_DIGITAL_INTERFACE_FORCE_EN_MASK, 1) |
+-            FIELD_PREP(RTL8365MB_DIGITAL_INTERFACE_FORCE_TXPAUSE_MASK,
+-                       r_tx_pause) |
+-            FIELD_PREP(RTL8365MB_DIGITAL_INTERFACE_FORCE_RXPAUSE_MASK,
+-                       r_rx_pause) |
+-            FIELD_PREP(RTL8365MB_DIGITAL_INTERFACE_FORCE_LINK_MASK, r_link) |
+-            FIELD_PREP(RTL8365MB_DIGITAL_INTERFACE_FORCE_DUPLEX_MASK,
+-                       r_duplex) |
+-            FIELD_PREP(RTL8365MB_DIGITAL_INTERFACE_FORCE_SPEED_MASK, r_speed);
+-      ret = regmap_write(priv->map,
+-                         RTL8365MB_DIGITAL_INTERFACE_FORCE_REG(extint->id),
+-                         val);
+-      if (ret)
+-              return ret;
+-
+-      return 0;
+-}
+-
+-static void rtl8365mb_phylink_get_caps(struct dsa_switch *ds, int port,
+-                                     struct phylink_config *config)
+-{
+-      const struct rtl8365mb_extint *extint =
+-              rtl8365mb_get_port_extint(ds->priv, port);
+-
+-      config->mac_capabilities = MAC_SYM_PAUSE | MAC_ASYM_PAUSE |
+-                                 MAC_10 | MAC_100 | MAC_1000FD;
+-
+-      if (!extint) {
+-              __set_bit(PHY_INTERFACE_MODE_INTERNAL,
+-                        config->supported_interfaces);
+-
+-              /* GMII is the default interface mode for phylib, so
+-               * we have to support it for ports with integrated PHY.
+-               */
+-              __set_bit(PHY_INTERFACE_MODE_GMII,
+-                        config->supported_interfaces);
+-              return;
+-      }
+-
+-      /* Populate according to the modes supported by _this driver_,
+-       * not necessarily the modes supported by the hardware, some of
+-       * which remain unimplemented.
+-       */
+-
+-      if (extint->supported_interfaces & RTL8365MB_PHY_INTERFACE_MODE_RGMII)
+-              phy_interface_set_rgmii(config->supported_interfaces);
+-}
+-
+-static void rtl8365mb_phylink_mac_config(struct phylink_config *config,
+-                                       unsigned int mode,
+-                                       const struct phylink_link_state *state)
+-{
+-      struct dsa_port *dp = dsa_phylink_to_port(config);
+-      struct realtek_priv *priv = dp->ds->priv;
+-      u8 port = dp->index;
+-      int ret;
+-
+-      if (mode != MLO_AN_PHY && mode != MLO_AN_FIXED) {
+-              dev_err(priv->dev,
+-                      "port %d supports only conventional PHY or fixed-link\n",
+-                      port);
+-              return;
+-      }
+-
+-      if (phy_interface_mode_is_rgmii(state->interface)) {
+-              ret = rtl8365mb_ext_config_rgmii(priv, port, state->interface);
+-              if (ret)
+-                      dev_err(priv->dev,
+-                              "failed to configure RGMII mode on port %d: %pe\n",
+-                              port, ERR_PTR(ret));
+-              return;
+-      }
+-
+-      /* TODO: Implement MII and RMII modes, which the RTL8365MB-VC also
+-       * supports
+-       */
+-}
+-
+-static void rtl8365mb_phylink_mac_link_down(struct phylink_config *config,
+-                                          unsigned int mode,
+-                                          phy_interface_t interface)
+-{
+-      struct dsa_port *dp = dsa_phylink_to_port(config);
+-      struct realtek_priv *priv = dp->ds->priv;
+-      struct rtl8365mb_port *p;
+-      struct rtl8365mb *mb;
+-      u8 port = dp->index;
+-      int ret;
+-
+-      mb = priv->chip_data;
+-      p = &mb->ports[port];
+-      cancel_delayed_work_sync(&p->mib_work);
+-
+-      if (phy_interface_mode_is_rgmii(interface)) {
+-              ret = rtl8365mb_ext_config_forcemode(priv, port, false, 0, 0,
+-                                                   false, false);
+-              if (ret)
+-                      dev_err(priv->dev,
+-                              "failed to reset forced mode on port %d: %pe\n",
+-                              port, ERR_PTR(ret));
+-
+-              return;
+-      }
+-}
+-
+-static void rtl8365mb_phylink_mac_link_up(struct phylink_config *config,
+-                                        struct phy_device *phydev,
+-                                        unsigned int mode,
+-                                        phy_interface_t interface,
+-                                        int speed, int duplex, bool tx_pause,
+-                                        bool rx_pause)
+-{
+-      struct dsa_port *dp = dsa_phylink_to_port(config);
+-      struct realtek_priv *priv = dp->ds->priv;
+-      struct rtl8365mb_port *p;
+-      struct rtl8365mb *mb;
+-      u8 port = dp->index;
+-      int ret;
+-
+-      mb = priv->chip_data;
+-      p = &mb->ports[port];
+-      schedule_delayed_work(&p->mib_work, 0);
+-
+-      if (phy_interface_mode_is_rgmii(interface)) {
+-              ret = rtl8365mb_ext_config_forcemode(priv, port, true, speed,
+-                                                   duplex, tx_pause,
+-                                                   rx_pause);
+-              if (ret)
+-                      dev_err(priv->dev,
+-                              "failed to force mode on port %d: %pe\n", port,
+-                              ERR_PTR(ret));
+-
+-              return;
+-      }
+-}
+-
+-static int rtl8365mb_port_change_mtu(struct dsa_switch *ds, int port,
+-                                   int new_mtu)
+-{
+-      struct realtek_priv *priv = ds->priv;
+-      int frame_size;
+-
+-      /* When a new MTU is set, DSA always sets the CPU port's MTU to the
+-       * largest MTU of the user ports. Because the switch only has a global
+-       * RX length register, only allowing CPU port here is enough.
+-       */
+-      if (!dsa_is_cpu_port(ds, port))
+-              return 0;
+-
+-      frame_size = new_mtu + VLAN_ETH_HLEN + ETH_FCS_LEN;
+-
+-      dev_dbg(priv->dev, "changing mtu to %d (frame size: %d)\n",
+-              new_mtu, frame_size);
+-
+-      return regmap_update_bits(priv->map, RTL8365MB_CFG0_MAX_LEN_REG,
+-                                RTL8365MB_CFG0_MAX_LEN_MASK,
+-                                FIELD_PREP(RTL8365MB_CFG0_MAX_LEN_MASK,
+-                                           frame_size));
+-}
+-
+-static int rtl8365mb_port_max_mtu(struct dsa_switch *ds, int port)
+-{
+-      return RTL8365MB_CFG0_MAX_LEN_MAX - VLAN_ETH_HLEN - ETH_FCS_LEN;
+-}
+-
+-static void rtl8365mb_port_stp_state_set(struct dsa_switch *ds, int port,
+-                                       u8 state)
+-{
+-      struct realtek_priv *priv = ds->priv;
+-      enum rtl8365mb_stp_state val;
+-      int msti = 0;
+-
+-      switch (state) {
+-      case BR_STATE_DISABLED:
+-              val = RTL8365MB_STP_STATE_DISABLED;
+-              break;
+-      case BR_STATE_BLOCKING:
+-      case BR_STATE_LISTENING:
+-              val = RTL8365MB_STP_STATE_BLOCKING;
+-              break;
+-      case BR_STATE_LEARNING:
+-              val = RTL8365MB_STP_STATE_LEARNING;
+-              break;
+-      case BR_STATE_FORWARDING:
+-              val = RTL8365MB_STP_STATE_FORWARDING;
+-              break;
+-      default:
+-              dev_err(priv->dev, "invalid STP state: %u\n", state);
+-              return;
+-      }
+-
+-      regmap_update_bits(priv->map, RTL8365MB_MSTI_CTRL_REG(msti, port),
+-                         RTL8365MB_MSTI_CTRL_PORT_STATE_MASK(port),
+-                         val << RTL8365MB_MSTI_CTRL_PORT_STATE_OFFSET(port));
+-}
+-
+-static int rtl8365mb_port_set_learning(struct realtek_priv *priv, int port,
+-                                     bool enable)
+-{
+-      /* Enable/disable learning by limiting the number of L2 addresses the
+-       * port can learn. Realtek documentation states that a limit of zero
+-       * disables learning. When enabling learning, set it to the chip's
+-       * maximum.
+-       */
+-      return regmap_write(priv->map, RTL8365MB_LUT_PORT_LEARN_LIMIT_REG(port),
+-                          enable ? RTL8365MB_LEARN_LIMIT_MAX : 0);
+-}
+-
+-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);
+-}
+-
+-static int rtl8365mb_mib_counter_read(struct realtek_priv *priv, int port,
+-                                    u32 offset, u32 length, u64 *mibvalue)
+-{
+-      u64 tmpvalue = 0;
+-      u32 val;
+-      int ret;
+-      int i;
+-
+-      /* The MIB address is an SRAM address. We request a particular address
+-       * and then poll the control register before reading the value from some
+-       * counter registers.
+-       */
+-      ret = regmap_write(priv->map, RTL8365MB_MIB_ADDRESS_REG,
+-                         RTL8365MB_MIB_ADDRESS(port, offset));
+-      if (ret)
+-              return ret;
+-
+-      /* Poll for completion */
+-      ret = regmap_read_poll_timeout(priv->map, RTL8365MB_MIB_CTRL0_REG, val,
+-                                     !(val & RTL8365MB_MIB_CTRL0_BUSY_MASK),
+-                                     10, 100);
+-      if (ret)
+-              return ret;
+-
+-      /* Presumably this indicates a MIB counter read failure */
+-      if (val & RTL8365MB_MIB_CTRL0_RESET_MASK)
+-              return -EIO;
+-
+-      /* There are four MIB counter registers each holding a 16 bit word of a
+-       * MIB counter. Depending on the offset, we should read from the upper
+-       * two or lower two registers. In case the MIB counter is 4 words, we
+-       * read from all four registers.
+-       */
+-      if (length == 4)
+-              offset = 3;
+-      else
+-              offset = (offset + 1) % 4;
+-
+-      /* Read the MIB counter 16 bits at a time */
+-      for (i = 0; i < length; i++) {
+-              ret = regmap_read(priv->map,
+-                                RTL8365MB_MIB_COUNTER_REG(offset - i), &val);
+-              if (ret)
+-                      return ret;
+-
+-              tmpvalue = ((tmpvalue) << 16) | (val & 0xFFFF);
+-      }
+-
+-      /* Only commit the result if no error occurred */
+-      *mibvalue = tmpvalue;
+-
+-      return 0;
+-}
+-
+-static void rtl8365mb_get_ethtool_stats(struct dsa_switch *ds, int port, u64 *data)
+-{
+-      struct realtek_priv *priv = ds->priv;
+-      struct rtl8365mb *mb;
+-      int ret;
+-      int i;
+-
+-      mb = priv->chip_data;
+-
+-      mutex_lock(&mb->mib_lock);
+-      for (i = 0; i < RTL8365MB_MIB_END; i++) {
+-              struct rtl8365mb_mib_counter *mib = &rtl8365mb_mib_counters[i];
+-
+-              ret = rtl8365mb_mib_counter_read(priv, port, mib->offset,
+-                                               mib->length, &data[i]);
+-              if (ret) {
+-                      dev_err(priv->dev,
+-                              "failed to read port %d counters: %pe\n", port,
+-                              ERR_PTR(ret));
+-                      break;
+-              }
+-      }
+-      mutex_unlock(&mb->mib_lock);
+-}
+-
+-static void rtl8365mb_get_strings(struct dsa_switch *ds, int port, u32 stringset, u8 *data)
+-{
+-      int i;
+-
+-      if (stringset != ETH_SS_STATS)
+-              return;
+-
+-      for (i = 0; i < RTL8365MB_MIB_END; i++) {
+-              struct rtl8365mb_mib_counter *mib = &rtl8365mb_mib_counters[i];
+-              ethtool_puts(&data, mib->name);
+-      }
+-}
+-
+-static int rtl8365mb_get_sset_count(struct dsa_switch *ds, int port, int sset)
+-{
+-      if (sset != ETH_SS_STATS)
+-              return -EOPNOTSUPP;
+-
+-      return RTL8365MB_MIB_END;
+-}
+-
+-static void rtl8365mb_get_phy_stats(struct dsa_switch *ds, int port,
+-                                  struct ethtool_eth_phy_stats *phy_stats)
+-{
+-      struct realtek_priv *priv = ds->priv;
+-      struct rtl8365mb_mib_counter *mib;
+-      struct rtl8365mb *mb;
+-
+-      mb = priv->chip_data;
+-      mib = &rtl8365mb_mib_counters[RTL8365MB_MIB_dot3StatsSymbolErrors];
+-
+-      mutex_lock(&mb->mib_lock);
+-      rtl8365mb_mib_counter_read(priv, port, mib->offset, mib->length,
+-                                 &phy_stats->SymbolErrorDuringCarrier);
+-      mutex_unlock(&mb->mib_lock);
+-}
+-
+-static void rtl8365mb_get_mac_stats(struct dsa_switch *ds, int port,
+-                                  struct ethtool_eth_mac_stats *mac_stats)
+-{
+-      u64 cnt[RTL8365MB_MIB_END] = {
+-              [RTL8365MB_MIB_ifOutOctets] = 1,
+-              [RTL8365MB_MIB_ifOutUcastPkts] = 1,
+-              [RTL8365MB_MIB_ifOutMulticastPkts] = 1,
+-              [RTL8365MB_MIB_ifOutBroadcastPkts] = 1,
+-              [RTL8365MB_MIB_dot3OutPauseFrames] = 1,
+-              [RTL8365MB_MIB_ifOutDiscards] = 1,
+-              [RTL8365MB_MIB_ifInOctets] = 1,
+-              [RTL8365MB_MIB_ifInUcastPkts] = 1,
+-              [RTL8365MB_MIB_ifInMulticastPkts] = 1,
+-              [RTL8365MB_MIB_ifInBroadcastPkts] = 1,
+-              [RTL8365MB_MIB_dot3InPauseFrames] = 1,
+-              [RTL8365MB_MIB_dot3StatsSingleCollisionFrames] = 1,
+-              [RTL8365MB_MIB_dot3StatsMultipleCollisionFrames] = 1,
+-              [RTL8365MB_MIB_dot3StatsFCSErrors] = 1,
+-              [RTL8365MB_MIB_dot3StatsDeferredTransmissions] = 1,
+-              [RTL8365MB_MIB_dot3StatsLateCollisions] = 1,
+-              [RTL8365MB_MIB_dot3StatsExcessiveCollisions] = 1,
+-
+-      };
+-      struct realtek_priv *priv = ds->priv;
+-      struct rtl8365mb *mb;
+-      int ret;
+-      int i;
+-
+-      mb = priv->chip_data;
+-
+-      mutex_lock(&mb->mib_lock);
+-      for (i = 0; i < RTL8365MB_MIB_END; i++) {
+-              struct rtl8365mb_mib_counter *mib = &rtl8365mb_mib_counters[i];
+-
+-              /* Only fetch required MIB counters (marked = 1 above) */
+-              if (!cnt[i])
+-                      continue;
+-
+-              ret = rtl8365mb_mib_counter_read(priv, port, mib->offset,
+-                                               mib->length, &cnt[i]);
+-              if (ret)
+-                      break;
+-      }
+-      mutex_unlock(&mb->mib_lock);
+-
+-      /* The RTL8365MB-VC exposes MIB objects, which we have to translate into
+-       * IEEE 802.3 Managed Objects. This is not always completely faithful,
+-       * but we try out best. See RFC 3635 for a detailed treatment of the
+-       * subject.
+-       */
+-
+-      mac_stats->FramesTransmittedOK = cnt[RTL8365MB_MIB_ifOutUcastPkts] +
+-                                       cnt[RTL8365MB_MIB_ifOutMulticastPkts] +
+-                                       cnt[RTL8365MB_MIB_ifOutBroadcastPkts] +
+-                                       cnt[RTL8365MB_MIB_dot3OutPauseFrames] -
+-                                       cnt[RTL8365MB_MIB_ifOutDiscards];
+-      mac_stats->SingleCollisionFrames =
+-              cnt[RTL8365MB_MIB_dot3StatsSingleCollisionFrames];
+-      mac_stats->MultipleCollisionFrames =
+-              cnt[RTL8365MB_MIB_dot3StatsMultipleCollisionFrames];
+-      mac_stats->FramesReceivedOK = cnt[RTL8365MB_MIB_ifInUcastPkts] +
+-                                    cnt[RTL8365MB_MIB_ifInMulticastPkts] +
+-                                    cnt[RTL8365MB_MIB_ifInBroadcastPkts] +
+-                                    cnt[RTL8365MB_MIB_dot3InPauseFrames];
+-      mac_stats->FrameCheckSequenceErrors =
+-              cnt[RTL8365MB_MIB_dot3StatsFCSErrors];
+-      mac_stats->OctetsTransmittedOK = cnt[RTL8365MB_MIB_ifOutOctets] -
+-                                       18 * mac_stats->FramesTransmittedOK;
+-      mac_stats->FramesWithDeferredXmissions =
+-              cnt[RTL8365MB_MIB_dot3StatsDeferredTransmissions];
+-      mac_stats->LateCollisions = cnt[RTL8365MB_MIB_dot3StatsLateCollisions];
+-      mac_stats->FramesAbortedDueToXSColls =
+-              cnt[RTL8365MB_MIB_dot3StatsExcessiveCollisions];
+-      mac_stats->OctetsReceivedOK = cnt[RTL8365MB_MIB_ifInOctets] -
+-                                    18 * mac_stats->FramesReceivedOK;
+-      mac_stats->MulticastFramesXmittedOK =
+-              cnt[RTL8365MB_MIB_ifOutMulticastPkts];
+-      mac_stats->BroadcastFramesXmittedOK =
+-              cnt[RTL8365MB_MIB_ifOutBroadcastPkts];
+-      mac_stats->MulticastFramesReceivedOK =
+-              cnt[RTL8365MB_MIB_ifInMulticastPkts];
+-      mac_stats->BroadcastFramesReceivedOK =
+-              cnt[RTL8365MB_MIB_ifInBroadcastPkts];
+-}
+-
+-static void rtl8365mb_get_ctrl_stats(struct dsa_switch *ds, int port,
+-                                   struct ethtool_eth_ctrl_stats *ctrl_stats)
+-{
+-      struct realtek_priv *priv = ds->priv;
+-      struct rtl8365mb_mib_counter *mib;
+-      struct rtl8365mb *mb;
+-
+-      mb = priv->chip_data;
+-      mib = &rtl8365mb_mib_counters[RTL8365MB_MIB_dot3ControlInUnknownOpcodes];
+-
+-      mutex_lock(&mb->mib_lock);
+-      rtl8365mb_mib_counter_read(priv, port, mib->offset, mib->length,
+-                                 &ctrl_stats->UnsupportedOpcodesReceived);
+-      mutex_unlock(&mb->mib_lock);
+-}
+-
+-static void rtl8365mb_stats_update(struct realtek_priv *priv, int port)
+-{
+-      u64 cnt[RTL8365MB_MIB_END] = {
+-              [RTL8365MB_MIB_ifOutOctets] = 1,
+-              [RTL8365MB_MIB_ifOutUcastPkts] = 1,
+-              [RTL8365MB_MIB_ifOutMulticastPkts] = 1,
+-              [RTL8365MB_MIB_ifOutBroadcastPkts] = 1,
+-              [RTL8365MB_MIB_ifOutDiscards] = 1,
+-              [RTL8365MB_MIB_ifInOctets] = 1,
+-              [RTL8365MB_MIB_ifInUcastPkts] = 1,
+-              [RTL8365MB_MIB_ifInMulticastPkts] = 1,
+-              [RTL8365MB_MIB_ifInBroadcastPkts] = 1,
+-              [RTL8365MB_MIB_etherStatsDropEvents] = 1,
+-              [RTL8365MB_MIB_etherStatsCollisions] = 1,
+-              [RTL8365MB_MIB_etherStatsFragments] = 1,
+-              [RTL8365MB_MIB_etherStatsJabbers] = 1,
+-              [RTL8365MB_MIB_dot3StatsFCSErrors] = 1,
+-              [RTL8365MB_MIB_dot3StatsLateCollisions] = 1,
+-      };
+-      struct rtl8365mb *mb = priv->chip_data;
+-      struct rtnl_link_stats64 *stats;
+-      int ret;
+-      int i;
+-
+-      stats = &mb->ports[port].stats;
+-
+-      mutex_lock(&mb->mib_lock);
+-      for (i = 0; i < RTL8365MB_MIB_END; i++) {
+-              struct rtl8365mb_mib_counter *c = &rtl8365mb_mib_counters[i];
+-
+-              /* Only fetch required MIB counters (marked = 1 above) */
+-              if (!cnt[i])
+-                      continue;
+-
+-              ret = rtl8365mb_mib_counter_read(priv, port, c->offset,
+-                                               c->length, &cnt[i]);
+-              if (ret)
+-                      break;
+-      }
+-      mutex_unlock(&mb->mib_lock);
+-
+-      /* Don't update statistics if there was an error reading the counters */
+-      if (ret)
+-              return;
+-
+-      spin_lock(&mb->ports[port].stats_lock);
+-
+-      stats->rx_packets = cnt[RTL8365MB_MIB_ifInUcastPkts] +
+-                          cnt[RTL8365MB_MIB_ifInMulticastPkts] +
+-                          cnt[RTL8365MB_MIB_ifInBroadcastPkts];
+-
+-      stats->tx_packets = cnt[RTL8365MB_MIB_ifOutUcastPkts] +
+-                          cnt[RTL8365MB_MIB_ifOutMulticastPkts] +
+-                          cnt[RTL8365MB_MIB_ifOutBroadcastPkts];
+-
+-      /* if{In,Out}Octets includes FCS - remove it */
+-      stats->rx_bytes = cnt[RTL8365MB_MIB_ifInOctets] - 4 * stats->rx_packets;
+-      stats->tx_bytes =
+-              cnt[RTL8365MB_MIB_ifOutOctets] - 4 * stats->tx_packets;
+-
+-      stats->rx_dropped = cnt[RTL8365MB_MIB_etherStatsDropEvents];
+-      stats->tx_dropped = cnt[RTL8365MB_MIB_ifOutDiscards];
+-
+-      stats->multicast = cnt[RTL8365MB_MIB_ifInMulticastPkts];
+-      stats->collisions = cnt[RTL8365MB_MIB_etherStatsCollisions];
+-
+-      stats->rx_length_errors = cnt[RTL8365MB_MIB_etherStatsFragments] +
+-                                cnt[RTL8365MB_MIB_etherStatsJabbers];
+-      stats->rx_crc_errors = cnt[RTL8365MB_MIB_dot3StatsFCSErrors];
+-      stats->rx_errors = stats->rx_length_errors + stats->rx_crc_errors;
+-
+-      stats->tx_aborted_errors = cnt[RTL8365MB_MIB_ifOutDiscards];
+-      stats->tx_window_errors = cnt[RTL8365MB_MIB_dot3StatsLateCollisions];
+-      stats->tx_errors = stats->tx_aborted_errors + stats->tx_window_errors;
+-
+-      spin_unlock(&mb->ports[port].stats_lock);
+-}
+-
+-static void rtl8365mb_stats_poll(struct work_struct *work)
+-{
+-      struct rtl8365mb_port *p = container_of(to_delayed_work(work),
+-                                              struct rtl8365mb_port,
+-                                              mib_work);
+-      struct realtek_priv *priv = p->priv;
+-
+-      rtl8365mb_stats_update(priv, p->index);
+-
+-      schedule_delayed_work(&p->mib_work, RTL8365MB_STATS_INTERVAL_JIFFIES);
+-}
+-
+-static void rtl8365mb_get_stats64(struct dsa_switch *ds, int port,
+-                                struct rtnl_link_stats64 *s)
+-{
+-      struct realtek_priv *priv = ds->priv;
+-      struct rtl8365mb_port *p;
+-      struct rtl8365mb *mb;
+-
+-      mb = priv->chip_data;
+-      p = &mb->ports[port];
+-
+-      spin_lock(&p->stats_lock);
+-      memcpy(s, &p->stats, sizeof(*s));
+-      spin_unlock(&p->stats_lock);
+-}
+-
+-static void rtl8365mb_stats_setup(struct realtek_priv *priv)
+-{
+-      struct rtl8365mb *mb = priv->chip_data;
+-      struct dsa_switch *ds = &priv->ds;
+-      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);
+-
+-      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);
+-
+-              /* This work polls the MIB counters and keeps the stats64 data
+-               * up-to-date.
+-               */
+-              INIT_DELAYED_WORK(&p->mib_work, rtl8365mb_stats_poll);
+-      }
+-}
+-
+-static void rtl8365mb_stats_teardown(struct realtek_priv *priv)
+-{
+-      struct rtl8365mb *mb = priv->chip_data;
+-      struct dsa_switch *ds = &priv->ds;
+-      struct dsa_port *dp;
+-
+-      dsa_switch_for_each_available_port(dp, ds) {
+-              struct rtl8365mb_port *p = &mb->ports[dp->index];
+-
+-              cancel_delayed_work_sync(&p->mib_work);
+-      }
+-}
+-
+-static int rtl8365mb_get_and_clear_status_reg(struct realtek_priv *priv, u32 reg,
+-                                            u32 *val)
+-{
+-      int ret;
+-
+-      ret = regmap_read(priv->map, reg, val);
+-      if (ret)
+-              return ret;
+-
+-      return regmap_write(priv->map, reg, *val);
+-}
+-
+-static irqreturn_t rtl8365mb_irq(int irq, void *data)
+-{
+-      struct realtek_priv *priv = data;
+-      unsigned long line_changes = 0;
+-      u32 stat;
+-      int line;
+-      int ret;
+-
+-      ret = rtl8365mb_get_and_clear_status_reg(priv, RTL8365MB_INTR_STATUS_REG,
+-                                               &stat);
+-      if (ret)
+-              goto out_error;
+-
+-      if (stat & RTL8365MB_INTR_LINK_CHANGE_MASK) {
+-              u32 linkdown_ind;
+-              u32 linkup_ind;
+-              u32 val;
+-
+-              ret = rtl8365mb_get_and_clear_status_reg(
+-                      priv, RTL8365MB_PORT_LINKUP_IND_REG, &val);
+-              if (ret)
+-                      goto out_error;
+-
+-              linkup_ind = FIELD_GET(RTL8365MB_PORT_LINKUP_IND_MASK, val);
+-
+-              ret = rtl8365mb_get_and_clear_status_reg(
+-                      priv, RTL8365MB_PORT_LINKDOWN_IND_REG, &val);
+-              if (ret)
+-                      goto out_error;
+-
+-              linkdown_ind = FIELD_GET(RTL8365MB_PORT_LINKDOWN_IND_MASK, val);
+-
+-              line_changes = linkup_ind | linkdown_ind;
+-      }
+-
+-      if (!line_changes)
+-              goto out_none;
+-
+-      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);
+-      }
+-
+-      return IRQ_HANDLED;
+-
+-out_error:
+-      dev_err(priv->dev, "failed to read interrupt status: %pe\n",
+-              ERR_PTR(ret));
+-
+-out_none:
+-      return IRQ_NONE;
+-}
+-
+-static struct irq_chip rtl8365mb_irq_chip = {
+-      .name = "rtl8365mb",
+-      /* The hardware doesn't support masking IRQs on a per-port basis */
+-};
+-
+-static int rtl8365mb_irq_map(struct irq_domain *domain, unsigned int irq,
+-                           irq_hw_number_t hwirq)
+-{
+-      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;
+-}
+-
+-static void rtl8365mb_irq_unmap(struct irq_domain *d, unsigned int irq)
+-{
+-      irq_set_nested_thread(irq, 0);
+-      irq_set_chip_and_handler(irq, NULL, NULL);
+-      irq_set_chip_data(irq, NULL);
+-}
+-
+-static const struct irq_domain_ops rtl8365mb_irqdomain_ops = {
+-      .map = rtl8365mb_irq_map,
+-      .unmap = rtl8365mb_irq_unmap,
+-      .xlate = irq_domain_xlate_onecell,
+-};
+-
+-static int rtl8365mb_set_irq_enable(struct realtek_priv *priv, bool enable)
+-{
+-      return regmap_update_bits(priv->map, RTL8365MB_INTR_CTRL_REG,
+-                                RTL8365MB_INTR_LINK_CHANGE_MASK,
+-                                FIELD_PREP(RTL8365MB_INTR_LINK_CHANGE_MASK,
+-                                           enable ? 1 : 0));
+-}
+-
+-static int rtl8365mb_irq_enable(struct realtek_priv *priv)
+-{
+-      return rtl8365mb_set_irq_enable(priv, true);
+-}
+-
+-static int rtl8365mb_irq_disable(struct realtek_priv *priv)
+-{
+-      return rtl8365mb_set_irq_enable(priv, false);
+-}
+-
+-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;
+-
+-      intc = of_get_child_by_name(priv->dev->of_node, "interrupt-controller");
+-      if (!intc) {
+-              dev_err(priv->dev, "missing child interrupt-controller node\n");
+-              return -EINVAL;
+-      }
+-
+-      /* rtl8365mb IRQs cascade off this one */
+-      irq = of_irq_get(intc, 0);
+-      if (irq <= 0) {
+-              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;
+-      }
+-
+-      /* 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) {
+-              dev_err(priv->dev, "failed to add irq domain\n");
+-              ret = -ENOMEM;
+-              goto out_put_node;
+-      }
+-
+-      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");
+-                      ret = -EINVAL;
+-                      goto out_remove_irqdomain;
+-              }
+-
+-              irq_set_parent(virq, irq);
+-      }
+-
+-      /* Configure chip interrupt signal polarity */
+-      irq_trig = irq_get_trigger_type(irq);
+-      switch (irq_trig) {
+-      case IRQF_TRIGGER_RISING:
+-      case IRQF_TRIGGER_HIGH:
+-              val = RTL8365MB_INTR_POLARITY_HIGH;
+-              break;
+-      case IRQF_TRIGGER_FALLING:
+-      case IRQF_TRIGGER_LOW:
+-              val = RTL8365MB_INTR_POLARITY_LOW;
+-              break;
+-      default:
+-              dev_err(priv->dev, "unsupported irq trigger type %u\n",
+-                      irq_trig);
+-              ret = -EINVAL;
+-              goto out_remove_irqdomain;
+-      }
+-
+-      ret = regmap_update_bits(priv->map, RTL8365MB_INTR_POLARITY_REG,
+-                               RTL8365MB_INTR_POLARITY_MASK,
+-                               FIELD_PREP(RTL8365MB_INTR_POLARITY_MASK, val));
+-      if (ret)
+-              goto out_remove_irqdomain;
+-
+-      /* Disable the interrupt in case the chip has it enabled on reset */
+-      ret = rtl8365mb_irq_disable(priv);
+-      if (ret)
+-              goto out_remove_irqdomain;
+-
+-      /* Clear the interrupt status register */
+-      ret = regmap_write(priv->map, RTL8365MB_INTR_STATUS_REG,
+-                         RTL8365MB_INTR_ALL_MASK);
+-      if (ret)
+-              goto out_remove_irqdomain;
+-
+-      ret = request_threaded_irq(irq, NULL, rtl8365mb_irq, IRQF_ONESHOT,
+-                                 "rtl8365mb", priv);
+-      if (ret) {
+-              dev_err(priv->dev, "failed to request irq: %pe\n",
+-                      ERR_PTR(ret));
+-              goto out_remove_irqdomain;
+-      }
+-
+-      ret = rtl8365mb_irq_enable(priv);
+-      if (ret)
+-              goto out_free_irq;
+-
+-      of_node_put(intc);
+-
+-      return 0;
+-
+-out_free_irq:
+-      free_irq(mb->irq, priv);
+-
+-out_remove_irqdomain:
+-      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;
+-}
+-
+-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;
+-
+-      if (mb->irq) {
+-              free_irq(mb->irq, priv);
+-              mb->irq = 0;
+-      }
+-
+-      if (priv->irqdomain) {
+-              /* 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);
+-              priv->irqdomain = NULL;
+-      }
+-}
+-
+-static int rtl8365mb_cpu_config(struct realtek_priv *priv)
+-{
+-      struct rtl8365mb *mb = priv->chip_data;
+-      struct rtl8365mb_cpu *cpu = &mb->cpu;
+-      u32 val;
+-      int ret;
+-
+-      ret = regmap_update_bits(priv->map, RTL8365MB_CPU_PORT_MASK_REG,
+-                               RTL8365MB_CPU_PORT_MASK_MASK,
+-                               FIELD_PREP(RTL8365MB_CPU_PORT_MASK_MASK,
+-                                          cpu->mask));
+-      if (ret)
+-              return ret;
+-
+-      val = FIELD_PREP(RTL8365MB_CPU_CTRL_EN_MASK, cpu->enable ? 1 : 0) |
+-            FIELD_PREP(RTL8365MB_CPU_CTRL_INSERTMODE_MASK, cpu->insert) |
+-            FIELD_PREP(RTL8365MB_CPU_CTRL_TAG_POSITION_MASK, cpu->position) |
+-            FIELD_PREP(RTL8365MB_CPU_CTRL_RXBYTECOUNT_MASK, cpu->rx_length) |
+-            FIELD_PREP(RTL8365MB_CPU_CTRL_TAG_FORMAT_MASK, cpu->format) |
+-            FIELD_PREP(RTL8365MB_CPU_CTRL_TRAP_PORT_MASK, cpu->trap_port & 0x7) |
+-            FIELD_PREP(RTL8365MB_CPU_CTRL_TRAP_PORT_EXT_MASK,
+-                       cpu->trap_port >> 3 & 0x1);
+-      ret = regmap_write(priv->map, RTL8365MB_CPU_CTRL_REG, val);
+-      if (ret)
+-              return ret;
+-
+-      return 0;
+-}
+-
+-static int rtl8365mb_change_tag_protocol(struct dsa_switch *ds,
+-                                       enum dsa_tag_protocol proto)
+-{
+-      struct realtek_priv *priv = ds->priv;
+-      struct rtl8365mb_cpu *cpu;
+-      struct rtl8365mb *mb;
+-
+-      mb = priv->chip_data;
+-      cpu = &mb->cpu;
+-
+-      switch (proto) {
+-      case DSA_TAG_PROTO_RTL8_4:
+-              cpu->format = RTL8365MB_CPU_FORMAT_8BYTES;
+-              cpu->position = RTL8365MB_CPU_POS_AFTER_SA;
+-              break;
+-      case DSA_TAG_PROTO_RTL8_4T:
+-              cpu->format = RTL8365MB_CPU_FORMAT_8BYTES;
+-              cpu->position = RTL8365MB_CPU_POS_BEFORE_CRC;
+-              break;
+-      /* The switch also supports a 4-byte format, similar to rtl4a but with
+-       * the same 0x04 8-bit version and probably 8-bit port source/dest.
+-       * There is no public doc about it. Not supported yet and it will probably
+-       * never be.
+-       */
+-      default:
+-              return -EPROTONOSUPPORT;
+-      }
+-
+-      return rtl8365mb_cpu_config(priv);
+-}
+-
+-static int rtl8365mb_switch_init(struct realtek_priv *priv)
+-{
+-      struct rtl8365mb *mb = priv->chip_data;
+-      const struct rtl8365mb_chip_info *ci;
+-      int ret;
+-      int i;
+-
+-      ci = mb->chip_info;
+-
+-      /* Do any chip-specific init jam before getting to the common stuff */
+-      if (ci->jam_table) {
+-              for (i = 0; i < ci->jam_size; i++) {
+-                      ret = regmap_write(priv->map, ci->jam_table[i].reg,
+-                                         ci->jam_table[i].val);
+-                      if (ret)
+-                              return ret;
+-              }
+-      }
+-
+-      /* Common init jam */
+-      for (i = 0; i < ARRAY_SIZE(rtl8365mb_init_jam_common); i++) {
+-              ret = regmap_write(priv->map, rtl8365mb_init_jam_common[i].reg,
+-                                 rtl8365mb_init_jam_common[i].val);
+-              if (ret)
+-                      return ret;
+-      }
+-
+-      return 0;
+-}
+-
+-static int rtl8365mb_reset_chip(struct realtek_priv *priv)
+-{
+-      u32 val;
+-
+-      priv->write_reg_noack(priv, RTL8365MB_CHIP_RESET_REG,
+-                            FIELD_PREP(RTL8365MB_CHIP_RESET_HW_MASK, 1));
+-
+-      /* Realtek documentation says the chip needs 1 second to reset. Sleep
+-       * for 100 ms before accessing any registers to prevent ACK timeouts.
+-       */
+-      msleep(100);
+-      return regmap_read_poll_timeout(priv->map, RTL8365MB_CHIP_RESET_REG, val,
+-                                      !(val & RTL8365MB_CHIP_RESET_HW_MASK),
+-                                      20000, 1e6);
+-}
+-
+-static int rtl8365mb_setup(struct dsa_switch *ds)
+-{
+-      struct realtek_priv *priv = ds->priv;
+-      struct rtl8365mb_cpu *cpu;
+-      u32 downports_mask = 0;
+-      u32 upports_mask = 0;
+-      struct rtl8365mb *mb;
+-      struct dsa_port *dp;
+-      int ret;
+-
+-      mb = priv->chip_data;
+-      cpu = &mb->cpu;
+-
+-      ret = rtl8365mb_reset_chip(priv);
+-      if (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: %pe\n",
+-                      ERR_PTR(ret));
+-              goto out_error;
+-      }
+-
+-      /* Set up cascading IRQs */
+-      ret = rtl8365mb_irq_setup(priv);
+-      if (ret == -EPROBE_DEFER)
+-              return ret;
+-      else if (ret)
+-              dev_info(priv->dev, "no interrupt support\n");
+-
+-      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_port_is_dsa(dp)) {
+-                      dev_err(priv->dev, "Cascading (DSA link) not supported\n");
+-                      ret = -EOPNOTSUPP;
+-                      goto out_teardown_irq;
+-              }
+-      }
+-
+-      /* Start with all ports blocked, including unused ports */
+-      dsa_switch_for_each_port(dp, ds) {
+-              struct rtl8365mb_port *p = &mb->ports[dp->index];
+-
+-              /* 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);
+-
+-              /* Start with all port completely isolated */
+-              ret = rtl8365mb_port_set_isolation(priv, dp->index, 0);
+-              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;
+-
+-              /* 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);
+-      }
+-
+-      /* 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, dp->index,
+-                                                 upports_mask);
+-              if (ret)
+-                      goto out_teardown_irq;
+-
+-              /* 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;
+-      }
+-
+-      cpu->mask = upports_mask;
+-      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;
+-
+-      ret = rtl8365mb_port_change_mtu(ds, cpu->trap_port, ETH_DATA_LEN);
+-      if (ret)
+-              goto out_teardown_irq;
+-
+-      ret = rtl83xx_setup_user_mdio(ds);
+-      if (ret) {
+-              dev_err(priv->dev, "could not set up MDIO bus\n");
+-              goto out_teardown_irq;
+-      }
+-
+-      /* Start statistics counter polling */
+-      rtl8365mb_stats_setup(priv);
+-
+-      return 0;
+-
+-out_teardown_irq:
+-      rtl8365mb_irq_teardown(priv);
+-
+-out_error:
+-      return ret;
+-}
+-
+-static void rtl8365mb_teardown(struct dsa_switch *ds)
+-{
+-      struct realtek_priv *priv = ds->priv;
+-
+-      rtl8365mb_stats_teardown(priv);
+-      rtl8365mb_irq_teardown(priv);
+-}
+-
+-static int rtl8365mb_get_chip_id_and_ver(struct regmap *map, u32 *id, u32 *ver)
+-{
+-      int ret;
+-
+-      /* For some reason we have to write a magic value to an arbitrary
+-       * register whenever accessing the chip ID/version registers.
+-       */
+-      ret = regmap_write(map, RTL8365MB_MAGIC_REG, RTL8365MB_MAGIC_VALUE);
+-      if (ret)
+-              return ret;
+-
+-      ret = regmap_read(map, RTL8365MB_CHIP_ID_REG, id);
+-      if (ret)
+-              return ret;
+-
+-      ret = regmap_read(map, RTL8365MB_CHIP_VER_REG, ver);
+-      if (ret)
+-              return ret;
+-
+-      /* Reset magic register */
+-      ret = regmap_write(map, RTL8365MB_MAGIC_REG, 0);
+-      if (ret)
+-              return ret;
+-
+-      return 0;
+-}
+-
+-static int rtl8365mb_detect(struct realtek_priv *priv)
+-{
+-      struct rtl8365mb *mb = priv->chip_data;
+-      u32 chip_id;
+-      u32 chip_ver;
+-      int ret;
+-      int i;
+-
+-      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: %pe\n",
+-                      ERR_PTR(ret));
+-              return ret;
+-      }
+-
+-      for (i = 0; i < ARRAY_SIZE(rtl8365mb_chip_infos); i++) {
+-              const struct rtl8365mb_chip_info *ci = &rtl8365mb_chip_infos[i];
+-
+-              if (ci->chip_id == chip_id && ci->chip_ver == chip_ver) {
+-                      mb->chip_info = ci;
+-                      break;
+-              }
+-      }
+-
+-      if (!mb->chip_info) {
+-              dev_err(priv->dev,
+-                      "unrecognized switch (id=0x%04x, ver=0x%04x)", chip_id,
+-                      chip_ver);
+-              return -ENODEV;
+-      }
+-
+-      dev_info(priv->dev, "found an %s switch\n", mb->chip_info->name);
+-
+-      priv->num_ports = RTL8365MB_MAX_NUM_PORTS;
+-      mb->priv = priv;
+-      mb->cpu.trap_port = RTL8365MB_MAX_NUM_PORTS;
+-      mb->cpu.insert = RTL8365MB_CPU_INSERT_TO_ALL;
+-      mb->cpu.position = RTL8365MB_CPU_POS_AFTER_SA;
+-      mb->cpu.rx_length = RTL8365MB_CPU_RXLEN_64BYTES;
+-      mb->cpu.format = RTL8365MB_CPU_FORMAT_8BYTES;
+-
+-      return 0;
+-}
+-
+-static const struct phylink_mac_ops rtl8365mb_phylink_mac_ops = {
+-      .mac_config = rtl8365mb_phylink_mac_config,
+-      .mac_link_down = rtl8365mb_phylink_mac_link_down,
+-      .mac_link_up = rtl8365mb_phylink_mac_link_up,
+-};
+-
+-static const struct dsa_switch_ops rtl8365mb_switch_ops = {
+-      .get_tag_protocol = rtl8365mb_get_tag_protocol,
+-      .change_tag_protocol = rtl8365mb_change_tag_protocol,
+-      .setup = rtl8365mb_setup,
+-      .teardown = rtl8365mb_teardown,
+-      .phylink_get_caps = rtl8365mb_phylink_get_caps,
+-      .port_stp_state_set = rtl8365mb_port_stp_state_set,
+-      .get_strings = rtl8365mb_get_strings,
+-      .get_ethtool_stats = rtl8365mb_get_ethtool_stats,
+-      .get_sset_count = rtl8365mb_get_sset_count,
+-      .get_eth_phy_stats = rtl8365mb_get_phy_stats,
+-      .get_eth_mac_stats = rtl8365mb_get_mac_stats,
+-      .get_eth_ctrl_stats = rtl8365mb_get_ctrl_stats,
+-      .get_stats64 = rtl8365mb_get_stats64,
+-      .port_change_mtu = rtl8365mb_port_change_mtu,
+-      .port_max_mtu = rtl8365mb_port_max_mtu,
+-};
+-
+-static const struct realtek_ops rtl8365mb_ops = {
+-      .detect = rtl8365mb_detect,
+-      .phy_read = rtl8365mb_phy_read,
+-      .phy_write = rtl8365mb_phy_write,
+-};
+-
+-const struct realtek_variant rtl8365mb_variant = {
+-      .ds_ops = &rtl8365mb_switch_ops,
+-      .ops = &rtl8365mb_ops,
+-      .phylink_mac_ops = &rtl8365mb_phylink_mac_ops,
+-      .clk_delay = 10,
+-      .cmd_read = 0xb9,
+-      .cmd_write = 0xb8,
+-      .chip_data_sz = sizeof(struct rtl8365mb),
+-};
+-
+-static const struct of_device_id rtl8365mb_of_match[] = {
+-      { .compatible = "realtek,rtl8365mb", .data = &rtl8365mb_variant, },
+-      { /* sentinel */ },
+-};
+-MODULE_DEVICE_TABLE(of, rtl8365mb_of_match);
+-
+-static struct platform_driver rtl8365mb_smi_driver = {
+-      .driver = {
+-              .name = "rtl8365mb-smi",
+-              .of_match_table = rtl8365mb_of_match,
+-      },
+-      .probe  = realtek_smi_probe,
+-      .remove = realtek_smi_remove,
+-      .shutdown = realtek_smi_shutdown,
+-};
+-
+-static struct mdio_driver rtl8365mb_mdio_driver = {
+-      .mdiodrv.driver = {
+-              .name = "rtl8365mb-mdio",
+-              .of_match_table = rtl8365mb_of_match,
+-      },
+-      .probe  = realtek_mdio_probe,
+-      .remove = realtek_mdio_remove,
+-      .shutdown = realtek_mdio_shutdown,
+-};
+-
+-static int rtl8365mb_init(void)
+-{
+-      int ret;
+-
+-      ret = realtek_mdio_driver_register(&rtl8365mb_mdio_driver);
+-      if (ret)
+-              return ret;
+-
+-      ret = realtek_smi_driver_register(&rtl8365mb_smi_driver);
+-      if (ret) {
+-              realtek_mdio_driver_unregister(&rtl8365mb_mdio_driver);
+-              return ret;
+-      }
+-
+-      return 0;
+-}
+-module_init(rtl8365mb_init);
+-
+-static void __exit rtl8365mb_exit(void)
+-{
+-      realtek_smi_driver_unregister(&rtl8365mb_smi_driver);
+-      realtek_mdio_driver_unregister(&rtl8365mb_mdio_driver);
+-}
+-module_exit(rtl8365mb_exit);
+-
+-MODULE_AUTHOR("Alvin Šipraga <alsi@bang-olufsen.dk>");
+-MODULE_DESCRIPTION("Driver for RTL8365MB-VC ethernet switch");
+-MODULE_LICENSE("GPL");
+-MODULE_IMPORT_NS("REALTEK_DSA");
+--- /dev/null
++++ b/drivers/net/dsa/realtek/rtl8365mb_main.c
+@@ -0,0 +1,2294 @@
++// SPDX-License-Identifier: GPL-2.0
++/* Realtek SMI subdriver for the Realtek RTL8365MB-VC ethernet switch.
++ *
++ * Copyright (C) 2021 Alvin Šipraga <alsi@bang-olufsen.dk>
++ * Copyright (C) 2021 Michael Rasmussen <mir@bang-olufsen.dk>
++ *
++ * The RTL8365MB-VC is a 4+1 port 10/100/1000M switch controller. It includes 4
++ * integrated PHYs for the user facing ports, and an extension interface which
++ * can be connected to the CPU - or another PHY - via either MII, RMII, or
++ * RGMII. The switch is configured via the Realtek Simple Management Interface
++ * (SMI), which uses the MDIO/MDC lines.
++ *
++ * Below is a simplified block diagram of the chip and its relevant interfaces.
++ *
++ *                          .-----------------------------------.
++ *                          |                                   |
++ *         UTP <---------------> Giga PHY <-> PCS <-> P0 GMAC   |
++ *         UTP <---------------> Giga PHY <-> PCS <-> P1 GMAC   |
++ *         UTP <---------------> Giga PHY <-> PCS <-> P2 GMAC   |
++ *         UTP <---------------> Giga PHY <-> PCS <-> P3 GMAC   |
++ *                          |                                   |
++ *     CPU/PHY <-MII/RMII/RGMII--->  Extension  <---> Extension |
++ *                          |       interface 1        GMAC 1   |
++ *                          |                                   |
++ *     SMI driver/ <-MDC/SCL---> Management    ~~~~~~~~~~~~~~   |
++ *        EEPROM   <-MDIO/SDA--> interface     ~REALTEK ~~~~~   |
++ *                          |                  ~RTL8365MB ~~~   |
++ *                          |                  ~GXXXC TAIWAN~   |
++ *        GPIO <--------------> Reset          ~~~~~~~~~~~~~~   |
++ *                          |                                   |
++ *      Interrupt  <----------> Link UP/DOWN events             |
++ *      controller          |                                   |
++ *                          '-----------------------------------'
++ *
++ * The driver uses DSA to integrate the 4 user and 1 extension ports into the
++ * kernel. Netdevices are created for the user ports, as are PHY devices for
++ * their integrated PHYs. The device tree firmware should also specify the link
++ * partner of the extension port - either via a fixed-link or other phy-handle.
++ * See the device tree bindings for more detailed information. Note that the
++ * driver has only been tested with a fixed-link, but in principle it should not
++ * matter.
++ *
++ * NOTE: Currently, only the RGMII interface is implemented in this driver.
++ *
++ * The interrupt line is asserted on link UP/DOWN events. The driver creates a
++ * custom irqchip to handle this interrupt and demultiplex the events by reading
++ * the status registers via SMI. Interrupts are then propagated to the relevant
++ * PHY device.
++ *
++ * The EEPROM contains initial register values which the chip will read over I2C
++ * upon hardware reset. It is also possible to omit the EEPROM. In both cases,
++ * the driver will manually reprogram some registers using jam tables to reach
++ * an initial state defined by the vendor driver.
++ *
++ * This Linux driver is written based on an OS-agnostic vendor driver from
++ * Realtek. The reference GPL-licensed sources can be found in the OpenWrt
++ * source tree under the name rtl8367c. The vendor driver claims to support a
++ * number of similar switch controllers from Realtek, but the only hardware we
++ * have is the RTL8365MB-VC. Moreover, there does not seem to be any chip under
++ * the name RTL8367C. Although one wishes that the 'C' stood for some kind of
++ * common hardware revision, there exist examples of chips with the suffix -VC
++ * which are explicitly not supported by the rtl8367c driver and which instead
++ * require the rtl8367d vendor driver. With all this uncertainty, the driver has
++ * been modestly named rtl8365mb. Future implementors may wish to rename things
++ * accordingly.
++ *
++ * In the same family of chips, some carry up to 8 user ports and up to 2
++ * extension ports. Where possible this driver tries to make things generic, but
++ * more work must be done to support these configurations. According to
++ * documentation from Realtek, the family should include the following chips:
++ *
++ *  - RTL8363NB
++ *  - RTL8363NB-VB
++ *  - RTL8363SC
++ *  - RTL8363SC-VB
++ *  - RTL8364NB
++ *  - RTL8364NB-VB
++ *  - RTL8365MB-VC
++ *  - RTL8366SC
++ *  - RTL8367RB-VB
++ *  - RTL8367SB
++ *  - RTL8367S
++ *  - RTL8370MB
++ *  - RTL8310SR
++ *
++ * Some of the register logic for these additional chips has been skipped over
++ * while implementing this driver. It is therefore not possible to assume that
++ * things will work out-of-the-box for other chips, and a careful review of the
++ * vendor driver may be needed to expand support. The RTL8365MB-VC seems to be
++ * one of the simpler chips.
++ */
++
++#include <linux/bitfield.h>
++#include <linux/bitops.h>
++#include <linux/interrupt.h>
++#include <linux/irqdomain.h>
++#include <linux/mutex.h>
++#include <linux/of_irq.h>
++#include <linux/regmap.h>
++#include <linux/if_bridge.h>
++#include <linux/if_vlan.h>
++
++#include "realtek.h"
++#include "realtek-smi.h"
++#include "realtek-mdio.h"
++#include "rtl83xx.h"
++
++/* Family-specific data and limits */
++#define RTL8365MB_PHYADDRMAX          7
++#define RTL8365MB_NUM_PHYREGS         32
++#define RTL8365MB_PHYREGMAX           (RTL8365MB_NUM_PHYREGS - 1)
++#define RTL8365MB_MAX_NUM_PORTS               11
++#define RTL8365MB_MAX_NUM_EXTINTS     3
++#define RTL8365MB_LEARN_LIMIT_MAX     2112
++
++/* Chip identification registers */
++#define RTL8365MB_CHIP_ID_REG         0x1300
++
++#define RTL8365MB_CHIP_VER_REG                0x1301
++
++#define RTL8365MB_MAGIC_REG           0x13C2
++#define   RTL8365MB_MAGIC_VALUE               0x0249
++
++/* Chip reset register */
++#define RTL8365MB_CHIP_RESET_REG      0x1322
++#define RTL8365MB_CHIP_RESET_SW_MASK  0x0002
++#define RTL8365MB_CHIP_RESET_HW_MASK  0x0001
++
++/* Interrupt polarity register */
++#define RTL8365MB_INTR_POLARITY_REG   0x1100
++#define   RTL8365MB_INTR_POLARITY_MASK        0x0001
++#define   RTL8365MB_INTR_POLARITY_HIGH        0
++#define   RTL8365MB_INTR_POLARITY_LOW 1
++
++/* Interrupt control/status register - enable/check specific interrupt types */
++#define RTL8365MB_INTR_CTRL_REG                       0x1101
++#define RTL8365MB_INTR_STATUS_REG             0x1102
++#define   RTL8365MB_INTR_SLIENT_START_2_MASK  0x1000
++#define   RTL8365MB_INTR_SLIENT_START_MASK    0x0800
++#define   RTL8365MB_INTR_ACL_ACTION_MASK      0x0200
++#define   RTL8365MB_INTR_CABLE_DIAG_FIN_MASK  0x0100
++#define   RTL8365MB_INTR_INTERRUPT_8051_MASK  0x0080
++#define   RTL8365MB_INTR_LOOP_DETECTION_MASK  0x0040
++#define   RTL8365MB_INTR_GREEN_TIMER_MASK     0x0020
++#define   RTL8365MB_INTR_SPECIAL_CONGEST_MASK 0x0010
++#define   RTL8365MB_INTR_SPEED_CHANGE_MASK    0x0008
++#define   RTL8365MB_INTR_LEARN_OVER_MASK      0x0004
++#define   RTL8365MB_INTR_METER_EXCEEDED_MASK  0x0002
++#define   RTL8365MB_INTR_LINK_CHANGE_MASK     0x0001
++#define   RTL8365MB_INTR_ALL_MASK                      \
++              (RTL8365MB_INTR_SLIENT_START_2_MASK |  \
++               RTL8365MB_INTR_SLIENT_START_MASK |    \
++               RTL8365MB_INTR_ACL_ACTION_MASK |      \
++               RTL8365MB_INTR_CABLE_DIAG_FIN_MASK |  \
++               RTL8365MB_INTR_INTERRUPT_8051_MASK |  \
++               RTL8365MB_INTR_LOOP_DETECTION_MASK |  \
++               RTL8365MB_INTR_GREEN_TIMER_MASK |     \
++               RTL8365MB_INTR_SPECIAL_CONGEST_MASK | \
++               RTL8365MB_INTR_SPEED_CHANGE_MASK |    \
++               RTL8365MB_INTR_LEARN_OVER_MASK |      \
++               RTL8365MB_INTR_METER_EXCEEDED_MASK |  \
++               RTL8365MB_INTR_LINK_CHANGE_MASK)
++
++/* Per-port interrupt type status registers */
++#define RTL8365MB_PORT_LINKDOWN_IND_REG               0x1106
++#define   RTL8365MB_PORT_LINKDOWN_IND_MASK    0x07FF
++
++#define RTL8365MB_PORT_LINKUP_IND_REG         0x1107
++#define   RTL8365MB_PORT_LINKUP_IND_MASK      0x07FF
++
++/* PHY indirect access registers */
++#define RTL8365MB_INDIRECT_ACCESS_CTRL_REG                    0x1F00
++#define   RTL8365MB_INDIRECT_ACCESS_CTRL_RW_MASK              0x0002
++#define   RTL8365MB_INDIRECT_ACCESS_CTRL_RW_READ              0
++#define   RTL8365MB_INDIRECT_ACCESS_CTRL_RW_WRITE             1
++#define   RTL8365MB_INDIRECT_ACCESS_CTRL_CMD_MASK             0x0001
++#define   RTL8365MB_INDIRECT_ACCESS_CTRL_CMD_VALUE            1
++#define RTL8365MB_INDIRECT_ACCESS_STATUS_REG                  0x1F01
++#define RTL8365MB_INDIRECT_ACCESS_ADDRESS_REG                 0x1F02
++#define   RTL8365MB_INDIRECT_ACCESS_ADDRESS_OCPADR_5_1_MASK   GENMASK(4, 0)
++#define   RTL8365MB_INDIRECT_ACCESS_ADDRESS_PHYNUM_MASK               GENMASK(7, 5)
++#define   RTL8365MB_INDIRECT_ACCESS_ADDRESS_OCPADR_9_6_MASK   GENMASK(11, 8)
++#define   RTL8365MB_PHY_BASE                                  0x2000
++#define RTL8365MB_INDIRECT_ACCESS_WRITE_DATA_REG              0x1F03
++#define RTL8365MB_INDIRECT_ACCESS_READ_DATA_REG                       0x1F04
++
++/* PHY OCP address prefix register */
++#define RTL8365MB_GPHY_OCP_MSB_0_REG                  0x1D15
++#define   RTL8365MB_GPHY_OCP_MSB_0_CFG_CPU_OCPADR_MASK        0x0FC0
++#define RTL8365MB_PHY_OCP_ADDR_PREFIX_MASK            0xFC00
++
++/* The PHY OCP addresses of PHY registers 0~31 start here */
++#define RTL8365MB_PHY_OCP_ADDR_PHYREG_BASE            0xA400
++
++/* External interface port mode values - used in DIGITAL_INTERFACE_SELECT */
++#define RTL8365MB_EXT_PORT_MODE_DISABLE               0
++#define RTL8365MB_EXT_PORT_MODE_RGMII         1
++#define RTL8365MB_EXT_PORT_MODE_MII_MAC               2
++#define RTL8365MB_EXT_PORT_MODE_MII_PHY               3
++#define RTL8365MB_EXT_PORT_MODE_TMII_MAC      4
++#define RTL8365MB_EXT_PORT_MODE_TMII_PHY      5
++#define RTL8365MB_EXT_PORT_MODE_GMII          6
++#define RTL8365MB_EXT_PORT_MODE_RMII_MAC      7
++#define RTL8365MB_EXT_PORT_MODE_RMII_PHY      8
++#define RTL8365MB_EXT_PORT_MODE_SGMII         9
++#define RTL8365MB_EXT_PORT_MODE_HSGMII                10
++#define RTL8365MB_EXT_PORT_MODE_1000X_100FX   11
++#define RTL8365MB_EXT_PORT_MODE_1000X         12
++#define RTL8365MB_EXT_PORT_MODE_100FX         13
++
++/* External interface mode configuration registers 0~1 */
++#define RTL8365MB_DIGITAL_INTERFACE_SELECT_REG0               0x1305 /* EXT0,EXT1 */
++#define RTL8365MB_DIGITAL_INTERFACE_SELECT_REG1               0x13C3 /* EXT2 */
++#define RTL8365MB_DIGITAL_INTERFACE_SELECT_REG(_extint) \
++              ((_extint) <= 1 ? RTL8365MB_DIGITAL_INTERFACE_SELECT_REG0 : \
++               (_extint) == 2 ? RTL8365MB_DIGITAL_INTERFACE_SELECT_REG1 : \
++               0x0)
++#define   RTL8365MB_DIGITAL_INTERFACE_SELECT_MODE_MASK(_extint) \
++              (0xF << (((_extint) % 2) * 4))
++#define   RTL8365MB_DIGITAL_INTERFACE_SELECT_MODE_OFFSET(_extint) \
++              (((_extint) % 2) * 4)
++
++/* External interface RGMII TX/RX delay configuration registers 0~2 */
++#define RTL8365MB_EXT_RGMXF_REG0              0x1306 /* EXT0 */
++#define RTL8365MB_EXT_RGMXF_REG1              0x1307 /* EXT1 */
++#define RTL8365MB_EXT_RGMXF_REG2              0x13C5 /* EXT2 */
++#define RTL8365MB_EXT_RGMXF_REG(_extint) \
++              ((_extint) == 0 ? RTL8365MB_EXT_RGMXF_REG0 : \
++               (_extint) == 1 ? RTL8365MB_EXT_RGMXF_REG1 : \
++               (_extint) == 2 ? RTL8365MB_EXT_RGMXF_REG2 : \
++               0x0)
++#define   RTL8365MB_EXT_RGMXF_RXDELAY_MASK    0x0007
++#define   RTL8365MB_EXT_RGMXF_TXDELAY_MASK    0x0008
++
++/* External interface port speed values - used in DIGITAL_INTERFACE_FORCE */
++#define RTL8365MB_PORT_SPEED_10M      0
++#define RTL8365MB_PORT_SPEED_100M     1
++#define RTL8365MB_PORT_SPEED_1000M    2
++
++/* External interface force configuration registers 0~2 */
++#define RTL8365MB_DIGITAL_INTERFACE_FORCE_REG0                0x1310 /* EXT0 */
++#define RTL8365MB_DIGITAL_INTERFACE_FORCE_REG1                0x1311 /* EXT1 */
++#define RTL8365MB_DIGITAL_INTERFACE_FORCE_REG2                0x13C4 /* EXT2 */
++#define RTL8365MB_DIGITAL_INTERFACE_FORCE_REG(_extint) \
++              ((_extint) == 0 ? RTL8365MB_DIGITAL_INTERFACE_FORCE_REG0 : \
++               (_extint) == 1 ? RTL8365MB_DIGITAL_INTERFACE_FORCE_REG1 : \
++               (_extint) == 2 ? RTL8365MB_DIGITAL_INTERFACE_FORCE_REG2 : \
++               0x0)
++#define   RTL8365MB_DIGITAL_INTERFACE_FORCE_EN_MASK           0x1000
++#define   RTL8365MB_DIGITAL_INTERFACE_FORCE_NWAY_MASK         0x0080
++#define   RTL8365MB_DIGITAL_INTERFACE_FORCE_TXPAUSE_MASK      0x0040
++#define   RTL8365MB_DIGITAL_INTERFACE_FORCE_RXPAUSE_MASK      0x0020
++#define   RTL8365MB_DIGITAL_INTERFACE_FORCE_LINK_MASK         0x0010
++#define   RTL8365MB_DIGITAL_INTERFACE_FORCE_DUPLEX_MASK               0x0004
++#define   RTL8365MB_DIGITAL_INTERFACE_FORCE_SPEED_MASK                0x0003
++
++/* CPU port mask register - controls which ports are treated as CPU ports */
++#define RTL8365MB_CPU_PORT_MASK_REG   0x1219
++#define   RTL8365MB_CPU_PORT_MASK_MASK        0x07FF
++
++/* CPU control register */
++#define RTL8365MB_CPU_CTRL_REG                        0x121A
++#define   RTL8365MB_CPU_CTRL_TRAP_PORT_EXT_MASK       0x0400
++#define   RTL8365MB_CPU_CTRL_TAG_FORMAT_MASK  0x0200
++#define   RTL8365MB_CPU_CTRL_RXBYTECOUNT_MASK 0x0080
++#define   RTL8365MB_CPU_CTRL_TAG_POSITION_MASK        0x0040
++#define   RTL8365MB_CPU_CTRL_TRAP_PORT_MASK   0x0038
++#define   RTL8365MB_CPU_CTRL_INSERTMODE_MASK  0x0006
++#define   RTL8365MB_CPU_CTRL_EN_MASK          0x0001
++
++/* Maximum packet length register */
++#define RTL8365MB_CFG0_MAX_LEN_REG    0x088C
++#define   RTL8365MB_CFG0_MAX_LEN_MASK 0x3FFF
++#define RTL8365MB_CFG0_MAX_LEN_MAX    0x3FFF
++
++/* Port learning limit registers */
++#define RTL8365MB_LUT_PORT_LEARN_LIMIT_BASE           0x0A20
++#define RTL8365MB_LUT_PORT_LEARN_LIMIT_REG(_physport) \
++              (RTL8365MB_LUT_PORT_LEARN_LIMIT_BASE + (_physport))
++
++/* Port isolation (forwarding mask) registers */
++#define RTL8365MB_PORT_ISOLATION_REG_BASE             0x08A2
++#define RTL8365MB_PORT_ISOLATION_REG(_physport) \
++              (RTL8365MB_PORT_ISOLATION_REG_BASE + (_physport))
++#define   RTL8365MB_PORT_ISOLATION_MASK                       0x07FF
++
++/* MSTP port state registers - indexed by tree instance */
++#define RTL8365MB_MSTI_CTRL_BASE                      0x0A00
++#define RTL8365MB_MSTI_CTRL_REG(_msti, _physport) \
++              (RTL8365MB_MSTI_CTRL_BASE + ((_msti) << 1) + ((_physport) >> 3))
++#define   RTL8365MB_MSTI_CTRL_PORT_STATE_OFFSET(_physport) ((_physport) << 1)
++#define   RTL8365MB_MSTI_CTRL_PORT_STATE_MASK(_physport) \
++              (0x3 << RTL8365MB_MSTI_CTRL_PORT_STATE_OFFSET((_physport)))
++
++/* MIB counter value registers */
++#define RTL8365MB_MIB_COUNTER_BASE    0x1000
++#define RTL8365MB_MIB_COUNTER_REG(_x) (RTL8365MB_MIB_COUNTER_BASE + (_x))
++
++/* MIB counter address register */
++#define RTL8365MB_MIB_ADDRESS_REG             0x1004
++#define   RTL8365MB_MIB_ADDRESS_PORT_OFFSET   0x007C
++#define   RTL8365MB_MIB_ADDRESS(_p, _x) \
++              (((RTL8365MB_MIB_ADDRESS_PORT_OFFSET) * (_p) + (_x)) >> 2)
++
++#define RTL8365MB_MIB_CTRL0_REG                       0x1005
++#define   RTL8365MB_MIB_CTRL0_RESET_MASK      0x0002
++#define   RTL8365MB_MIB_CTRL0_BUSY_MASK               0x0001
++
++/* The DSA callback .get_stats64 runs in atomic context, so we are not allowed
++ * to block. On the other hand, accessing MIB counters absolutely requires us to
++ * block. The solution is thus to schedule work which polls the MIB counters
++ * asynchronously and updates some private data, which the callback can then
++ * fetch atomically. Three seconds should be a good enough polling interval.
++ */
++#define RTL8365MB_STATS_INTERVAL_JIFFIES      (3 * HZ)
++
++enum rtl8365mb_mib_counter_index {
++      RTL8365MB_MIB_ifInOctets,
++      RTL8365MB_MIB_dot3StatsFCSErrors,
++      RTL8365MB_MIB_dot3StatsSymbolErrors,
++      RTL8365MB_MIB_dot3InPauseFrames,
++      RTL8365MB_MIB_dot3ControlInUnknownOpcodes,
++      RTL8365MB_MIB_etherStatsFragments,
++      RTL8365MB_MIB_etherStatsJabbers,
++      RTL8365MB_MIB_ifInUcastPkts,
++      RTL8365MB_MIB_etherStatsDropEvents,
++      RTL8365MB_MIB_ifInMulticastPkts,
++      RTL8365MB_MIB_ifInBroadcastPkts,
++      RTL8365MB_MIB_inMldChecksumError,
++      RTL8365MB_MIB_inIgmpChecksumError,
++      RTL8365MB_MIB_inMldSpecificQuery,
++      RTL8365MB_MIB_inMldGeneralQuery,
++      RTL8365MB_MIB_inIgmpSpecificQuery,
++      RTL8365MB_MIB_inIgmpGeneralQuery,
++      RTL8365MB_MIB_inMldLeaves,
++      RTL8365MB_MIB_inIgmpLeaves,
++      RTL8365MB_MIB_etherStatsOctets,
++      RTL8365MB_MIB_etherStatsUnderSizePkts,
++      RTL8365MB_MIB_etherOversizeStats,
++      RTL8365MB_MIB_etherStatsPkts64Octets,
++      RTL8365MB_MIB_etherStatsPkts65to127Octets,
++      RTL8365MB_MIB_etherStatsPkts128to255Octets,
++      RTL8365MB_MIB_etherStatsPkts256to511Octets,
++      RTL8365MB_MIB_etherStatsPkts512to1023Octets,
++      RTL8365MB_MIB_etherStatsPkts1024to1518Octets,
++      RTL8365MB_MIB_ifOutOctets,
++      RTL8365MB_MIB_dot3StatsSingleCollisionFrames,
++      RTL8365MB_MIB_dot3StatsMultipleCollisionFrames,
++      RTL8365MB_MIB_dot3StatsDeferredTransmissions,
++      RTL8365MB_MIB_dot3StatsLateCollisions,
++      RTL8365MB_MIB_etherStatsCollisions,
++      RTL8365MB_MIB_dot3StatsExcessiveCollisions,
++      RTL8365MB_MIB_dot3OutPauseFrames,
++      RTL8365MB_MIB_ifOutDiscards,
++      RTL8365MB_MIB_dot1dTpPortInDiscards,
++      RTL8365MB_MIB_ifOutUcastPkts,
++      RTL8365MB_MIB_ifOutMulticastPkts,
++      RTL8365MB_MIB_ifOutBroadcastPkts,
++      RTL8365MB_MIB_outOampduPkts,
++      RTL8365MB_MIB_inOampduPkts,
++      RTL8365MB_MIB_inIgmpJoinsSuccess,
++      RTL8365MB_MIB_inIgmpJoinsFail,
++      RTL8365MB_MIB_inMldJoinsSuccess,
++      RTL8365MB_MIB_inMldJoinsFail,
++      RTL8365MB_MIB_inReportSuppressionDrop,
++      RTL8365MB_MIB_inLeaveSuppressionDrop,
++      RTL8365MB_MIB_outIgmpReports,
++      RTL8365MB_MIB_outIgmpLeaves,
++      RTL8365MB_MIB_outIgmpGeneralQuery,
++      RTL8365MB_MIB_outIgmpSpecificQuery,
++      RTL8365MB_MIB_outMldReports,
++      RTL8365MB_MIB_outMldLeaves,
++      RTL8365MB_MIB_outMldGeneralQuery,
++      RTL8365MB_MIB_outMldSpecificQuery,
++      RTL8365MB_MIB_inKnownMulticastPkts,
++      RTL8365MB_MIB_END,
++};
++
++struct rtl8365mb_mib_counter {
++      u32 offset;
++      u32 length;
++      const char *name;
++};
++
++#define RTL8365MB_MAKE_MIB_COUNTER(_offset, _length, _name) \
++              [RTL8365MB_MIB_ ## _name] = { _offset, _length, #_name }
++
++static struct rtl8365mb_mib_counter rtl8365mb_mib_counters[] = {
++      RTL8365MB_MAKE_MIB_COUNTER(0, 4, ifInOctets),
++      RTL8365MB_MAKE_MIB_COUNTER(4, 2, dot3StatsFCSErrors),
++      RTL8365MB_MAKE_MIB_COUNTER(6, 2, dot3StatsSymbolErrors),
++      RTL8365MB_MAKE_MIB_COUNTER(8, 2, dot3InPauseFrames),
++      RTL8365MB_MAKE_MIB_COUNTER(10, 2, dot3ControlInUnknownOpcodes),
++      RTL8365MB_MAKE_MIB_COUNTER(12, 2, etherStatsFragments),
++      RTL8365MB_MAKE_MIB_COUNTER(14, 2, etherStatsJabbers),
++      RTL8365MB_MAKE_MIB_COUNTER(16, 2, ifInUcastPkts),
++      RTL8365MB_MAKE_MIB_COUNTER(18, 2, etherStatsDropEvents),
++      RTL8365MB_MAKE_MIB_COUNTER(20, 2, ifInMulticastPkts),
++      RTL8365MB_MAKE_MIB_COUNTER(22, 2, ifInBroadcastPkts),
++      RTL8365MB_MAKE_MIB_COUNTER(24, 2, inMldChecksumError),
++      RTL8365MB_MAKE_MIB_COUNTER(26, 2, inIgmpChecksumError),
++      RTL8365MB_MAKE_MIB_COUNTER(28, 2, inMldSpecificQuery),
++      RTL8365MB_MAKE_MIB_COUNTER(30, 2, inMldGeneralQuery),
++      RTL8365MB_MAKE_MIB_COUNTER(32, 2, inIgmpSpecificQuery),
++      RTL8365MB_MAKE_MIB_COUNTER(34, 2, inIgmpGeneralQuery),
++      RTL8365MB_MAKE_MIB_COUNTER(36, 2, inMldLeaves),
++      RTL8365MB_MAKE_MIB_COUNTER(38, 2, inIgmpLeaves),
++      RTL8365MB_MAKE_MIB_COUNTER(40, 4, etherStatsOctets),
++      RTL8365MB_MAKE_MIB_COUNTER(44, 2, etherStatsUnderSizePkts),
++      RTL8365MB_MAKE_MIB_COUNTER(46, 2, etherOversizeStats),
++      RTL8365MB_MAKE_MIB_COUNTER(48, 2, etherStatsPkts64Octets),
++      RTL8365MB_MAKE_MIB_COUNTER(50, 2, etherStatsPkts65to127Octets),
++      RTL8365MB_MAKE_MIB_COUNTER(52, 2, etherStatsPkts128to255Octets),
++      RTL8365MB_MAKE_MIB_COUNTER(54, 2, etherStatsPkts256to511Octets),
++      RTL8365MB_MAKE_MIB_COUNTER(56, 2, etherStatsPkts512to1023Octets),
++      RTL8365MB_MAKE_MIB_COUNTER(58, 2, etherStatsPkts1024to1518Octets),
++      RTL8365MB_MAKE_MIB_COUNTER(60, 4, ifOutOctets),
++      RTL8365MB_MAKE_MIB_COUNTER(64, 2, dot3StatsSingleCollisionFrames),
++      RTL8365MB_MAKE_MIB_COUNTER(66, 2, dot3StatsMultipleCollisionFrames),
++      RTL8365MB_MAKE_MIB_COUNTER(68, 2, dot3StatsDeferredTransmissions),
++      RTL8365MB_MAKE_MIB_COUNTER(70, 2, dot3StatsLateCollisions),
++      RTL8365MB_MAKE_MIB_COUNTER(72, 2, etherStatsCollisions),
++      RTL8365MB_MAKE_MIB_COUNTER(74, 2, dot3StatsExcessiveCollisions),
++      RTL8365MB_MAKE_MIB_COUNTER(76, 2, dot3OutPauseFrames),
++      RTL8365MB_MAKE_MIB_COUNTER(78, 2, ifOutDiscards),
++      RTL8365MB_MAKE_MIB_COUNTER(80, 2, dot1dTpPortInDiscards),
++      RTL8365MB_MAKE_MIB_COUNTER(82, 2, ifOutUcastPkts),
++      RTL8365MB_MAKE_MIB_COUNTER(84, 2, ifOutMulticastPkts),
++      RTL8365MB_MAKE_MIB_COUNTER(86, 2, ifOutBroadcastPkts),
++      RTL8365MB_MAKE_MIB_COUNTER(88, 2, outOampduPkts),
++      RTL8365MB_MAKE_MIB_COUNTER(90, 2, inOampduPkts),
++      RTL8365MB_MAKE_MIB_COUNTER(92, 4, inIgmpJoinsSuccess),
++      RTL8365MB_MAKE_MIB_COUNTER(96, 2, inIgmpJoinsFail),
++      RTL8365MB_MAKE_MIB_COUNTER(98, 2, inMldJoinsSuccess),
++      RTL8365MB_MAKE_MIB_COUNTER(100, 2, inMldJoinsFail),
++      RTL8365MB_MAKE_MIB_COUNTER(102, 2, inReportSuppressionDrop),
++      RTL8365MB_MAKE_MIB_COUNTER(104, 2, inLeaveSuppressionDrop),
++      RTL8365MB_MAKE_MIB_COUNTER(106, 2, outIgmpReports),
++      RTL8365MB_MAKE_MIB_COUNTER(108, 2, outIgmpLeaves),
++      RTL8365MB_MAKE_MIB_COUNTER(110, 2, outIgmpGeneralQuery),
++      RTL8365MB_MAKE_MIB_COUNTER(112, 2, outIgmpSpecificQuery),
++      RTL8365MB_MAKE_MIB_COUNTER(114, 2, outMldReports),
++      RTL8365MB_MAKE_MIB_COUNTER(116, 2, outMldLeaves),
++      RTL8365MB_MAKE_MIB_COUNTER(118, 2, outMldGeneralQuery),
++      RTL8365MB_MAKE_MIB_COUNTER(120, 2, outMldSpecificQuery),
++      RTL8365MB_MAKE_MIB_COUNTER(122, 2, inKnownMulticastPkts),
++};
++
++static_assert(ARRAY_SIZE(rtl8365mb_mib_counters) == RTL8365MB_MIB_END);
++
++struct rtl8365mb_jam_tbl_entry {
++      u16 reg;
++      u16 val;
++};
++
++/* Lifted from the vendor driver sources */
++static const struct rtl8365mb_jam_tbl_entry rtl8365mb_init_jam_8365mb_vc[] = {
++      { 0x13EB, 0x15BB }, { 0x1303, 0x06D6 }, { 0x1304, 0x0700 },
++      { 0x13E2, 0x003F }, { 0x13F9, 0x0090 }, { 0x121E, 0x03CA },
++      { 0x1233, 0x0352 }, { 0x1237, 0x00A0 }, { 0x123A, 0x0030 },
++      { 0x1239, 0x0084 }, { 0x0301, 0x1000 }, { 0x1349, 0x001F },
++      { 0x18E0, 0x4004 }, { 0x122B, 0x241C }, { 0x1305, 0xC000 },
++      { 0x13F0, 0x0000 },
++};
++
++static const struct rtl8365mb_jam_tbl_entry rtl8365mb_init_jam_common[] = {
++      { 0x1200, 0x7FCB }, { 0x0884, 0x0003 }, { 0x06EB, 0x0001 },
++      { 0x03Fa, 0x0007 }, { 0x08C8, 0x00C0 }, { 0x0A30, 0x020E },
++      { 0x0800, 0x0000 }, { 0x0802, 0x0000 }, { 0x09DA, 0x0013 },
++      { 0x1D32, 0x0002 },
++};
++
++enum rtl8365mb_phy_interface_mode {
++      RTL8365MB_PHY_INTERFACE_MODE_INVAL = 0,
++      RTL8365MB_PHY_INTERFACE_MODE_INTERNAL = BIT(0),
++      RTL8365MB_PHY_INTERFACE_MODE_MII = BIT(1),
++      RTL8365MB_PHY_INTERFACE_MODE_TMII = BIT(2),
++      RTL8365MB_PHY_INTERFACE_MODE_RMII = BIT(3),
++      RTL8365MB_PHY_INTERFACE_MODE_RGMII = BIT(4),
++      RTL8365MB_PHY_INTERFACE_MODE_SGMII = BIT(5),
++      RTL8365MB_PHY_INTERFACE_MODE_HSGMII = BIT(6),
++};
++
++/**
++ * struct rtl8365mb_extint - external interface info
++ * @port: the port with an external interface
++ * @id: the external interface ID, which is either 0, 1, or 2
++ * @supported_interfaces: a bitmask of supported PHY interface modes
++ *
++ * Represents a mapping: port -> { id, supported_interfaces }. To be embedded
++ * in &struct rtl8365mb_chip_info for every port with an external interface.
++ */
++struct rtl8365mb_extint {
++      int port;
++      int id;
++      unsigned int supported_interfaces;
++};
++
++/**
++ * struct rtl8365mb_chip_info - static chip-specific info
++ * @name: human-readable chip name
++ * @chip_id: chip identifier
++ * @chip_ver: chip silicon revision
++ * @extints: available external interfaces
++ * @jam_table: chip-specific initialization jam table
++ * @jam_size: size of the chip's jam table
++ *
++ * These data are specific to a given chip in the family of switches supported
++ * by this driver. When adding support for another chip in the family, a new
++ * chip info should be added to the rtl8365mb_chip_infos array.
++ */
++struct rtl8365mb_chip_info {
++      const char *name;
++      u32 chip_id;
++      u32 chip_ver;
++      const struct rtl8365mb_extint extints[RTL8365MB_MAX_NUM_EXTINTS];
++      const struct rtl8365mb_jam_tbl_entry *jam_table;
++      size_t jam_size;
++};
++
++/* Chip info for each supported switch in the family */
++#define PHY_INTF(_mode) (RTL8365MB_PHY_INTERFACE_MODE_ ## _mode)
++static const struct rtl8365mb_chip_info rtl8365mb_chip_infos[] = {
++      {
++              .name = "RTL8365MB-VC",
++              .chip_id = 0x6367,
++              .chip_ver = 0x0040,
++              .extints = {
++                      { 6, 1, PHY_INTF(MII) | PHY_INTF(TMII) |
++                              PHY_INTF(RMII) | PHY_INTF(RGMII) },
++              },
++              .jam_table = rtl8365mb_init_jam_8365mb_vc,
++              .jam_size = ARRAY_SIZE(rtl8365mb_init_jam_8365mb_vc),
++      },
++      {
++              .name = "RTL8367S",
++              .chip_id = 0x6367,
++              .chip_ver = 0x00A0,
++              .extints = {
++                      { 6, 1, PHY_INTF(SGMII) | PHY_INTF(HSGMII) },
++                      { 7, 2, PHY_INTF(MII) | PHY_INTF(TMII) |
++                              PHY_INTF(RMII) | PHY_INTF(RGMII) },
++              },
++              .jam_table = rtl8365mb_init_jam_8365mb_vc,
++              .jam_size = ARRAY_SIZE(rtl8365mb_init_jam_8365mb_vc),
++      },
++      {
++              .name = "RTL8367SB",
++              .chip_id = 0x6367,
++              .chip_ver = 0x0010,
++              .extints = {
++                      { 6, 1, PHY_INTF(MII) | PHY_INTF(TMII) |
++                              PHY_INTF(RMII) | PHY_INTF(RGMII) |
++                              PHY_INTF(SGMII) | PHY_INTF(HSGMII) },
++                      { 7, 2, PHY_INTF(MII) | PHY_INTF(TMII) |
++                              PHY_INTF(RMII) | PHY_INTF(RGMII) },
++              },
++              .jam_table = rtl8365mb_init_jam_8365mb_vc,
++              .jam_size = ARRAY_SIZE(rtl8365mb_init_jam_8365mb_vc),
++      },
++      {
++              .name = "RTL8367RB-VB",
++              .chip_id = 0x6367,
++              .chip_ver = 0x0020,
++              .extints = {
++                      { 6, 1, PHY_INTF(MII) | PHY_INTF(TMII) |
++                              PHY_INTF(RMII) | PHY_INTF(RGMII) },
++                      { 7, 2, PHY_INTF(MII) | PHY_INTF(TMII) |
++                              PHY_INTF(RMII) | PHY_INTF(RGMII) },
++              },
++              .jam_table = rtl8365mb_init_jam_8365mb_vc,
++              .jam_size = ARRAY_SIZE(rtl8365mb_init_jam_8365mb_vc),
++      },
++};
++
++enum rtl8365mb_stp_state {
++      RTL8365MB_STP_STATE_DISABLED = 0,
++      RTL8365MB_STP_STATE_BLOCKING = 1,
++      RTL8365MB_STP_STATE_LEARNING = 2,
++      RTL8365MB_STP_STATE_FORWARDING = 3,
++};
++
++enum rtl8365mb_cpu_insert {
++      RTL8365MB_CPU_INSERT_TO_ALL = 0,
++      RTL8365MB_CPU_INSERT_TO_TRAPPING = 1,
++      RTL8365MB_CPU_INSERT_TO_NONE = 2,
++};
++
++enum rtl8365mb_cpu_position {
++      RTL8365MB_CPU_POS_AFTER_SA = 0,
++      RTL8365MB_CPU_POS_BEFORE_CRC = 1,
++};
++
++enum rtl8365mb_cpu_format {
++      RTL8365MB_CPU_FORMAT_8BYTES = 0,
++      RTL8365MB_CPU_FORMAT_4BYTES = 1,
++};
++
++enum rtl8365mb_cpu_rxlen {
++      RTL8365MB_CPU_RXLEN_72BYTES = 0,
++      RTL8365MB_CPU_RXLEN_64BYTES = 1,
++};
++
++/**
++ * struct rtl8365mb_cpu - CPU port configuration
++ * @enable: enable/disable hardware insertion of CPU tag in switch->CPU frames
++ * @mask: port mask of ports that parse should parse CPU tags
++ * @trap_port: forward trapped frames to this port
++ * @insert: CPU tag insertion mode in switch->CPU frames
++ * @position: position of CPU tag in frame
++ * @rx_length: minimum CPU RX length
++ * @format: CPU tag format
++ *
++ * Represents the CPU tagging and CPU port configuration of the switch. These
++ * settings are configurable at runtime.
++ */
++struct rtl8365mb_cpu {
++      bool enable;
++      u32 mask;
++      u32 trap_port;
++      enum rtl8365mb_cpu_insert insert;
++      enum rtl8365mb_cpu_position position;
++      enum rtl8365mb_cpu_rxlen rx_length;
++      enum rtl8365mb_cpu_format format;
++};
++
++/**
++ * struct rtl8365mb_port - private per-port data
++ * @priv: pointer to parent realtek_priv data
++ * @index: DSA port index, same as dsa_port::index
++ * @stats: link statistics populated by rtl8365mb_stats_poll, ready for atomic
++ *         access via rtl8365mb_get_stats64
++ * @stats_lock: protect the stats structure during read/update
++ * @mib_work: delayed work for polling MIB counters
++ */
++struct rtl8365mb_port {
++      struct realtek_priv *priv;
++      unsigned int index;
++      struct rtnl_link_stats64 stats;
++      spinlock_t stats_lock;
++      struct delayed_work mib_work;
++};
++
++/**
++ * struct rtl8365mb - driver private data
++ * @priv: pointer to parent realtek_priv data
++ * @irq: registered IRQ or zero
++ * @chip_info: chip-specific info about the attached switch
++ * @cpu: CPU tagging and CPU port configuration for this chip
++ * @mib_lock: prevent concurrent reads of MIB counters
++ * @ports: per-port data
++ *
++ * Private data for this driver.
++ */
++struct rtl8365mb {
++      struct realtek_priv *priv;
++      int irq;
++      const struct rtl8365mb_chip_info *chip_info;
++      struct rtl8365mb_cpu cpu;
++      struct mutex mib_lock;
++      struct rtl8365mb_port ports[RTL8365MB_MAX_NUM_PORTS];
++};
++
++static int rtl8365mb_phy_poll_busy(struct realtek_priv *priv)
++{
++      u32 val;
++
++      return regmap_read_poll_timeout(priv->map_nolock,
++                                      RTL8365MB_INDIRECT_ACCESS_STATUS_REG,
++                                      val, !val, 10, 100);
++}
++
++static int rtl8365mb_phy_ocp_prepare(struct realtek_priv *priv, int phy,
++                                   u32 ocp_addr)
++{
++      u32 val;
++      int ret;
++
++      /* Set OCP prefix */
++      val = FIELD_GET(RTL8365MB_PHY_OCP_ADDR_PREFIX_MASK, ocp_addr);
++      ret = regmap_update_bits(
++              priv->map_nolock, RTL8365MB_GPHY_OCP_MSB_0_REG,
++              RTL8365MB_GPHY_OCP_MSB_0_CFG_CPU_OCPADR_MASK,
++              FIELD_PREP(RTL8365MB_GPHY_OCP_MSB_0_CFG_CPU_OCPADR_MASK, val));
++      if (ret)
++              return ret;
++
++      /* Set PHY register address */
++      val = RTL8365MB_PHY_BASE;
++      val |= FIELD_PREP(RTL8365MB_INDIRECT_ACCESS_ADDRESS_PHYNUM_MASK, phy);
++      val |= FIELD_PREP(RTL8365MB_INDIRECT_ACCESS_ADDRESS_OCPADR_5_1_MASK,
++                        ocp_addr >> 1);
++      val |= FIELD_PREP(RTL8365MB_INDIRECT_ACCESS_ADDRESS_OCPADR_9_6_MASK,
++                        ocp_addr >> 6);
++      ret = regmap_write(priv->map_nolock,
++                         RTL8365MB_INDIRECT_ACCESS_ADDRESS_REG, val);
++      if (ret)
++              return ret;
++
++      return 0;
++}
++
++static int rtl8365mb_phy_ocp_read(struct realtek_priv *priv, int phy,
++                                u32 ocp_addr, u16 *data)
++{
++      u32 val;
++      int ret;
++
++      rtl83xx_lock(priv);
++
++      ret = rtl8365mb_phy_poll_busy(priv);
++      if (ret)
++              goto out;
++
++      ret = rtl8365mb_phy_ocp_prepare(priv, phy, ocp_addr);
++      if (ret)
++              goto out;
++
++      /* Execute read operation */
++      val = FIELD_PREP(RTL8365MB_INDIRECT_ACCESS_CTRL_CMD_MASK,
++                       RTL8365MB_INDIRECT_ACCESS_CTRL_CMD_VALUE) |
++            FIELD_PREP(RTL8365MB_INDIRECT_ACCESS_CTRL_RW_MASK,
++                       RTL8365MB_INDIRECT_ACCESS_CTRL_RW_READ);
++      ret = regmap_write(priv->map_nolock, RTL8365MB_INDIRECT_ACCESS_CTRL_REG,
++                         val);
++      if (ret)
++              goto out;
++
++      ret = rtl8365mb_phy_poll_busy(priv);
++      if (ret)
++              goto out;
++
++      /* Get PHY register data */
++      ret = regmap_read(priv->map_nolock,
++                        RTL8365MB_INDIRECT_ACCESS_READ_DATA_REG, &val);
++      if (ret)
++              goto out;
++
++      *data = val & 0xFFFF;
++
++out:
++      rtl83xx_unlock(priv);
++
++      return ret;
++}
++
++static int rtl8365mb_phy_ocp_write(struct realtek_priv *priv, int phy,
++                                 u32 ocp_addr, u16 data)
++{
++      u32 val;
++      int ret;
++
++      rtl83xx_lock(priv);
++
++      ret = rtl8365mb_phy_poll_busy(priv);
++      if (ret)
++              goto out;
++
++      ret = rtl8365mb_phy_ocp_prepare(priv, phy, ocp_addr);
++      if (ret)
++              goto out;
++
++      /* Set PHY register data */
++      ret = regmap_write(priv->map_nolock,
++                         RTL8365MB_INDIRECT_ACCESS_WRITE_DATA_REG, data);
++      if (ret)
++              goto out;
++
++      /* Execute write operation */
++      val = FIELD_PREP(RTL8365MB_INDIRECT_ACCESS_CTRL_CMD_MASK,
++                       RTL8365MB_INDIRECT_ACCESS_CTRL_CMD_VALUE) |
++            FIELD_PREP(RTL8365MB_INDIRECT_ACCESS_CTRL_RW_MASK,
++                       RTL8365MB_INDIRECT_ACCESS_CTRL_RW_WRITE);
++      ret = regmap_write(priv->map_nolock, RTL8365MB_INDIRECT_ACCESS_CTRL_REG,
++                         val);
++      if (ret)
++              goto out;
++
++      ret = rtl8365mb_phy_poll_busy(priv);
++      if (ret)
++              goto out;
++
++out:
++      rtl83xx_unlock(priv);
++
++      return ret;
++}
++
++static int rtl8365mb_phy_read(struct realtek_priv *priv, int phy, int regnum)
++{
++      u32 ocp_addr;
++      u16 val;
++      int ret;
++
++      if (phy > RTL8365MB_PHYADDRMAX)
++              return -EINVAL;
++
++      if (regnum > RTL8365MB_PHYREGMAX)
++              return -EINVAL;
++
++      ocp_addr = RTL8365MB_PHY_OCP_ADDR_PHYREG_BASE + regnum * 2;
++
++      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 %pe\n", phy,
++                      regnum, ocp_addr, ERR_PTR(ret));
++              return ret;
++      }
++
++      dev_dbg(priv->dev, "read PHY%d register 0x%02x @ %04x, val <- %04x\n",
++              phy, regnum, ocp_addr, val);
++
++      return val;
++}
++
++static int rtl8365mb_phy_write(struct realtek_priv *priv, int phy, int regnum,
++                             u16 val)
++{
++      u32 ocp_addr;
++      int ret;
++
++      if (phy > RTL8365MB_PHYADDRMAX)
++              return -EINVAL;
++
++      if (regnum > RTL8365MB_PHYREGMAX)
++              return -EINVAL;
++
++      ocp_addr = RTL8365MB_PHY_OCP_ADDR_PHYREG_BASE + regnum * 2;
++
++      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 %pe\n", phy,
++                      regnum, ocp_addr, ERR_PTR(ret));
++              return ret;
++      }
++
++      dev_dbg(priv->dev, "write PHY%d register 0x%02x @ %04x, val -> %04x\n",
++              phy, regnum, ocp_addr, val);
++
++      return 0;
++}
++
++static const struct rtl8365mb_extint *
++rtl8365mb_get_port_extint(struct realtek_priv *priv, int port)
++{
++      struct rtl8365mb *mb = priv->chip_data;
++      int i;
++
++      for (i = 0; i < RTL8365MB_MAX_NUM_EXTINTS; i++) {
++              const struct rtl8365mb_extint *extint =
++                      &mb->chip_info->extints[i];
++
++              if (!extint->supported_interfaces)
++                      continue;
++
++              if (extint->port == port)
++                      return extint;
++      }
++
++      return NULL;
++}
++
++static enum dsa_tag_protocol
++rtl8365mb_get_tag_protocol(struct dsa_switch *ds, int port,
++                         enum dsa_tag_protocol mp)
++{
++      struct realtek_priv *priv = ds->priv;
++      struct rtl8365mb_cpu *cpu;
++      struct rtl8365mb *mb;
++
++      mb = priv->chip_data;
++      cpu = &mb->cpu;
++
++      if (cpu->position == RTL8365MB_CPU_POS_BEFORE_CRC)
++              return DSA_TAG_PROTO_RTL8_4T;
++
++      return DSA_TAG_PROTO_RTL8_4;
++}
++
++static int rtl8365mb_ext_config_rgmii(struct realtek_priv *priv, int port,
++                                    phy_interface_t interface)
++{
++      const struct rtl8365mb_extint *extint =
++              rtl8365mb_get_port_extint(priv, port);
++      struct dsa_switch *ds = &priv->ds;
++      struct device_node *dn;
++      struct dsa_port *dp;
++      int tx_delay = 0;
++      int rx_delay = 0;
++      u32 val;
++      int ret;
++
++      if (!extint)
++              return -ENODEV;
++
++      dp = dsa_to_port(ds, port);
++      dn = dp->dn;
++
++      /* Set the RGMII TX/RX delay
++       *
++       * The Realtek vendor driver indicates the following possible
++       * configuration settings:
++       *
++       *   TX delay:
++       *     0 = no delay, 1 = 2 ns delay
++       *   RX delay:
++       *     0 = no delay, 7 = maximum delay
++       *     Each step is approximately 0.3 ns, so the maximum delay is about
++       *     2.1 ns.
++       *
++       * The vendor driver also states that this must be configured *before*
++       * forcing the external interface into a particular mode, which is done
++       * in the rtl8365mb_phylink_mac_link_{up,down} functions.
++       *
++       * Only configure an RGMII TX (resp. RX) delay if the
++       * tx-internal-delay-ps (resp. rx-internal-delay-ps) OF property is
++       * specified. We ignore the detail of the RGMII interface mode
++       * (RGMII_{RXID, TXID, etc.}), as this is considered to be a PHY-only
++       * property.
++       */
++      if (!of_property_read_u32(dn, "tx-internal-delay-ps", &val)) {
++              val = val / 1000; /* convert to ns */
++
++              if (val == 0 || val == 2)
++                      tx_delay = val / 2;
++              else
++                      dev_warn(priv->dev,
++                               "RGMII TX delay must be 0 or 2 ns\n");
++      }
++
++      if (!of_property_read_u32(dn, "rx-internal-delay-ps", &val)) {
++              val = DIV_ROUND_CLOSEST(val, 300); /* convert to 0.3 ns step */
++
++              if (val <= 7)
++                      rx_delay = val;
++              else
++                      dev_warn(priv->dev,
++                               "RGMII RX delay must be 0 to 2.1 ns\n");
++      }
++
++      ret = regmap_update_bits(
++              priv->map, RTL8365MB_EXT_RGMXF_REG(extint->id),
++              RTL8365MB_EXT_RGMXF_TXDELAY_MASK |
++                      RTL8365MB_EXT_RGMXF_RXDELAY_MASK,
++              FIELD_PREP(RTL8365MB_EXT_RGMXF_TXDELAY_MASK, tx_delay) |
++                      FIELD_PREP(RTL8365MB_EXT_RGMXF_RXDELAY_MASK, rx_delay));
++      if (ret)
++              return ret;
++
++      ret = regmap_update_bits(
++              priv->map, RTL8365MB_DIGITAL_INTERFACE_SELECT_REG(extint->id),
++              RTL8365MB_DIGITAL_INTERFACE_SELECT_MODE_MASK(extint->id),
++              RTL8365MB_EXT_PORT_MODE_RGMII
++                      << RTL8365MB_DIGITAL_INTERFACE_SELECT_MODE_OFFSET(
++                                 extint->id));
++      if (ret)
++              return ret;
++
++      return 0;
++}
++
++static int rtl8365mb_ext_config_forcemode(struct realtek_priv *priv, int port,
++                                        bool link, int speed, int duplex,
++                                        bool tx_pause, bool rx_pause)
++{
++      const struct rtl8365mb_extint *extint =
++              rtl8365mb_get_port_extint(priv, port);
++      u32 r_tx_pause;
++      u32 r_rx_pause;
++      u32 r_duplex;
++      u32 r_speed;
++      u32 r_link;
++      int val;
++      int ret;
++
++      if (!extint)
++              return -ENODEV;
++
++      if (link) {
++              /* Force the link up with the desired configuration */
++              r_link = 1;
++              r_rx_pause = rx_pause ? 1 : 0;
++              r_tx_pause = tx_pause ? 1 : 0;
++
++              if (speed == SPEED_1000) {
++                      r_speed = RTL8365MB_PORT_SPEED_1000M;
++              } else if (speed == SPEED_100) {
++                      r_speed = RTL8365MB_PORT_SPEED_100M;
++              } else if (speed == SPEED_10) {
++                      r_speed = RTL8365MB_PORT_SPEED_10M;
++              } else {
++                      dev_err(priv->dev, "unsupported port speed %s\n",
++                              phy_speed_to_str(speed));
++                      return -EINVAL;
++              }
++
++              if (duplex == DUPLEX_FULL) {
++                      r_duplex = 1;
++              } else if (duplex == DUPLEX_HALF) {
++                      r_duplex = 0;
++              } else {
++                      dev_err(priv->dev, "unsupported duplex %s\n",
++                              phy_duplex_to_str(duplex));
++                      return -EINVAL;
++              }
++      } else {
++              /* Force the link down and reset any programmed configuration */
++              r_link = 0;
++              r_tx_pause = 0;
++              r_rx_pause = 0;
++              r_speed = 0;
++              r_duplex = 0;
++      }
++
++      val = FIELD_PREP(RTL8365MB_DIGITAL_INTERFACE_FORCE_EN_MASK, 1) |
++            FIELD_PREP(RTL8365MB_DIGITAL_INTERFACE_FORCE_TXPAUSE_MASK,
++                       r_tx_pause) |
++            FIELD_PREP(RTL8365MB_DIGITAL_INTERFACE_FORCE_RXPAUSE_MASK,
++                       r_rx_pause) |
++            FIELD_PREP(RTL8365MB_DIGITAL_INTERFACE_FORCE_LINK_MASK, r_link) |
++            FIELD_PREP(RTL8365MB_DIGITAL_INTERFACE_FORCE_DUPLEX_MASK,
++                       r_duplex) |
++            FIELD_PREP(RTL8365MB_DIGITAL_INTERFACE_FORCE_SPEED_MASK, r_speed);
++      ret = regmap_write(priv->map,
++                         RTL8365MB_DIGITAL_INTERFACE_FORCE_REG(extint->id),
++                         val);
++      if (ret)
++              return ret;
++
++      return 0;
++}
++
++static void rtl8365mb_phylink_get_caps(struct dsa_switch *ds, int port,
++                                     struct phylink_config *config)
++{
++      const struct rtl8365mb_extint *extint =
++              rtl8365mb_get_port_extint(ds->priv, port);
++
++      config->mac_capabilities = MAC_SYM_PAUSE | MAC_ASYM_PAUSE |
++                                 MAC_10 | MAC_100 | MAC_1000FD;
++
++      if (!extint) {
++              __set_bit(PHY_INTERFACE_MODE_INTERNAL,
++                        config->supported_interfaces);
++
++              /* GMII is the default interface mode for phylib, so
++               * we have to support it for ports with integrated PHY.
++               */
++              __set_bit(PHY_INTERFACE_MODE_GMII,
++                        config->supported_interfaces);
++              return;
++      }
++
++      /* Populate according to the modes supported by _this driver_,
++       * not necessarily the modes supported by the hardware, some of
++       * which remain unimplemented.
++       */
++
++      if (extint->supported_interfaces & RTL8365MB_PHY_INTERFACE_MODE_RGMII)
++              phy_interface_set_rgmii(config->supported_interfaces);
++}
++
++static void rtl8365mb_phylink_mac_config(struct phylink_config *config,
++                                       unsigned int mode,
++                                       const struct phylink_link_state *state)
++{
++      struct dsa_port *dp = dsa_phylink_to_port(config);
++      struct realtek_priv *priv = dp->ds->priv;
++      u8 port = dp->index;
++      int ret;
++
++      if (mode != MLO_AN_PHY && mode != MLO_AN_FIXED) {
++              dev_err(priv->dev,
++                      "port %d supports only conventional PHY or fixed-link\n",
++                      port);
++              return;
++      }
++
++      if (phy_interface_mode_is_rgmii(state->interface)) {
++              ret = rtl8365mb_ext_config_rgmii(priv, port, state->interface);
++              if (ret)
++                      dev_err(priv->dev,
++                              "failed to configure RGMII mode on port %d: %pe\n",
++                              port, ERR_PTR(ret));
++              return;
++      }
++
++      /* TODO: Implement MII and RMII modes, which the RTL8365MB-VC also
++       * supports
++       */
++}
++
++static void rtl8365mb_phylink_mac_link_down(struct phylink_config *config,
++                                          unsigned int mode,
++                                          phy_interface_t interface)
++{
++      struct dsa_port *dp = dsa_phylink_to_port(config);
++      struct realtek_priv *priv = dp->ds->priv;
++      struct rtl8365mb_port *p;
++      struct rtl8365mb *mb;
++      u8 port = dp->index;
++      int ret;
++
++      mb = priv->chip_data;
++      p = &mb->ports[port];
++      cancel_delayed_work_sync(&p->mib_work);
++
++      if (phy_interface_mode_is_rgmii(interface)) {
++              ret = rtl8365mb_ext_config_forcemode(priv, port, false, 0, 0,
++                                                   false, false);
++              if (ret)
++                      dev_err(priv->dev,
++                              "failed to reset forced mode on port %d: %pe\n",
++                              port, ERR_PTR(ret));
++
++              return;
++      }
++}
++
++static void rtl8365mb_phylink_mac_link_up(struct phylink_config *config,
++                                        struct phy_device *phydev,
++                                        unsigned int mode,
++                                        phy_interface_t interface,
++                                        int speed, int duplex, bool tx_pause,
++                                        bool rx_pause)
++{
++      struct dsa_port *dp = dsa_phylink_to_port(config);
++      struct realtek_priv *priv = dp->ds->priv;
++      struct rtl8365mb_port *p;
++      struct rtl8365mb *mb;
++      u8 port = dp->index;
++      int ret;
++
++      mb = priv->chip_data;
++      p = &mb->ports[port];
++      schedule_delayed_work(&p->mib_work, 0);
++
++      if (phy_interface_mode_is_rgmii(interface)) {
++              ret = rtl8365mb_ext_config_forcemode(priv, port, true, speed,
++                                                   duplex, tx_pause,
++                                                   rx_pause);
++              if (ret)
++                      dev_err(priv->dev,
++                              "failed to force mode on port %d: %pe\n", port,
++                              ERR_PTR(ret));
++
++              return;
++      }
++}
++
++static int rtl8365mb_port_change_mtu(struct dsa_switch *ds, int port,
++                                   int new_mtu)
++{
++      struct realtek_priv *priv = ds->priv;
++      int frame_size;
++
++      /* When a new MTU is set, DSA always sets the CPU port's MTU to the
++       * largest MTU of the user ports. Because the switch only has a global
++       * RX length register, only allowing CPU port here is enough.
++       */
++      if (!dsa_is_cpu_port(ds, port))
++              return 0;
++
++      frame_size = new_mtu + VLAN_ETH_HLEN + ETH_FCS_LEN;
++
++      dev_dbg(priv->dev, "changing mtu to %d (frame size: %d)\n",
++              new_mtu, frame_size);
++
++      return regmap_update_bits(priv->map, RTL8365MB_CFG0_MAX_LEN_REG,
++                                RTL8365MB_CFG0_MAX_LEN_MASK,
++                                FIELD_PREP(RTL8365MB_CFG0_MAX_LEN_MASK,
++                                           frame_size));
++}
++
++static int rtl8365mb_port_max_mtu(struct dsa_switch *ds, int port)
++{
++      return RTL8365MB_CFG0_MAX_LEN_MAX - VLAN_ETH_HLEN - ETH_FCS_LEN;
++}
++
++static void rtl8365mb_port_stp_state_set(struct dsa_switch *ds, int port,
++                                       u8 state)
++{
++      struct realtek_priv *priv = ds->priv;
++      enum rtl8365mb_stp_state val;
++      int msti = 0;
++
++      switch (state) {
++      case BR_STATE_DISABLED:
++              val = RTL8365MB_STP_STATE_DISABLED;
++              break;
++      case BR_STATE_BLOCKING:
++      case BR_STATE_LISTENING:
++              val = RTL8365MB_STP_STATE_BLOCKING;
++              break;
++      case BR_STATE_LEARNING:
++              val = RTL8365MB_STP_STATE_LEARNING;
++              break;
++      case BR_STATE_FORWARDING:
++              val = RTL8365MB_STP_STATE_FORWARDING;
++              break;
++      default:
++              dev_err(priv->dev, "invalid STP state: %u\n", state);
++              return;
++      }
++
++      regmap_update_bits(priv->map, RTL8365MB_MSTI_CTRL_REG(msti, port),
++                         RTL8365MB_MSTI_CTRL_PORT_STATE_MASK(port),
++                         val << RTL8365MB_MSTI_CTRL_PORT_STATE_OFFSET(port));
++}
++
++static int rtl8365mb_port_set_learning(struct realtek_priv *priv, int port,
++                                     bool enable)
++{
++      /* Enable/disable learning by limiting the number of L2 addresses the
++       * port can learn. Realtek documentation states that a limit of zero
++       * disables learning. When enabling learning, set it to the chip's
++       * maximum.
++       */
++      return regmap_write(priv->map, RTL8365MB_LUT_PORT_LEARN_LIMIT_REG(port),
++                          enable ? RTL8365MB_LEARN_LIMIT_MAX : 0);
++}
++
++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);
++}
++
++static int rtl8365mb_mib_counter_read(struct realtek_priv *priv, int port,
++                                    u32 offset, u32 length, u64 *mibvalue)
++{
++      u64 tmpvalue = 0;
++      u32 val;
++      int ret;
++      int i;
++
++      /* The MIB address is an SRAM address. We request a particular address
++       * and then poll the control register before reading the value from some
++       * counter registers.
++       */
++      ret = regmap_write(priv->map, RTL8365MB_MIB_ADDRESS_REG,
++                         RTL8365MB_MIB_ADDRESS(port, offset));
++      if (ret)
++              return ret;
++
++      /* Poll for completion */
++      ret = regmap_read_poll_timeout(priv->map, RTL8365MB_MIB_CTRL0_REG, val,
++                                     !(val & RTL8365MB_MIB_CTRL0_BUSY_MASK),
++                                     10, 100);
++      if (ret)
++              return ret;
++
++      /* Presumably this indicates a MIB counter read failure */
++      if (val & RTL8365MB_MIB_CTRL0_RESET_MASK)
++              return -EIO;
++
++      /* There are four MIB counter registers each holding a 16 bit word of a
++       * MIB counter. Depending on the offset, we should read from the upper
++       * two or lower two registers. In case the MIB counter is 4 words, we
++       * read from all four registers.
++       */
++      if (length == 4)
++              offset = 3;
++      else
++              offset = (offset + 1) % 4;
++
++      /* Read the MIB counter 16 bits at a time */
++      for (i = 0; i < length; i++) {
++              ret = regmap_read(priv->map,
++                                RTL8365MB_MIB_COUNTER_REG(offset - i), &val);
++              if (ret)
++                      return ret;
++
++              tmpvalue = ((tmpvalue) << 16) | (val & 0xFFFF);
++      }
++
++      /* Only commit the result if no error occurred */
++      *mibvalue = tmpvalue;
++
++      return 0;
++}
++
++static void rtl8365mb_get_ethtool_stats(struct dsa_switch *ds, int port, u64 *data)
++{
++      struct realtek_priv *priv = ds->priv;
++      struct rtl8365mb *mb;
++      int ret;
++      int i;
++
++      mb = priv->chip_data;
++
++      mutex_lock(&mb->mib_lock);
++      for (i = 0; i < RTL8365MB_MIB_END; i++) {
++              struct rtl8365mb_mib_counter *mib = &rtl8365mb_mib_counters[i];
++
++              ret = rtl8365mb_mib_counter_read(priv, port, mib->offset,
++                                               mib->length, &data[i]);
++              if (ret) {
++                      dev_err(priv->dev,
++                              "failed to read port %d counters: %pe\n", port,
++                              ERR_PTR(ret));
++                      break;
++              }
++      }
++      mutex_unlock(&mb->mib_lock);
++}
++
++static void rtl8365mb_get_strings(struct dsa_switch *ds, int port, u32 stringset, u8 *data)
++{
++      int i;
++
++      if (stringset != ETH_SS_STATS)
++              return;
++
++      for (i = 0; i < RTL8365MB_MIB_END; i++) {
++              struct rtl8365mb_mib_counter *mib = &rtl8365mb_mib_counters[i];
++              ethtool_puts(&data, mib->name);
++      }
++}
++
++static int rtl8365mb_get_sset_count(struct dsa_switch *ds, int port, int sset)
++{
++      if (sset != ETH_SS_STATS)
++              return -EOPNOTSUPP;
++
++      return RTL8365MB_MIB_END;
++}
++
++static void rtl8365mb_get_phy_stats(struct dsa_switch *ds, int port,
++                                  struct ethtool_eth_phy_stats *phy_stats)
++{
++      struct realtek_priv *priv = ds->priv;
++      struct rtl8365mb_mib_counter *mib;
++      struct rtl8365mb *mb;
++
++      mb = priv->chip_data;
++      mib = &rtl8365mb_mib_counters[RTL8365MB_MIB_dot3StatsSymbolErrors];
++
++      mutex_lock(&mb->mib_lock);
++      rtl8365mb_mib_counter_read(priv, port, mib->offset, mib->length,
++                                 &phy_stats->SymbolErrorDuringCarrier);
++      mutex_unlock(&mb->mib_lock);
++}
++
++static void rtl8365mb_get_mac_stats(struct dsa_switch *ds, int port,
++                                  struct ethtool_eth_mac_stats *mac_stats)
++{
++      u64 cnt[RTL8365MB_MIB_END] = {
++              [RTL8365MB_MIB_ifOutOctets] = 1,
++              [RTL8365MB_MIB_ifOutUcastPkts] = 1,
++              [RTL8365MB_MIB_ifOutMulticastPkts] = 1,
++              [RTL8365MB_MIB_ifOutBroadcastPkts] = 1,
++              [RTL8365MB_MIB_dot3OutPauseFrames] = 1,
++              [RTL8365MB_MIB_ifOutDiscards] = 1,
++              [RTL8365MB_MIB_ifInOctets] = 1,
++              [RTL8365MB_MIB_ifInUcastPkts] = 1,
++              [RTL8365MB_MIB_ifInMulticastPkts] = 1,
++              [RTL8365MB_MIB_ifInBroadcastPkts] = 1,
++              [RTL8365MB_MIB_dot3InPauseFrames] = 1,
++              [RTL8365MB_MIB_dot3StatsSingleCollisionFrames] = 1,
++              [RTL8365MB_MIB_dot3StatsMultipleCollisionFrames] = 1,
++              [RTL8365MB_MIB_dot3StatsFCSErrors] = 1,
++              [RTL8365MB_MIB_dot3StatsDeferredTransmissions] = 1,
++              [RTL8365MB_MIB_dot3StatsLateCollisions] = 1,
++              [RTL8365MB_MIB_dot3StatsExcessiveCollisions] = 1,
++
++      };
++      struct realtek_priv *priv = ds->priv;
++      struct rtl8365mb *mb;
++      int ret;
++      int i;
++
++      mb = priv->chip_data;
++
++      mutex_lock(&mb->mib_lock);
++      for (i = 0; i < RTL8365MB_MIB_END; i++) {
++              struct rtl8365mb_mib_counter *mib = &rtl8365mb_mib_counters[i];
++
++              /* Only fetch required MIB counters (marked = 1 above) */
++              if (!cnt[i])
++                      continue;
++
++              ret = rtl8365mb_mib_counter_read(priv, port, mib->offset,
++                                               mib->length, &cnt[i]);
++              if (ret)
++                      break;
++      }
++      mutex_unlock(&mb->mib_lock);
++
++      /* The RTL8365MB-VC exposes MIB objects, which we have to translate into
++       * IEEE 802.3 Managed Objects. This is not always completely faithful,
++       * but we try out best. See RFC 3635 for a detailed treatment of the
++       * subject.
++       */
++
++      mac_stats->FramesTransmittedOK = cnt[RTL8365MB_MIB_ifOutUcastPkts] +
++                                       cnt[RTL8365MB_MIB_ifOutMulticastPkts] +
++                                       cnt[RTL8365MB_MIB_ifOutBroadcastPkts] +
++                                       cnt[RTL8365MB_MIB_dot3OutPauseFrames] -
++                                       cnt[RTL8365MB_MIB_ifOutDiscards];
++      mac_stats->SingleCollisionFrames =
++              cnt[RTL8365MB_MIB_dot3StatsSingleCollisionFrames];
++      mac_stats->MultipleCollisionFrames =
++              cnt[RTL8365MB_MIB_dot3StatsMultipleCollisionFrames];
++      mac_stats->FramesReceivedOK = cnt[RTL8365MB_MIB_ifInUcastPkts] +
++                                    cnt[RTL8365MB_MIB_ifInMulticastPkts] +
++                                    cnt[RTL8365MB_MIB_ifInBroadcastPkts] +
++                                    cnt[RTL8365MB_MIB_dot3InPauseFrames];
++      mac_stats->FrameCheckSequenceErrors =
++              cnt[RTL8365MB_MIB_dot3StatsFCSErrors];
++      mac_stats->OctetsTransmittedOK = cnt[RTL8365MB_MIB_ifOutOctets] -
++                                       18 * mac_stats->FramesTransmittedOK;
++      mac_stats->FramesWithDeferredXmissions =
++              cnt[RTL8365MB_MIB_dot3StatsDeferredTransmissions];
++      mac_stats->LateCollisions = cnt[RTL8365MB_MIB_dot3StatsLateCollisions];
++      mac_stats->FramesAbortedDueToXSColls =
++              cnt[RTL8365MB_MIB_dot3StatsExcessiveCollisions];
++      mac_stats->OctetsReceivedOK = cnt[RTL8365MB_MIB_ifInOctets] -
++                                    18 * mac_stats->FramesReceivedOK;
++      mac_stats->MulticastFramesXmittedOK =
++              cnt[RTL8365MB_MIB_ifOutMulticastPkts];
++      mac_stats->BroadcastFramesXmittedOK =
++              cnt[RTL8365MB_MIB_ifOutBroadcastPkts];
++      mac_stats->MulticastFramesReceivedOK =
++              cnt[RTL8365MB_MIB_ifInMulticastPkts];
++      mac_stats->BroadcastFramesReceivedOK =
++              cnt[RTL8365MB_MIB_ifInBroadcastPkts];
++}
++
++static void rtl8365mb_get_ctrl_stats(struct dsa_switch *ds, int port,
++                                   struct ethtool_eth_ctrl_stats *ctrl_stats)
++{
++      struct realtek_priv *priv = ds->priv;
++      struct rtl8365mb_mib_counter *mib;
++      struct rtl8365mb *mb;
++
++      mb = priv->chip_data;
++      mib = &rtl8365mb_mib_counters[RTL8365MB_MIB_dot3ControlInUnknownOpcodes];
++
++      mutex_lock(&mb->mib_lock);
++      rtl8365mb_mib_counter_read(priv, port, mib->offset, mib->length,
++                                 &ctrl_stats->UnsupportedOpcodesReceived);
++      mutex_unlock(&mb->mib_lock);
++}
++
++static void rtl8365mb_stats_update(struct realtek_priv *priv, int port)
++{
++      u64 cnt[RTL8365MB_MIB_END] = {
++              [RTL8365MB_MIB_ifOutOctets] = 1,
++              [RTL8365MB_MIB_ifOutUcastPkts] = 1,
++              [RTL8365MB_MIB_ifOutMulticastPkts] = 1,
++              [RTL8365MB_MIB_ifOutBroadcastPkts] = 1,
++              [RTL8365MB_MIB_ifOutDiscards] = 1,
++              [RTL8365MB_MIB_ifInOctets] = 1,
++              [RTL8365MB_MIB_ifInUcastPkts] = 1,
++              [RTL8365MB_MIB_ifInMulticastPkts] = 1,
++              [RTL8365MB_MIB_ifInBroadcastPkts] = 1,
++              [RTL8365MB_MIB_etherStatsDropEvents] = 1,
++              [RTL8365MB_MIB_etherStatsCollisions] = 1,
++              [RTL8365MB_MIB_etherStatsFragments] = 1,
++              [RTL8365MB_MIB_etherStatsJabbers] = 1,
++              [RTL8365MB_MIB_dot3StatsFCSErrors] = 1,
++              [RTL8365MB_MIB_dot3StatsLateCollisions] = 1,
++      };
++      struct rtl8365mb *mb = priv->chip_data;
++      struct rtnl_link_stats64 *stats;
++      int ret;
++      int i;
++
++      stats = &mb->ports[port].stats;
++
++      mutex_lock(&mb->mib_lock);
++      for (i = 0; i < RTL8365MB_MIB_END; i++) {
++              struct rtl8365mb_mib_counter *c = &rtl8365mb_mib_counters[i];
++
++              /* Only fetch required MIB counters (marked = 1 above) */
++              if (!cnt[i])
++                      continue;
++
++              ret = rtl8365mb_mib_counter_read(priv, port, c->offset,
++                                               c->length, &cnt[i]);
++              if (ret)
++                      break;
++      }
++      mutex_unlock(&mb->mib_lock);
++
++      /* Don't update statistics if there was an error reading the counters */
++      if (ret)
++              return;
++
++      spin_lock(&mb->ports[port].stats_lock);
++
++      stats->rx_packets = cnt[RTL8365MB_MIB_ifInUcastPkts] +
++                          cnt[RTL8365MB_MIB_ifInMulticastPkts] +
++                          cnt[RTL8365MB_MIB_ifInBroadcastPkts];
++
++      stats->tx_packets = cnt[RTL8365MB_MIB_ifOutUcastPkts] +
++                          cnt[RTL8365MB_MIB_ifOutMulticastPkts] +
++                          cnt[RTL8365MB_MIB_ifOutBroadcastPkts];
++
++      /* if{In,Out}Octets includes FCS - remove it */
++      stats->rx_bytes = cnt[RTL8365MB_MIB_ifInOctets] - 4 * stats->rx_packets;
++      stats->tx_bytes =
++              cnt[RTL8365MB_MIB_ifOutOctets] - 4 * stats->tx_packets;
++
++      stats->rx_dropped = cnt[RTL8365MB_MIB_etherStatsDropEvents];
++      stats->tx_dropped = cnt[RTL8365MB_MIB_ifOutDiscards];
++
++      stats->multicast = cnt[RTL8365MB_MIB_ifInMulticastPkts];
++      stats->collisions = cnt[RTL8365MB_MIB_etherStatsCollisions];
++
++      stats->rx_length_errors = cnt[RTL8365MB_MIB_etherStatsFragments] +
++                                cnt[RTL8365MB_MIB_etherStatsJabbers];
++      stats->rx_crc_errors = cnt[RTL8365MB_MIB_dot3StatsFCSErrors];
++      stats->rx_errors = stats->rx_length_errors + stats->rx_crc_errors;
++
++      stats->tx_aborted_errors = cnt[RTL8365MB_MIB_ifOutDiscards];
++      stats->tx_window_errors = cnt[RTL8365MB_MIB_dot3StatsLateCollisions];
++      stats->tx_errors = stats->tx_aborted_errors + stats->tx_window_errors;
++
++      spin_unlock(&mb->ports[port].stats_lock);
++}
++
++static void rtl8365mb_stats_poll(struct work_struct *work)
++{
++      struct rtl8365mb_port *p = container_of(to_delayed_work(work),
++                                              struct rtl8365mb_port,
++                                              mib_work);
++      struct realtek_priv *priv = p->priv;
++
++      rtl8365mb_stats_update(priv, p->index);
++
++      schedule_delayed_work(&p->mib_work, RTL8365MB_STATS_INTERVAL_JIFFIES);
++}
++
++static void rtl8365mb_get_stats64(struct dsa_switch *ds, int port,
++                                struct rtnl_link_stats64 *s)
++{
++      struct realtek_priv *priv = ds->priv;
++      struct rtl8365mb_port *p;
++      struct rtl8365mb *mb;
++
++      mb = priv->chip_data;
++      p = &mb->ports[port];
++
++      spin_lock(&p->stats_lock);
++      memcpy(s, &p->stats, sizeof(*s));
++      spin_unlock(&p->stats_lock);
++}
++
++static void rtl8365mb_stats_setup(struct realtek_priv *priv)
++{
++      struct rtl8365mb *mb = priv->chip_data;
++      struct dsa_switch *ds = &priv->ds;
++      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);
++
++      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);
++
++              /* This work polls the MIB counters and keeps the stats64 data
++               * up-to-date.
++               */
++              INIT_DELAYED_WORK(&p->mib_work, rtl8365mb_stats_poll);
++      }
++}
++
++static void rtl8365mb_stats_teardown(struct realtek_priv *priv)
++{
++      struct rtl8365mb *mb = priv->chip_data;
++      struct dsa_switch *ds = &priv->ds;
++      struct dsa_port *dp;
++
++      dsa_switch_for_each_available_port(dp, ds) {
++              struct rtl8365mb_port *p = &mb->ports[dp->index];
++
++              cancel_delayed_work_sync(&p->mib_work);
++      }
++}
++
++static int rtl8365mb_get_and_clear_status_reg(struct realtek_priv *priv, u32 reg,
++                                            u32 *val)
++{
++      int ret;
++
++      ret = regmap_read(priv->map, reg, val);
++      if (ret)
++              return ret;
++
++      return regmap_write(priv->map, reg, *val);
++}
++
++static irqreturn_t rtl8365mb_irq(int irq, void *data)
++{
++      struct realtek_priv *priv = data;
++      unsigned long line_changes = 0;
++      u32 stat;
++      int line;
++      int ret;
++
++      ret = rtl8365mb_get_and_clear_status_reg(priv, RTL8365MB_INTR_STATUS_REG,
++                                               &stat);
++      if (ret)
++              goto out_error;
++
++      if (stat & RTL8365MB_INTR_LINK_CHANGE_MASK) {
++              u32 linkdown_ind;
++              u32 linkup_ind;
++              u32 val;
++
++              ret = rtl8365mb_get_and_clear_status_reg(
++                      priv, RTL8365MB_PORT_LINKUP_IND_REG, &val);
++              if (ret)
++                      goto out_error;
++
++              linkup_ind = FIELD_GET(RTL8365MB_PORT_LINKUP_IND_MASK, val);
++
++              ret = rtl8365mb_get_and_clear_status_reg(
++                      priv, RTL8365MB_PORT_LINKDOWN_IND_REG, &val);
++              if (ret)
++                      goto out_error;
++
++              linkdown_ind = FIELD_GET(RTL8365MB_PORT_LINKDOWN_IND_MASK, val);
++
++              line_changes = linkup_ind | linkdown_ind;
++      }
++
++      if (!line_changes)
++              goto out_none;
++
++      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);
++      }
++
++      return IRQ_HANDLED;
++
++out_error:
++      dev_err(priv->dev, "failed to read interrupt status: %pe\n",
++              ERR_PTR(ret));
++
++out_none:
++      return IRQ_NONE;
++}
++
++static struct irq_chip rtl8365mb_irq_chip = {
++      .name = "rtl8365mb",
++      /* The hardware doesn't support masking IRQs on a per-port basis */
++};
++
++static int rtl8365mb_irq_map(struct irq_domain *domain, unsigned int irq,
++                           irq_hw_number_t hwirq)
++{
++      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;
++}
++
++static void rtl8365mb_irq_unmap(struct irq_domain *d, unsigned int irq)
++{
++      irq_set_nested_thread(irq, 0);
++      irq_set_chip_and_handler(irq, NULL, NULL);
++      irq_set_chip_data(irq, NULL);
++}
++
++static const struct irq_domain_ops rtl8365mb_irqdomain_ops = {
++      .map = rtl8365mb_irq_map,
++      .unmap = rtl8365mb_irq_unmap,
++      .xlate = irq_domain_xlate_onecell,
++};
++
++static int rtl8365mb_set_irq_enable(struct realtek_priv *priv, bool enable)
++{
++      return regmap_update_bits(priv->map, RTL8365MB_INTR_CTRL_REG,
++                                RTL8365MB_INTR_LINK_CHANGE_MASK,
++                                FIELD_PREP(RTL8365MB_INTR_LINK_CHANGE_MASK,
++                                           enable ? 1 : 0));
++}
++
++static int rtl8365mb_irq_enable(struct realtek_priv *priv)
++{
++      return rtl8365mb_set_irq_enable(priv, true);
++}
++
++static int rtl8365mb_irq_disable(struct realtek_priv *priv)
++{
++      return rtl8365mb_set_irq_enable(priv, false);
++}
++
++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;
++
++      intc = of_get_child_by_name(priv->dev->of_node, "interrupt-controller");
++      if (!intc) {
++              dev_err(priv->dev, "missing child interrupt-controller node\n");
++              return -EINVAL;
++      }
++
++      /* rtl8365mb IRQs cascade off this one */
++      irq = of_irq_get(intc, 0);
++      if (irq <= 0) {
++              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;
++      }
++
++      /* 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) {
++              dev_err(priv->dev, "failed to add irq domain\n");
++              ret = -ENOMEM;
++              goto out_put_node;
++      }
++
++      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");
++                      ret = -EINVAL;
++                      goto out_remove_irqdomain;
++              }
++
++              irq_set_parent(virq, irq);
++      }
++
++      /* Configure chip interrupt signal polarity */
++      irq_trig = irq_get_trigger_type(irq);
++      switch (irq_trig) {
++      case IRQF_TRIGGER_RISING:
++      case IRQF_TRIGGER_HIGH:
++              val = RTL8365MB_INTR_POLARITY_HIGH;
++              break;
++      case IRQF_TRIGGER_FALLING:
++      case IRQF_TRIGGER_LOW:
++              val = RTL8365MB_INTR_POLARITY_LOW;
++              break;
++      default:
++              dev_err(priv->dev, "unsupported irq trigger type %u\n",
++                      irq_trig);
++              ret = -EINVAL;
++              goto out_remove_irqdomain;
++      }
++
++      ret = regmap_update_bits(priv->map, RTL8365MB_INTR_POLARITY_REG,
++                               RTL8365MB_INTR_POLARITY_MASK,
++                               FIELD_PREP(RTL8365MB_INTR_POLARITY_MASK, val));
++      if (ret)
++              goto out_remove_irqdomain;
++
++      /* Disable the interrupt in case the chip has it enabled on reset */
++      ret = rtl8365mb_irq_disable(priv);
++      if (ret)
++              goto out_remove_irqdomain;
++
++      /* Clear the interrupt status register */
++      ret = regmap_write(priv->map, RTL8365MB_INTR_STATUS_REG,
++                         RTL8365MB_INTR_ALL_MASK);
++      if (ret)
++              goto out_remove_irqdomain;
++
++      ret = request_threaded_irq(irq, NULL, rtl8365mb_irq, IRQF_ONESHOT,
++                                 "rtl8365mb", priv);
++      if (ret) {
++              dev_err(priv->dev, "failed to request irq: %pe\n",
++                      ERR_PTR(ret));
++              goto out_remove_irqdomain;
++      }
++
++      ret = rtl8365mb_irq_enable(priv);
++      if (ret)
++              goto out_free_irq;
++
++      of_node_put(intc);
++
++      return 0;
++
++out_free_irq:
++      free_irq(mb->irq, priv);
++
++out_remove_irqdomain:
++      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;
++}
++
++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;
++
++      if (mb->irq) {
++              free_irq(mb->irq, priv);
++              mb->irq = 0;
++      }
++
++      if (priv->irqdomain) {
++              /* 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);
++              priv->irqdomain = NULL;
++      }
++}
++
++static int rtl8365mb_cpu_config(struct realtek_priv *priv)
++{
++      struct rtl8365mb *mb = priv->chip_data;
++      struct rtl8365mb_cpu *cpu = &mb->cpu;
++      u32 val;
++      int ret;
++
++      ret = regmap_update_bits(priv->map, RTL8365MB_CPU_PORT_MASK_REG,
++                               RTL8365MB_CPU_PORT_MASK_MASK,
++                               FIELD_PREP(RTL8365MB_CPU_PORT_MASK_MASK,
++                                          cpu->mask));
++      if (ret)
++              return ret;
++
++      val = FIELD_PREP(RTL8365MB_CPU_CTRL_EN_MASK, cpu->enable ? 1 : 0) |
++            FIELD_PREP(RTL8365MB_CPU_CTRL_INSERTMODE_MASK, cpu->insert) |
++            FIELD_PREP(RTL8365MB_CPU_CTRL_TAG_POSITION_MASK, cpu->position) |
++            FIELD_PREP(RTL8365MB_CPU_CTRL_RXBYTECOUNT_MASK, cpu->rx_length) |
++            FIELD_PREP(RTL8365MB_CPU_CTRL_TAG_FORMAT_MASK, cpu->format) |
++            FIELD_PREP(RTL8365MB_CPU_CTRL_TRAP_PORT_MASK, cpu->trap_port & 0x7) |
++            FIELD_PREP(RTL8365MB_CPU_CTRL_TRAP_PORT_EXT_MASK,
++                       cpu->trap_port >> 3 & 0x1);
++      ret = regmap_write(priv->map, RTL8365MB_CPU_CTRL_REG, val);
++      if (ret)
++              return ret;
++
++      return 0;
++}
++
++static int rtl8365mb_change_tag_protocol(struct dsa_switch *ds,
++                                       enum dsa_tag_protocol proto)
++{
++      struct realtek_priv *priv = ds->priv;
++      struct rtl8365mb_cpu *cpu;
++      struct rtl8365mb *mb;
++
++      mb = priv->chip_data;
++      cpu = &mb->cpu;
++
++      switch (proto) {
++      case DSA_TAG_PROTO_RTL8_4:
++              cpu->format = RTL8365MB_CPU_FORMAT_8BYTES;
++              cpu->position = RTL8365MB_CPU_POS_AFTER_SA;
++              break;
++      case DSA_TAG_PROTO_RTL8_4T:
++              cpu->format = RTL8365MB_CPU_FORMAT_8BYTES;
++              cpu->position = RTL8365MB_CPU_POS_BEFORE_CRC;
++              break;
++      /* The switch also supports a 4-byte format, similar to rtl4a but with
++       * the same 0x04 8-bit version and probably 8-bit port source/dest.
++       * There is no public doc about it. Not supported yet and it will probably
++       * never be.
++       */
++      default:
++              return -EPROTONOSUPPORT;
++      }
++
++      return rtl8365mb_cpu_config(priv);
++}
++
++static int rtl8365mb_switch_init(struct realtek_priv *priv)
++{
++      struct rtl8365mb *mb = priv->chip_data;
++      const struct rtl8365mb_chip_info *ci;
++      int ret;
++      int i;
++
++      ci = mb->chip_info;
++
++      /* Do any chip-specific init jam before getting to the common stuff */
++      if (ci->jam_table) {
++              for (i = 0; i < ci->jam_size; i++) {
++                      ret = regmap_write(priv->map, ci->jam_table[i].reg,
++                                         ci->jam_table[i].val);
++                      if (ret)
++                              return ret;
++              }
++      }
++
++      /* Common init jam */
++      for (i = 0; i < ARRAY_SIZE(rtl8365mb_init_jam_common); i++) {
++              ret = regmap_write(priv->map, rtl8365mb_init_jam_common[i].reg,
++                                 rtl8365mb_init_jam_common[i].val);
++              if (ret)
++                      return ret;
++      }
++
++      return 0;
++}
++
++static int rtl8365mb_reset_chip(struct realtek_priv *priv)
++{
++      u32 val;
++
++      priv->write_reg_noack(priv, RTL8365MB_CHIP_RESET_REG,
++                            FIELD_PREP(RTL8365MB_CHIP_RESET_HW_MASK, 1));
++
++      /* Realtek documentation says the chip needs 1 second to reset. Sleep
++       * for 100 ms before accessing any registers to prevent ACK timeouts.
++       */
++      msleep(100);
++      return regmap_read_poll_timeout(priv->map, RTL8365MB_CHIP_RESET_REG, val,
++                                      !(val & RTL8365MB_CHIP_RESET_HW_MASK),
++                                      20000, 1e6);
++}
++
++static int rtl8365mb_setup(struct dsa_switch *ds)
++{
++      struct realtek_priv *priv = ds->priv;
++      struct rtl8365mb_cpu *cpu;
++      u32 downports_mask = 0;
++      u32 upports_mask = 0;
++      struct rtl8365mb *mb;
++      struct dsa_port *dp;
++      int ret;
++
++      mb = priv->chip_data;
++      cpu = &mb->cpu;
++
++      ret = rtl8365mb_reset_chip(priv);
++      if (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: %pe\n",
++                      ERR_PTR(ret));
++              goto out_error;
++      }
++
++      /* Set up cascading IRQs */
++      ret = rtl8365mb_irq_setup(priv);
++      if (ret == -EPROBE_DEFER)
++              return ret;
++      else if (ret)
++              dev_info(priv->dev, "no interrupt support\n");
++
++      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_port_is_dsa(dp)) {
++                      dev_err(priv->dev, "Cascading (DSA link) not supported\n");
++                      ret = -EOPNOTSUPP;
++                      goto out_teardown_irq;
++              }
++      }
++
++      /* Start with all ports blocked, including unused ports */
++      dsa_switch_for_each_port(dp, ds) {
++              struct rtl8365mb_port *p = &mb->ports[dp->index];
++
++              /* 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);
++
++              /* Start with all port completely isolated */
++              ret = rtl8365mb_port_set_isolation(priv, dp->index, 0);
++              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;
++
++              /* 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);
++      }
++
++      /* 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, dp->index,
++                                                 upports_mask);
++              if (ret)
++                      goto out_teardown_irq;
++
++              /* 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;
++      }
++
++      cpu->mask = upports_mask;
++      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;
++
++      ret = rtl8365mb_port_change_mtu(ds, cpu->trap_port, ETH_DATA_LEN);
++      if (ret)
++              goto out_teardown_irq;
++
++      ret = rtl83xx_setup_user_mdio(ds);
++      if (ret) {
++              dev_err(priv->dev, "could not set up MDIO bus\n");
++              goto out_teardown_irq;
++      }
++
++      /* Start statistics counter polling */
++      rtl8365mb_stats_setup(priv);
++
++      return 0;
++
++out_teardown_irq:
++      rtl8365mb_irq_teardown(priv);
++
++out_error:
++      return ret;
++}
++
++static void rtl8365mb_teardown(struct dsa_switch *ds)
++{
++      struct realtek_priv *priv = ds->priv;
++
++      rtl8365mb_stats_teardown(priv);
++      rtl8365mb_irq_teardown(priv);
++}
++
++static int rtl8365mb_get_chip_id_and_ver(struct regmap *map, u32 *id, u32 *ver)
++{
++      int ret;
++
++      /* For some reason we have to write a magic value to an arbitrary
++       * register whenever accessing the chip ID/version registers.
++       */
++      ret = regmap_write(map, RTL8365MB_MAGIC_REG, RTL8365MB_MAGIC_VALUE);
++      if (ret)
++              return ret;
++
++      ret = regmap_read(map, RTL8365MB_CHIP_ID_REG, id);
++      if (ret)
++              return ret;
++
++      ret = regmap_read(map, RTL8365MB_CHIP_VER_REG, ver);
++      if (ret)
++              return ret;
++
++      /* Reset magic register */
++      ret = regmap_write(map, RTL8365MB_MAGIC_REG, 0);
++      if (ret)
++              return ret;
++
++      return 0;
++}
++
++static int rtl8365mb_detect(struct realtek_priv *priv)
++{
++      struct rtl8365mb *mb = priv->chip_data;
++      u32 chip_id;
++      u32 chip_ver;
++      int ret;
++      int i;
++
++      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: %pe\n",
++                      ERR_PTR(ret));
++              return ret;
++      }
++
++      for (i = 0; i < ARRAY_SIZE(rtl8365mb_chip_infos); i++) {
++              const struct rtl8365mb_chip_info *ci = &rtl8365mb_chip_infos[i];
++
++              if (ci->chip_id == chip_id && ci->chip_ver == chip_ver) {
++                      mb->chip_info = ci;
++                      break;
++              }
++      }
++
++      if (!mb->chip_info) {
++              dev_err(priv->dev,
++                      "unrecognized switch (id=0x%04x, ver=0x%04x)", chip_id,
++                      chip_ver);
++              return -ENODEV;
++      }
++
++      dev_info(priv->dev, "found an %s switch\n", mb->chip_info->name);
++
++      priv->num_ports = RTL8365MB_MAX_NUM_PORTS;
++      mb->priv = priv;
++      mb->cpu.trap_port = RTL8365MB_MAX_NUM_PORTS;
++      mb->cpu.insert = RTL8365MB_CPU_INSERT_TO_ALL;
++      mb->cpu.position = RTL8365MB_CPU_POS_AFTER_SA;
++      mb->cpu.rx_length = RTL8365MB_CPU_RXLEN_64BYTES;
++      mb->cpu.format = RTL8365MB_CPU_FORMAT_8BYTES;
++
++      return 0;
++}
++
++static const struct phylink_mac_ops rtl8365mb_phylink_mac_ops = {
++      .mac_config = rtl8365mb_phylink_mac_config,
++      .mac_link_down = rtl8365mb_phylink_mac_link_down,
++      .mac_link_up = rtl8365mb_phylink_mac_link_up,
++};
++
++static const struct dsa_switch_ops rtl8365mb_switch_ops = {
++      .get_tag_protocol = rtl8365mb_get_tag_protocol,
++      .change_tag_protocol = rtl8365mb_change_tag_protocol,
++      .setup = rtl8365mb_setup,
++      .teardown = rtl8365mb_teardown,
++      .phylink_get_caps = rtl8365mb_phylink_get_caps,
++      .port_stp_state_set = rtl8365mb_port_stp_state_set,
++      .get_strings = rtl8365mb_get_strings,
++      .get_ethtool_stats = rtl8365mb_get_ethtool_stats,
++      .get_sset_count = rtl8365mb_get_sset_count,
++      .get_eth_phy_stats = rtl8365mb_get_phy_stats,
++      .get_eth_mac_stats = rtl8365mb_get_mac_stats,
++      .get_eth_ctrl_stats = rtl8365mb_get_ctrl_stats,
++      .get_stats64 = rtl8365mb_get_stats64,
++      .port_change_mtu = rtl8365mb_port_change_mtu,
++      .port_max_mtu = rtl8365mb_port_max_mtu,
++};
++
++static const struct realtek_ops rtl8365mb_ops = {
++      .detect = rtl8365mb_detect,
++      .phy_read = rtl8365mb_phy_read,
++      .phy_write = rtl8365mb_phy_write,
++};
++
++const struct realtek_variant rtl8365mb_variant = {
++      .ds_ops = &rtl8365mb_switch_ops,
++      .ops = &rtl8365mb_ops,
++      .phylink_mac_ops = &rtl8365mb_phylink_mac_ops,
++      .clk_delay = 10,
++      .cmd_read = 0xb9,
++      .cmd_write = 0xb8,
++      .chip_data_sz = sizeof(struct rtl8365mb),
++};
++
++static const struct of_device_id rtl8365mb_of_match[] = {
++      { .compatible = "realtek,rtl8365mb", .data = &rtl8365mb_variant, },
++      { /* sentinel */ },
++};
++MODULE_DEVICE_TABLE(of, rtl8365mb_of_match);
++
++static struct platform_driver rtl8365mb_smi_driver = {
++      .driver = {
++              .name = "rtl8365mb-smi",
++              .of_match_table = rtl8365mb_of_match,
++      },
++      .probe  = realtek_smi_probe,
++      .remove = realtek_smi_remove,
++      .shutdown = realtek_smi_shutdown,
++};
++
++static struct mdio_driver rtl8365mb_mdio_driver = {
++      .mdiodrv.driver = {
++              .name = "rtl8365mb-mdio",
++              .of_match_table = rtl8365mb_of_match,
++      },
++      .probe  = realtek_mdio_probe,
++      .remove = realtek_mdio_remove,
++      .shutdown = realtek_mdio_shutdown,
++};
++
++static int rtl8365mb_init(void)
++{
++      int ret;
++
++      ret = realtek_mdio_driver_register(&rtl8365mb_mdio_driver);
++      if (ret)
++              return ret;
++
++      ret = realtek_smi_driver_register(&rtl8365mb_smi_driver);
++      if (ret) {
++              realtek_mdio_driver_unregister(&rtl8365mb_mdio_driver);
++              return ret;
++      }
++
++      return 0;
++}
++module_init(rtl8365mb_init);
++
++static void __exit rtl8365mb_exit(void)
++{
++      realtek_smi_driver_unregister(&rtl8365mb_smi_driver);
++      realtek_mdio_driver_unregister(&rtl8365mb_mdio_driver);
++}
++module_exit(rtl8365mb_exit);
++
++MODULE_AUTHOR("Alvin Šipraga <alsi@bang-olufsen.dk>");
++MODULE_DESCRIPTION("Driver for RTL8365MB-VC ethernet switch");
++MODULE_LICENSE("GPL");
++MODULE_IMPORT_NS("REALTEK_DSA");
diff --git a/target/linux/generic/backport-6.18/942-05-7.2-net-dsa-realtek-rtl8365mb-add-table-lookup-interface.patch b/target/linux/generic/backport-6.18/942-05-7.2-net-dsa-realtek-rtl8365mb-add-table-lookup-interface.patch
new file mode 100644 (file)
index 0000000..1239c79
--- /dev/null
@@ -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 */
diff --git a/target/linux/generic/backport-6.18/942-06-7.2-net-dsa-realtek-rtl8365mb-add-VLAN-support.patch b/target/linux/generic/backport-6.18/942-06-7.2-net-dsa-realtek-rtl8365mb-add-VLAN-support.patch
new file mode 100644 (file)
index 0000000..c9edb77
--- /dev/null
@@ -0,0 +1,1461 @@
+From 9da2c8672f77108a1f09232320f22225ab53dde9 Mon Sep 17 00:00:00 2001
+From: Alvin Šipraga <alsi@bang-olufsen.dk>
+Date: Sat, 6 Jun 2026 05:29:30 -0300
+Subject: net: dsa: realtek: rtl8365mb: add VLAN support
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+Realtek RTL8365MB switches (a.k.a. RTL8367C family) use two different
+structures for VLANs:
+
+- VLAN4K: A full table with 4096 entries defining port membership and
+  tagging.
+- VLANMC: A smaller table with 32 entries used primarily for PVID
+  assignment.
+
+In this hardware, a port's PVID must point to an index in the VLANMC
+table rather than a VID directly. Since the VLANMC table is limited to
+32 entries, the driver implements a dynamic allocation scheme to
+maximize resource usage:
+
+- VLAN4K is treated by the driver as the source of truth for membership.
+- A VLANMC entry is only allocated when a port is configured to use a
+  specific VID as its PVID.
+- VLANMC entries are deleted when no longer needed as a PVID by any port.
+
+Although VLANMC has a members field, the switch only checks membership
+in the VLAN4K table. This driver will use VLANMC members field as way to
+track which ports are using that entry as PVID.
+
+VLANMC index 0, although a valid entry, is reserved in this driver as a
+neutral PVID value for ports not using a specific PVID.
+
+In the subsequent RTL8367D switch family, VLANMC table was
+removed and PVID assignment was delegated to a dedicated set of
+registers.
+
+The use of FIELD_PREP for reconstructing LO/HI values was suggested by
+Yury Norov.
+
+Fix for vlan_setup and vlan_filtering was suggested by Abdulkader
+Alrezej.
+
+Suggested-by: Yury Norov <ynorov@nvidia.com>
+Suggested-by: Abdulkader Alrezej <abdulkader.alrezej@gmail.com>
+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-6-b9e409687cbe@gmail.com
+Signed-off-by: Jakub Kicinski <kuba@kernel.org>
+---
+ drivers/net/dsa/realtek/Makefile         |   1 +
+ drivers/net/dsa/realtek/realtek.h        |   5 +
+ drivers/net/dsa/realtek/rtl8365mb_main.c | 342 +++++++++++
+ drivers/net/dsa/realtek/rtl8365mb_vlan.c | 944 +++++++++++++++++++++++++++++++
+ drivers/net/dsa/realtek/rtl8365mb_vlan.h |  39 ++
+ drivers/net/dsa/realtek/rtl83xx.c        |   1 +
+ 6 files changed, 1332 insertions(+)
+
+--- a/drivers/net/dsa/realtek/Makefile
++++ b/drivers/net/dsa/realtek/Makefile
+@@ -18,4 +18,5 @@ endif
+ obj-$(CONFIG_NET_DSA_REALTEK_RTL8365MB) += rtl8365mb.o
+ rtl8365mb-objs := rtl8365mb_main.o \
+                 rtl8365mb_table.o \
++                rtl8365mb_vlan.o \
+ # end of rtl8365mb-objs
+--- a/drivers/net/dsa/realtek/realtek.h
++++ b/drivers/net/dsa/realtek/realtek.h
+@@ -54,6 +54,11 @@ struct realtek_priv {
+       struct regmap           *map;
+       struct regmap           *map_nolock;
+       struct mutex            map_lock;
++      /* vlan_lock protects against concurrent Read-Modify-Write operations
++       * on the global VLAN 4K and VLANMC tables, such as when adding or
++       * deleting port VLAN memberships and PVID configurations.
++       */
++      struct mutex            vlan_lock;
+       struct mii_bus          *user_mii_bus;
+       struct mii_bus          *bus;
+       int                     mdio_addr;
+--- a/drivers/net/dsa/realtek/rtl8365mb_main.c
++++ b/drivers/net/dsa/realtek/rtl8365mb_main.c
+@@ -104,6 +104,7 @@
+ #include "realtek-smi.h"
+ #include "realtek-mdio.h"
+ #include "rtl83xx.h"
++#include "rtl8365mb_vlan.h"
+ /* Family-specific data and limits */
+ #define RTL8365MB_PHYADDRMAX          7
+@@ -292,6 +293,57 @@
+ #define   RTL8365MB_MSTI_CTRL_PORT_STATE_MASK(_physport) \
+               (0x3 << RTL8365MB_MSTI_CTRL_PORT_STATE_OFFSET((_physport)))
++/* Miscellaneous port configuration register, incl. VLAN egress mode */
++#define RTL8365MB_PORT_MISC_CFG_REG_BASE                      0x000E
++#define RTL8365MB_PORT_MISC_CFG_REG(_p) \
++              (RTL8365MB_PORT_MISC_CFG_REG_BASE + ((_p) << 5))
++#define   RTL8365MB_PORT_MISC_CFG_SMALL_TAG_IPG_MASK          0x8000
++#define   RTL8365MB_PORT_MISC_CFG_TX_ITFSP_MODE_MASK          0x4000
++#define   RTL8365MB_PORT_MISC_CFG_FLOWCTRL_INDEP_MASK         0x2000
++#define   RTL8365MB_PORT_MISC_CFG_DOT1Q_REMARK_ENABLE_MASK    0x1000
++#define   RTL8365MB_PORT_MISC_CFG_INGRESSBW_FLOWCTRL_MASK     0x0800
++#define   RTL8365MB_PORT_MISC_CFG_INGRESSBW_IFG_MASK          0x0400
++#define   RTL8365MB_PORT_MISC_CFG_RX_SPC_MASK                 0x0200
++#define   RTL8365MB_PORT_MISC_CFG_CRC_SKIP_MASK                       0x0100
++#define   RTL8365MB_PORT_MISC_CFG_PKTGEN_TX_FIRST_MASK                0x0080
++#define   RTL8365MB_PORT_MISC_CFG_MAC_LOOPBACK_MASK           0x0040
++/* See &rtl8365mb_vlan_egress_mode */
++#define   RTL8365MB_PORT_MISC_CFG_VLAN_EGRESS_MODE_MASK               0x0030
++#define   RTL8365MB_PORT_MISC_CFG_CONGESTION_SUSTAIN_TIME_MASK        0x000F
++
++/**
++ * enum rtl8365mb_vlan_egress_mode - port VLAN egress mode
++ * @RTL8365MB_VLAN_EGRESS_MODE_ORIGINAL: follow untag mask in VLAN4k table entry
++ * @RTL8365MB_VLAN_EGRESS_MODE_KEEP: the VLAN tag format of egressed packets
++ * will remain the same as their ingressed format, but the priority and VID
++ * fields may be altered
++ * @RTL8365MB_VLAN_EGRESS_MODE_PRI_TAG: always egress with priority tag
++ * @RTL8365MB_VLAN_EGRESS_MODE_REAL_KEEP: the VLAN tag format of egressed
++ * packets will remain the same as their ingressed format, and neither the
++ * priority nor VID fields can be altered
++ */
++enum rtl8365mb_vlan_egress_mode {
++      RTL8365MB_VLAN_EGRESS_MODE_ORIGINAL = 0,
++      RTL8365MB_VLAN_EGRESS_MODE_KEEP = 1,
++      RTL8365MB_VLAN_EGRESS_MODE_PRI_TAG = 2,
++      RTL8365MB_VLAN_EGRESS_MODE_REAL_KEEP = 3,
++};
++
++/* VLAN control register */
++#define RTL8365MB_VLAN_CTRL_REG                       0x07A8
++#define   RTL8365MB_VLAN_CTRL_EN_MASK         0x0001
++
++/* VLAN ingress filter register */
++#define RTL8365MB_VLAN_INGRESS_REG                            0x07A9
++#define   RTL8365MB_VLAN_INGRESS_MASK                         GENMASK(10, 0)
++#define   RTL8365MB_VLAN_INGRESS_FILTER_PORT_EN_OFFSET(_p)    (_p)
++#define   RTL8365MB_VLAN_INGRESS_FILTER_PORT_EN_MASK(_p)      BIT(_p)
++
++/* VLAN "transparent" setting registers */
++#define RTL8365MB_VLAN_EGRESS_TRANSPARENT_REG_BASE    0x09D0
++#define RTL8365MB_VLAN_EGRESS_TRANSPARENT_REG(_p) \
++              (RTL8365MB_VLAN_EGRESS_TRANSPARENT_REG_BASE + (_p))
++
+ /* MIB counter value registers */
+ #define RTL8365MB_MIB_COUNTER_BASE    0x1000
+ #define RTL8365MB_MIB_COUNTER_REG(_x) (RTL8365MB_MIB_COUNTER_BASE + (_x))
+@@ -1210,6 +1262,286 @@ static void rtl8365mb_port_stp_state_set
+                          val << RTL8365MB_MSTI_CTRL_PORT_STATE_OFFSET(port));
+ }
++static int rtl8365mb_port_set_transparent(struct realtek_priv *priv,
++                                        int igr_port, int egr_port,
++                                        bool enable)
++{
++      dev_dbg(priv->dev, "%s transparent VLAN from %d to %d\n",
++              enable ? "Enable" : "Disable", igr_port, egr_port);
++
++      /* "Transparent" between the two ports means that packets forwarded by
++       * igr_port and egressed on egr_port will not be filtered by the usual
++       * VLAN membership settings.
++       */
++      return regmap_update_bits(priv->map,
++                      RTL8365MB_VLAN_EGRESS_TRANSPARENT_REG(egr_port),
++                      BIT(igr_port), enable ? BIT(igr_port) : 0);
++}
++
++static int rtl8365mb_port_set_ingress_filtering(struct realtek_priv *priv,
++                                              int port, bool enable)
++{
++      /* Ingress filtering enabled: Discard VLAN-tagged frames if the port is
++       * not a member of the VLAN with which the packet is associated.
++       * Untagged packets will also be discarded unless the port has a PVID
++       * programmed. Priority-tagged frames are treated as untagged frames.
++       *
++       * Ingress filtering disabled: Accept all tagged and untagged frames.
++       */
++      return regmap_update_bits(priv->map, RTL8365MB_VLAN_INGRESS_REG,
++                      RTL8365MB_VLAN_INGRESS_FILTER_PORT_EN_MASK(port),
++                      enable ?
++                      RTL8365MB_VLAN_INGRESS_FILTER_PORT_EN_MASK(port) :
++                      0);
++}
++
++static int
++rtl8365mb_port_set_vlan_egress_mode(struct realtek_priv *priv, int port,
++                                  enum rtl8365mb_vlan_egress_mode mode)
++{
++      u32 val;
++
++      val = FIELD_PREP(RTL8365MB_PORT_MISC_CFG_VLAN_EGRESS_MODE_MASK, mode);
++      return regmap_update_bits(priv->map,
++                      RTL8365MB_PORT_MISC_CFG_REG(port),
++                      RTL8365MB_PORT_MISC_CFG_VLAN_EGRESS_MODE_MASK, val);
++}
++
++static int rtl8365mb_port_vlan_filtering(struct dsa_switch *ds, int port,
++                                       bool vlan_filtering,
++                                       struct netlink_ext_ack *extack)
++{
++      enum rtl8365mb_frame_ingress accepted_frame, prev_accepted_frame;
++      enum rtl8365mb_vlan_egress_mode mode;
++      struct realtek_priv *priv = ds->priv;
++      u32 configured_ports = 0;
++      struct dsa_port *dp;
++      u16 pvid_vid;
++      int ret;
++
++      dev_dbg(priv->dev, "port %d: %s VLAN filtering\n", port,
++              vlan_filtering ? "enable" : "disable");
++
++      ret = rtl8365mb_vlan_port_get_framefilter(priv, port,
++                                                &prev_accepted_frame);
++      if (ret) {
++              NL_SET_ERR_MSG_MOD(extack,
++                                 "Failed to get current framefilter");
++              return ret;
++      }
++
++      /* While filtering, only accepts untagged frames if PVID is enabled */
++      if (vlan_filtering) {
++              ret = rtl8365mb_vlan_port_get_pvid(priv, port, &pvid_vid);
++              if (ret)
++                      return ret;
++
++              if (pvid_vid)
++                      accepted_frame = RTL8365MB_FRAME_TYPE_ANY_FRAME;
++              else
++                      accepted_frame = RTL8365MB_FRAME_TYPE_TAGGED_ONLY;
++      } else {
++              accepted_frame = RTL8365MB_FRAME_TYPE_ANY_FRAME;
++      }
++
++      /* When vlan filter is enable/disabled in a bridge, this function is
++       * called for all member ports. We need to enable/disable ingress
++       * VLAN membership check.
++       */
++      ret = rtl8365mb_port_set_ingress_filtering(priv, port, vlan_filtering);
++      if (ret)
++              return ret;
++
++      /* However, we also enable/disable egress filtering because the switch
++       * still consider the egress interface VLAN membership to forward the
++       * traffic. We enable/disable that check disabling/enabling transparent
++       * VLAN between the ingress port and all other available ports.
++       */
++      dsa_switch_for_each_available_port(dp, ds) {
++              /* port isolation will still keep traffic inside the bridge */
++              ret = rtl8365mb_port_set_transparent(priv, port, dp->index,
++                                                   !vlan_filtering);
++              if (ret)
++                      goto undo_transparent;
++
++              configured_ports |= BIT(dp->index);
++      }
++
++      if (accepted_frame != prev_accepted_frame) {
++              ret = rtl8365mb_vlan_port_set_framefilter(priv, port,
++                                                        accepted_frame);
++              if (ret) {
++                      NL_SET_ERR_MSG_MOD(extack,
++                                         "Failed to set port framefilter");
++                      goto undo_transparent;
++              }
++      }
++
++      /* When VLAN filtering is disabled, preserve frames exactly as received.
++       * Otherwise, the VLAN egress pipeline may still alter tag state
++       * according to VLAN membership and untag configuration.
++       */
++      if (vlan_filtering)
++              mode = RTL8365MB_VLAN_EGRESS_MODE_ORIGINAL;
++      else
++              mode = RTL8365MB_VLAN_EGRESS_MODE_REAL_KEEP;
++
++      ret = rtl8365mb_port_set_vlan_egress_mode(priv, port, mode);
++      if (ret)
++              goto undo_set_framefilter;
++
++      return ret;
++
++undo_set_framefilter:
++      if (prev_accepted_frame != accepted_frame)
++              rtl8365mb_vlan_port_set_framefilter(priv, port,
++                                                  prev_accepted_frame);
++undo_transparent:
++      /* The DSA core guarantees this callback is only invoked on an actual
++       * state transition, ensuring the previous hardware state was the
++       * opposite (!vlan_filtering). It is also called during setup but, in
++       * that case, any failure here aborts the entire switch initialization.
++       *
++       * VLAN_INGRESS and VLAN_EGRESS_TRANSPARENT states are directly derived
++       * from vlan_filtering. That way, we can simply undo it without
++       * checking the current HW state as we do with VLAN_EGRESS_MODE.
++       */
++      dsa_switch_for_each_port(dp, ds) {
++              if (configured_ports & BIT(dp->index))
++                      rtl8365mb_port_set_transparent(priv, port, dp->index,
++                                                     vlan_filtering);
++      }
++
++      rtl8365mb_port_set_ingress_filtering(priv, port, !vlan_filtering);
++
++      return ret;
++}
++
++static int rtl8365mb_port_vlan_add(struct dsa_switch *ds, int port,
++                                 const struct switchdev_obj_port_vlan *vlan,
++                                 struct netlink_ext_ack *extack)
++{
++      bool untagged = !!(vlan->flags & BRIDGE_VLAN_INFO_UNTAGGED);
++      bool pvid = !!(vlan->flags & BRIDGE_VLAN_INFO_PVID);
++      u16 pvid_vid;
++      struct realtek_priv *priv = ds->priv;
++      int ret;
++
++      dev_dbg(priv->dev, "add VLAN %d on port %d, %s, %s\n",
++              vlan->vid, port, untagged ? "untagged" : "tagged",
++              pvid ? "PVID" : "no PVID");
++
++      /* VID == 0 is reserved in this driver */
++      if (vlan->vid == 0) {
++              NL_SET_ERR_MSG_MOD(extack,
++                                 "VLAN 0 is reserved by this driver");
++              return -EOPNOTSUPP;
++      }
++
++      mutex_lock(&priv->vlan_lock);
++
++      ret = rtl8365mb_vlan_port_get_pvid(priv, port, &pvid_vid);
++      if (ret)
++              goto out_unlock;
++
++      /* Set PVID if needed */
++      if (pvid) {
++              ret = rtl8365mb_vlan_pvid_port_set(ds, port, vlan->vid,
++                                                 extack);
++              if (ret)
++                      goto out_unlock;
++      } else {
++              /* or try to unset it if not */
++              ret = rtl8365mb_vlan_pvid_port_clear(ds, port, vlan->vid);
++              if (ret)
++                      goto out_unlock;
++      }
++
++      /* add port to vlan4k. It knows nothing about PVID */
++      ret = rtl8365mb_vlan_4k_port_add(ds, port, vlan, extack);
++      if (ret)
++              goto undo_set_pvid;
++
++      ret = 0;
++      goto out_unlock;
++
++undo_set_pvid:
++      /* undo the pvid definition */
++      if (pvid != (pvid_vid == vlan->vid)) {
++              if (pvid_vid)
++                      (void)rtl8365mb_vlan_pvid_port_set(ds, port, pvid_vid,
++                                                         NULL);
++              else
++                      (void)rtl8365mb_vlan_pvid_port_clear(ds, port,
++                                                           vlan->vid);
++      }
++out_unlock:
++      mutex_unlock(&priv->vlan_lock);
++      return ret;
++}
++
++static int rtl8365mb_port_vlan_del(struct dsa_switch *ds, int port,
++                                 const struct switchdev_obj_port_vlan *vlan)
++{
++      bool untagged = !!(vlan->flags & BRIDGE_VLAN_INFO_UNTAGGED);
++      bool pvid = !!(vlan->flags & BRIDGE_VLAN_INFO_PVID);
++      struct realtek_priv *priv = ds->priv;
++      int ret;
++
++      dev_dbg(priv->dev, "del VLAN %d on port %d, %s, %s\n",
++              vlan->vid, port, untagged ? "untagged" : "tagged",
++              pvid ? "PVID" : "no PVID");
++
++      /* VID == 0 is reserved in this driver */
++      if (vlan->vid == 0)
++              return -EOPNOTSUPP;
++
++      mutex_lock(&priv->vlan_lock);
++      ret = rtl8365mb_vlan_pvid_port_clear(ds, port, vlan->vid);
++      if (ret)
++              goto out_unlock;
++
++      ret = rtl8365mb_vlan_4k_port_del(ds, port, vlan);
++      /* There is little incentive to try to undo the removal of PVID (if it
++       * was really in use) as an error here might indicate the ASIC stopped
++       * to answer.
++       */
++
++out_unlock:
++      mutex_unlock(&priv->vlan_lock);
++      return ret;
++}
++
++/* VLAN support is always enabled in the switch.
++ *
++ * Standalone forwarding relies on transparent VLAN mode combined with per-port
++ * isolation masks restricting egress to CPU ports only.
++ *
++ */
++static int rtl8365mb_vlan_setup(struct dsa_switch *ds)
++{
++      struct realtek_priv *priv = ds->priv;
++      struct dsa_port *dp;
++      int ret;
++
++      dsa_switch_for_each_available_port(dp, ds) {
++              /* Disable vlan-filtering for all ports */
++              ret = rtl8365mb_port_vlan_filtering(ds, dp->index, false, NULL);
++              if (ret) {
++                      dev_err(priv->dev,
++                              "Failed to disable vlan filtering on port %d\n",
++                              dp->index);
++                      return ret;
++              }
++      }
++
++      /* VLAN is always enabled. */
++      ret = regmap_update_bits(priv->map, RTL8365MB_VLAN_CTRL_REG,
++                               RTL8365MB_VLAN_CTRL_EN_MASK,
++                               FIELD_PREP(RTL8365MB_VLAN_CTRL_EN_MASK, 1));
++      return ret;
++}
++
+ static int rtl8365mb_port_set_learning(struct realtek_priv *priv, int port,
+                                      bool enable)
+ {
+@@ -2100,6 +2432,13 @@ static int rtl8365mb_setup(struct dsa_sw
+       if (ret)
+               goto out_teardown_irq;
++      ds->configure_vlan_while_not_filtering = true;
++
++      /* Set up VLAN */
++      ret = rtl8365mb_vlan_setup(ds);
++      if (ret)
++              goto out_teardown_irq;
++
+       ret = rtl83xx_setup_user_mdio(ds);
+       if (ret) {
+               dev_err(priv->dev, "could not set up MDIO bus\n");
+@@ -2210,6 +2549,9 @@ static const struct dsa_switch_ops rtl83
+       .teardown = rtl8365mb_teardown,
+       .phylink_get_caps = rtl8365mb_phylink_get_caps,
+       .port_stp_state_set = rtl8365mb_port_stp_state_set,
++      .port_vlan_add = rtl8365mb_port_vlan_add,
++      .port_vlan_del = rtl8365mb_port_vlan_del,
++      .port_vlan_filtering = rtl8365mb_port_vlan_filtering,
+       .get_strings = rtl8365mb_get_strings,
+       .get_ethtool_stats = rtl8365mb_get_ethtool_stats,
+       .get_sset_count = rtl8365mb_get_sset_count,
+--- /dev/null
++++ b/drivers/net/dsa/realtek/rtl8365mb_vlan.c
+@@ -0,0 +1,944 @@
++// SPDX-License-Identifier: GPL-2.0
++/* VLAN configuration interface for the rtl8365mb switch family
++ *
++ * Copyright (C) 2022 Alvin Šipraga <alsi@bang-olufsen.dk>
++ *
++ * VLAN configuration takes place in two separate domains of the switch: the
++ * VLAN4k table and the VLAN membership configuration (MC) database. While the
++ * VLAN4k table is exhaustive and can be fully populated with 4096 VLAN
++ * configurations, the same does not hold for the VLAN membership configuration
++ * database, which is limited to 32 entries.
++ *
++ * The switch will normally only use the VLAN4k table when making forwarding
++ * decisions. The VLAN membership configuration database is a vestigial ASIC
++ * design and is only used for a few specific features in the rtl8365mb
++ * family. This means that the limit of 32 entries should not hinder us in
++ * programming a huge number of VLANs into the switch.
++ *
++ * One necessary use of the VLAN membership configuration database is for the
++ * programming of a port-based VLAN ID (PVID). The PVID is programmed on a
++ * per-port basis via register field, which refers to a specific VLAN membership
++ * configuration via an index 0~31. In order to maintain coherent behaviour on a
++ * port with a PVID, it is necessary to keep the VLAN configuration synchronized
++ * between the VLAN4k table and the VLAN membership configuration database.
++ *
++ * Since VLAN membership configs are a scarce resource, it will only be used
++ * when strictly needed (i.e. a VLAN with members using PVID). Otherwise, the
++ * VLAN4k will be enough.
++ *
++ * With some exceptions, the entries in both the VLAN4k table and the VLAN
++ * membership configuration database offer the same configuration options. The
++ * differences are as follows:
++ *
++ * 1. VLAN4k entries can specify whether to use Independent or Shared VLAN
++ *    Learning (IVL or SVL respectively). VLAN membership config entries
++ *    cannot. This underscores the fact that VLAN membership configs are not
++ *    involved in the learning process of the ASIC.
++ *
++ * 2. VLAN membership config entries use an "enhanced VLAN ID" (efid), which has
++ *    a range 0~8191 compared with the standard 0~4095 range of the VLAN4k
++ *    table. This underscores the fact that VLAN membership configs can be used
++ *    to group ports on a layer beyond the standard VLAN configuration, which
++ *    may be useful for ACL rules which specify alternative forwarding
++ *    decisions.
++ *
++ * VLANMC index 0 is reserved as a neutral PVID, used for standalone ports.
++ *
++ */
++
++#include "rtl8365mb_vlan.h"
++#include "rtl8365mb_table.h"
++#include <linux/if_bridge.h>
++#include <linux/lockdep.h>
++#include <linux/regmap.h>
++
++/* CVLAN (i.e. VLAN4k) table entry layout, u16[3] */
++#define RTL8365MB_CVLAN_ENTRY_SIZE                    3 /* 48-bits */
++#define RTL8365MB_CVLAN_ENTRY_D0_MBR_MASK             GENMASK(7, 0)
++#define   RTL8365MB_CVLAN_MBR_LO_MASK                 GENMASK(7, 0)
++#define RTL8365MB_CVLAN_ENTRY_D0_UNTAG_MASK           GENMASK(15, 8)
++#define   RTL8365MB_CVLAN_UNTAG_LO_MASK                       GENMASK(7, 0)
++#define RTL8365MB_CVLAN_ENTRY_D1_FID_MASK             GENMASK(3, 0)
++#define RTL8365MB_CVLAN_ENTRY_D1_VBPEN_MASK           GENMASK(4, 4)
++#define RTL8365MB_CVLAN_ENTRY_D1_VBPRI_MASK           GENMASK(7, 5)
++#define RTL8365MB_CVLAN_ENTRY_D1_ENVLANPOL_MASK               GENMASK(8, 8)
++#define RTL8365MB_CVLAN_ENTRY_D1_METERIDX_MASK                GENMASK(13, 9)
++#define   RTL8365MB_CVLAN_METERIDX_LO_MASK            GENMASK(4, 0)
++#define RTL8365MB_CVLAN_ENTRY_D1_IVL_SVL_MASK         GENMASK(14, 14)
++/* extends RTL8365MB_CVLAN_ENTRY_D0_MBR_MASK */
++#define RTL8365MB_CVLAN_ENTRY_D2_MBR_EXT_MASK         GENMASK(2, 0)
++#define   RTL8365MB_CVLAN_MBR_HI_MASK                 GENMASK(10, 8)
++/* extends RTL8365MB_CVLAN_ENTRY_D0_UNTAG_MASK */
++#define RTL8365MB_CVLAN_ENTRY_D2_UNTAG_EXT_MASK               GENMASK(5, 3)
++#define   RTL8365MB_CVLAN_UNTAG_HI_MASK                       GENMASK(10, 8)
++/* extends RTL8365MB_CVLAN_ENTRY_D1_METERIDX_MASK */
++#define RTL8365MB_CVLAN_ENTRY_D2_METERIDX_EXT_MASK    GENMASK(6, 6)
++#define   RTL8365MB_CVLAN_METERIDX_HI_MASK            GENMASK(5, 5)
++
++/* VLAN member configuration registers 0~31, u16[3] */
++#define RTL8365MB_VLAN_MC_BASE                                0x0728
++#define RTL8365MB_VLAN_MC_ENTRY_SIZE                  4 /* 64-bit */
++#define RTL8365MB_VLAN_MC_REG(index) \
++              (RTL8365MB_VLAN_MC_BASE + \
++               (RTL8365MB_VLAN_MC_ENTRY_SIZE * (index)))
++#define   RTL8365MB_VLAN_MC_D0_MBR_MASK                       GENMASK(10, 0)
++#define   RTL8365MB_VLAN_MC_D1_FID_MASK                       GENMASK(3, 0)
++
++#define   RTL8365MB_VLAN_MC_D2_VBPEN_MASK             GENMASK(0, 0)
++#define   RTL8365MB_VLAN_MC_D2_VBPRI_MASK             GENMASK(3, 1)
++#define   RTL8365MB_VLAN_MC_D2_ENVLANPOL_MASK         GENMASK(4, 4)
++#define   RTL8365MB_VLAN_MC_D2_METERIDX_MASK          GENMASK(10, 5)
++#define   RTL8365MB_VLAN_MC_D3_EVID_MASK              GENMASK(12, 0)
++
++/* Some limits for VLAN4k/VLAN membership config entries */
++#define RTL8365MB_PRIORITYMAX 7
++#define RTL8365MB_FIDMAX      15
++#define RTL8365MB_METERMAX    63
++#define RTL8365MB_VLAN_MCMAX  31
++
++/* RTL8367S supports 4k vlans (vid<=4095) and 32 enhanced vlans
++ * for VIDs up to 8191
++ */
++#define RTL8365MB_MAX_4K_VID  0x0FFF /* 4095 */
++#define RTL8365MB_MAX_MC_VID  0x1FFF /* 8191 */
++
++ /* Port-based VID registers 0~5 - each one holds an MC index for two ports */
++#define RTL8365MB_VLAN_PVID_CTRL_BASE                 0x0700
++#define RTL8365MB_VLAN_PVID_CTRL_REG(_p) \
++              (RTL8365MB_VLAN_PVID_CTRL_BASE + ((_p) >> 1))
++#define   RTL8365MB_VLAN_PVID_CTRL_PORT0_MCIDX_MASK   0x001F
++#define   RTL8365MB_VLAN_PVID_CTRL_PORT1_MCIDX_MASK   0x1F00
++#define   RTL8365MB_VLAN_PVID_CTRL_PORT_MCIDX_OFFSET(_p) \
++              (((_p) & 1) << 3)
++#define   RTL8365MB_VLAN_PVID_CTRL_PORT_MCIDX_MASK(_p) \
++              (0x1F << RTL8365MB_VLAN_PVID_CTRL_PORT_MCIDX_OFFSET(_p))
++
++/* Frame type filtering registers */
++#define RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_BASE 0x07aa
++#define RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_REG(port) \
++              (RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_BASE + ((port) >> 3))
++/* required as FIELD_PREP cannot use non-constant masks */
++#define RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_MASK(port) \
++              (0x3 << RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_OFFSET(port))
++#define RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_OFFSET(port) \
++              (((port) & 0x7) << 1)
++
++/*
++ * struct rtl8365mb_vlan4k - VLAN4k table entry
++ * @vid: VLAN ID (0~4095)
++ * @member: port mask of ports in this VLAN
++ * @untag: port mask of ports which untag on egress
++ * @fid: filter ID - only used with SVL (unused)
++ * @priority: priority classification (unused)
++ * @priority_en: enable priority (unused)
++ * @policing_en: enable policing (unused)
++ * @ivl_en: enable IVL instead of default SVL
++ * @meteridx: metering index (unused)
++ *
++ * This structure is used to get/set entries in the VLAN4k table. The
++ * VLAN4k table dictates the VLAN configuration for the switch for the
++ * vast majority of features.
++ */
++struct rtl8365mb_vlan4k {
++      u16 vid;
++      u16 member;
++      u16 untag;
++      u8 fid : 4;
++      u8 priority : 3;
++      u8 priority_en : 1;
++      u8 policing_en : 1;
++      u8 ivl_en : 1;
++      u8 meteridx : 6;
++};
++
++/*
++ * struct rtl8365mb_vlanmc - VLAN membership config
++ * @evid: Enhanced VLAN ID (0~8191)
++ * @member: port mask of ports in this VLAN
++ * @fid: filter ID - only used with SVL (unused)
++ * @priority: priority classification (unused)
++ * @priority_en: enable priority (unused)
++ * @policing_en: enable policing (unused)
++ * @meteridx: metering index (unused)
++ *
++ * This structure is used to get/set entries in the VLAN membership
++ * configuration database. This feature is largely vestigial, but
++ * still needed for at least the following features:
++ *   - PVID configuration
++ *   - ACL configuration
++ *   - selection of VLAN by the CPU tag when VSEL=1, although the switch
++ *     can also select VLAN based on the VLAN tag if VSEL=0
++ *
++ * This is a low-level structure and it is recommended to interface with
++ * the VLAN membership config database via &struct rtl8365mb_vlanmc_entry.
++ */
++struct rtl8365mb_vlanmc {
++      u16 evid;
++      u16 member;
++      u8 fid : 4;
++      u8 priority : 3;
++      u8 priority_en : 1;
++      u8 policing_en : 1;
++      u8 meteridx : 6;
++};
++
++static int rtl8365mb_vlan_4k_read(struct realtek_priv *priv, u16 vid,
++                                struct rtl8365mb_vlan4k *vlan4k)
++{
++      u16 data[RTL8365MB_CVLAN_ENTRY_SIZE];
++      int val;
++      int ret;
++
++      ret = rtl8365mb_table_query(priv, RTL8365MB_TABLE_CVLAN,
++                                  RTL8365MB_TABLE_OP_READ, &vid, 0, 0,
++                                  data, ARRAY_SIZE(data));
++      if (ret)
++              return ret;
++
++      /* Unpack table entry */
++      memset(vlan4k, 0, sizeof(*vlan4k));
++      vlan4k->vid = vid;
++
++      val = FIELD_GET(RTL8365MB_CVLAN_ENTRY_D0_MBR_MASK, data[0]);
++      vlan4k->member = FIELD_PREP(RTL8365MB_CVLAN_MBR_LO_MASK, val);
++      val = FIELD_GET(RTL8365MB_CVLAN_ENTRY_D2_MBR_EXT_MASK, data[2]);
++      vlan4k->member |= FIELD_PREP(RTL8365MB_CVLAN_MBR_HI_MASK, val);
++
++      val = FIELD_GET(RTL8365MB_CVLAN_ENTRY_D0_UNTAG_MASK, data[0]);
++      vlan4k->untag = FIELD_PREP(RTL8365MB_CVLAN_UNTAG_LO_MASK, val);
++      val = FIELD_GET(RTL8365MB_CVLAN_ENTRY_D2_UNTAG_EXT_MASK, data[2]);
++      vlan4k->untag |= FIELD_PREP(RTL8365MB_CVLAN_UNTAG_HI_MASK, val);
++
++      vlan4k->fid = FIELD_GET(RTL8365MB_CVLAN_ENTRY_D1_FID_MASK, data[1]);
++      vlan4k->priority_en =
++              FIELD_GET(RTL8365MB_CVLAN_ENTRY_D1_VBPEN_MASK, data[1]);
++      vlan4k->priority =
++              FIELD_GET(RTL8365MB_CVLAN_ENTRY_D1_VBPRI_MASK, data[1]);
++      vlan4k->policing_en =
++              FIELD_GET(RTL8365MB_CVLAN_ENTRY_D1_ENVLANPOL_MASK, data[1]);
++
++      val = FIELD_GET(RTL8365MB_CVLAN_ENTRY_D1_METERIDX_MASK, data[1]);
++      val = FIELD_PREP(RTL8365MB_CVLAN_METERIDX_LO_MASK, val);
++      vlan4k->meteridx = val;
++      val = FIELD_GET(RTL8365MB_CVLAN_ENTRY_D2_METERIDX_EXT_MASK, data[2]);
++      val = FIELD_PREP(RTL8365MB_CVLAN_METERIDX_HI_MASK, val);
++      vlan4k->meteridx |= val;
++
++      vlan4k->ivl_en =
++              FIELD_GET(RTL8365MB_CVLAN_ENTRY_D1_IVL_SVL_MASK, data[1]);
++
++      return 0;
++}
++
++static int rtl8365mb_vlan_4k_write(struct realtek_priv *priv,
++                                 const struct rtl8365mb_vlan4k *vlan4k)
++{
++      u16 data[RTL8365MB_CVLAN_ENTRY_SIZE] = { 0 };
++      u16 vid;
++      int val;
++
++      /* Pack table entry value */
++      val = FIELD_GET(RTL8365MB_CVLAN_MBR_LO_MASK, vlan4k->member);
++      data[0] |= FIELD_PREP(RTL8365MB_CVLAN_ENTRY_D0_MBR_MASK, val);
++
++      val = FIELD_GET(RTL8365MB_CVLAN_UNTAG_LO_MASK, vlan4k->untag);
++      data[0] |= FIELD_PREP(RTL8365MB_CVLAN_ENTRY_D0_UNTAG_MASK, val);
++
++      data[1] |= FIELD_PREP(RTL8365MB_CVLAN_ENTRY_D1_FID_MASK, vlan4k->fid);
++      data[1] |= FIELD_PREP(RTL8365MB_CVLAN_ENTRY_D1_VBPEN_MASK,
++                            vlan4k->priority_en);
++      data[1] |= FIELD_PREP(RTL8365MB_CVLAN_ENTRY_D1_VBPRI_MASK,
++                            vlan4k->priority);
++      data[1] |= FIELD_PREP(RTL8365MB_CVLAN_ENTRY_D1_ENVLANPOL_MASK,
++                            vlan4k->policing_en);
++
++      /* FIELD_* does not play nice with struct bitfield. */
++      val = vlan4k->meteridx;
++      val = FIELD_GET(RTL8365MB_CVLAN_METERIDX_LO_MASK, val);
++      data[1] |= FIELD_PREP(RTL8365MB_CVLAN_ENTRY_D1_METERIDX_MASK, val);
++
++      data[1] |= FIELD_PREP(RTL8365MB_CVLAN_ENTRY_D1_IVL_SVL_MASK,
++                            vlan4k->ivl_en);
++
++      val = FIELD_GET(RTL8365MB_CVLAN_MBR_HI_MASK, vlan4k->member);
++      data[2] |= FIELD_PREP(RTL8365MB_CVLAN_ENTRY_D2_MBR_EXT_MASK, val);
++
++      val = FIELD_GET(RTL8365MB_CVLAN_UNTAG_HI_MASK, vlan4k->untag);
++      data[2] |= FIELD_PREP(RTL8365MB_CVLAN_ENTRY_D2_UNTAG_EXT_MASK, val);
++
++      val = vlan4k->meteridx;
++      val = FIELD_GET(RTL8365MB_CVLAN_METERIDX_HI_MASK, val);
++      data[2] |= FIELD_PREP(RTL8365MB_CVLAN_ENTRY_D2_METERIDX_EXT_MASK, val);
++
++      vid = vlan4k->vid;
++      return rtl8365mb_table_query(priv, RTL8365MB_TABLE_CVLAN,
++                                   RTL8365MB_TABLE_OP_WRITE, &vid, 0, 0,
++                                   data, ARRAY_SIZE(data));
++}
++
++static int
++rtl8365mb_vlan_4k_port_set(struct dsa_switch *ds, int port,
++                         const struct switchdev_obj_port_vlan *vlan,
++                         struct netlink_ext_ack *extack,
++                         bool include)
++{
++      struct realtek_priv *priv = ds->priv;
++      struct rtl8365mb_vlan4k vlan4k = {0};
++      int ret;
++
++      dev_dbg(priv->dev, "%s VLAN %d 4K on port %d\n",
++              include ? "add" : "del",
++              vlan->vid, port);
++
++      if (vlan->vid > RTL8365MB_MAX_4K_VID) {
++              NL_SET_ERR_MSG_MOD(extack, "VLAN ID greater than "
++                                 __stringify(RTL8365MB_MAX_4K_VID));
++              return -EINVAL;
++      }
++
++      ret = rtl8365mb_vlan_4k_read(priv, vlan->vid, &vlan4k);
++      if (ret) {
++              dev_err(priv->dev, "Failed to read VLAN 4k table\n");
++              return ret;
++      }
++
++      if (include)
++              vlan4k.member |= BIT(port);
++      else
++              vlan4k.member &= ~BIT(port);
++
++      if (include && (vlan->flags & BRIDGE_VLAN_INFO_UNTAGGED))
++              vlan4k.untag |= BIT(port);
++      else
++              vlan4k.untag &= ~BIT(port);
++      vlan4k.ivl_en = true; /* always use Independent VLAN Learning */
++
++      ret = rtl8365mb_vlan_4k_write(priv, &vlan4k);
++      if (ret) {
++              dev_err(priv->dev, "Failed to write VLAN 4k table\n");
++              return ret;
++      }
++
++      return 0;
++}
++
++/*
++ * rtl8365mb_vlan_4k_port_add() - Add a port to a VLAN 4K table entry
++ * @ds: dsa switch instance
++ * @port: port index
++ * @vlan: switchdev VLAN object containing the target VID and flags
++ * @extack: netlink extended ACK for error reporting
++ *
++ * Adds the specified port to the hardware VLAN 4K membership table.
++ *
++ * Context: Can sleep. Must be called with &priv->vlan_lock held.
++ * Takes and releases &priv->map_lock.
++ * Return: 0 on success, or a negative error code on failure.
++ */
++int rtl8365mb_vlan_4k_port_add(struct dsa_switch *ds, int port,
++                             const struct switchdev_obj_port_vlan *vlan,
++                             struct netlink_ext_ack *extack)
++{
++      struct realtek_priv *priv = ds->priv;
++
++      lockdep_assert_held(&priv->vlan_lock);
++
++      return rtl8365mb_vlan_4k_port_set(ds, port, vlan, extack, true);
++}
++
++/*
++ * rtl8365mb_vlan_4k_port_del() - Remove a port from a VLAN 4K table entry
++ * @ds: dsa switch instance
++ * @port: port index
++ * @vlan: switchdev VLAN object containing the target VID
++ *
++ * Removes the specified port from the hardware VLAN 4K membership table.
++ *
++ * Context: Can sleep. Must be called with &priv->vlan_lock held.
++ * Takes and releases &priv->map_lock.
++ * Return: 0 on success, or a negative error code on failure.
++ */
++int rtl8365mb_vlan_4k_port_del(struct dsa_switch *ds, int port,
++                             const struct switchdev_obj_port_vlan *vlan)
++{
++      struct realtek_priv *priv = ds->priv;
++
++      lockdep_assert_held(&priv->vlan_lock);
++
++      return rtl8365mb_vlan_4k_port_set(ds, port, vlan, NULL, false);
++}
++
++static int rtl8365mb_vlan_mc_read(struct realtek_priv *priv, u32 index,
++                                struct rtl8365mb_vlanmc *vlanmc)
++{
++      u16 data[RTL8365MB_VLAN_MC_ENTRY_SIZE];
++      int ret;
++
++      ret = regmap_bulk_read(priv->map, RTL8365MB_VLAN_MC_REG(index), &data,
++                             RTL8365MB_VLAN_MC_ENTRY_SIZE);
++      if (ret)
++              return ret;
++
++      vlanmc->member = FIELD_GET(RTL8365MB_VLAN_MC_D0_MBR_MASK, data[0]);
++      vlanmc->fid = FIELD_GET(RTL8365MB_VLAN_MC_D1_FID_MASK, data[1]);
++      vlanmc->meteridx = FIELD_GET(RTL8365MB_VLAN_MC_D2_METERIDX_MASK,
++                                   data[2]);
++      vlanmc->policing_en = FIELD_GET(RTL8365MB_VLAN_MC_D2_ENVLANPOL_MASK,
++                                      data[2]);
++      vlanmc->priority = FIELD_GET(RTL8365MB_VLAN_MC_D2_VBPRI_MASK, data[2]);
++      vlanmc->priority_en = FIELD_GET(RTL8365MB_VLAN_MC_D2_VBPEN_MASK,
++                                      data[2]);
++      vlanmc->evid = FIELD_GET(RTL8365MB_VLAN_MC_D3_EVID_MASK, data[3]);
++
++      return 0;
++}
++
++static int rtl8365mb_vlan_mc_write(struct realtek_priv *priv, u32 index,
++                                 const struct rtl8365mb_vlanmc *vlanmc)
++{
++      u16 data[RTL8365MB_VLAN_MC_ENTRY_SIZE] = { 0 };
++      int ret;
++
++      data[0] |= FIELD_PREP(RTL8365MB_VLAN_MC_D0_MBR_MASK, vlanmc->member);
++      data[1] |= FIELD_PREP(RTL8365MB_VLAN_MC_D1_FID_MASK, vlanmc->fid);
++      data[2] |= FIELD_PREP(RTL8365MB_VLAN_MC_D2_METERIDX_MASK,
++                            vlanmc->meteridx);
++      data[2] |= FIELD_PREP(RTL8365MB_VLAN_MC_D2_ENVLANPOL_MASK,
++                            vlanmc->policing_en);
++      data[2] |=
++              FIELD_PREP(RTL8365MB_VLAN_MC_D2_VBPRI_MASK, vlanmc->priority);
++      data[2] |= FIELD_PREP(RTL8365MB_VLAN_MC_D2_VBPEN_MASK,
++                            vlanmc->priority_en);
++      data[3] |= FIELD_PREP(RTL8365MB_VLAN_MC_D3_EVID_MASK, vlanmc->evid);
++
++      ret = regmap_bulk_write(priv->map, RTL8365MB_VLAN_MC_REG(index), &data,
++                              RTL8365MB_VLAN_MC_ENTRY_SIZE);
++
++      return ret;
++}
++
++static int rtl8365mb_vlan_mc_erase(struct realtek_priv *priv, u32 index)
++{
++      u16 data[RTL8365MB_VLAN_MC_ENTRY_SIZE] = { 0 };
++      int ret;
++
++      ret = regmap_bulk_write(priv->map, RTL8365MB_VLAN_MC_REG(index), &data,
++                              RTL8365MB_VLAN_MC_ENTRY_SIZE);
++
++      return ret;
++}
++
++/*
++ * rtl8365mb_vlan_mc_find() - find VLANMC index by VID or the first free index
++ *
++ * @priv: realtek_priv pointer
++ * @vid: VLAN ID
++ * @index: found index
++ * @first_free: found free index
++ *
++ * If a VLAN MC entry using @vid was found, @index will return the matched index
++ * and @first_free is undefined. If not found, @index will return 0 and
++ * @first_free will return the first found free index in VLAN MC or 0 if the
++ * table is full.
++ *
++ * Although 0 is a valid VLAN MC index, it is reserved for ports without PVID,
++ * including standalone, non-member ports. It uses VID == 0.
++ *
++ * Both @index and @first_free will be in the * 1..@RTL8365MB_VLAN_MCMAX range.
++ *
++ * Return: Returns 0 on success, a negative error on failure.
++ */
++static int rtl8365mb_vlan_mc_find(struct realtek_priv *priv, u16 vid,
++                                u8 *index, u8 *first_free)
++{
++      u32 vlan_entry_d3;
++      u8 vlanmc_idx;
++      u16 evid;
++      int ret;
++
++      *index = 0;
++      *first_free = 0;
++
++      /* look for existing entry or an empty one */
++      /* By design, VlanMC[0] is reserved as a neutral PVID value for
++       * standalone ports. It always has EVID == 0. That way, we assume that
++       * all entries after index 0 with VID == 0 are empty.
++       **/
++      for (vlanmc_idx = 1; vlanmc_idx <= RTL8365MB_VLAN_MCMAX; vlanmc_idx++) {
++              /* just read the 4th word, where the evid is */
++              ret = regmap_read(priv->map,
++                                RTL8365MB_VLAN_MC_REG(vlanmc_idx) + 3,
++                                &vlan_entry_d3);
++              if (ret)
++                      return ret;
++
++              evid = FIELD_GET(RTL8365MB_VLAN_MC_D3_EVID_MASK, vlan_entry_d3);
++
++              if (evid == vid) {
++                      *index = vlanmc_idx;
++                      return 0;
++              }
++
++              if (evid == 0x0 && *first_free < 1)
++                      *first_free = vlanmc_idx;
++      }
++      return 0;
++}
++
++static int rtl8365mb_vlan_port_get_pvid_idx(struct realtek_priv *priv,
++                                          int port, u8 *vlanmc_idx)
++{
++      u32 data;
++      int ret;
++
++      ret = regmap_read(priv->map, RTL8365MB_VLAN_PVID_CTRL_REG(port), &data);
++      if (ret)
++              return ret;
++
++      *vlanmc_idx = (data & RTL8365MB_VLAN_PVID_CTRL_PORT_MCIDX_MASK(port))
++                    >> RTL8365MB_VLAN_PVID_CTRL_PORT_MCIDX_OFFSET(port);
++
++      return 0;
++}
++
++/*
++ * rtl8365mb_vlan_mc_port_set() - include or exclude a port from VlanMC
++ * @ds: dsa switch
++ * @port: the port number
++ * @vid: the vlan VID to include/exclude @port
++ * @pvid: inform if vid is used as pvid in @port
++ * @extack: optional extack to return errors
++ * @include: whether to include or exclude @port
++ *
++ * This function is used to include/exclude ports to the VlanMC table.
++ *
++ * VlanMC stands for VLAN membership config and it is used exclusively for
++ * PVID. If @vlan members are not using PVID, this function will either
++ * remove or not create a new VlanMC entry.
++ *
++ * VlanMC members are used as a reference port map, cleaning the entry once
++ * no port is using it.
++ *
++ * Port PVID and accepted frame type are updated as well.
++ *
++ * Context: Can sleep. Must be called with &priv->vlan_lock held.
++ * Takes and releases &priv->map_lock.
++ * Return: Returns 0 on success, a negative error on failure.
++ */
++static
++int rtl8365mb_vlan_mc_port_set(struct dsa_switch *ds, int port,
++                             u16 vid, bool pvid,
++                             struct netlink_ext_ack *extack,
++                             bool include)
++{
++      struct realtek_priv *priv = ds->priv;
++      struct rtl8365mb_vlanmc vlanmc = {0};
++      u8 first_unused = 0;
++      u8 vlanmc_idx = 0;
++      int ret;
++
++      dev_dbg(priv->dev, "%s VLAN %d MC on port %d\n",
++              include ? "add" : "del",
++              vid, port);
++
++      if (vid > RTL8365MB_MAX_MC_VID) {
++              NL_SET_ERR_MSG_MOD(extack, "VLAN ID greater than "
++                                 __stringify(RTL8365MB_MAX_MC_VID));
++              return -EINVAL;
++      }
++
++      /* look for existing entry or an empty slot */
++      ret = rtl8365mb_vlan_mc_find(priv, vid, &vlanmc_idx,
++                                   &first_unused);
++      if (ret) {
++              dev_err(priv->dev, "Failed to find a VLAN MC table index\n");
++              return ret;
++      }
++
++      if (vlanmc_idx) {
++              ret = rtl8365mb_vlan_mc_read(priv, vlanmc_idx, &vlanmc);
++              if (ret) {
++                      dev_err(priv->dev, "Failed to read VLAN MC table\n");
++                      return ret;
++              }
++      } else if (include) {
++              /* for now, vlan_mc is only required for PVID. Defer allocation
++               * until at least one port uses PVID.
++               */
++              if (!pvid) {
++                      dev_dbg(priv->dev,
++                              "Not creating VlanMC for vlan %d until a port uses PVID (%d does not)\n",
++                              vid, port);
++                      return 0;
++              }
++
++              if (!first_unused) {
++                      NL_SET_ERR_MSG_MOD(extack, "All VLAN MC entries (0.."
++                                         __stringify(RTL8365MB_VLAN_MCMAX)
++                                         ") are in use.");
++                      return -ENOSPC;
++              }
++
++              vlanmc_idx = first_unused;
++              vlanmc.evid = vid;
++
++      } else /* excluding and VLANMC not found */ {
++              return 0;
++      }
++
++      dev_dbg(priv->dev,
++              "VLAN %d (idx: %d) PVID curr members: %08x\n",
++              vid, vlanmc_idx, vlanmc.member);
++
++      /* here we either have an existing VLANMC (with PVID members) or the
++       * added port is using this VLAN as PVID
++       */
++      if (include)
++              vlanmc.member |= BIT(port);
++      else
++              vlanmc.member &= ~BIT(port);
++
++      /* just like we don't need to create a VLAN_MC when there is no port
++       * using it as PVID, we can erase it when there is no more port using
++       * it as PVID.
++       */
++      if (!vlanmc.member) {
++              dev_dbg(priv->dev,
++                      "Clearing VlanMC index %d previously used by VID %d\n",
++                      vlanmc_idx, vid);
++              ret = rtl8365mb_vlan_mc_erase(priv, vlanmc_idx);
++      } else {
++              dev_dbg(priv->dev,
++                      "Saving VlanMC index %d with VID %d\n",
++                      vlanmc_idx, vid);
++              ret = rtl8365mb_vlan_mc_write(priv, vlanmc_idx, &vlanmc);
++      }
++      if (ret) {
++              dev_err(priv->dev, "Failed to write vlan MC entry\n");
++              return ret;
++      }
++
++      return 0;
++}
++
++static int rtl8365mb_vlan_port_set_pvid(struct realtek_priv *priv,
++                                      int port, u16 vlanmc_idx)
++{
++      int ret;
++      u32 val;
++
++      dev_dbg(priv->dev, "set PVID IDX %d on port %d\n", vlanmc_idx, port);
++
++      val = vlanmc_idx << RTL8365MB_VLAN_PVID_CTRL_PORT_MCIDX_OFFSET(port);
++      ret = regmap_update_bits(priv->map,
++                               RTL8365MB_VLAN_PVID_CTRL_REG(port),
++                               RTL8365MB_VLAN_PVID_CTRL_PORT_MCIDX_MASK(port),
++                               val);
++      if (ret)
++              return ret;
++
++      return 0;
++}
++
++static int rtl8365mb_vlan_get_pvid_mc(struct realtek_priv *priv,
++                                    int port, u8 *vlanmc_idx,
++                                    struct rtl8365mb_vlanmc *vlanmc)
++{
++      int ret;
++
++      ret = rtl8365mb_vlan_port_get_pvid_idx(priv, port, vlanmc_idx);
++      if (ret)
++              return ret;
++
++      memset(vlanmc, 0, sizeof(*vlanmc));
++
++      if (!*vlanmc_idx)
++              return 0;
++
++      ret = rtl8365mb_vlan_mc_read(priv, *vlanmc_idx, vlanmc);
++      if (ret)
++              return ret;
++
++      return 0;
++}
++
++/*
++ * rtl8365mb_vlan_port_get_pvid - Retrieve the port PVID
++ * @priv: realtek switch private structure
++ * @port: port index
++ * @pvid: pointer to store the retrieved VLAN ID
++ *
++ * Returns the port PVID if defined or 0 if not.
++ *
++ * Context: Can sleep. Takes and releases &priv->map_lock.
++ * Return: 0 on success or a negative error code on failure.
++ */
++int rtl8365mb_vlan_port_get_pvid(struct realtek_priv *priv, int port, u16 *pvid)
++{
++      struct rtl8365mb_vlanmc vlanmc;
++      u8 vlanmc_idx;
++      int ret;
++
++      ret = rtl8365mb_vlan_get_pvid_mc(priv, port, &vlanmc_idx, &vlanmc);
++      if (ret)
++              return ret;
++
++      *pvid = vlanmc.evid;
++      return 0;
++}
++
++/*
++ * rtl8365mb_vlan_port_get_framefilter() - Get the ingress frame filtering mode
++ * for a port
++ * @priv: realtek switch private structure
++ * @port: port index
++ * @frame_type: pointer to store the retrieved ingress frame filter type
++ *
++ * Context: Can sleep. Takes and releases &priv->map_lock.
++ * Return: 0 on success, or a negative error code on failure.
++ */
++int
++rtl8365mb_vlan_port_get_framefilter(struct realtek_priv *priv,
++                                  int port,
++                                  enum rtl8365mb_frame_ingress *frame_type)
++{
++      u32 val;
++      int ret;
++
++      /* Even if ACCEPT_FRAME_TYPE_ANY, the switch will still check if the
++       * port is a member of vlan PVID
++       */
++
++      ret = regmap_read(priv->map, RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_REG(port),
++                        &val);
++      if (ret)
++              return ret;
++
++      *frame_type = field_get(RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_MASK(port),
++                              val);
++
++      return 0;
++}
++
++/*
++ * rtl8365mb_vlan_port_set_framefilter() - Set the ingress frame filtering mode
++ * for a port
++ * @priv: realtek switch private structure
++ * @port: port index
++ * @frame_type: the ingress frame filter type to configure
++ *
++ * Context: Can sleep. Takes and releases &priv->map_lock.
++ * Return: 0 on success, or a negative error code on failure.
++ */
++int
++rtl8365mb_vlan_port_set_framefilter(struct realtek_priv *priv,
++                                  int port,
++                                  enum rtl8365mb_frame_ingress frame_type)
++{
++      u32 val;
++
++      /* Even if ACCEPT_FRAME_TYPE_ANY, the switch will still check if the
++       * port is a member of vlan PVID
++       */
++      val = frame_type << RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_OFFSET(port);
++
++      return regmap_update_bits(priv->map,
++                                RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_REG(port),
++                                RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_MASK(port),
++                                val);
++}
++
++/*
++ * rtl8365mb_vlan_pvid_port_set() - Configure a port's PVID and associated
++ * VLANMC entry
++ * @ds: dsa switch instance
++ * @port: port index
++ * @vid: target VID
++ * @extack: netlink extended ACK for error reporting
++ *
++ * Allocates or reuses a hardware VLANMC entry to map the given port to its new
++ * PVID. Gracefully unwinds and restores previous configuration if a hardware
++ * write operation fails during execution.
++ *
++ * Context: Can sleep. Must be called with &priv->vlan_lock held.
++ * Takes and releases &priv->map_lock.
++ * Return: 0 on success, or a negative error code on failure.
++ */
++int rtl8365mb_vlan_pvid_port_set(struct dsa_switch *ds, int port, u16 vid,
++                               struct netlink_ext_ack *extack)
++{
++      enum rtl8365mb_frame_ingress accepted_frame, prev_accepted_frame;
++      struct realtek_priv *priv = ds->priv;
++      struct rtl8365mb_vlanmc prev_vlanmc = {0};
++      u8 _unused_first_free_idx;
++      u8 prev_vlanmc_idx;
++      u8 vlanmc_idx;
++      int ret;
++
++      lockdep_assert_held(&priv->vlan_lock);
++
++      /* Read the old PVID exclusively to undo in case of error */
++      ret = rtl8365mb_vlan_get_pvid_mc(priv, port, &prev_vlanmc_idx,
++                                       &prev_vlanmc);
++      if (ret) {
++              dev_err(priv->dev, "Failed to read current VLAN MC\n");
++              return ret;
++      }
++
++      ret = rtl8365mb_vlan_port_get_framefilter(priv, port,
++                                                &prev_accepted_frame);
++      if (ret) {
++              dev_err(priv->dev, "Failed to get current framefilter\n");
++              return ret;
++      }
++
++      /* Find or allocate a new vlan MC and add port to members,
++       * although members are not checked by the HW in vlan MC.
++       */
++      ret = rtl8365mb_vlan_mc_port_set(ds, port, vid, true, extack, true);
++      if (ret)
++              return ret;
++
++      /* look for existing entry */
++      ret = rtl8365mb_vlan_mc_find(priv, vid, &vlanmc_idx,
++                                   &_unused_first_free_idx);
++      if (ret) {
++              dev_err(priv->dev, "Failed to find a VLAN MC table index\n");
++              goto undo_vlan_mc_port_set;
++      }
++
++      if (!vlanmc_idx) {
++              dev_err(priv->dev, "VLAN should already exist in VLAN MC\n");
++              ret = -ENOENT;
++              goto undo_vlan_mc_port_set;
++      }
++
++      ret = rtl8365mb_vlan_port_set_pvid(priv, port, vlanmc_idx);
++      if (ret) {
++              dev_err(priv->dev, "Failed to set port PVID\n");
++              goto undo_vlan_mc_port_set;
++      }
++
++      /* Changing accept frame is what enables PVID (if not enabled before) */
++      accepted_frame = RTL8365MB_FRAME_TYPE_ANY_FRAME;
++      ret = rtl8365mb_vlan_port_set_framefilter(priv, port, accepted_frame);
++      if (ret) {
++              dev_err(priv->dev, "Failed to set port frame filter\n");
++              goto undo_vlan_port_set_pvid;
++      }
++
++      /* A VLAN can be added with PVID without removing from the old
++       * PVID VLAN. Clear PVID from the old VLAN MC (if needed).
++       */
++      if (prev_vlanmc_idx && (prev_vlanmc.evid != vid)) {
++              ret = rtl8365mb_vlan_mc_port_set(ds, port, prev_vlanmc.evid,
++                                               false, NULL, false);
++              if (ret) {
++                      dev_err(priv->dev, "Failed to clear old VLAN MC\n");
++                      goto undo_set_framefilter;
++              }
++      }
++
++      return 0;
++
++undo_set_framefilter:
++      (void)rtl8365mb_vlan_port_set_framefilter(priv, port,
++                                                prev_accepted_frame);
++
++undo_vlan_port_set_pvid:
++      (void)rtl8365mb_vlan_port_set_pvid(priv, port, prev_vlanmc_idx);
++
++undo_vlan_mc_port_set:
++      if (prev_vlanmc.evid != vid)
++              (void)rtl8365mb_vlan_mc_port_set(ds, port, vid, false, NULL,
++                                               false);
++
++      return ret;
++}
++
++/*
++ * rtl8365mb_vlan_pvid_port_clear() - Remove a port's PVID configuration
++ * @ds: dsa switch instance
++ * @port: port index
++ * @vid:  VLAN VID for PVID
++ *
++ * Resets the target port's hardware PVID allocation to 0. Cleans up and frees
++ * the associated VLANMC entry if no other ports are referencing it.
++ *
++ * Context: Can sleep. Must be called with &priv->vlan_lock held.
++ * Takes and releases &priv->map_lock.
++ * Return: 0 on success, or a negative error code on failure.
++ */
++int rtl8365mb_vlan_pvid_port_clear(struct dsa_switch *ds, int port, u16 vid)
++{
++      enum rtl8365mb_frame_ingress accepted_frame, prev_accepted_frame;
++      struct realtek_priv *priv = ds->priv;
++      struct rtl8365mb_vlanmc vlanmc = {0};
++      u8 vlanmc_idx;
++      int ret;
++
++      lockdep_assert_held(&priv->vlan_lock);
++
++      ret = rtl8365mb_vlan_get_pvid_mc(priv, port, &vlanmc_idx,
++                                       &vlanmc);
++      if (ret) {
++              dev_err(priv->dev, "Failed to read current VLAN MC\n");
++              return ret;
++      }
++
++      /* Port is not using PVID. Nothing to remove. */
++      if (!vlanmc_idx)
++              return 0;
++
++      /* We are leaving a non PVID vlan, Nothing to remove. */
++      if (vlanmc.evid != vid)
++              return 0;
++
++      ret = rtl8365mb_vlan_port_get_framefilter(priv, port,
++                                                &prev_accepted_frame);
++      if (ret) {
++              dev_err(priv->dev, "Failed to get current framefilter\n");
++              return ret;
++      }
++
++      /* Changing accept frame is what really removes PVID. But only do
++       * that if we are filtering vlan
++       */
++      if (dsa_port_is_vlan_filtering(dsa_to_port(ds, port))) {
++              accepted_frame = RTL8365MB_FRAME_TYPE_TAGGED_ONLY;
++
++              ret = rtl8365mb_vlan_port_set_framefilter(priv, port,
++                                                        accepted_frame);
++              if (ret) {
++                      dev_err(priv->dev, "Failed to set port frame filter\n");
++                      return ret;
++              }
++      } else {
++              /* skip undo_set_framefilter */
++              accepted_frame = prev_accepted_frame;
++      }
++
++      ret = rtl8365mb_vlan_port_set_pvid(priv, port, 0);
++      if (ret) {
++              dev_err(priv->dev, "Failed to set port PVID to 0\n");
++              goto undo_set_framefilter;
++      }
++
++      /* Clears the VLAN MC membership and maybe VLAN MC entry if empty */
++      ret = rtl8365mb_vlan_mc_port_set(ds, port, vlanmc.evid,
++                                       false, NULL, false);
++      if (ret)
++              goto undo_port_set_pvid;
++
++      return 0;
++
++undo_port_set_pvid:
++      (void)rtl8365mb_vlan_port_set_pvid(priv, port, vlanmc_idx);
++
++undo_set_framefilter:
++      if (prev_accepted_frame != accepted_frame)
++              (void)rtl8365mb_vlan_port_set_framefilter(priv, port,
++                                                        prev_accepted_frame);
++
++      return ret;
++}
+--- /dev/null
++++ b/drivers/net/dsa/realtek/rtl8365mb_vlan.h
+@@ -0,0 +1,39 @@
++/* SPDX-License-Identifier: GPL-2.0 */
++/* VLAN configuration interface for the rtl8365mb switch family
++ *
++ * Copyright (C) 2022 Alvin Šipraga <alsi@bang-olufsen.dk>
++ *
++ */
++
++#ifndef _REALTEK_RTL8365MB_VLAN_H
++#define _REALTEK_RTL8365MB_VLAN_H
++
++#include <linux/types.h>
++
++#include "realtek.h"
++
++enum rtl8365mb_frame_ingress {
++      RTL8365MB_FRAME_TYPE_ANY_FRAME = 0,
++      RTL8365MB_FRAME_TYPE_TAGGED_ONLY,
++      RTL8365MB_FRAME_TYPE_UNTAGGED_ONLY,
++};
++
++int rtl8365mb_vlan_port_get_pvid(struct realtek_priv *priv, int port,
++                               u16 *pvid);
++int
++rtl8365mb_vlan_port_get_framefilter(struct realtek_priv *priv,
++                                  int port,
++                                  enum rtl8365mb_frame_ingress *frame_type);
++int
++rtl8365mb_vlan_port_set_framefilter(struct realtek_priv *priv,
++                                  int port,
++                                  enum rtl8365mb_frame_ingress frame_type);
++int rtl8365mb_vlan_4k_port_add(struct dsa_switch *ds, int port,
++                             const struct switchdev_obj_port_vlan *vlan,
++                             struct netlink_ext_ack *extack);
++int rtl8365mb_vlan_4k_port_del(struct dsa_switch *ds, int port,
++                             const struct switchdev_obj_port_vlan *vlan);
++int rtl8365mb_vlan_pvid_port_set(struct dsa_switch *ds, int port, u16 vid,
++                               struct netlink_ext_ack *extack);
++int rtl8365mb_vlan_pvid_port_clear(struct dsa_switch *ds, int port, u16 vid);
++#endif /* _REALTEK_RTL8365MB_VLAN_H */
+--- a/drivers/net/dsa/realtek/rtl83xx.c
++++ b/drivers/net/dsa/realtek/rtl83xx.c
+@@ -155,6 +155,7 @@ rtl83xx_probe(struct device *dev,
+               return ERR_PTR(-ENOMEM);
+       mutex_init(&priv->map_lock);
++      mutex_init(&priv->vlan_lock);
+       rc.lock_arg = priv;
+       priv->map = devm_regmap_init(dev, NULL, priv, &rc);
diff --git a/target/linux/generic/backport-6.18/942-07-7.2-net-dsa-realtek-rtl8365mb-add-FDB-support.patch b/target/linux/generic/backport-6.18/942-07-7.2-net-dsa-realtek-rtl8365mb-add-FDB-support.patch
new file mode 100644 (file)
index 0000000..1f4b925
--- /dev/null
@@ -0,0 +1,1131 @@
+From 336e3e4a1ab37b6826fae27e53cd2ac43c9a96ca Mon Sep 17 00:00:00 2001
+From: Alvin Šipraga <alsi@bang-olufsen.dk>
+Date: Sat, 6 Jun 2026 05:29:31 -0300
+Subject: net: dsa: realtek: rtl8365mb: add FDB support
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+Implement support for FDB and MDB management for the RTL8365MB series
+switches.
+
+The hardware supports IVL by keying the unicast forwarding database with
+the {MAC, VID, EFID} tuple.  The Extended Filtering ID (EFID) is 3 bits
+wide, providing 8 unique filtering domains. This driver reserves EFID 0
+for standalone ports, effectively limiting the hardware offload to a
+maximum of 7 bridges. The multicast database uses a {MAC, VID} key, with
+ports from different bridges sharing the same multicast group.
+
+Introduce a mutex lock (l2_lock) to protect concurrent L2 table updates.
+
+Add support for forwarding database operations, including unicast and
+multicast entry handling as well as fast aging support.
+
+Set DSA switch flags assisted_learning_on_cpu_port and fdb_isolation.
+
+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-7-b9e409687cbe@gmail.com
+Signed-off-by: Jakub Kicinski <kuba@kernel.org>
+---
+ drivers/net/dsa/realtek/Makefile         |   1 +
+ drivers/net/dsa/realtek/realtek.h        |  28 ++
+ drivers/net/dsa/realtek/rtl8365mb_l2.c   | 576 +++++++++++++++++++++++++++++++
+ drivers/net/dsa/realtek/rtl8365mb_l2.h   |  32 ++
+ drivers/net/dsa/realtek/rtl8365mb_main.c |  33 +-
+ drivers/net/dsa/realtek/rtl83xx.c        | 292 ++++++++++++++++
+ drivers/net/dsa/realtek/rtl83xx.h        |  16 +
+ 7 files changed, 977 insertions(+), 1 deletion(-)
+
+--- a/drivers/net/dsa/realtek/Makefile
++++ b/drivers/net/dsa/realtek/Makefile
+@@ -19,4 +19,5 @@ obj-$(CONFIG_NET_DSA_REALTEK_RTL8365MB)
+ rtl8365mb-objs := rtl8365mb_main.o \
+                 rtl8365mb_table.o \
+                 rtl8365mb_vlan.o \
++                rtl8365mb_l2.o \
+ # end of rtl8365mb-objs
+--- a/drivers/net/dsa/realtek/realtek.h
++++ b/drivers/net/dsa/realtek/realtek.h
+@@ -45,6 +45,12 @@ struct rtl8366_vlan_4k {
+       u8      fid;
+ };
++struct realtek_fdb_entry {
++      u8 mac_addr[ETH_ALEN];
++      u16 vid;
++      bool is_static;
++};
++
+ struct realtek_priv {
+       struct device           *dev;
+       struct reset_control    *reset_ctl;
+@@ -59,6 +65,15 @@ struct realtek_priv {
+        * deleting port VLAN memberships and PVID configurations.
+        */
+       struct mutex            vlan_lock;
++      /* l2_lock is used to prevent concurrent modifications of L2 table
++       * entries while another function is reading it. l2_(add,del)_mc
++       * is an example that first read current table entry and then
++       * create/update it. l2_(add|del)_uc uses a single table op and,
++       * internally, it might not need this lock. However, altering FDB
++       * may still collide, as well as l2_flush, with fdb_dump iterating
++       * over FDB.
++       */
++      struct mutex            l2_lock;
+       struct mii_bus          *user_mii_bus;
+       struct mii_bus          *bus;
+       int                     mdio_addr;
+@@ -112,6 +127,19 @@ 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     (*l2_add_uc)(struct realtek_priv *priv, int port,
++                           const unsigned char addr[ETH_ALEN],
++                           u16 efid, u16 vid);
++      int     (*l2_del_uc)(struct realtek_priv *priv, int port,
++                           const unsigned char addr[ETH_ALEN],
++                           u16 efid, u16 vid);
++      int     (*l2_get_next_uc)(struct realtek_priv *priv, u16 *addr,
++                                int port, struct realtek_fdb_entry *entry);
++      int     (*l2_add_mc)(struct realtek_priv *priv, int port,
++                           const unsigned char addr[ETH_ALEN], u16 vid);
++      int     (*l2_del_mc)(struct realtek_priv *priv, int port,
++                           const unsigned char addr[ETH_ALEN], u16 vid);
++      int     (*l2_flush)(struct realtek_priv *priv, int port, u16 vid);
+       int     (*phy_read)(struct realtek_priv *priv, int phy, int regnum);
+       int     (*phy_write)(struct realtek_priv *priv, int phy, int regnum,
+                            u16 val);
+--- /dev/null
++++ b/drivers/net/dsa/realtek/rtl8365mb_l2.c
+@@ -0,0 +1,576 @@
++// SPDX-License-Identifier: GPL-2.0
++/* Forwarding and multicast database interface for the rtl8365mb switch family
++ *
++ * Copyright (C) 2022 Alvin Šipraga <alsi@bang-olufsen.dk>
++ */
++
++#include <linux/etherdevice.h>
++
++#include "rtl8365mb_l2.h"
++#include "rtl8365mb_table.h"
++#include <linux/regmap.h>
++
++#define RTL8365MB_L2_ENTRY_SIZE                       6
++
++#define RTL8365MB_L2_UC_D0_MAC5_MSK           GENMASK(7, 0)
++#define RTL8365MB_L2_UC_D0_MAC4_MSK           GENMASK(15, 8)
++#define RTL8365MB_L2_UC_D1_MAC3_MSK           GENMASK(7, 0)
++#define RTL8365MB_L2_UC_D1_MAC2_MSK           GENMASK(15, 8)
++#define RTL8365MB_L2_UC_D2_MAC1_MSK           GENMASK(7, 0)
++#define RTL8365MB_L2_UC_D2_MAC0_MSK           GENMASK(15, 8)
++#define RTL8365MB_L2_UC_D3_VID_MSK            GENMASK(11, 0)
++#define RTL8365MB_L2_UC_D3_IVL_MSK            GENMASK(13, 13)
++#define RTL8365MB_L2_UC_D3_PORT_EXT_MSK       GENMASK(15, 15)
++#define   RTL8365MB_L2_UC_PORT_HI_MSK         GENMASK(3, 3)
++#define RTL8365MB_L2_UC_D4_EFID_MSK           GENMASK(2, 0)
++#define RTL8365MB_L2_UC_D4_FID_MSK            GENMASK(6, 3)
++#define RTL8365MB_L2_UC_D4_SA_PRI_MSK         GENMASK(7, 7)
++#define RTL8365MB_L2_UC_D4_PORT_MSK           GENMASK(10, 8)
++#define   RTL8365MB_L2_UC_PORT_LO_MSK         GENMASK(2, 0)
++#define RTL8365MB_L2_UC_D4_AGE_MSK            GENMASK(13, 11)
++#define RTL8365MB_L2_UC_D4_AUTH_MSK           GENMASK(14, 14)
++#define RTL8365MB_L2_UC_D4_SA_BLOCK_MSK       GENMASK(15, 15)
++
++#define RTL8365MB_L2_UC_D5_DA_BLOCK_MSK       GENMASK(0, 0)
++#define RTL8365MB_L2_UC_D5_PRIORITY_MSK       GENMASK(3, 1)
++#define RTL8365MB_L2_UC_D5_FWD_PRI_MSK                GENMASK(4, 4)
++#define RTL8365MB_L2_UC_D5_STATIC_MSK         GENMASK(5, 5)
++
++#define RTL8365MB_L2_MC_D0_MAC5_MSK           GENMASK(7, 0)
++#define RTL8365MB_L2_MC_D0_MAC4_MSK           GENMASK(15, 8)
++#define RTL8365MB_L2_MC_D1_MAC3_MSK           GENMASK(7, 0)
++#define RTL8365MB_L2_MC_D1_MAC2_MSK           GENMASK(15, 8)
++#define RTL8365MB_L2_MC_D2_MAC1_MSK           GENMASK(7, 0)
++#define RTL8365MB_L2_MC_D2_MAC0_MSK           GENMASK(15, 8)
++#define RTL8365MB_L2_MC_D3_VID_MSK            GENMASK(11, 0)
++#define RTL8365MB_L2_MC_D3_IVL_MSK            GENMASK(13, 13)
++#define RTL8365MB_L2_MC_D3_MBR_HI1_MSK                GENMASK(15, 14)
++#define   RTL8365MB_L2_MC_MBR_HI1_MSK         GENMASK(9, 8)
++
++#define RTL8365MB_L2_MC_D4_MBR_MSK            GENMASK(7, 0)
++#define   RTL8365MB_L2_MC_MBR_LO_MSK          GENMASK(7, 0)
++#define RTL8365MB_L2_MC_D4_IGMPIDX_MSK                GENMASK(15, 8)
++
++#define RTL8365MB_L2_MC_D5_IGMP_ASIC_MSK      GENMASK(0, 0)
++#define RTL8365MB_L2_MC_D5_PRIORITY_MSK       GENMASK(3, 1)
++#define RTL8365MB_L2_MC_D5_FWD_PRI_MSK                GENMASK(4, 4)
++#define RTL8365MB_L2_MC_D5_STATIC_MSK         GENMASK(5, 5)
++#define RTL8365MB_L2_MC_D5_MBR_HI2_MSK                GENMASK(7, 7)
++#define   RTL8365MB_L2_MC_MBR_HI2_MSK         GENMASK(10, 10)
++
++/* Port flush command registers - writing a 1 to the port's MASK bit will
++ * initiate the flush procedure. Completion is signalled when the corresponding
++ * BUSY bit is 0.
++ */
++#define RTL8365MB_L2_FLUSH_PORT_REG           0x0A36
++#define   RTL8365MB_L2_FLUSH_PORT_MSK_MSK     GENMASK(7, 0)
++#define   RTL8365MB_L2_FLUSH_PORT_BUSY_MSK    GENMASK(15, 8)
++
++#define RTL8365MB_L2_FLUSH_PORT_EXT_REG               0x0A35
++#define   RTL8365MB_L2_FLUSH_PORT_EXT_MSK_MSK GENMASK(2, 0)
++#define   RTL8365MB_L2_FLUSH_PORT_EXT_BUSY_MSK        GENMASK(5, 3)
++
++#define RTL8365MB_L2_FLUSH_CTRL1_REG          0x0A37
++#define   RTL8365MB_L2_FLUSH_CTRL1_VID_MSK    GENMASK(11, 0)
++#define   RTL8365MB_L2_FLUSH_CTRL1_FID_MSK    GENMASK(15, 12)
++
++#define RTL8365MB_L2_FLUSH_CTRL2_REG          0x0A38
++#define   RTL8365MB_L2_FLUSH_CTRL2_MODE_MSK   GENMASK(1, 0)
++#define   RTL8365MB_L2_FLUSH_CTRL2_MODE_PORT  0
++#define   RTL8365MB_L2_FLUSH_CTRL2_MODE_PORT_VID 1
++#define   RTL8365MB_L2_FLUSH_CTRL2_MODE_PORT_FID 2
++#define   RTL8365MB_L2_FLUSH_CTRL2_TYPE_MSK   GENMASK(2, 2)
++#define   RTL8365MB_L2_FLUSH_CTRL2_TYPE_DYNAMIC       0
++#define   RTL8365MB_L2_FLUSH_CTRL2_TYPE_BOTH  1
++
++/* This flushes the entire LUT, reading it back it will turn 0 when the
++ * operation is complete
++ */
++#define RTL8365MB_L2_FLUSH_CTRL3_REG          0x0A39
++#define   RTL8365MB_L2_FLUSH_CTRL3_MSK                GENMASK(0, 0)
++
++struct rtl8365mb_l2_uc_key {
++      u8 mac_addr[ETH_ALEN];
++      u16 vid;
++      u16 fid;
++      bool ivl;
++      u16 efid;
++};
++
++struct rtl8365mb_l2_uc {
++      struct rtl8365mb_l2_uc_key key;
++      u8 port;
++      u8 age;
++      u8 priority;
++
++      bool sa_block;
++      bool da_block;
++      bool auth;
++      bool is_static;
++      bool sa_pri;
++      bool fwd_pri;
++};
++
++struct rtl8365mb_l2_mc_key {
++      u8 mac_addr[ETH_ALEN];
++      union {
++              u16 vid; /* IVL */
++              u16 fid; /* SVL */
++      };
++      bool ivl;
++};
++
++struct rtl8365mb_l2_mc {
++      struct rtl8365mb_l2_mc_key key;
++      u16 member;
++      u8 priority;
++      u8 igmpidx;
++
++      bool is_static;
++      bool fwd_pri;
++      bool igmp_asic;
++};
++
++static void rtl8365mb_l2_data_to_uc(const u16 *data, struct rtl8365mb_l2_uc *uc)
++{
++      u32 val;
++
++      uc->key.mac_addr[5] = FIELD_GET(RTL8365MB_L2_UC_D0_MAC5_MSK, data[0]);
++      uc->key.mac_addr[4] = FIELD_GET(RTL8365MB_L2_UC_D0_MAC4_MSK, data[0]);
++      uc->key.mac_addr[3] = FIELD_GET(RTL8365MB_L2_UC_D1_MAC3_MSK, data[1]);
++      uc->key.mac_addr[2] = FIELD_GET(RTL8365MB_L2_UC_D1_MAC2_MSK, data[1]);
++      uc->key.mac_addr[1] = FIELD_GET(RTL8365MB_L2_UC_D2_MAC1_MSK, data[2]);
++      uc->key.mac_addr[0] = FIELD_GET(RTL8365MB_L2_UC_D2_MAC0_MSK, data[2]);
++      uc->key.efid = FIELD_GET(RTL8365MB_L2_UC_D4_EFID_MSK, data[4]);
++      uc->key.vid = FIELD_GET(RTL8365MB_L2_UC_D3_VID_MSK, data[3]);
++      uc->key.ivl = FIELD_GET(RTL8365MB_L2_UC_D3_IVL_MSK, data[3]);
++      uc->key.fid = FIELD_GET(RTL8365MB_L2_UC_D4_FID_MSK, data[4]);
++      uc->age = FIELD_GET(RTL8365MB_L2_UC_D4_AGE_MSK, data[4]);
++      uc->auth = FIELD_GET(RTL8365MB_L2_UC_D4_AUTH_MSK, data[4]);
++
++      val = FIELD_GET(RTL8365MB_L2_UC_D4_PORT_MSK, data[4]);
++      uc->port = FIELD_PREP(RTL8365MB_L2_UC_PORT_LO_MSK, val);
++      val = FIELD_GET(RTL8365MB_L2_UC_D3_PORT_EXT_MSK, data[3]);
++      uc->port |= FIELD_PREP(RTL8365MB_L2_UC_PORT_HI_MSK, val);
++
++      uc->sa_pri = FIELD_GET(RTL8365MB_L2_UC_D4_SA_PRI_MSK, data[4]);
++      uc->fwd_pri = FIELD_GET(RTL8365MB_L2_UC_D5_FWD_PRI_MSK, data[5]);
++      uc->sa_block = FIELD_GET(RTL8365MB_L2_UC_D4_SA_BLOCK_MSK, data[4]);
++      uc->da_block = FIELD_GET(RTL8365MB_L2_UC_D5_DA_BLOCK_MSK, data[5]);
++      uc->priority = FIELD_GET(RTL8365MB_L2_UC_D5_PRIORITY_MSK, data[5]);
++      uc->is_static = FIELD_GET(RTL8365MB_L2_UC_D5_STATIC_MSK, data[5]);
++}
++
++static void rtl8365mb_l2_uc_to_data(const struct rtl8365mb_l2_uc *uc, u16 *data)
++{
++      u32 val;
++
++      memset(data, 0, RTL8365MB_L2_ENTRY_SIZE * 2);
++      data[0] |=
++              FIELD_PREP(RTL8365MB_L2_UC_D0_MAC5_MSK, uc->key.mac_addr[5]);
++      data[0] |=
++              FIELD_PREP(RTL8365MB_L2_UC_D0_MAC4_MSK, uc->key.mac_addr[4]);
++      data[1] |=
++              FIELD_PREP(RTL8365MB_L2_UC_D1_MAC3_MSK, uc->key.mac_addr[3]);
++      data[1] |=
++              FIELD_PREP(RTL8365MB_L2_UC_D1_MAC2_MSK, uc->key.mac_addr[2]);
++      data[2] |=
++              FIELD_PREP(RTL8365MB_L2_UC_D2_MAC1_MSK, uc->key.mac_addr[1]);
++      data[2] |=
++              FIELD_PREP(RTL8365MB_L2_UC_D2_MAC0_MSK, uc->key.mac_addr[0]);
++      data[3] |= FIELD_PREP(RTL8365MB_L2_UC_D3_VID_MSK, uc->key.vid);
++      data[3] |= FIELD_PREP(RTL8365MB_L2_UC_D3_IVL_MSK, uc->key.ivl);
++
++      val = FIELD_GET(RTL8365MB_L2_UC_PORT_HI_MSK, uc->port);
++      data[3] |= FIELD_PREP(RTL8365MB_L2_UC_D3_PORT_EXT_MSK, val);
++
++      data[4] |= FIELD_PREP(RTL8365MB_L2_UC_D4_FID_MSK, uc->key.fid);
++      data[4] |= FIELD_PREP(RTL8365MB_L2_UC_D4_EFID_MSK, uc->key.efid);
++      data[4] |= FIELD_PREP(RTL8365MB_L2_UC_D4_AGE_MSK, uc->age);
++      data[4] |= FIELD_PREP(RTL8365MB_L2_UC_D4_AUTH_MSK, uc->auth);
++
++      val = FIELD_GET(RTL8365MB_L2_UC_PORT_LO_MSK, uc->port);
++      data[4] |= FIELD_PREP(RTL8365MB_L2_UC_D4_PORT_MSK, val);
++
++      data[4] |= FIELD_PREP(RTL8365MB_L2_UC_D4_SA_PRI_MSK, uc->sa_pri);
++      data[4] |= FIELD_PREP(RTL8365MB_L2_UC_D4_SA_BLOCK_MSK, uc->sa_block);
++      data[5] |= FIELD_PREP(RTL8365MB_L2_UC_D5_FWD_PRI_MSK, uc->fwd_pri);
++      data[5] |= FIELD_PREP(RTL8365MB_L2_UC_D5_DA_BLOCK_MSK, uc->da_block);
++      data[5] |= FIELD_PREP(RTL8365MB_L2_UC_D5_PRIORITY_MSK, uc->priority);
++      data[5] |= FIELD_PREP(RTL8365MB_L2_UC_D5_STATIC_MSK, uc->is_static);
++}
++
++static void rtl8365mb_l2_data_to_mc(const u16 *data, struct rtl8365mb_l2_mc *mc)
++{
++      u32 val;
++
++      mc->key.mac_addr[5] = FIELD_GET(RTL8365MB_L2_MC_D0_MAC5_MSK, data[0]);
++      mc->key.mac_addr[4] = FIELD_GET(RTL8365MB_L2_MC_D0_MAC4_MSK, data[0]);
++      mc->key.mac_addr[3] = FIELD_GET(RTL8365MB_L2_MC_D1_MAC3_MSK, data[1]);
++      mc->key.mac_addr[2] = FIELD_GET(RTL8365MB_L2_MC_D1_MAC2_MSK, data[1]);
++      mc->key.mac_addr[1] = FIELD_GET(RTL8365MB_L2_MC_D2_MAC1_MSK, data[2]);
++      mc->key.mac_addr[0] = FIELD_GET(RTL8365MB_L2_MC_D2_MAC0_MSK, data[2]);
++      /* key.vid,key.fid shares the same memory space */
++      mc->key.vid = FIELD_GET(RTL8365MB_L2_MC_D3_VID_MSK, data[3]);
++      mc->key.ivl = FIELD_GET(RTL8365MB_L2_MC_D3_IVL_MSK, data[3]);
++      mc->priority = FIELD_GET(RTL8365MB_L2_MC_D5_PRIORITY_MSK, data[5]);
++      mc->fwd_pri = FIELD_GET(RTL8365MB_L2_MC_D5_FWD_PRI_MSK, data[5]);
++      mc->is_static = FIELD_GET(RTL8365MB_L2_MC_D5_STATIC_MSK, data[5]);
++
++      val = FIELD_GET(RTL8365MB_L2_MC_D4_MBR_MSK, data[4]);
++      mc->member = FIELD_PREP(RTL8365MB_L2_MC_MBR_LO_MSK, val);
++      val = FIELD_GET(RTL8365MB_L2_MC_D3_MBR_HI1_MSK, data[3]);
++      mc->member |= FIELD_PREP(RTL8365MB_L2_MC_MBR_HI1_MSK, val);
++      val = FIELD_GET(RTL8365MB_L2_MC_D5_MBR_HI2_MSK, data[5]);
++      mc->member |= FIELD_PREP(RTL8365MB_L2_MC_MBR_HI2_MSK, val);
++
++      mc->igmpidx = FIELD_GET(RTL8365MB_L2_MC_D4_IGMPIDX_MSK, data[4]);
++      mc->igmp_asic = FIELD_GET(RTL8365MB_L2_MC_D5_IGMP_ASIC_MSK, data[5]);
++}
++
++static void rtl8365mb_l2_mc_to_data(const struct rtl8365mb_l2_mc *mc, u16 *data)
++{
++      u32 val;
++
++      memset(data, 0, RTL8365MB_L2_ENTRY_SIZE * 2);
++      data[0] |= FIELD_PREP(RTL8365MB_L2_MC_D0_MAC5_MSK, mc->key.mac_addr[5]);
++      data[0] |= FIELD_PREP(RTL8365MB_L2_MC_D0_MAC4_MSK, mc->key.mac_addr[4]);
++      data[1] |= FIELD_PREP(RTL8365MB_L2_MC_D1_MAC3_MSK, mc->key.mac_addr[3]);
++      data[1] |= FIELD_PREP(RTL8365MB_L2_MC_D1_MAC2_MSK, mc->key.mac_addr[2]);
++      data[2] |= FIELD_PREP(RTL8365MB_L2_MC_D2_MAC1_MSK, mc->key.mac_addr[1]);
++      data[2] |= FIELD_PREP(RTL8365MB_L2_MC_D2_MAC0_MSK, mc->key.mac_addr[0]);
++      data[3] |= FIELD_PREP(RTL8365MB_L2_MC_D3_VID_MSK, mc->key.vid);
++      data[3] |= FIELD_PREP(RTL8365MB_L2_MC_D3_IVL_MSK, mc->key.ivl);
++
++      val = FIELD_GET(RTL8365MB_L2_MC_MBR_HI1_MSK, mc->member);
++      data[3] |= FIELD_PREP(RTL8365MB_L2_MC_D3_MBR_HI1_MSK, val);
++
++      val = FIELD_GET(RTL8365MB_L2_MC_MBR_LO_MSK, mc->member);
++      data[4] |= FIELD_PREP(RTL8365MB_L2_MC_D4_MBR_MSK, val);
++
++      data[4] |= FIELD_PREP(RTL8365MB_L2_MC_D4_IGMPIDX_MSK, mc->igmpidx);
++      data[5] |= FIELD_PREP(RTL8365MB_L2_MC_D5_IGMP_ASIC_MSK, mc->igmp_asic);
++      data[5] |= FIELD_PREP(RTL8365MB_L2_MC_D5_PRIORITY_MSK, mc->priority);
++      data[5] |= FIELD_PREP(RTL8365MB_L2_MC_D5_FWD_PRI_MSK, mc->fwd_pri);
++      data[5] |= FIELD_PREP(RTL8365MB_L2_MC_D5_STATIC_MSK, mc->is_static);
++
++      val = FIELD_GET(RTL8365MB_L2_MC_MBR_HI2_MSK, mc->member);
++      data[5] |= FIELD_PREP(RTL8365MB_L2_MC_D5_MBR_HI2_MSK, val);
++}
++
++/*
++ * rtl8365mb_l2_get_next_uc() - get the next Unicast L2 entry
++ * @priv: realtek_priv pointer
++ * @addr: as input, the table index to start the walk
++ *        as output, the found table index
++ * @port: restrict the walk on entries related to port
++ * @entry: returned L2 Unicast table entry
++ *
++ * This function gets the next unicast L2 table entry starting from @addr
++ * and checking exclusively entries related to @port.
++ *
++ * On success, it returns 0, updates @addr to the index of the found entry,
++ * and populates @entry. If the search reaches the end of the table and
++ * wraps around and @addr will be strictly lower than the input @addr.
++ * Callers must detect this wrap-around condition to prevent infinite loops.
++ *
++ * If the table contains no matching entries at all, it returns -ENOENT
++ * and leaves @addr and @entry unmodified.
++ *
++ * Return: Returns 0 on success, a negative error on failure.
++ **/
++int rtl8365mb_l2_get_next_uc(struct realtek_priv *priv, u16 *addr, int port,
++                           struct realtek_fdb_entry *entry)
++{
++      u16 data[RTL8365MB_L2_ENTRY_SIZE] = { 0 };
++      struct rtl8365mb_l2_uc uc;
++      int ret;
++
++      ret = rtl8365mb_table_query(priv, RTL8365MB_TABLE_L2,
++                                  RTL8365MB_TABLE_OP_READ, addr,
++                                  RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT_UC_PORT,
++                                  port, data, RTL8365MB_L2_ENTRY_SIZE);
++      if (ret)
++              return ret;
++
++      rtl8365mb_l2_data_to_uc(data, &uc);
++
++      ether_addr_copy(entry->mac_addr, uc.key.mac_addr);
++      entry->vid = uc.key.vid;
++      entry->is_static = uc.is_static;
++
++      return 0;
++}
++
++int rtl8365mb_l2_add_uc(struct realtek_priv *priv, int port,
++                      const unsigned char mac_addr[static ETH_ALEN],
++                      u16 efid, u16 vid)
++{
++      u16 data[RTL8365MB_L2_ENTRY_SIZE] = { 0 };
++      struct rtl8365mb_l2_uc uc = { 0 };
++      u16 addr;
++      int ret;
++
++      memcpy(uc.key.mac_addr, mac_addr, ETH_ALEN);
++      uc.key.efid = efid;
++      uc.key.fid = 0;
++      uc.key.ivl = true;
++      uc.key.vid = vid;
++
++      uc.port = port;
++      /* Entries programmed by DSA (including those dynamically learned by
++       * the software bridge and injected into the CPU port via assisted
++       * learning) must be static. We do not let HW decrease age behind the
++       * OS's back. As a trade-off, these will show up as permanent to users.
++       */
++      uc.is_static = true;
++      /* age greater than 0 adds/updates entries */
++      uc.age = 1;
++      rtl8365mb_l2_uc_to_data(&uc, data);
++
++      /* add the new entry or update an existing one */
++      ret = rtl8365mb_table_query(priv, RTL8365MB_TABLE_L2,
++                                  RTL8365MB_TABLE_OP_WRITE, &addr,
++                                  0, 0,
++                                  data, RTL8365MB_L2_ENTRY_SIZE);
++
++      /* Assume the missing new entry as the table is full */
++      if (ret == -ENOENT)
++              return -ENOSPC;
++
++      /* addr will hold the table index, but it is not used here */
++      return ret;
++}
++
++int rtl8365mb_l2_del_uc(struct realtek_priv *priv, int port,
++                      const unsigned char mac_addr[static ETH_ALEN],
++                      u16 efid, u16 vid)
++{
++      u16 data[RTL8365MB_L2_ENTRY_SIZE] = { 0 };
++      struct rtl8365mb_l2_uc uc = { 0 };
++      u16 addr;
++      int ret;
++
++      memcpy(uc.key.mac_addr, mac_addr, ETH_ALEN);
++      uc.key.efid = efid;
++      uc.key.fid = 0;
++      uc.key.ivl = true;
++      uc.key.vid = vid;
++      /* age 0 deletes the entry */
++      uc.age = 0;
++      rtl8365mb_l2_uc_to_data(&uc, data);
++
++      /* it looks like the switch will always add/update the entry,
++       * even when age is 0 or uc.key did not match an existing entry,
++       * just to immediately drop it because age is zero. You can still
++       * get the added/updated address from @addr
++       */
++      ret = rtl8365mb_table_query(priv, RTL8365MB_TABLE_L2,
++                                  RTL8365MB_TABLE_OP_WRITE, &addr,
++                                  0, 0,
++                                  data, RTL8365MB_L2_ENTRY_SIZE);
++
++      if (ret == -ENOENT) {
++              dev_dbg(priv->dev, "%s: %pM vid=%d efid=%d missing\n",
++                      __func__, mac_addr, vid, efid);
++              /* Silently return success */
++              return 0;
++      }
++
++      /* addr will hold the table index, but it is not used here */
++      return ret;
++}
++
++int rtl8365mb_l2_flush(struct realtek_priv *priv, int port, u16 vid)
++{
++      int mode = vid ? RTL8365MB_L2_FLUSH_CTRL2_MODE_PORT_VID :
++                       RTL8365MB_L2_FLUSH_CTRL2_MODE_PORT;
++      u32 val, mask;
++      int ret;
++
++      mutex_lock(&priv->map_lock);
++
++      /* Configure flushing mode; only flush dynamic entries */
++      ret = regmap_write(priv->map_nolock, RTL8365MB_L2_FLUSH_CTRL2_REG,
++                         FIELD_PREP(RTL8365MB_L2_FLUSH_CTRL2_MODE_MSK,
++                                    mode) |
++                         FIELD_PREP(RTL8365MB_L2_FLUSH_CTRL2_TYPE_MSK,
++                                    RTL8365MB_L2_FLUSH_CTRL2_TYPE_DYNAMIC));
++      if (ret)
++              goto out;
++
++      ret = regmap_write(priv->map_nolock, RTL8365MB_L2_FLUSH_CTRL1_REG,
++                         FIELD_PREP(RTL8365MB_L2_FLUSH_CTRL1_VID_MSK, vid));
++
++      if (ret)
++              goto out;
++      /* Now issue the flush command and wait for its completion. There are
++       * two registers for this purpose, and which one to use depends on the
++       * port number. The _EXT register is for ports 8 or higher.
++       */
++      if (port < 8) {
++              val = FIELD_PREP(RTL8365MB_L2_FLUSH_PORT_MSK_MSK,
++                               BIT(port) & 0xFF);
++              ret = regmap_write(priv->map_nolock,
++                                 RTL8365MB_L2_FLUSH_PORT_REG, val);
++              if (ret)
++                      goto out;
++
++              mask = FIELD_PREP(RTL8365MB_L2_FLUSH_PORT_BUSY_MSK,
++                                BIT(port) & 0xFF);
++              ret = regmap_read_poll_timeout(priv->map_nolock,
++                                             RTL8365MB_L2_FLUSH_PORT_REG,
++                                             val, !(val & mask), 10, 10000);
++              if (ret)
++                      goto out;
++      } else {
++              val = FIELD_PREP(RTL8365MB_L2_FLUSH_PORT_EXT_MSK_MSK,
++                               BIT(port) >> 8);
++              ret = regmap_write(priv->map_nolock,
++                                 RTL8365MB_L2_FLUSH_PORT_EXT_REG, val);
++              if (ret)
++                      goto out;
++
++              mask = FIELD_PREP(RTL8365MB_L2_FLUSH_PORT_EXT_BUSY_MSK,
++                                BIT(port) >> 8);
++              ret = regmap_read_poll_timeout(priv->map_nolock,
++                                             RTL8365MB_L2_FLUSH_PORT_EXT_REG,
++                                             val, !(val & mask), 10, 10000);
++              if (ret)
++                      goto out;
++      }
++
++out:
++      mutex_unlock(&priv->map_lock);
++
++      return ret;
++}
++
++int rtl8365mb_l2_add_mc(struct realtek_priv *priv, int port,
++                      const unsigned char mac_addr[static ETH_ALEN],
++                      u16 vid)
++{
++      u16 data[RTL8365MB_L2_ENTRY_SIZE] = { 0 };
++      struct rtl8365mb_l2_mc mc = { 0 };
++      u16 addr;
++      int ret;
++
++      memcpy(mc.key.mac_addr, mac_addr, ETH_ALEN);
++      mc.key.vid = vid;
++      mc.key.ivl = true;
++      /* Already set the port and is_static, although not used in OP_READ,
++       * data will be ready for OP_WRITE if it is a new entry.
++       */
++      mc.member |= BIT(port);
++      mc.is_static = 1;
++      rtl8365mb_l2_mc_to_data(&mc, data);
++
++      /* First look for an existing entry (to get existing port members) */
++      ret = rtl8365mb_table_query(priv, RTL8365MB_TABLE_L2,
++                                  RTL8365MB_TABLE_OP_READ, &addr,
++                                  RTL8365MB_TABLE_L2_METHOD_MAC, 0,
++                                  data, RTL8365MB_L2_ENTRY_SIZE);
++      if (!ret) {
++              /* There is already an entry... */
++              rtl8365mb_l2_data_to_mc(data, &mc);
++              dev_dbg(priv->dev,
++                      "%s: found %pM addr=%d member=0x%x igmpidx=0x%x %s\n",
++                      __func__, mac_addr, addr, mc.member, mc.igmpidx,
++                      mc.is_static ? "static" : "dynamic");
++              /* the port must be added as a member */
++              mc.member |= BIT(port);
++
++              if (!mc.is_static) {
++                      dev_dbg(priv->dev,
++                              "%s: promoting addr=%d group to static\n",
++                              __func__, addr);
++                      mc.is_static = 1;
++              }
++
++              rtl8365mb_l2_mc_to_data(&mc, data);
++      } else if (ret == -ENOENT) {
++              /* New entry, no need to update data again as it already
++               * includes the member.
++               *
++               * Multicast hardware entries do not support EFID (bridge
++               * isolation). However, traffic isolation is still maintained
++               * because the hardware applies the port isolation masks
++               * (pmasks) configured in bridge_join after the L2 lookup.
++               * Entries from different bridges will collide on the same
++               * MAC+VID slot with an OR'ed member mask, but packets will
++               * only exit through ports allowed by the source port's pmask.
++               */
++      } else {
++              return ret;
++      }
++
++      /* add the new entry or update an existing one */
++      ret = rtl8365mb_table_query(priv, RTL8365MB_TABLE_L2,
++                                  RTL8365MB_TABLE_OP_WRITE, &addr,
++                                  0, 0,
++                                  data, RTL8365MB_L2_ENTRY_SIZE);
++
++      /* Assume the missing new entry as the table is full */
++      if (ret == -ENOENT)
++              return -ENOSPC;
++
++      return ret;
++}
++
++int rtl8365mb_l2_del_mc(struct realtek_priv *priv, int port,
++                      const unsigned char mac_addr[static ETH_ALEN],
++                      u16 vid)
++{
++      u16 data[RTL8365MB_L2_ENTRY_SIZE] = { 0 };
++      struct rtl8365mb_l2_mc mc = { 0 };
++      u16 addr;
++      int ret;
++
++      memcpy(mc.key.mac_addr, mac_addr, ETH_ALEN);
++      mc.key.vid = vid;
++      mc.key.ivl = true;
++      rtl8365mb_l2_mc_to_data(&mc, data);
++
++      /* First look for an existing entry (to get existing port members) */
++      ret = rtl8365mb_table_query(priv, RTL8365MB_TABLE_L2,
++                                  RTL8365MB_TABLE_OP_READ, &addr,
++                                  RTL8365MB_TABLE_L2_METHOD_MAC, 0,
++                                  data, RTL8365MB_L2_ENTRY_SIZE);
++      if (ret == -ENOENT) {
++              dev_dbg(priv->dev, "%s: %pM vid=%d missing\n",
++                      __func__, mac_addr, vid);
++              /* Silently return success */
++              return 0;
++      }
++
++      if (ret)
++              /* Return on any other error */
++              return ret;
++
++      rtl8365mb_l2_data_to_mc(data, &mc);
++      dev_dbg(priv->dev,
++              "%s: found %pM addr=%d member=0x%x igmpidx=0x%x %s\n",
++              __func__, mac_addr, addr, mc.member, mc.igmpidx,
++              mc.is_static ? "static" : "dynamic");
++      /* the port must be removed as a member */
++      mc.member &= ~BIT(port);
++      if (!mc.member) {
++              /* Multicast entries do not have an age field. Clearing both
++               * the member portmask and is_static flags is the hardware
++               * signal to invalidate and reclaim the L2 table slot.
++               */
++              mc.is_static = 0;
++              mc.igmpidx = 0;
++              mc.priority = 0;
++              mc.fwd_pri = 0;
++              mc.igmp_asic = 0;
++      }
++      rtl8365mb_l2_mc_to_data(&mc, data);
++
++      /* update the existing entry. */
++      ret = rtl8365mb_table_query(priv, RTL8365MB_TABLE_L2,
++                                  RTL8365MB_TABLE_OP_WRITE, &addr,
++                                  0, 0,
++                                  data, RTL8365MB_L2_ENTRY_SIZE);
++      return ret;
++}
+--- /dev/null
++++ b/drivers/net/dsa/realtek/rtl8365mb_l2.h
+@@ -0,0 +1,32 @@
++/* SPDX-License-Identifier: GPL-2.0 */
++/* Forwarding and multicast database interface for the rtl8365mb switch family
++ *
++ * Copyright (C) 2022 Alvin Šipraga <alsi@bang-olufsen.dk>
++ */
++
++#ifndef _REALTEK_RTL8365MB_L2_H
++#define _REALTEK_RTL8365MB_L2_H
++
++#include <linux/if_ether.h>
++#include <linux/types.h>
++
++#include "realtek.h"
++
++int rtl8365mb_l2_get_next_uc(struct realtek_priv *priv, u16 *addr, int port,
++                           struct realtek_fdb_entry *entry);
++int rtl8365mb_l2_add_uc(struct realtek_priv *priv, int port,
++                      const unsigned char addr[static ETH_ALEN],
++                      u16 efid, u16 vid);
++int rtl8365mb_l2_del_uc(struct realtek_priv *priv, int port,
++                      const unsigned char addr[static ETH_ALEN],
++                      u16 efid, u16 vid);
++int rtl8365mb_l2_flush(struct realtek_priv *priv, int port, u16 vid);
++
++int rtl8365mb_l2_add_mc(struct realtek_priv *priv, int port,
++                      const unsigned char mac_addr[static ETH_ALEN],
++                      u16 vid);
++int rtl8365mb_l2_del_mc(struct realtek_priv *priv, int port,
++                      const unsigned char mac_addr[static ETH_ALEN],
++                      u16 vid);
++
++#endif /* _REALTEK_RTL8365MB_L2_H */
+--- a/drivers/net/dsa/realtek/rtl8365mb_main.c
++++ b/drivers/net/dsa/realtek/rtl8365mb_main.c
+@@ -104,6 +104,7 @@
+ #include "realtek-smi.h"
+ #include "realtek-mdio.h"
+ #include "rtl83xx.h"
++#include "rtl8365mb_l2.h"
+ #include "rtl8365mb_vlan.h"
+ /* Family-specific data and limits */
+@@ -111,8 +112,12 @@
+ #define RTL8365MB_NUM_PHYREGS         32
+ #define RTL8365MB_PHYREGMAX           (RTL8365MB_NUM_PHYREGS - 1)
+ #define RTL8365MB_MAX_NUM_PORTS               11
+-#define RTL8365MB_MAX_NUM_EXTINTS     3
++/* Valid for the whole family except RTL8370B, which has 4160 entries.
++ * RTL8370B is mentioned in vendor code but it might not even belong
++ * to the same RTL8367C family.
++ */
+ #define RTL8365MB_LEARN_LIMIT_MAX     2112
++#define RTL8365MB_MAX_NUM_EXTINTS     3
+ /* Chip identification registers */
+ #define RTL8365MB_CHIP_ID_REG         0x1300
+@@ -285,6 +290,15 @@
+               (RTL8365MB_PORT_ISOLATION_REG_BASE + (_physport))
+ #define   RTL8365MB_PORT_ISOLATION_MASK                       0x07FF
++/* Extended filter ID registers - used to key forwarding database with IVL */
++#define RTL8365MB_EFID_MASK                   GENMASK(2, 0)
++#define RTL8365MB_PORT_EFID_REG_BASE          0x0A32
++#define RTL8365MB_PORT_EFID_REG(_p) \
++              (RTL8365MB_PORT_EFID_REG_BASE + ((_p) >> 2))
++#define   RTL8365MB_PORT_EFID_OFFSET(_p)      (((_p) & 0x3) << 2)
++#define   RTL8365MB_PORT_EFID_MASK(_p) \
++              (RTL8365MB_EFID_MASK << RTL8365MB_PORT_EFID_OFFSET(_p))
++
+ /* MSTP port state registers - indexed by tree instance */
+ #define RTL8365MB_MSTI_CTRL_BASE                      0x0A00
+ #define RTL8365MB_MSTI_CTRL_REG(_msti, _physport) \
+@@ -2432,6 +2446,11 @@ static int rtl8365mb_setup(struct dsa_sw
+       if (ret)
+               goto out_teardown_irq;
++      ds->assisted_learning_on_cpu_port = true;
++      ds->fdb_isolation = true;
++      /* The EFID is 3 bits, but EFID 0 is reserved for standalone ports */
++      ds->max_num_bridges = FIELD_MAX(RTL8365MB_EFID_MASK);
++
+       ds->configure_vlan_while_not_filtering = true;
+       /* Set up VLAN */
+@@ -2549,6 +2568,12 @@ static const struct dsa_switch_ops rtl83
+       .teardown = rtl8365mb_teardown,
+       .phylink_get_caps = rtl8365mb_phylink_get_caps,
+       .port_stp_state_set = rtl8365mb_port_stp_state_set,
++      .port_fast_age = rtl83xx_port_fast_age,
++      .port_fdb_add = rtl83xx_port_fdb_add,
++      .port_fdb_del = rtl83xx_port_fdb_del,
++      .port_fdb_dump = rtl83xx_port_fdb_dump,
++      .port_mdb_add = rtl83xx_port_mdb_add,
++      .port_mdb_del = rtl83xx_port_mdb_del,
+       .port_vlan_add = rtl8365mb_port_vlan_add,
+       .port_vlan_del = rtl8365mb_port_vlan_del,
+       .port_vlan_filtering = rtl8365mb_port_vlan_filtering,
+@@ -2565,6 +2590,12 @@ static const struct dsa_switch_ops rtl83
+ static const struct realtek_ops rtl8365mb_ops = {
+       .detect = rtl8365mb_detect,
++      .l2_add_uc = rtl8365mb_l2_add_uc,
++      .l2_del_uc = rtl8365mb_l2_del_uc,
++      .l2_get_next_uc = rtl8365mb_l2_get_next_uc,
++      .l2_add_mc = rtl8365mb_l2_add_mc,
++      .l2_del_mc = rtl8365mb_l2_del_mc,
++      .l2_flush = rtl8365mb_l2_flush,
+       .phy_read = rtl8365mb_phy_read,
+       .phy_write = rtl8365mb_phy_write,
+ };
+--- 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/etherdevice.h>
+ #include "realtek.h"
+ #include "rtl83xx.h"
+@@ -156,6 +157,7 @@ rtl83xx_probe(struct device *dev,
+       mutex_init(&priv->map_lock);
+       mutex_init(&priv->vlan_lock);
++      mutex_init(&priv->l2_lock);
+       rc.lock_arg = priv;
+       priv->map = devm_regmap_init(dev, NULL, priv, &rc);
+@@ -326,6 +328,296 @@ void rtl83xx_reset_deassert(struct realt
+       gpiod_set_value(priv->reset, false);
+ }
++/**
++ * rtl83xx_port_fast_age() - flush dynamic FDB entries learned on a port
++ * @ds: DSA switch instance
++ * @port: port index
++ *
++ * This function requests the switch to age out dynamic FDB entries learned on
++ * @port.
++ *
++ * Context: Can sleep.
++ * Return: Nothing.
++ */
++void rtl83xx_port_fast_age(struct dsa_switch *ds, int port)
++{
++      struct realtek_priv *priv = ds->priv;
++      int ret;
++
++      if (!priv->ops->l2_flush) {
++              dev_warn_once(priv->dev, "l2_flush op not defined\n");
++              return;
++      }
++
++      dev_dbg(priv->dev, "fast_age port %d\n", port);
++
++      mutex_lock(&priv->l2_lock);
++      ret = priv->ops->l2_flush(priv, port, 0);
++      mutex_unlock(&priv->l2_lock);
++      if (ret)
++              dev_err(priv->dev, "failed to fast age on port %d: %d\n", port,
++                      ret);
++}
++EXPORT_SYMBOL_NS_GPL(rtl83xx_port_fast_age, "REALTEK_DSA");
++
++/**
++ * rtl83xx_port_fdb_add() - add a static FDB entry to a port database
++ * @ds: DSA switch instance
++ * @port: port index
++ * @addr: MAC address to add
++ * @vid: VLAN ID associated with @addr
++ * @db: database where the entry should be added
++ *
++ * This function adds a static unicast FDB entry to the standalone port
++ * database or to a bridge database.
++ *
++ * Context: Can sleep.
++ * Return: 0 on success, negative value for failure.
++ */
++int rtl83xx_port_fdb_add(struct dsa_switch *ds, int port,
++                       const unsigned char *addr, u16 vid,
++                       struct dsa_db db)
++{
++      struct realtek_priv *priv = ds->priv;
++      int efid;
++      int ret;
++
++      if (is_multicast_ether_addr(addr))
++              return -EOPNOTSUPP;
++
++      if (!priv->ops->l2_add_uc)
++              return -EOPNOTSUPP;
++
++      if (db.type != DSA_DB_PORT && db.type != DSA_DB_BRIDGE)
++              return -EOPNOTSUPP;
++
++      /* Bridge ports use bridge.num as EFID, while standalone ports use
++       * EFID 0. FDB entries for the CPU port follow the bridge EFID due
++       * to assisted learning.
++       */
++      efid = db.type == DSA_DB_BRIDGE ? db.bridge.num : 0;
++
++      dev_dbg(priv->dev, "%s: port:%d addr:%pM efid:%d vid:%d dbtype:%d\n",
++              __func__, port, addr, efid, vid, db.type);
++
++      mutex_lock(&priv->l2_lock);
++      ret = priv->ops->l2_add_uc(priv, port, addr, efid, vid);
++
++      mutex_unlock(&priv->l2_lock);
++
++      if (ret)
++              dev_err(priv->dev, "fdb_add ERROR %pe\n", ERR_PTR(ret));
++      return ret;
++}
++EXPORT_SYMBOL_NS_GPL(rtl83xx_port_fdb_add, "REALTEK_DSA");
++
++/**
++ * rtl83xx_port_fdb_del() - delete a static FDB entry from a port database
++ * @ds: DSA switch instance
++ * @port: port index
++ * @addr: MAC address to delete
++ * @vid: VLAN ID associated with @addr
++ * @db: database where the entry should be removed
++ *
++ * This function deletes a static unicast FDB entry from the standalone port
++ * database or from a bridge database.
++ *
++ * Context: Can sleep.
++ * Return: 0 on success, negative value for failure.
++ */
++int rtl83xx_port_fdb_del(struct dsa_switch *ds, int port,
++                       const unsigned char *addr, u16 vid,
++                       struct dsa_db db)
++{
++      struct realtek_priv *priv = ds->priv;
++      int efid;
++      int ret;
++
++      if (is_multicast_ether_addr(addr))
++              return -EOPNOTSUPP;
++
++      if (!priv->ops->l2_del_uc)
++              return -EOPNOTSUPP;
++
++      if (db.type != DSA_DB_PORT && db.type != DSA_DB_BRIDGE)
++              return -EOPNOTSUPP;
++
++      /*
++       * DSA_DB_BRIDGE ports use bridge number [1..N] as EFID, while
++       * DSA_DB_PORT use the default EFID (0), not used by any bridge.
++       */
++      efid = db.type == DSA_DB_BRIDGE ? db.bridge.num : 0;
++
++      dev_dbg(priv->dev, "%s: port:%d addr:%pM efid:%d vid:%d dbtype:%d\n",
++              __func__, port, addr, efid, vid, db.type);
++
++      mutex_lock(&priv->l2_lock);
++      ret = priv->ops->l2_del_uc(priv, port, addr, efid, vid);
++      mutex_unlock(&priv->l2_lock);
++
++      if (ret)
++              dev_err(priv->dev, "fdb_del ERROR %pe\n", ERR_PTR(ret));
++      return ret;
++}
++EXPORT_SYMBOL_NS_GPL(rtl83xx_port_fdb_del, "REALTEK_DSA");
++
++/**
++ * rtl83xx_port_fdb_dump() - iterate over FDB entries associated with a port
++ * @ds: DSA switch instance
++ * @port: port index
++ * @cb: callback invoked for each entry
++ * @data: opaque pointer passed to @cb
++ *
++ * This function walks the unicast FDB entries associated with @port and calls
++ * @cb for each matching entry.
++ *
++ * Context: Can sleep.
++ * Return: 0 on success, or negative value for failure.
++ */
++int rtl83xx_port_fdb_dump(struct dsa_switch *ds, int port,
++                        dsa_fdb_dump_cb_t *cb, void *data)
++{
++      struct realtek_fdb_entry entry = { 0 };
++      struct realtek_priv *priv = ds->priv;
++      u16 start_addr, addr = 0;
++      int ret = 0;
++
++      if (!priv->ops->l2_get_next_uc)
++              return -EOPNOTSUPP;
++
++      mutex_lock(&priv->l2_lock);
++      while (true) {
++              start_addr = addr;
++
++              dev_dbg(priv->dev, "l2_get_next_uc, addr:%d, port:%d\n",
++                      addr, port);
++              ret = priv->ops->l2_get_next_uc(priv, &addr, port, &entry);
++              dev_dbg(priv->dev,
++                      "%s addr:%d mac:%pM vid:%d static:%d ret:%pe\n",
++                      __func__, addr, entry.mac_addr, entry.vid,
++                      entry.is_static, ERR_PTR(ret));
++
++              if (ret == -ENOENT) {
++                      /* If the table is empty, returns without errors. Note
++                       * that the l2_get_next_uc overflow to the first match
++                       * when it reaches the end of the table.
++                       */
++                      ret = 0;
++                      break;
++              }
++
++              if (ret)
++                      break;
++
++              /* When the addr returned is before the requested one, it
++               * indicates that we reached the end.
++               */
++              if (addr < start_addr)
++                      break;
++
++              ret = cb(entry.mac_addr, entry.vid, entry.is_static, data);
++              if (ret)
++                      break;
++
++              addr++;
++      }
++      mutex_unlock(&priv->l2_lock);
++
++      return ret;
++}
++EXPORT_SYMBOL_NS_GPL(rtl83xx_port_fdb_dump, "REALTEK_DSA");
++
++/**
++ * rtl83xx_port_mdb_add() - add a multicast database entry to a port database
++ * @ds: DSA switch instance
++ * @port: port index
++ * @mdb: multicast database entry to add
++ * @db: database where the entry should be added
++ *
++ * This function adds a multicast database entry to the standalone port
++ * database or to a bridge database.
++ *
++ * Context: Can sleep.
++ * Return: 0 on success, negative value for failure.
++ */
++int rtl83xx_port_mdb_add(struct dsa_switch *ds, int port,
++                       const struct switchdev_obj_port_mdb *mdb,
++                       struct dsa_db db)
++{
++      struct realtek_priv *priv = ds->priv;
++      const unsigned char *addr = mdb->addr;
++      u16 vid = mdb->vid;
++      int efid;
++      int ret;
++
++      if (!priv->ops->l2_add_mc)
++              return -EOPNOTSUPP;
++
++      if (db.type != DSA_DB_PORT && db.type != DSA_DB_BRIDGE)
++              return -EOPNOTSUPP;
++
++      /* EFID is not used by hardware MDB entries; debugging only */
++      efid = db.type == DSA_DB_BRIDGE ? db.bridge.num : 0;
++
++      dev_dbg(priv->dev, "%s: port:%d addr:%pM efid:%d vid:%d dbtype:%d\n",
++              __func__, port, addr, efid, vid, db.type);
++
++      mutex_lock(&priv->l2_lock);
++      ret = priv->ops->l2_add_mc(priv, port, addr, vid);
++      mutex_unlock(&priv->l2_lock);
++
++      if (ret)
++              dev_err(priv->dev, "mdb_add ERROR %pe\n", ERR_PTR(ret));
++      return ret;
++}
++EXPORT_SYMBOL_NS_GPL(rtl83xx_port_mdb_add, "REALTEK_DSA");
++
++/**
++ * rtl83xx_port_mdb_del() - delete a multicast database entry from a port
++ * database
++ * @ds: DSA switch instance
++ * @port: port index
++ * @mdb: multicast database entry to delete
++ * @db: database where the entry should be removed
++ *
++ * This function deletes a multicast database entry from the standalone port
++ * database or from a bridge database.
++ *
++ * Context: Can sleep.
++ * Return: 0 on success, negative value for failure.
++ */
++int rtl83xx_port_mdb_del(struct dsa_switch *ds, int port,
++                       const struct switchdev_obj_port_mdb *mdb,
++                       struct dsa_db db)
++{
++      struct realtek_priv *priv = ds->priv;
++      const unsigned char *addr = mdb->addr;
++      u16 vid = mdb->vid;
++      int efid;
++      int ret;
++
++      if (!priv->ops->l2_del_mc)
++              return -EOPNOTSUPP;
++
++      if (db.type != DSA_DB_PORT && db.type != DSA_DB_BRIDGE)
++              return -EOPNOTSUPP;
++
++      /* EFID is not used by hardware MDB entries; debugging only */
++      efid = db.type == DSA_DB_BRIDGE ? db.bridge.num : 0;
++
++      dev_dbg(priv->dev, "%s: port:%d addr:%pM efid:%d vid:%d dbtype:%d\n",
++              __func__, port, addr, efid, vid, db.type);
++
++      mutex_lock(&priv->l2_lock);
++      ret = priv->ops->l2_del_mc(priv, port, addr, vid);
++      mutex_unlock(&priv->l2_lock);
++
++      if (ret)
++              dev_err(priv->dev, "mdb_del ERROR %pe\n", ERR_PTR(ret));
++      return ret;
++}
++EXPORT_SYMBOL_NS_GPL(rtl83xx_port_mdb_del, "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
+@@ -21,4 +21,20 @@ void rtl83xx_remove(struct realtek_priv
+ void rtl83xx_reset_assert(struct realtek_priv *priv);
+ void rtl83xx_reset_deassert(struct realtek_priv *priv);
++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,
++                       struct dsa_db db);
++int rtl83xx_port_fdb_del(struct dsa_switch *ds, int port,
++                       const unsigned char *addr, u16 vid,
++                       struct dsa_db db);
++int rtl83xx_port_fdb_dump(struct dsa_switch *ds, int port,
++                        dsa_fdb_dump_cb_t *cb, void *data);
++int rtl83xx_port_mdb_add(struct dsa_switch *ds, int port,
++                       const struct switchdev_obj_port_mdb *mdb,
++                       struct dsa_db db);
++int rtl83xx_port_mdb_del(struct dsa_switch *ds, int port,
++                       const struct switchdev_obj_port_mdb *mdb,
++                       struct dsa_db db);
++
+ #endif /* _RTL83XX_H */
diff --git a/target/linux/generic/backport-6.18/942-08-7.2-net-dsa-realtek-rtl8365mb-add-port_bridge_-join-leav.patch b/target/linux/generic/backport-6.18/942-08-7.2-net-dsa-realtek-rtl8365mb-add-port_bridge_-join-leav.patch
new file mode 100644 (file)
index 0000000..16cb7f4
--- /dev/null
@@ -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,
diff --git a/target/linux/generic/backport-6.18/942-09-7.2-net-dsa-realtek-rtl8365mb-add-bridge-port-flags.patch b/target/linux/generic/backport-6.18/942-09-7.2-net-dsa-realtek-rtl8365mb-add-bridge-port-flags.patch
new file mode 100644 (file)
index 0000000..0d029bf
--- /dev/null
@@ -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,