--- /dev/null
+// 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;
+}
#include <linux/module.h>
#include <linux/regmap.h>
#include <linux/of_mdio.h>
+#include <linux/etherdevice.h>
#include "realtek.h"
#include "rtl83xx.h"
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);
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");