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