Contributors: 5
Author Tokens Token Proportion Commits Commit Proportion
Alvin Šipraga 2649 83.59% 2 14.29%
Linus Walleij 421 13.28% 3 21.43%
Luiz Angelo Daros de Luca 74 2.34% 7 50.00%
Andrew Lunn 20 0.63% 1 7.14%
Vladimir Oltean 5 0.16% 1 7.14%
Total 3169 14


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