summaryrefslogtreecommitdiffstats
path: root/drivers/net/dsa/mxl862xx
diff options
context:
space:
mode:
authorBreno Leitao <leitao@debian.org>2026-08-05 07:52:36 -0700
committerTejun Heo <tj@kernel.org>2026-08-10 11:47:20 -1000
commita5bde5d8fde8a8cb28e59a672d5ddc5b9c1e7656 (patch)
tree39f5b258386e1127d253d293194e9a2991bf3c34 /drivers/net/dsa/mxl862xx
downloadlinux-stable-a5bde5d8fde8a8cb28e59a672d5ddc5b9c1e7656.tar.gz
linux-stable-a5bde5d8fde8a8cb28e59a672d5ddc5b9c1e7656.zip
workqueue: skip the node_nr_active update for non-unbound workqueuesgrafted
apply_wqattrs_commit() updates node_nr_active->max unconditionally. wq->node_nr_active[] is only allocated for unbound workqueues, so guard the call before per-cpu workqueues start using this path. No functional change: only unbound workqueues reach apply_wqattrs_*() today. Signed-off-by: Breno Leitao <leitao@debian.org> Signed-off-by: Tejun Heo <tj@kernel.org>
Diffstat (limited to 'drivers/net/dsa/mxl862xx')
-rw-r--r--drivers/net/dsa/mxl862xx/Kconfig12
-rw-r--r--drivers/net/dsa/mxl862xx/Makefile3
-rw-r--r--drivers/net/dsa/mxl862xx/mxl862xx-api.h1584
-rw-r--r--drivers/net/dsa/mxl862xx/mxl862xx-cmd.h85
-rw-r--r--drivers/net/dsa/mxl862xx/mxl862xx-host.c504
-rw-r--r--drivers/net/dsa/mxl862xx/mxl862xx-host.h22
-rw-r--r--drivers/net/dsa/mxl862xx/mxl862xx-phylink.c446
-rw-r--r--drivers/net/dsa/mxl862xx/mxl862xx-phylink.h21
-rw-r--r--drivers/net/dsa/mxl862xx/mxl862xx.c2213
-rw-r--r--drivers/net/dsa/mxl862xx/mxl862xx.h343
10 files changed, 5233 insertions, 0 deletions
diff --git a/drivers/net/dsa/mxl862xx/Kconfig b/drivers/net/dsa/mxl862xx/Kconfig
new file mode 100644
index 000000000..e51a67a3c
--- /dev/null
+++ b/drivers/net/dsa/mxl862xx/Kconfig
@@ -0,0 +1,12 @@
+# SPDX-License-Identifier: GPL-2.0-only
+config NET_DSA_MXL862
+ tristate "MaxLinear MxL862xx"
+ depends on NET_DSA
+ select CRC16
+ select NET_DSA_TAG_MXL_862XX
+ help
+ This enables support for the MaxLinear MxL862xx switch family.
+ These switches have two 10GE SerDes interfaces, one typically
+ used as CPU port.
+ - MxL86282 has eight 2.5 Gigabit PHYs
+ - MxL86252 has five 2.5 Gigabit PHYs
diff --git a/drivers/net/dsa/mxl862xx/Makefile b/drivers/net/dsa/mxl862xx/Makefile
new file mode 100644
index 000000000..a7be0e666
--- /dev/null
+++ b/drivers/net/dsa/mxl862xx/Makefile
@@ -0,0 +1,3 @@
+# SPDX-License-Identifier: GPL-2.0
+obj-$(CONFIG_NET_DSA_MXL862) += mxl862xx_dsa.o
+mxl862xx_dsa-y := mxl862xx.o mxl862xx-host.o mxl862xx-phylink.o
diff --git a/drivers/net/dsa/mxl862xx/mxl862xx-api.h b/drivers/net/dsa/mxl862xx/mxl862xx-api.h
new file mode 100644
index 000000000..a180a5dec
--- /dev/null
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-api.h
@@ -0,0 +1,1584 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#ifndef __MXL862XX_API_H
+#define __MXL862XX_API_H
+
+#include <linux/bits.h>
+#include <linux/if_ether.h>
+
+/**
+ * struct mdio_relay_data - relayed access to the switch internal MDIO bus
+ * @data: data to be read or written
+ * @phy: PHY index
+ * @mmd: MMD device
+ * @reg: register index
+ */
+struct mdio_relay_data {
+ __le16 data;
+ u8 phy;
+ u8 mmd;
+ __le16 reg;
+} __packed;
+
+/**
+ * struct mxl862xx_register_mod - Register access parameter to directly
+ * modify internal registers
+ * @addr: Register address offset for modification
+ * @data: Value to write to the register address
+ * @mask: Mask of bits to be modified (1 to modify, 0 to ignore)
+ *
+ * Used for direct register modification operations.
+ */
+struct mxl862xx_register_mod {
+ __le16 addr;
+ __le16 data;
+ __le16 mask;
+} __packed;
+
+/**
+ * enum mxl862xx_mac_table_filter - Source/Destination MAC address filtering
+ *
+ * @MXL862XX_MAC_FILTER_NONE: no filter
+ * @MXL862XX_MAC_FILTER_SRC: source address filter
+ * @MXL862XX_MAC_FILTER_DEST: destination address filter
+ * @MXL862XX_MAC_FILTER_BOTH: both source and destination filter
+ */
+enum mxl862xx_mac_table_filter {
+ MXL862XX_MAC_FILTER_NONE = 0,
+ MXL862XX_MAC_FILTER_SRC = BIT(0),
+ MXL862XX_MAC_FILTER_DEST = BIT(1),
+ MXL862XX_MAC_FILTER_BOTH = BIT(0) | BIT(1),
+};
+
+#define MXL862XX_TCI_VLAN_ID GENMASK(11, 0)
+#define MXL862XX_TCI_VLAN_CFI_DEI BIT(12)
+#define MXL862XX_TCI_VLAN_PRI GENMASK(15, 13)
+
+/* Set in port_id to use port_map[] as a portmap bitmap instead of a single
+ * port ID. When clear, port_id selects one port; when set, the firmware
+ * ignores the lower bits of port_id and writes port_map[] directly into
+ * the PCE bridge port map.
+ */
+#define MXL862XX_PORTMAP_FLAG BIT(31)
+
+/**
+ * struct mxl862xx_mac_table_add - MAC Table Entry to be added
+ * @fid: Filtering Identifier (FID) (not supported by all switches)
+ * @port_id: Ethernet Port number
+ * @port_map: Bridge Port Map
+ * @sub_if_id: Sub-Interface Identifier Destination
+ * @age_timer: Aging Time in seconds
+ * @vlan_id: STAG VLAN Id
+ * @static_entry: Static Entry (value will be aged out if not set to static)
+ * @traffic_class: Egress queue traffic class
+ * @mac: MAC Address to add to the table
+ * @filter_flag: See &enum mxl862xx_mac_table_filter
+ * @igmp_controlled: Packet is marked as IGMP controlled if destination MAC
+ * address matches MAC in this entry
+ * @associated_mac: Associated Mac address
+ * @tci: TCI for B-Step
+ * Bit [0:11] - VLAN ID
+ * Bit [12] - VLAN CFI/DEI
+ * Bit [13:15] - VLAN PRI
+ */
+struct mxl862xx_mac_table_add {
+ __le16 fid;
+ __le32 port_id;
+ __le16 port_map[8];
+ __le16 sub_if_id;
+ __le32 age_timer;
+ __le16 vlan_id;
+ u8 static_entry;
+ u8 traffic_class;
+ u8 mac[ETH_ALEN];
+ u8 filter_flag;
+ u8 igmp_controlled;
+ u8 associated_mac[ETH_ALEN];
+ __le16 tci;
+} __packed;
+
+/**
+ * struct mxl862xx_mac_table_remove - MAC Table Entry to be removed
+ * @fid: Filtering Identifier (FID)
+ * @mac: MAC Address to be removed from the table.
+ * @filter_flag: See &enum mxl862xx_mac_table_filter
+ * @tci: TCI for B-Step
+ * Bit [0:11] - VLAN ID
+ * Bit [12] - VLAN CFI/DEI
+ * Bit [13:15] - VLAN PRI
+ */
+struct mxl862xx_mac_table_remove {
+ __le16 fid;
+ u8 mac[ETH_ALEN];
+ u8 filter_flag;
+ __le16 tci;
+} __packed;
+
+/**
+ * struct mxl862xx_mac_table_read - MAC Table Entry to be read
+ * @initial: Restart the get operation from the beginning of the table
+ * @last: Indicates that the read operation returned last entry
+ * @fid: Get the MAC table entry belonging to the given Filtering Identifier
+ * @port_id: The Bridge Port ID
+ * @port_map: Bridge Port Map
+ * @age_timer: Aging Time
+ * @vlan_id: STAG VLAN Id
+ * @static_entry: Indicates if this is a Static Entry
+ * @sub_if_id: Sub-Interface Identifier Destination
+ * @mac: MAC Address. Filled out by the switch API implementation.
+ * @filter_flag: See &enum mxl862xx_mac_table_filter
+ * @igmp_controlled: Packet is marked as IGMP controlled if destination MAC
+ * address matches the MAC in this entry
+ * @entry_changed: Indicate if the Entry has Changed
+ * @associated_mac: Associated MAC address
+ * @hit_status: MAC Table Hit Status Update
+ * @tci: TCI for B-Step
+ * Bit [0:11] - VLAN ID
+ * Bit [12] - VLAN CFI/DEI
+ * Bit [13:15] - VLAN PRI
+ * @first_bridge_port_id: The port this MAC address has first been learned.
+ * This is used for loop detection.
+ */
+struct mxl862xx_mac_table_read {
+ u8 initial;
+ u8 last;
+ __le16 fid;
+ __le32 port_id;
+ __le16 port_map[8];
+ __le32 age_timer;
+ __le16 vlan_id;
+ u8 static_entry;
+ __le16 sub_if_id;
+ u8 mac[ETH_ALEN];
+ u8 filter_flag;
+ u8 igmp_controlled;
+ u8 entry_changed;
+ u8 associated_mac[ETH_ALEN];
+ u8 hit_status;
+ __le16 tci;
+ __le16 first_bridge_port_id;
+} __packed;
+
+/**
+ * struct mxl862xx_mac_table_query - MAC Table Entry key-based lookup
+ * @mac: MAC Address to search for (input)
+ * @fid: Filtering Identifier (input)
+ * @found: Set by firmware: 1 if entry was found, 0 if not
+ * @port_id: Bridge Port ID (output; MSB set if portmap mode)
+ * @port_map: Bridge Port Map (output; valid for static entries)
+ * @sub_if_id: Sub-Interface Identifier Destination
+ * @age_timer: Aging Time
+ * @vlan_id: STAG VLAN Id
+ * @static_entry: Indicates if this is a Static Entry
+ * @filter_flag: See &enum mxl862xx_mac_table_filter (input+output)
+ * @igmp_controlled: IGMP controlled flag
+ * @entry_changed: Entry changed flag
+ * @associated_mac: Associated MAC address
+ * @hit_status: MAC Table Hit Status Update
+ * @tci: TCI (VLAN ID + CFI/DEI + PRI) (input)
+ * @first_bridge_port_id: First learned bridge port
+ */
+struct mxl862xx_mac_table_query {
+ u8 mac[ETH_ALEN];
+ __le16 fid;
+ u8 found;
+ __le32 port_id;
+ __le16 port_map[8];
+ __le16 sub_if_id;
+ __le32 age_timer;
+ __le16 vlan_id;
+ u8 static_entry;
+ u8 filter_flag;
+ u8 igmp_controlled;
+ u8 entry_changed;
+ u8 associated_mac[ETH_ALEN];
+ u8 hit_status;
+ __le16 tci;
+ __le16 first_bridge_port_id;
+} __packed;
+
+/**
+ * enum mxl862xx_mac_clear_type - MAC table clear type
+ * @MXL862XX_MAC_CLEAR_PHY_PORT: clear dynamic entries based on port_id
+ * @MXL862XX_MAC_CLEAR_DYNAMIC: clear all dynamic entries
+ */
+enum mxl862xx_mac_clear_type {
+ MXL862XX_MAC_CLEAR_PHY_PORT = 0,
+ MXL862XX_MAC_CLEAR_DYNAMIC,
+};
+
+/**
+ * struct mxl862xx_mac_table_clear - MAC table clear
+ * @type: see &enum mxl862xx_mac_clear_type
+ * @port_id: physical port id
+ */
+struct mxl862xx_mac_table_clear {
+ u8 type;
+ u8 port_id;
+} __packed;
+
+/**
+ * enum mxl862xx_age_timer - Aging Timer Value.
+ * @MXL862XX_AGETIMER_1_SEC: 1 second aging time
+ * @MXL862XX_AGETIMER_10_SEC: 10 seconds aging time
+ * @MXL862XX_AGETIMER_300_SEC: 300 seconds aging time
+ * @MXL862XX_AGETIMER_1_HOUR: 1 hour aging time
+ * @MXL862XX_AGETIMER_1_DAY: 24 hours aging time
+ * @MXL862XX_AGETIMER_CUSTOM: Custom aging time in seconds
+ */
+enum mxl862xx_age_timer {
+ MXL862XX_AGETIMER_1_SEC = 1,
+ MXL862XX_AGETIMER_10_SEC,
+ MXL862XX_AGETIMER_300_SEC,
+ MXL862XX_AGETIMER_1_HOUR,
+ MXL862XX_AGETIMER_1_DAY,
+ MXL862XX_AGETIMER_CUSTOM,
+};
+
+/**
+ * struct mxl862xx_bridge_alloc - Bridge Allocation
+ * @bridge_id: If the bridge allocation is successful, a valid ID will be
+ * returned in this field. Otherwise, INVALID_HANDLE is
+ * returned. For bridge free, this field should contain a
+ * valid ID returned by the bridge allocation. ID 0 is not
+ * used for historic reasons.
+ *
+ * Used by MXL862XX_BRIDGE_ALLOC and MXL862XX_BRIDGE_FREE.
+ */
+struct mxl862xx_bridge_alloc {
+ __le16 bridge_id;
+};
+
+/**
+ * enum mxl862xx_bridge_config_mask - Bridge configuration mask
+ * @MXL862XX_BRIDGE_CONFIG_MASK_MAC_LEARNING_LIMIT:
+ * Mask for mac_learning_limit_enable and mac_learning_limit.
+ * @MXL862XX_BRIDGE_CONFIG_MASK_MAC_LEARNED_COUNT:
+ * Mask for mac_learning_count
+ * @MXL862XX_BRIDGE_CONFIG_MASK_MAC_DISCARD_COUNT:
+ * Mask for learning_discard_event
+ * @MXL862XX_BRIDGE_CONFIG_MASK_SUB_METER:
+ * Mask for sub_metering_enable and traffic_sub_meter_id
+ * @MXL862XX_BRIDGE_CONFIG_MASK_FORWARDING_MODE:
+ * Mask for forward_broadcast, forward_unknown_multicast_ip,
+ * forward_unknown_multicast_non_ip and forward_unknown_unicast.
+ * @MXL862XX_BRIDGE_CONFIG_MASK_ALL: Enable all
+ * @MXL862XX_BRIDGE_CONFIG_MASK_FORCE: Bypass any check for debug purpose
+ */
+enum mxl862xx_bridge_config_mask {
+ MXL862XX_BRIDGE_CONFIG_MASK_MAC_LEARNING_LIMIT = BIT(0),
+ MXL862XX_BRIDGE_CONFIG_MASK_MAC_LEARNED_COUNT = BIT(1),
+ MXL862XX_BRIDGE_CONFIG_MASK_MAC_DISCARD_COUNT = BIT(2),
+ MXL862XX_BRIDGE_CONFIG_MASK_SUB_METER = BIT(3),
+ MXL862XX_BRIDGE_CONFIG_MASK_FORWARDING_MODE = BIT(4),
+ MXL862XX_BRIDGE_CONFIG_MASK_ALL = 0x7FFFFFFF,
+ MXL862XX_BRIDGE_CONFIG_MASK_FORCE = BIT(31)
+};
+
+/**
+ * enum mxl862xx_bridge_port_egress_meter - Meters for egress traffic type
+ * @MXL862XX_BRIDGE_PORT_EGRESS_METER_BROADCAST:
+ * Index of broadcast traffic meter
+ * @MXL862XX_BRIDGE_PORT_EGRESS_METER_MULTICAST:
+ * Index of known multicast traffic meter
+ * @MXL862XX_BRIDGE_PORT_EGRESS_METER_UNKNOWN_MC_IP:
+ * Index of unknown multicast IP traffic meter
+ * @MXL862XX_BRIDGE_PORT_EGRESS_METER_UNKNOWN_MC_NON_IP:
+ * Index of unknown multicast non-IP traffic meter
+ * @MXL862XX_BRIDGE_PORT_EGRESS_METER_UNKNOWN_UC:
+ * Index of unknown unicast traffic meter
+ * @MXL862XX_BRIDGE_PORT_EGRESS_METER_OTHERS:
+ * Index of traffic meter for other types
+ * @MXL862XX_BRIDGE_PORT_EGRESS_METER_MAX: Number of index
+ */
+enum mxl862xx_bridge_port_egress_meter {
+ MXL862XX_BRIDGE_PORT_EGRESS_METER_BROADCAST = 0,
+ MXL862XX_BRIDGE_PORT_EGRESS_METER_MULTICAST,
+ MXL862XX_BRIDGE_PORT_EGRESS_METER_UNKNOWN_MC_IP,
+ MXL862XX_BRIDGE_PORT_EGRESS_METER_UNKNOWN_MC_NON_IP,
+ MXL862XX_BRIDGE_PORT_EGRESS_METER_UNKNOWN_UC,
+ MXL862XX_BRIDGE_PORT_EGRESS_METER_OTHERS,
+ MXL862XX_BRIDGE_PORT_EGRESS_METER_MAX,
+};
+
+/**
+ * struct mxl862xx_qos_meter_cfg - Rate meter configuration
+ * @enable: Enable/disable meter
+ * @meter_id: Meter ID (assigned by firmware on alloc)
+ * @meter_name: Meter name string
+ * @meter_type: Meter algorithm type (srTCM = 0, trTCM = 1)
+ * @cbs: Committed Burst Size (in bytes)
+ * @res1: Reserved
+ * @ebs: Excess Burst Size (in bytes)
+ * @res2: Reserved
+ * @rate: Committed Information Rate (in kbit/s)
+ * @pi_rate: Peak Information Rate (in kbit/s)
+ * @colour_blind_mode: Colour-blind mode enable
+ * @pkt_mode: Packet mode enable
+ * @local_overhd: Local overhead accounting enable
+ * @local_overhd_val: Local overhead accounting value
+ */
+struct mxl862xx_qos_meter_cfg {
+ u8 enable;
+ __le16 meter_id;
+ char meter_name[32];
+ __le32 meter_type;
+ __le32 cbs;
+ __le32 res1;
+ __le32 ebs;
+ __le32 res2;
+ __le32 rate;
+ __le32 pi_rate;
+ u8 colour_blind_mode;
+ u8 pkt_mode;
+ u8 local_overhd;
+ __le16 local_overhd_val;
+} __packed;
+
+/**
+ * enum mxl862xx_bridge_forward_mode - Bridge forwarding type of packet
+ * @MXL862XX_BRIDGE_FORWARD_FLOOD: Packet is flooded to port members of
+ * ingress bridge port
+ * @MXL862XX_BRIDGE_FORWARD_DISCARD: Packet is discarded
+ */
+enum mxl862xx_bridge_forward_mode {
+ MXL862XX_BRIDGE_FORWARD_FLOOD = 0,
+ MXL862XX_BRIDGE_FORWARD_DISCARD,
+};
+
+/**
+ * struct mxl862xx_bridge_config - Bridge Configuration
+ * @bridge_id: Bridge ID (FID)
+ * @mask: See &enum mxl862xx_bridge_config_mask
+ * @mac_learning_limit_enable: Enable MAC learning limitation
+ * @mac_learning_limit: Max number of MAC addresses that can be learned in
+ * this bridge (all bridge ports)
+ * @mac_learning_count: Number of MAC addresses learned from this bridge
+ * @learning_discard_event: Number of learning discard events due to
+ * hardware resource not available
+ * @sub_metering_enable: Traffic metering on type of traffic (such as
+ * broadcast, multicast, unknown unicast, etc) applies
+ * @traffic_sub_meter_id: Meter for bridge process with specific type (such
+ * as broadcast, multicast, unknown unicast, etc)
+ * @forward_broadcast: Forwarding mode of broadcast traffic. See
+ * &enum mxl862xx_bridge_forward_mode
+ * @forward_unknown_multicast_ip: Forwarding mode of unknown multicast IP
+ * traffic.
+ * See &enum mxl862xx_bridge_forward_mode
+ * @forward_unknown_multicast_non_ip: Forwarding mode of unknown multicast
+ * non-IP traffic.
+ * See &enum mxl862xx_bridge_forward_mode
+ * @forward_unknown_unicast: Forwarding mode of unknown unicast traffic. See
+ * &enum mxl862xx_bridge_forward_mode
+ */
+struct mxl862xx_bridge_config {
+ __le16 bridge_id;
+ __le32 mask; /* enum mxl862xx_bridge_config_mask */
+ u8 mac_learning_limit_enable;
+ __le16 mac_learning_limit;
+ __le16 mac_learning_count;
+ __le32 learning_discard_event;
+ u8 sub_metering_enable[MXL862XX_BRIDGE_PORT_EGRESS_METER_MAX];
+ __le16 traffic_sub_meter_id[MXL862XX_BRIDGE_PORT_EGRESS_METER_MAX];
+ __le32 forward_broadcast; /* enum mxl862xx_bridge_forward_mode */
+ __le32 forward_unknown_multicast_ip; /* enum mxl862xx_bridge_forward_mode */
+ __le32 forward_unknown_multicast_non_ip; /* enum mxl862xx_bridge_forward_mode */
+ __le32 forward_unknown_unicast; /* enum mxl862xx_bridge_forward_mode */
+} __packed;
+
+/**
+ * struct mxl862xx_bridge_port_alloc - Bridge Port Allocation
+ * @bridge_port_id: If the bridge port allocation is successful, a valid ID
+ * will be returned in this field. Otherwise, INVALID_HANDLE
+ * is returned. For bridge port free, this field should
+ * contain a valid ID returned by the bridge port allocation.
+ *
+ * Used by MXL862XX_BRIDGE_PORT_ALLOC and MXL862XX_BRIDGE_PORT_FREE.
+ */
+struct mxl862xx_bridge_port_alloc {
+ __le16 bridge_port_id;
+};
+
+/**
+ * enum mxl862xx_bridge_port_config_mask - Bridge Port configuration mask
+ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_BRIDGE_ID:
+ * Mask for bridge_id
+ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_INGRESS_VLAN:
+ * Mask for ingress_extended_vlan_enable,
+ * ingress_extended_vlan_block_id and ingress_extended_vlan_block_size
+ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_EGRESS_VLAN:
+ * Mask for egress_extended_vlan_enable, egress_extended_vlan_block_id
+ * and egress_extended_vlan_block_size
+ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_INGRESS_MARKING:
+ * Mask for ingress_marking_mode
+ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_EGRESS_REMARKING:
+ * Mask for egress_remarking_mode
+ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_INGRESS_METER:
+ * Mask for ingress_metering_enable and ingress_traffic_meter_id
+ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_EGRESS_SUB_METER:
+ * Mask for egress_sub_metering_enable and egress_traffic_sub_meter_id
+ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_EGRESS_CTP_MAPPING:
+ * Mask for dest_logical_port_id, pmapper_enable, dest_sub_if_id_group,
+ * pmapper_mapping_mode, pmapper_id_valid and pmapper
+ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_BRIDGE_PORT_MAP:
+ * Mask for bridge_port_map
+ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_MC_DEST_IP_LOOKUP:
+ * Mask for mc_dest_ip_lookup_disable
+ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_MC_SRC_IP_LOOKUP:
+ * Mask for mc_src_ip_lookup_enable
+ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_MC_DEST_MAC_LOOKUP:
+ * Mask for dest_mac_lookup_disable
+ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_MC_SRC_MAC_LEARNING:
+ * Mask for src_mac_learning_disable
+ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_MAC_SPOOFING:
+ * Mask for mac_spoofing_detect_enable
+ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_PORT_LOCK:
+ * Mask for port_lock_enable
+ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_MAC_LEARNING_LIMIT:
+ * Mask for mac_learning_limit_enable and mac_learning_limit
+ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_MAC_LEARNED_COUNT:
+ * Mask for mac_learning_count
+ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_INGRESS_VLAN_FILTER:
+ * Mask for ingress_vlan_filter_enable, ingress_vlan_filter_block_id
+ * and ingress_vlan_filter_block_size
+ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_EGRESS_VLAN_FILTER1:
+ * Mask for bypass_egress_vlan_filter1, egress_vlan_filter1enable,
+ * egress_vlan_filter1block_id and egress_vlan_filter1block_size
+ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_EGRESS_VLAN_FILTER2:
+ * Mask for egress_vlan_filter2enable, egress_vlan_filter2block_id and
+ * egress_vlan_filter2block_size
+ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_VLAN_BASED_MAC_LEARNING:
+ * Mask for vlan_tag_selection, vlan_src_mac_priority_enable,
+ * vlan_src_mac_dei_enable, vlan_src_mac_vid_enable,
+ * vlan_dst_mac_priority_enable, vlan_dst_mac_dei_enable and
+ * vlan_dst_mac_vid_enable
+ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_VLAN_BASED_MULTICAST_LOOKUP:
+ * Mask for vlan_multicast_priority_enable,
+ * vlan_multicast_dei_enable and vlan_multicast_vid_enable
+ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_LOOP_VIOLATION_COUNTER:
+ * Mask for loop_violation_count
+ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_ALL: Enable all
+ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_FORCE: Bypass any check for debug purpose
+ */
+enum mxl862xx_bridge_port_config_mask {
+ MXL862XX_BRIDGE_PORT_CONFIG_MASK_BRIDGE_ID = BIT(0),
+ MXL862XX_BRIDGE_PORT_CONFIG_MASK_INGRESS_VLAN = BIT(1),
+ MXL862XX_BRIDGE_PORT_CONFIG_MASK_EGRESS_VLAN = BIT(2),
+ MXL862XX_BRIDGE_PORT_CONFIG_MASK_INGRESS_MARKING = BIT(3),
+ MXL862XX_BRIDGE_PORT_CONFIG_MASK_EGRESS_REMARKING = BIT(4),
+ MXL862XX_BRIDGE_PORT_CONFIG_MASK_INGRESS_METER = BIT(5),
+ MXL862XX_BRIDGE_PORT_CONFIG_MASK_EGRESS_SUB_METER = BIT(6),
+ MXL862XX_BRIDGE_PORT_CONFIG_MASK_EGRESS_CTP_MAPPING = BIT(7),
+ MXL862XX_BRIDGE_PORT_CONFIG_MASK_BRIDGE_PORT_MAP = BIT(8),
+ MXL862XX_BRIDGE_PORT_CONFIG_MASK_MC_DEST_IP_LOOKUP = BIT(9),
+ MXL862XX_BRIDGE_PORT_CONFIG_MASK_MC_SRC_IP_LOOKUP = BIT(10),
+ MXL862XX_BRIDGE_PORT_CONFIG_MASK_MC_DEST_MAC_LOOKUP = BIT(11),
+ MXL862XX_BRIDGE_PORT_CONFIG_MASK_MC_SRC_MAC_LEARNING = BIT(12),
+ MXL862XX_BRIDGE_PORT_CONFIG_MASK_MAC_SPOOFING = BIT(13),
+ MXL862XX_BRIDGE_PORT_CONFIG_MASK_PORT_LOCK = BIT(14),
+ MXL862XX_BRIDGE_PORT_CONFIG_MASK_MAC_LEARNING_LIMIT = BIT(15),
+ MXL862XX_BRIDGE_PORT_CONFIG_MASK_MAC_LEARNED_COUNT = BIT(16),
+ MXL862XX_BRIDGE_PORT_CONFIG_MASK_INGRESS_VLAN_FILTER = BIT(17),
+ MXL862XX_BRIDGE_PORT_CONFIG_MASK_EGRESS_VLAN_FILTER1 = BIT(18),
+ MXL862XX_BRIDGE_PORT_CONFIG_MASK_EGRESS_VLAN_FILTER2 = BIT(19),
+ MXL862XX_BRIDGE_PORT_CONFIG_MASK_VLAN_BASED_MAC_LEARNING = BIT(20),
+ MXL862XX_BRIDGE_PORT_CONFIG_MASK_VLAN_BASED_MULTICAST_LOOKUP = BIT(21),
+ MXL862XX_BRIDGE_PORT_CONFIG_MASK_LOOP_VIOLATION_COUNTER = BIT(22),
+ MXL862XX_BRIDGE_PORT_CONFIG_MASK_ALL = 0x7FFFFFFF,
+ MXL862XX_BRIDGE_PORT_CONFIG_MASK_FORCE = BIT(31)
+};
+
+/**
+ * enum mxl862xx_color_marking_mode - Color Marking Mode
+ * @MXL862XX_MARKING_ALL_GREEN: mark packets (except critical) to green
+ * @MXL862XX_MARKING_INTERNAL_MARKING: do not change color and priority
+ * @MXL862XX_MARKING_DEI: DEI mark mode
+ * @MXL862XX_MARKING_PCP_8P0D: PCP 8P0D mark mode
+ * @MXL862XX_MARKING_PCP_7P1D: PCP 7P1D mark mode
+ * @MXL862XX_MARKING_PCP_6P2D: PCP 6P2D mark mode
+ * @MXL862XX_MARKING_PCP_5P3D: PCP 5P3D mark mode
+ * @MXL862XX_MARKING_DSCP_AF: DSCP AF class
+ */
+enum mxl862xx_color_marking_mode {
+ MXL862XX_MARKING_ALL_GREEN = 0,
+ MXL862XX_MARKING_INTERNAL_MARKING,
+ MXL862XX_MARKING_DEI,
+ MXL862XX_MARKING_PCP_8P0D,
+ MXL862XX_MARKING_PCP_7P1D,
+ MXL862XX_MARKING_PCP_6P2D,
+ MXL862XX_MARKING_PCP_5P3D,
+ MXL862XX_MARKING_DSCP_AF,
+};
+
+/**
+ * enum mxl862xx_color_remarking_mode - Color Remarking Mode
+ * @MXL862XX_REMARKING_NONE: values from last process stage
+ * @MXL862XX_REMARKING_DEI: DEI mark mode
+ * @MXL862XX_REMARKING_PCP_8P0D: PCP 8P0D mark mode
+ * @MXL862XX_REMARKING_PCP_7P1D: PCP 7P1D mark mode
+ * @MXL862XX_REMARKING_PCP_6P2D: PCP 6P2D mark mode
+ * @MXL862XX_REMARKING_PCP_5P3D: PCP 5P3D mark mode
+ * @MXL862XX_REMARKING_DSCP_AF: DSCP AF class
+ */
+enum mxl862xx_color_remarking_mode {
+ MXL862XX_REMARKING_NONE = 0,
+ MXL862XX_REMARKING_DEI = 2,
+ MXL862XX_REMARKING_PCP_8P0D,
+ MXL862XX_REMARKING_PCP_7P1D,
+ MXL862XX_REMARKING_PCP_6P2D,
+ MXL862XX_REMARKING_PCP_5P3D,
+ MXL862XX_REMARKING_DSCP_AF,
+};
+
+/**
+ * enum mxl862xx_pmapper_mapping_mode - P-mapper Mapping Mode
+ * @MXL862XX_PMAPPER_MAPPING_PCP: Use PCP for VLAN tagged packets to derive
+ * sub interface ID group
+ * @MXL862XX_PMAPPER_MAPPING_LAG: Use LAG Index for Pmapper access
+ * regardless of IP and VLAN packet
+ * @MXL862XX_PMAPPER_MAPPING_DSCP: Use DSCP for VLAN tagged IP packets to
+ * derive sub interface ID group
+ */
+enum mxl862xx_pmapper_mapping_mode {
+ MXL862XX_PMAPPER_MAPPING_PCP = 0,
+ MXL862XX_PMAPPER_MAPPING_LAG,
+ MXL862XX_PMAPPER_MAPPING_DSCP,
+};
+
+/**
+ * struct mxl862xx_pmapper - P-mapper Configuration
+ * @pmapper_id: Index of P-mapper (0-31)
+ * @dest_sub_if_id_group: Sub interface ID group. Entry 0 is for non-IP and
+ * non-VLAN tagged packets.
+ * Entries 1-8 are PCP mapping entries for VLAN tagged
+ * packets.
+ * Entries 9-72 are DSCP or LAG mapping entries.
+ *
+ * Used by CTP port config and bridge port config. In case of LAG, it is
+ * user's responsibility to provide the mapped entries in given P-mapper
+ * table. In other modes the entries are auto mapped from input packet.
+ */
+struct mxl862xx_pmapper {
+ __le16 pmapper_id;
+ u8 dest_sub_if_id_group[73];
+} __packed;
+
+/**
+ * struct mxl862xx_bridge_port_config - Bridge Port Configuration
+ * @bridge_port_id: Bridge Port ID allocated by bridge port allocation
+ * @mask: See &enum mxl862xx_bridge_port_config_mask
+ * @bridge_id: Bridge ID (FID) to which this bridge port is associated
+ * @ingress_extended_vlan_enable: Enable extended VLAN processing for
+ * ingress traffic
+ * @ingress_extended_vlan_block_id: Extended VLAN block allocated for
+ * ingress traffic
+ * @ingress_extended_vlan_block_size: Extended VLAN block size for ingress
+ * traffic
+ * @egress_extended_vlan_enable: Enable extended VLAN processing for egress
+ * traffic
+ * @egress_extended_vlan_block_id: Extended VLAN block allocated for egress
+ * traffic
+ * @egress_extended_vlan_block_size: Extended VLAN block size for egress
+ * traffic
+ * @ingress_marking_mode: Ingress color marking mode. See
+ * &enum mxl862xx_color_marking_mode
+ * @egress_remarking_mode: Color remarking for egress traffic. See
+ * &enum mxl862xx_color_remarking_mode
+ * @ingress_metering_enable: Traffic metering on ingress traffic applies
+ * @ingress_traffic_meter_id: Meter for ingress Bridge Port process
+ * @egress_sub_metering_enable: Traffic metering on various types of egress
+ * traffic
+ * @egress_traffic_sub_meter_id: Meter for egress Bridge Port process with
+ * specific type
+ * @dest_logical_port_id: Destination logical port
+ * @pmapper_enable: Enable P-mapper
+ * @dest_sub_if_id_group: Destination sub interface ID group when
+ * pmapper_enable is false
+ * @pmapper_mapping_mode: P-mapper mapping mode. See
+ * &enum mxl862xx_pmapper_mapping_mode
+ * @pmapper_id_valid: When true, P-mapper is re-used; when false,
+ * allocation is handled by API
+ * @pmapper: P-mapper configuration used when pmapper_enable is true
+ * @bridge_port_map: Port map defining broadcast domain. Each bit
+ * represents one bridge port. Bridge port ID is
+ * index * 16 + bit offset.
+ * @mc_dest_ip_lookup_disable: Disable multicast IP destination table
+ * lookup
+ * @mc_src_ip_lookup_enable: Enable multicast IP source table lookup
+ * @dest_mac_lookup_disable: Disable destination MAC lookup; packet treated
+ * as unknown
+ * @src_mac_learning_disable: Disable source MAC address learning
+ * @mac_spoofing_detect_enable: Enable MAC spoofing detection
+ * @port_lock_enable: Enable port locking
+ * @mac_learning_limit_enable: Enable MAC learning limitation
+ * @mac_learning_limit: Maximum number of MAC addresses that can be learned
+ * from this bridge port
+ * @loop_violation_count: Number of loop violation events from this bridge
+ * port
+ * @mac_learning_count: Number of MAC addresses learned from this bridge
+ * port
+ * @ingress_vlan_filter_enable: Enable ingress VLAN filter
+ * @ingress_vlan_filter_block_id: VLAN filter block of ingress traffic
+ * @ingress_vlan_filter_block_size: VLAN filter block size for ingress
+ * traffic
+ * @bypass_egress_vlan_filter1: For ingress traffic, bypass VLAN filter 1
+ * at egress bridge port processing
+ * @egress_vlan_filter1enable: Enable egress VLAN filter 1
+ * @egress_vlan_filter1block_id: VLAN filter block 1 of egress traffic
+ * @egress_vlan_filter1block_size: VLAN filter block 1 size
+ * @egress_vlan_filter2enable: Enable egress VLAN filter 2
+ * @egress_vlan_filter2block_id: VLAN filter block 2 of egress traffic
+ * @egress_vlan_filter2block_size: VLAN filter block 2 size
+ * @vlan_tag_selection: VLAN tag selection for MAC address/multicast
+ * learning, lookup and filtering.
+ * 0 - Intermediate outer VLAN tag is used.
+ * 1 - Original outer VLAN tag is used.
+ * @vlan_src_mac_priority_enable: Enable VLAN Priority field for source MAC
+ * learning and filtering
+ * @vlan_src_mac_dei_enable: Enable VLAN DEI/CFI field for source MAC
+ * learning and filtering
+ * @vlan_src_mac_vid_enable: Enable VLAN ID field for source MAC learning
+ * and filtering
+ * @vlan_dst_mac_priority_enable: Enable VLAN Priority field for destination
+ * MAC lookup and filtering
+ * @vlan_dst_mac_dei_enable: Enable VLAN CFI/DEI field for destination MAC
+ * lookup and filtering
+ * @vlan_dst_mac_vid_enable: Enable VLAN ID field for destination MAC lookup
+ * and filtering
+ * @vlan_multicast_priority_enable: Enable VLAN Priority field for IP
+ * multicast lookup
+ * @vlan_multicast_dei_enable: Enable VLAN CFI/DEI field for IP multicast
+ * lookup
+ * @vlan_multicast_vid_enable: Enable VLAN ID field for IP multicast lookup
+ */
+struct mxl862xx_bridge_port_config {
+ __le16 bridge_port_id;
+ __le32 mask; /* enum mxl862xx_bridge_port_config_mask */
+ __le16 bridge_id;
+ u8 ingress_extended_vlan_enable;
+ __le16 ingress_extended_vlan_block_id;
+ __le16 ingress_extended_vlan_block_size;
+ u8 egress_extended_vlan_enable;
+ __le16 egress_extended_vlan_block_id;
+ __le16 egress_extended_vlan_block_size;
+ __le32 ingress_marking_mode; /* enum mxl862xx_color_marking_mode */
+ __le32 egress_remarking_mode; /* enum mxl862xx_color_remarking_mode */
+ u8 ingress_metering_enable;
+ __le16 ingress_traffic_meter_id;
+ u8 egress_sub_metering_enable[MXL862XX_BRIDGE_PORT_EGRESS_METER_MAX];
+ __le16 egress_traffic_sub_meter_id[MXL862XX_BRIDGE_PORT_EGRESS_METER_MAX];
+ u8 dest_logical_port_id;
+ u8 pmapper_enable;
+ __le16 dest_sub_if_id_group;
+ __le32 pmapper_mapping_mode; /* enum mxl862xx_pmapper_mapping_mode */
+ u8 pmapper_id_valid;
+ struct mxl862xx_pmapper pmapper;
+ __le16 bridge_port_map[8];
+ u8 mc_dest_ip_lookup_disable;
+ u8 mc_src_ip_lookup_enable;
+ u8 dest_mac_lookup_disable;
+ u8 src_mac_learning_disable;
+ u8 mac_spoofing_detect_enable;
+ u8 port_lock_enable;
+ u8 mac_learning_limit_enable;
+ __le16 mac_learning_limit;
+ __le16 loop_violation_count;
+ __le16 mac_learning_count;
+ u8 ingress_vlan_filter_enable;
+ __le16 ingress_vlan_filter_block_id;
+ __le16 ingress_vlan_filter_block_size;
+ u8 bypass_egress_vlan_filter1;
+ u8 egress_vlan_filter1enable;
+ __le16 egress_vlan_filter1block_id;
+ __le16 egress_vlan_filter1block_size;
+ u8 egress_vlan_filter2enable;
+ __le16 egress_vlan_filter2block_id;
+ __le16 egress_vlan_filter2block_size;
+ u8 vlan_tag_selection;
+ u8 vlan_src_mac_priority_enable;
+ u8 vlan_src_mac_dei_enable;
+ u8 vlan_src_mac_vid_enable;
+ u8 vlan_dst_mac_priority_enable;
+ u8 vlan_dst_mac_dei_enable;
+ u8 vlan_dst_mac_vid_enable;
+ u8 vlan_multicast_priority_enable;
+ u8 vlan_multicast_dei_enable;
+ u8 vlan_multicast_vid_enable;
+} __packed;
+
+/**
+ * struct mxl862xx_cfg - Global Switch configuration Attributes
+ * @mac_table_age_timer: See &enum mxl862xx_age_timer
+ * @age_timer: Custom MAC table aging timer in seconds
+ * @max_packet_len: Maximum Ethernet packet length
+ * @learning_limit_action: Automatic MAC address table learning limitation
+ * consecutive action
+ * @mac_locking_action: Accept or discard MAC port locking violation
+ * packets
+ * @mac_spoofing_action: Accept or discard MAC spoofing and port MAC locking
+ * violation packets
+ * @pause_mac_mode_src: Pause frame MAC source address mode
+ * @pause_mac_src: Pause frame MAC source address
+ */
+struct mxl862xx_cfg {
+ __le32 mac_table_age_timer; /* enum mxl862xx_age_timer */
+ __le32 age_timer;
+ __le16 max_packet_len;
+ u8 learning_limit_action;
+ u8 mac_locking_action;
+ u8 mac_spoofing_action;
+ u8 pause_mac_mode_src;
+ u8 pause_mac_src[ETH_ALEN];
+} __packed;
+
+/**
+ * enum mxl862xx_extended_vlan_filter_type - Extended VLAN filter tag type
+ * @MXL862XX_EXTENDEDVLAN_FILTER_TYPE_NORMAL: Normal tagged
+ * @MXL862XX_EXTENDEDVLAN_FILTER_TYPE_NO_FILTER: No filter (wildcard)
+ * @MXL862XX_EXTENDEDVLAN_FILTER_TYPE_DEFAULT: Default entry
+ * @MXL862XX_EXTENDEDVLAN_FILTER_TYPE_NO_TAG: Untagged
+ */
+enum mxl862xx_extended_vlan_filter_type {
+ MXL862XX_EXTENDEDVLAN_FILTER_TYPE_NORMAL = 0,
+ MXL862XX_EXTENDEDVLAN_FILTER_TYPE_NO_FILTER = 1,
+ MXL862XX_EXTENDEDVLAN_FILTER_TYPE_DEFAULT = 2,
+ MXL862XX_EXTENDEDVLAN_FILTER_TYPE_NO_TAG = 3,
+};
+
+/**
+ * enum mxl862xx_extended_vlan_filter_tpid - Extended VLAN filter TPID
+ * @MXL862XX_EXTENDEDVLAN_FILTER_TPID_NO_FILTER: No TPID filter
+ * @MXL862XX_EXTENDEDVLAN_FILTER_TPID_8021Q: 802.1Q TPID
+ * @MXL862XX_EXTENDEDVLAN_FILTER_TPID_VTETYPE: VLAN type extension
+ */
+enum mxl862xx_extended_vlan_filter_tpid {
+ MXL862XX_EXTENDEDVLAN_FILTER_TPID_NO_FILTER = 0,
+ MXL862XX_EXTENDEDVLAN_FILTER_TPID_8021Q = 1,
+ MXL862XX_EXTENDEDVLAN_FILTER_TPID_VTETYPE = 2,
+};
+
+/**
+ * enum mxl862xx_extended_vlan_filter_dei - Extended VLAN filter DEI
+ * @MXL862XX_EXTENDEDVLAN_FILTER_DEI_NO_FILTER: No DEI filter
+ * @MXL862XX_EXTENDEDVLAN_FILTER_DEI_0: DEI = 0
+ * @MXL862XX_EXTENDEDVLAN_FILTER_DEI_1: DEI = 1
+ */
+enum mxl862xx_extended_vlan_filter_dei {
+ MXL862XX_EXTENDEDVLAN_FILTER_DEI_NO_FILTER = 0,
+ MXL862XX_EXTENDEDVLAN_FILTER_DEI_0 = 1,
+ MXL862XX_EXTENDEDVLAN_FILTER_DEI_1 = 2,
+};
+
+/**
+ * enum mxl862xx_extended_vlan_treatment_remove_tag - Tag removal action
+ * @MXL862XX_EXTENDEDVLAN_TREATMENT_NOT_REMOVE_TAG: Do not remove tag
+ * @MXL862XX_EXTENDEDVLAN_TREATMENT_REMOVE_1_TAG: Remove one tag
+ * @MXL862XX_EXTENDEDVLAN_TREATMENT_REMOVE_2_TAG: Remove two tags
+ * @MXL862XX_EXTENDEDVLAN_TREATMENT_DISCARD_UPSTREAM: Discard frame
+ */
+enum mxl862xx_extended_vlan_treatment_remove_tag {
+ MXL862XX_EXTENDEDVLAN_TREATMENT_NOT_REMOVE_TAG = 0,
+ MXL862XX_EXTENDEDVLAN_TREATMENT_REMOVE_1_TAG = 1,
+ MXL862XX_EXTENDEDVLAN_TREATMENT_REMOVE_2_TAG = 2,
+ MXL862XX_EXTENDEDVLAN_TREATMENT_DISCARD_UPSTREAM = 3,
+};
+
+/**
+ * enum mxl862xx_extended_vlan_treatment_priority - Treatment priority mode
+ * @MXL862XX_EXTENDEDVLAN_TREATMENT_PRIORITY_VAL: Use explicit value
+ * @MXL862XX_EXTENDEDVLAN_TREATMENT_INNER_PRIORITY: Copy from inner tag
+ * @MXL862XX_EXTENDEDVLAN_TREATMENT_OUTER_PRIORITY: Copy from outer tag
+ * @MXL862XX_EXTENDEDVLAN_TREATMENT_DSCP: Derive from DSCP
+ */
+enum mxl862xx_extended_vlan_treatment_priority {
+ MXL862XX_EXTENDEDVLAN_TREATMENT_PRIORITY_VAL = 0,
+ MXL862XX_EXTENDEDVLAN_TREATMENT_INNER_PRIORITY = 1,
+ MXL862XX_EXTENDEDVLAN_TREATMENT_OUTER_PRIORITY = 2,
+ MXL862XX_EXTENDEDVLAN_TREATMENT_DSCP = 3,
+};
+
+/**
+ * enum mxl862xx_extended_vlan_treatment_vid - Treatment VID mode
+ * @MXL862XX_EXTENDEDVLAN_TREATMENT_VID_VAL: Use explicit VID value
+ * @MXL862XX_EXTENDEDVLAN_TREATMENT_INNER_VID: Copy from inner tag
+ * @MXL862XX_EXTENDEDVLAN_TREATMENT_OUTER_VID: Copy from outer tag
+ */
+enum mxl862xx_extended_vlan_treatment_vid {
+ MXL862XX_EXTENDEDVLAN_TREATMENT_VID_VAL = 0,
+ MXL862XX_EXTENDEDVLAN_TREATMENT_INNER_VID = 1,
+ MXL862XX_EXTENDEDVLAN_TREATMENT_OUTER_VID = 2,
+};
+
+/**
+ * enum mxl862xx_extended_vlan_treatment_tpid - Treatment TPID mode
+ * @MXL862XX_EXTENDEDVLAN_TREATMENT_INNER_TPID: Copy from inner tag
+ * @MXL862XX_EXTENDEDVLAN_TREATMENT_OUTER_TPID: Copy from outer tag
+ * @MXL862XX_EXTENDEDVLAN_TREATMENT_VTETYPE: Use VLAN type extension
+ * @MXL862XX_EXTENDEDVLAN_TREATMENT_8021Q: Use 802.1Q TPID
+ */
+enum mxl862xx_extended_vlan_treatment_tpid {
+ MXL862XX_EXTENDEDVLAN_TREATMENT_INNER_TPID = 0,
+ MXL862XX_EXTENDEDVLAN_TREATMENT_OUTER_TPID = 1,
+ MXL862XX_EXTENDEDVLAN_TREATMENT_VTETYPE = 2,
+ MXL862XX_EXTENDEDVLAN_TREATMENT_8021Q = 3,
+};
+
+/**
+ * enum mxl862xx_extended_vlan_treatment_dei - Treatment DEI mode
+ * @MXL862XX_EXTENDEDVLAN_TREATMENT_INNER_DEI: Copy from inner tag
+ * @MXL862XX_EXTENDEDVLAN_TREATMENT_OUTER_DEI: Copy from outer tag
+ * @MXL862XX_EXTENDEDVLAN_TREATMENT_DEI_0: Set DEI to 0
+ * @MXL862XX_EXTENDEDVLAN_TREATMENT_DEI_1: Set DEI to 1
+ */
+enum mxl862xx_extended_vlan_treatment_dei {
+ MXL862XX_EXTENDEDVLAN_TREATMENT_INNER_DEI = 0,
+ MXL862XX_EXTENDEDVLAN_TREATMENT_OUTER_DEI = 1,
+ MXL862XX_EXTENDEDVLAN_TREATMENT_DEI_0 = 2,
+ MXL862XX_EXTENDEDVLAN_TREATMENT_DEI_1 = 3,
+};
+
+/**
+ * enum mxl862xx_extended_vlan_4_tpid_mode - 4-TPID mode selector
+ * @MXL862XX_EXTENDEDVLAN_TPID_VTETYPE_1: VLAN TPID type 1
+ * @MXL862XX_EXTENDEDVLAN_TPID_VTETYPE_2: VLAN TPID type 2
+ * @MXL862XX_EXTENDEDVLAN_TPID_VTETYPE_3: VLAN TPID type 3
+ * @MXL862XX_EXTENDEDVLAN_TPID_VTETYPE_4: VLAN TPID type 4
+ */
+enum mxl862xx_extended_vlan_4_tpid_mode {
+ MXL862XX_EXTENDEDVLAN_TPID_VTETYPE_1 = 0,
+ MXL862XX_EXTENDEDVLAN_TPID_VTETYPE_2 = 1,
+ MXL862XX_EXTENDEDVLAN_TPID_VTETYPE_3 = 2,
+ MXL862XX_EXTENDEDVLAN_TPID_VTETYPE_4 = 3,
+};
+
+/**
+ * enum mxl862xx_extended_vlan_filter_ethertype - Filter EtherType match
+ * @MXL862XX_EXTENDEDVLAN_FILTER_ETHERTYPE_NO_FILTER: No filter
+ * @MXL862XX_EXTENDEDVLAN_FILTER_ETHERTYPE_IPOE: IPoE
+ * @MXL862XX_EXTENDEDVLAN_FILTER_ETHERTYPE_PPPOE: PPPoE
+ * @MXL862XX_EXTENDEDVLAN_FILTER_ETHERTYPE_ARP: ARP
+ * @MXL862XX_EXTENDEDVLAN_FILTER_ETHERTYPE_IPV6IPOE: IPv6 IPoE
+ * @MXL862XX_EXTENDEDVLAN_FILTER_ETHERTYPE_EAPOL: EAPOL
+ * @MXL862XX_EXTENDEDVLAN_FILTER_ETHERTYPE_DHCPV4: DHCPv4
+ * @MXL862XX_EXTENDEDVLAN_FILTER_ETHERTYPE_DHCPV6: DHCPv6
+ */
+enum mxl862xx_extended_vlan_filter_ethertype {
+ MXL862XX_EXTENDEDVLAN_FILTER_ETHERTYPE_NO_FILTER = 0,
+ MXL862XX_EXTENDEDVLAN_FILTER_ETHERTYPE_IPOE = 1,
+ MXL862XX_EXTENDEDVLAN_FILTER_ETHERTYPE_PPPOE = 2,
+ MXL862XX_EXTENDEDVLAN_FILTER_ETHERTYPE_ARP = 3,
+ MXL862XX_EXTENDEDVLAN_FILTER_ETHERTYPE_IPV6IPOE = 4,
+ MXL862XX_EXTENDEDVLAN_FILTER_ETHERTYPE_EAPOL = 5,
+ MXL862XX_EXTENDEDVLAN_FILTER_ETHERTYPE_DHCPV4 = 6,
+ MXL862XX_EXTENDEDVLAN_FILTER_ETHERTYPE_DHCPV6 = 7,
+};
+
+/**
+ * struct mxl862xx_extendedvlan_filter_vlan - Per-tag filter in Extended VLAN
+ * @type: Tag presence/type match (see &enum mxl862xx_extended_vlan_filter_type)
+ * @priority_enable: Enable PCP value matching
+ * @priority_val: PCP value to match
+ * @vid_enable: Enable VID matching
+ * @vid_val: VID value to match
+ * @tpid: TPID match mode (see &enum mxl862xx_extended_vlan_filter_tpid)
+ * @dei: DEI match mode (see &enum mxl862xx_extended_vlan_filter_dei)
+ */
+struct mxl862xx_extendedvlan_filter_vlan {
+ __le32 type;
+ u8 priority_enable;
+ __le32 priority_val;
+ u8 vid_enable;
+ __le32 vid_val;
+ __le32 tpid;
+ __le32 dei;
+} __packed;
+
+/**
+ * struct mxl862xx_extendedvlan_filter - Extended VLAN filter configuration
+ * @original_packet_filter_mode: If true, filter on original (pre-treatment)
+ * packet
+ * @filter_4_tpid_mode: 4-TPID mode (see &enum mxl862xx_extended_vlan_4_tpid_mode)
+ * @outer_vlan: Outer VLAN tag filter
+ * @inner_vlan: Inner VLAN tag filter
+ * @ether_type: EtherType filter (see
+ * &enum mxl862xx_extended_vlan_filter_ethertype)
+ */
+struct mxl862xx_extendedvlan_filter {
+ u8 original_packet_filter_mode;
+ __le32 filter_4_tpid_mode;
+ struct mxl862xx_extendedvlan_filter_vlan outer_vlan;
+ struct mxl862xx_extendedvlan_filter_vlan inner_vlan;
+ __le32 ether_type;
+} __packed;
+
+/**
+ * struct mxl862xx_extendedvlan_treatment_vlan - Per-tag treatment in
+ * Extended VLAN
+ * @priority_mode: Priority assignment mode
+ * (see &enum mxl862xx_extended_vlan_treatment_priority)
+ * @priority_val: Priority value (when mode is VAL)
+ * @vid_mode: VID assignment mode
+ * (see &enum mxl862xx_extended_vlan_treatment_vid)
+ * @vid_val: VID value (when mode is VAL)
+ * @tpid: TPID assignment mode
+ * (see &enum mxl862xx_extended_vlan_treatment_tpid)
+ * @dei: DEI assignment mode
+ * (see &enum mxl862xx_extended_vlan_treatment_dei)
+ */
+struct mxl862xx_extendedvlan_treatment_vlan {
+ __le32 priority_mode;
+ __le32 priority_val;
+ __le32 vid_mode;
+ __le32 vid_val;
+ __le32 tpid;
+ __le32 dei;
+} __packed;
+
+/**
+ * struct mxl862xx_extendedvlan_treatment - Extended VLAN treatment
+ * @remove_tag: Tag removal action
+ * (see &enum mxl862xx_extended_vlan_treatment_remove_tag)
+ * @treatment_4_tpid_mode: 4-TPID treatment mode
+ * @add_outer_vlan: Add outer VLAN tag
+ * @outer_vlan: Outer VLAN tag treatment parameters
+ * @add_inner_vlan: Add inner VLAN tag
+ * @inner_vlan: Inner VLAN tag treatment parameters
+ * @reassign_bridge_port: Reassign to different bridge port
+ * @new_bridge_port_id: New bridge port ID
+ * @new_dscp_enable: Enable new DSCP assignment
+ * @new_dscp: New DSCP value
+ * @new_traffic_class_enable: Enable new traffic class assignment
+ * @new_traffic_class: New traffic class value
+ * @new_meter_enable: Enable new metering
+ * @s_new_traffic_meter_id: New traffic meter ID
+ * @dscp2pcp_map: DSCP to PCP mapping table (64 entries)
+ * @loopback_enable: Enable loopback
+ * @da_sa_swap_enable: Enable DA/SA swap
+ * @mirror_enable: Enable mirroring
+ */
+struct mxl862xx_extendedvlan_treatment {
+ __le32 remove_tag;
+ __le32 treatment_4_tpid_mode;
+ u8 add_outer_vlan;
+ struct mxl862xx_extendedvlan_treatment_vlan outer_vlan;
+ u8 add_inner_vlan;
+ struct mxl862xx_extendedvlan_treatment_vlan inner_vlan;
+ u8 reassign_bridge_port;
+ __le16 new_bridge_port_id;
+ u8 new_dscp_enable;
+ __le16 new_dscp;
+ u8 new_traffic_class_enable;
+ u8 new_traffic_class;
+ u8 new_meter_enable;
+ __le16 s_new_traffic_meter_id;
+ u8 dscp2pcp_map[64];
+ u8 loopback_enable;
+ u8 da_sa_swap_enable;
+ u8 mirror_enable;
+} __packed;
+
+/**
+ * struct mxl862xx_extendedvlan_alloc - Extended VLAN block allocation
+ * @number_of_entries: Number of entries to allocate (input) / allocated
+ * (output)
+ * @extended_vlan_block_id: Block ID assigned by firmware (output on alloc,
+ * input on free)
+ *
+ * Used with %MXL862XX_EXTENDEDVLAN_ALLOC and %MXL862XX_EXTENDEDVLAN_FREE.
+ */
+struct mxl862xx_extendedvlan_alloc {
+ __le16 number_of_entries;
+ __le16 extended_vlan_block_id;
+} __packed;
+
+/**
+ * struct mxl862xx_extendedvlan_config - Extended VLAN entry configuration
+ * @extended_vlan_block_id: Block ID from allocation
+ * @entry_index: Entry index within the block
+ * @filter: Filter (match) configuration
+ * @treatment: Treatment (action) configuration
+ *
+ * Used with %MXL862XX_EXTENDEDVLAN_SET and %MXL862XX_EXTENDEDVLAN_GET.
+ */
+struct mxl862xx_extendedvlan_config {
+ __le16 extended_vlan_block_id;
+ __le16 entry_index;
+ struct mxl862xx_extendedvlan_filter filter;
+ struct mxl862xx_extendedvlan_treatment treatment;
+} __packed;
+
+/**
+ * enum mxl862xx_vlan_filter_tci_mask - VLAN Filter TCI mask
+ * @MXL862XX_VLAN_FILTER_TCI_MASK_VID: TCI mask for VLAN ID
+ * @MXL862XX_VLAN_FILTER_TCI_MASK_PCP: TCI mask for VLAN PCP
+ * @MXL862XX_VLAN_FILTER_TCI_MASK_TCI: TCI mask for VLAN TCI
+ */
+enum mxl862xx_vlan_filter_tci_mask {
+ MXL862XX_VLAN_FILTER_TCI_MASK_VID = 0,
+ MXL862XX_VLAN_FILTER_TCI_MASK_PCP = 1,
+ MXL862XX_VLAN_FILTER_TCI_MASK_TCI = 2,
+};
+
+/**
+ * struct mxl862xx_vlanfilter_alloc - VLAN Filter block allocation
+ * @number_of_entries: Number of entries to allocate (input) / allocated
+ * (output)
+ * @vlan_filter_block_id: Block ID assigned by firmware (output on alloc,
+ * input on free)
+ * @discard_untagged: Discard untagged packets
+ * @discard_unmatched_tagged: Discard tagged packets that do not match any
+ * entry in the block
+ * @use_default_port_vid: Use default port VLAN ID for filtering
+ *
+ * Used with %MXL862XX_VLANFILTER_ALLOC and %MXL862XX_VLANFILTER_FREE.
+ */
+struct mxl862xx_vlanfilter_alloc {
+ __le16 number_of_entries;
+ __le16 vlan_filter_block_id;
+ u8 discard_untagged;
+ u8 discard_unmatched_tagged;
+ u8 use_default_port_vid;
+} __packed;
+
+/**
+ * struct mxl862xx_vlanfilter_config - VLAN Filter entry configuration
+ * @vlan_filter_block_id: Block ID from allocation
+ * @entry_index: Entry index within the block
+ * @vlan_filter_mask: TCI field(s) to match (see
+ * &enum mxl862xx_vlan_filter_tci_mask)
+ * @val: TCI value(s) to match (VID, PCP, or full TCI depending on mask)
+ * @discard_matched: When true, discard frames matching this entry;
+ * when false, allow them
+ *
+ * Used with %MXL862XX_VLANFILTER_SET and %MXL862XX_VLANFILTER_GET.
+ */
+struct mxl862xx_vlanfilter_config {
+ __le16 vlan_filter_block_id;
+ __le16 entry_index;
+ __le32 vlan_filter_mask; /* enum mxl862xx_vlan_filter_tci_mask */
+ __le32 val;
+ u8 discard_matched;
+} __packed;
+
+/**
+ * enum mxl862xx_ss_sp_tag_mask - Special tag valid field indicator bits
+ * @MXL862XX_SS_SP_TAG_MASK_RX: valid RX special tag mode
+ * @MXL862XX_SS_SP_TAG_MASK_TX: valid TX special tag mode
+ * @MXL862XX_SS_SP_TAG_MASK_RX_PEN: valid RX special tag info over preamble
+ * @MXL862XX_SS_SP_TAG_MASK_TX_PEN: valid TX special tag info over preamble
+ */
+enum mxl862xx_ss_sp_tag_mask {
+ MXL862XX_SS_SP_TAG_MASK_RX = BIT(0),
+ MXL862XX_SS_SP_TAG_MASK_TX = BIT(1),
+ MXL862XX_SS_SP_TAG_MASK_RX_PEN = BIT(2),
+ MXL862XX_SS_SP_TAG_MASK_TX_PEN = BIT(3),
+};
+
+/**
+ * enum mxl862xx_ss_sp_tag_rx - RX special tag mode
+ * @MXL862XX_SS_SP_TAG_RX_NO_TAG_NO_INSERT: packet does NOT have special
+ * tag and special tag is NOT inserted
+ * @MXL862XX_SS_SP_TAG_RX_NO_TAG_INSERT: packet does NOT have special tag
+ * and special tag is inserted
+ * @MXL862XX_SS_SP_TAG_RX_TAG_NO_INSERT: packet has special tag and special
+ * tag is NOT inserted
+ */
+enum mxl862xx_ss_sp_tag_rx {
+ MXL862XX_SS_SP_TAG_RX_NO_TAG_NO_INSERT = 0,
+ MXL862XX_SS_SP_TAG_RX_NO_TAG_INSERT = 1,
+ MXL862XX_SS_SP_TAG_RX_TAG_NO_INSERT = 2,
+};
+
+/**
+ * enum mxl862xx_ss_sp_tag_tx - TX special tag mode
+ * @MXL862XX_SS_SP_TAG_TX_NO_TAG_NO_REMOVE: packet does NOT have special
+ * tag and special tag is NOT removed
+ * @MXL862XX_SS_SP_TAG_TX_TAG_REPLACE: packet has special tag and special
+ * tag is replaced
+ * @MXL862XX_SS_SP_TAG_TX_TAG_NO_REMOVE: packet has special tag and special
+ * tag is NOT removed
+ * @MXL862XX_SS_SP_TAG_TX_TAG_REMOVE: packet has special tag and special
+ * tag is removed
+ */
+enum mxl862xx_ss_sp_tag_tx {
+ MXL862XX_SS_SP_TAG_TX_NO_TAG_NO_REMOVE = 0,
+ MXL862XX_SS_SP_TAG_TX_TAG_REPLACE = 1,
+ MXL862XX_SS_SP_TAG_TX_TAG_NO_REMOVE = 2,
+ MXL862XX_SS_SP_TAG_TX_TAG_REMOVE = 3,
+};
+
+/**
+ * enum mxl862xx_ss_sp_tag_rx_pen - RX special tag info over preamble
+ * @MXL862XX_SS_SP_TAG_RX_PEN_ALL_0: special tag info inserted from byte 2
+ * to 7 are all 0
+ * @MXL862XX_SS_SP_TAG_RX_PEN_BYTE_5_IS_16: special tag byte 5 is 16, other
+ * bytes from 2 to 7 are 0
+ * @MXL862XX_SS_SP_TAG_RX_PEN_BYTE_5_FROM_PREAMBLE: special tag byte 5 is
+ * from preamble field, others
+ * are 0
+ * @MXL862XX_SS_SP_TAG_RX_PEN_BYTE_2_TO_7_FROM_PREAMBLE: special tag byte 2
+ * to 7 are from preamble
+ * field
+ */
+enum mxl862xx_ss_sp_tag_rx_pen {
+ MXL862XX_SS_SP_TAG_RX_PEN_ALL_0 = 0,
+ MXL862XX_SS_SP_TAG_RX_PEN_BYTE_5_IS_16 = 1,
+ MXL862XX_SS_SP_TAG_RX_PEN_BYTE_5_FROM_PREAMBLE = 2,
+ MXL862XX_SS_SP_TAG_RX_PEN_BYTE_2_TO_7_FROM_PREAMBLE = 3,
+};
+
+/**
+ * struct mxl862xx_ss_sp_tag - Special tag port settings
+ * @pid: port ID (1~16)
+ * @mask: See &enum mxl862xx_ss_sp_tag_mask
+ * @rx: See &enum mxl862xx_ss_sp_tag_rx
+ * @tx: See &enum mxl862xx_ss_sp_tag_tx
+ * @rx_pen: See &enum mxl862xx_ss_sp_tag_rx_pen
+ * @tx_pen: TX special tag info over preamble
+ * 0 - disabled
+ * 1 - enabled
+ */
+struct mxl862xx_ss_sp_tag {
+ u8 pid;
+ u8 mask; /* enum mxl862xx_ss_sp_tag_mask */
+ u8 rx; /* enum mxl862xx_ss_sp_tag_rx */
+ u8 tx; /* enum mxl862xx_ss_sp_tag_tx */
+ u8 rx_pen; /* enum mxl862xx_ss_sp_tag_rx_pen */
+ u8 tx_pen; /* boolean */
+} __packed;
+
+/**
+ * enum mxl862xx_logical_port_mode - Logical port mode
+ * @MXL862XX_LOGICAL_PORT_8BIT_WLAN: WLAN with 8-bit station ID
+ * @MXL862XX_LOGICAL_PORT_9BIT_WLAN: WLAN with 9-bit station ID
+ * @MXL862XX_LOGICAL_PORT_ETHERNET: Ethernet port
+ * @MXL862XX_LOGICAL_PORT_OTHER: Others
+ */
+enum mxl862xx_logical_port_mode {
+ MXL862XX_LOGICAL_PORT_8BIT_WLAN = 0,
+ MXL862XX_LOGICAL_PORT_9BIT_WLAN,
+ MXL862XX_LOGICAL_PORT_ETHERNET,
+ MXL862XX_LOGICAL_PORT_OTHER = 0xFF,
+};
+
+/**
+ * struct mxl862xx_ctp_port_assignment - CTP Port Assignment/association
+ * with logical port
+ * @logical_port_id: Logical Port Id. The valid range is hardware dependent
+ * @first_ctp_port_id: First CTP (Connectivity Termination Port) ID mapped
+ * to above logical port ID
+ * @number_of_ctp_port: Total number of CTP Ports mapped above logical port
+ * ID
+ * @mode: Logical port mode to define sub interface ID format. See
+ * &enum mxl862xx_logical_port_mode
+ * @bridge_port_id: Bridge Port ID (not FID). For allocation, each CTP
+ * allocated is mapped to the Bridge Port given by this field.
+ * The Bridge Port will be configured to use first CTP as
+ * egress CTP.
+ */
+struct mxl862xx_ctp_port_assignment {
+ u8 logical_port_id;
+ __le16 first_ctp_port_id;
+ __le16 number_of_ctp_port;
+ __le32 mode; /* enum mxl862xx_logical_port_mode */
+ __le16 bridge_port_id;
+} __packed;
+
+/**
+ * enum mxl862xx_stp_port_state - Spanning Tree Protocol port states
+ * @MXL862XX_STP_PORT_STATE_FORWARD: Forwarding state
+ * @MXL862XX_STP_PORT_STATE_DISABLE: Disabled/Discarding state
+ * @MXL862XX_STP_PORT_STATE_LEARNING: Learning state
+ * @MXL862XX_STP_PORT_STATE_BLOCKING: Blocking/Listening
+ */
+enum mxl862xx_stp_port_state {
+ MXL862XX_STP_PORT_STATE_FORWARD = 0,
+ MXL862XX_STP_PORT_STATE_DISABLE,
+ MXL862XX_STP_PORT_STATE_LEARNING,
+ MXL862XX_STP_PORT_STATE_BLOCKING,
+};
+
+/**
+ * struct mxl862xx_stp_port_cfg - Configures the Spanning Tree Protocol state
+ * @port_id: Port number
+ * @fid: Filtering Identifier (FID)
+ * @port_state: See &enum mxl862xx_stp_port_state
+ */
+struct mxl862xx_stp_port_cfg {
+ __le16 port_id;
+ __le16 fid;
+ __le32 port_state; /* enum mxl862xx_stp_port_state */
+} __packed;
+
+/**
+ * struct mxl862xx_sys_fw_image_version - Firmware version information
+ * @iv_major: firmware major version
+ * @iv_minor: firmware minor version
+ * @iv_revision: firmware revision
+ * @iv_build_num: firmware build number
+ */
+struct mxl862xx_sys_fw_image_version {
+ u8 iv_major;
+ u8 iv_minor;
+ __le16 iv_revision;
+ __le32 iv_build_num;
+} __packed;
+
+/**
+ * enum mxl862xx_port_type - Port Type
+ * @MXL862XX_LOGICAL_PORT: Logical Port
+ * @MXL862XX_PHYSICAL_PORT: Physical Port
+ * @MXL862XX_CTP_PORT: Connectivity Termination Port (CTP)
+ * @MXL862XX_BRIDGE_PORT: Bridge Port
+ */
+enum mxl862xx_port_type {
+ MXL862XX_LOGICAL_PORT = 0,
+ MXL862XX_PHYSICAL_PORT,
+ MXL862XX_CTP_PORT,
+ MXL862XX_BRIDGE_PORT,
+};
+
+/**
+ * enum mxl862xx_rmon_port_type - RMON counter table type
+ * @MXL862XX_RMON_CTP_PORT_RX: CTP RX counters
+ * @MXL862XX_RMON_CTP_PORT_TX: CTP TX counters
+ * @MXL862XX_RMON_BRIDGE_PORT_RX: Bridge port RX counters
+ * @MXL862XX_RMON_BRIDGE_PORT_TX: Bridge port TX counters
+ * @MXL862XX_RMON_CTP_PORT_PCE_BYPASS: CTP PCE bypass counters
+ * @MXL862XX_RMON_TFLOW_RX: TFLOW RX counters
+ * @MXL862XX_RMON_TFLOW_TX: TFLOW TX counters
+ * @MXL862XX_RMON_QMAP: QMAP counters
+ * @MXL862XX_RMON_METER: Meter counters
+ * @MXL862XX_RMON_PMAC: PMAC counters
+ */
+enum mxl862xx_rmon_port_type {
+ MXL862XX_RMON_CTP_PORT_RX = 0,
+ MXL862XX_RMON_CTP_PORT_TX,
+ MXL862XX_RMON_BRIDGE_PORT_RX,
+ MXL862XX_RMON_BRIDGE_PORT_TX,
+ MXL862XX_RMON_CTP_PORT_PCE_BYPASS,
+ MXL862XX_RMON_TFLOW_RX,
+ MXL862XX_RMON_TFLOW_TX,
+ MXL862XX_RMON_QMAP = 0x0e,
+ MXL862XX_RMON_METER = 0x19,
+ MXL862XX_RMON_PMAC = 0x1c,
+};
+
+/**
+ * struct mxl862xx_rmon_port_cnt - RMON counters for a port
+ * @port_type: Port type for counter retrieval (see &enum mxl862xx_port_type)
+ * @port_id: Ethernet port number (zero-based)
+ * @sub_if_id_group: Sub-interface ID group
+ * @pce_bypass: Separate CTP Tx counters when PCE is bypassed
+ * @rx_extended_vlan_discard_pkts: Discarded at extended VLAN operation
+ * @mtu_exceed_discard_pkts: Discarded due to MTU exceeded
+ * @tx_under_size_good_pkts: Tx undersize (<64) packet count
+ * @tx_oversize_good_pkts: Tx oversize (>1518) packet count
+ * @rx_good_pkts: Received good packet count
+ * @rx_unicast_pkts: Received unicast packet count
+ * @rx_broadcast_pkts: Received broadcast packet count
+ * @rx_multicast_pkts: Received multicast packet count
+ * @rx_fcserror_pkts: Received FCS error packet count
+ * @rx_under_size_good_pkts: Received undersize good packet count
+ * @rx_oversize_good_pkts: Received oversize good packet count
+ * @rx_under_size_error_pkts: Received undersize error packet count
+ * @rx_good_pause_pkts: Received good pause packet count
+ * @rx_oversize_error_pkts: Received oversize error packet count
+ * @rx_align_error_pkts: Received alignment error packet count
+ * @rx_filtered_pkts: Filtered packet count
+ * @rx64byte_pkts: Received 64-byte packet count
+ * @rx127byte_pkts: Received 65-127 byte packet count
+ * @rx255byte_pkts: Received 128-255 byte packet count
+ * @rx511byte_pkts: Received 256-511 byte packet count
+ * @rx1023byte_pkts: Received 512-1023 byte packet count
+ * @rx_max_byte_pkts: Received 1024-max byte packet count
+ * @tx_good_pkts: Transmitted good packet count
+ * @tx_unicast_pkts: Transmitted unicast packet count
+ * @tx_broadcast_pkts: Transmitted broadcast packet count
+ * @tx_multicast_pkts: Transmitted multicast packet count
+ * @tx_single_coll_count: Transmit single collision count
+ * @tx_mult_coll_count: Transmit multiple collision count
+ * @tx_late_coll_count: Transmit late collision count
+ * @tx_excess_coll_count: Transmit excessive collision count
+ * @tx_coll_count: Transmit collision count
+ * @tx_pause_count: Transmit pause packet count
+ * @tx64byte_pkts: Transmitted 64-byte packet count
+ * @tx127byte_pkts: Transmitted 65-127 byte packet count
+ * @tx255byte_pkts: Transmitted 128-255 byte packet count
+ * @tx511byte_pkts: Transmitted 256-511 byte packet count
+ * @tx1023byte_pkts: Transmitted 512-1023 byte packet count
+ * @tx_max_byte_pkts: Transmitted 1024-max byte packet count
+ * @tx_dropped_pkts: Transmit dropped packet count
+ * @tx_acm_dropped_pkts: Transmit ACM dropped packet count
+ * @rx_dropped_pkts: Received dropped packet count
+ * @rx_good_bytes: Received good byte count (64-bit)
+ * @rx_bad_bytes: Received bad byte count (64-bit)
+ * @tx_good_bytes: Transmitted good byte count (64-bit)
+ */
+struct mxl862xx_rmon_port_cnt {
+ __le32 port_type; /* enum mxl862xx_port_type */
+ __le16 port_id;
+ __le16 sub_if_id_group;
+ u8 pce_bypass;
+ __le32 rx_extended_vlan_discard_pkts;
+ __le32 mtu_exceed_discard_pkts;
+ __le32 tx_under_size_good_pkts;
+ __le32 tx_oversize_good_pkts;
+ __le32 rx_good_pkts;
+ __le32 rx_unicast_pkts;
+ __le32 rx_broadcast_pkts;
+ __le32 rx_multicast_pkts;
+ __le32 rx_fcserror_pkts;
+ __le32 rx_under_size_good_pkts;
+ __le32 rx_oversize_good_pkts;
+ __le32 rx_under_size_error_pkts;
+ __le32 rx_good_pause_pkts;
+ __le32 rx_oversize_error_pkts;
+ __le32 rx_align_error_pkts;
+ __le32 rx_filtered_pkts;
+ __le32 rx64byte_pkts;
+ __le32 rx127byte_pkts;
+ __le32 rx255byte_pkts;
+ __le32 rx511byte_pkts;
+ __le32 rx1023byte_pkts;
+ __le32 rx_max_byte_pkts;
+ __le32 tx_good_pkts;
+ __le32 tx_unicast_pkts;
+ __le32 tx_broadcast_pkts;
+ __le32 tx_multicast_pkts;
+ __le32 tx_single_coll_count;
+ __le32 tx_mult_coll_count;
+ __le32 tx_late_coll_count;
+ __le32 tx_excess_coll_count;
+ __le32 tx_coll_count;
+ __le32 tx_pause_count;
+ __le32 tx64byte_pkts;
+ __le32 tx127byte_pkts;
+ __le32 tx255byte_pkts;
+ __le32 tx511byte_pkts;
+ __le32 tx1023byte_pkts;
+ __le32 tx_max_byte_pkts;
+ __le32 tx_dropped_pkts;
+ __le32 tx_acm_dropped_pkts;
+ __le32 rx_dropped_pkts;
+ __le64 rx_good_bytes;
+ __le64 rx_bad_bytes;
+ __le64 tx_good_bytes;
+} __packed;
+
+/* XPCS interface mode, MXL862XX_XPCS_*_INTERFACE field values */
+#define MXL862XX_XPCS_IF_SGMII 0
+#define MXL862XX_XPCS_IF_1000BASEX 1
+#define MXL862XX_XPCS_IF_2500BASEX 2
+#define MXL862XX_XPCS_IF_USXGMII 3 /* single or quad */
+#define MXL862XX_XPCS_IF_10GBASER 4
+#define MXL862XX_XPCS_IF_10GKR 5 /* 10GBASE-KR */
+#define MXL862XX_XPCS_IF_5GBASER 6
+#define MXL862XX_XPCS_IF_QSGMII 7
+
+/* PCS negotiation mode, MXL862XX_XPCS_CFG_NEG_MODE field values */
+#define MXL862XX_XPCS_NEG_NONE 0 /* no inband negotiation */
+#define MXL862XX_XPCS_NEG_INBAND_AN_OFF 1 /* inband, AN disabled */
+#define MXL862XX_XPCS_NEG_INBAND_AN_ON 2 /* inband, AN enabled */
+
+/*
+ * PCS protocol role, MXL862XX_XPCS_CFG_ROLE field value. Selects the role
+ * the XPCS plays in protocols with an asymmetric AN code word (Cisco SGMII
+ * / QSGMII / USXGMII), driving VR_MII_AN_CTRL.TX_CONFIG: MAC means the
+ * local end receives the partner's AN word, PHY means it sources one.
+ * Ignored for symmetric protocols (1000BASE-X, 2500BASE-X, 10GBASE-R/KR).
+ */
+#define MXL862XX_XPCS_ROLE_MAC 0 /* local end is MAC side */
+#define MXL862XX_XPCS_ROLE_PHY 1 /* local end is PHY side */
+
+/* USXGMII lane mode, MXL862XX_XPCS_*_USX_LANE_MODE field values */
+#define MXL862XX_XPCS_USX_SINGLE 0 /* single USXGMII lane */
+#define MXL862XX_XPCS_USX_QUAD 1 /* quad USXGMII, 4 ports/lane */
+
+/**
+ * union mxl862xx_xpcs_an_word - XPCS AN code word, tagged by interface mode
+ * @cl37: 16-bit base-page word exchanged over the CL37 hardware AN path
+ * (SR_MII_AN_ADV on write, SR_MII_LP_BABL on read). Carries the
+ * 802.3 CL37 base page for 1000BASE-X/2500BASE-X and the Cisco
+ * SGMII config word for SGMII/QSGMII.
+ * @usx: USXGMII 16-bit AN code word, MDIO_USXGMII_* layout
+ * @cl73: CL73 48-bit base page (10GBASE-KR), three 16-bit registers per
+ * 802.3 Annex 28C
+ * @cl73.adv1: CL73 SR_AN_ADV1 / SR_AN_LP_ABL1
+ * @cl73.adv2: CL73 SR_AN_ADV2 / SR_AN_LP_ABL2
+ * @cl73.adv3: CL73 SR_AN_ADV3 / SR_AN_LP_ABL3
+ *
+ * The host picks the right member based on the interface field of the
+ * surrounding struct (and, for the asymmetric protocols, on the role).
+ */
+union mxl862xx_xpcs_an_word {
+ __le16 cl37;
+ __le16 usx;
+ struct {
+ __le16 adv1;
+ __le16 adv2;
+ __le16 adv3;
+ } cl73;
+} __packed;
+
+/* PCS duplex mode, MXL862XX_XPCS_*_DUPLEX field values */
+#define MXL862XX_XPCS_DUPLEX_HALF 0
+#define MXL862XX_XPCS_DUPLEX_FULL 1
+
+/**
+ * enum mxl862xx_xpcs_loopback_mode - XPCS loopback mode
+ * @MXL862XX_XPCS_LB_DISABLE: disable all loopback
+ * @MXL862XX_XPCS_LB_PCS_SERIAL: PCS TX-to-RX serial loopback
+ * @MXL862XX_XPCS_LB_PCS_PARALLEL: PCS RX-to-TX parallel loopback
+ * @MXL862XX_XPCS_LB_PMA_SERIAL: PMA TX-to-RX serial loopback
+ * @MXL862XX_XPCS_LB_PMA_PARALLEL: PMA RX-to-TX parallel loopback
+ */
+enum mxl862xx_xpcs_loopback_mode {
+ MXL862XX_XPCS_LB_DISABLE = 0,
+ MXL862XX_XPCS_LB_PCS_SERIAL = 1,
+ MXL862XX_XPCS_LB_PCS_PARALLEL = 2,
+ MXL862XX_XPCS_LB_PMA_SERIAL = 3,
+ MXL862XX_XPCS_LB_PMA_PARALLEL = 4,
+};
+
+/* Fields of mxl862xx_xpcs_pcs_cfg.mode */
+#define MXL862XX_XPCS_CFG_PORT_ID GENMASK(1, 0)
+#define MXL862XX_XPCS_CFG_INTERFACE GENMASK(7, 2)
+#define MXL862XX_XPCS_CFG_NEG_MODE GENMASK(9, 8)
+#define MXL862XX_XPCS_CFG_PERMIT_PAUSE BIT(10)
+#define MXL862XX_XPCS_CFG_USX_LANE_MODE GENMASK(12, 11)
+#define MXL862XX_XPCS_CFG_ROLE BIT(13)
+#define MXL862XX_XPCS_CFG_USX_SUBPORT GENMASK(15, 14)
+
+/**
+ * struct mxl862xx_xpcs_pcs_cfg - PCS configuration parameters
+ * @mode: Packed interface and negotiation parameters, see
+ * MXL862XX_XPCS_CFG_*. port_id is the XPCS port index (0-3);
+ * interface is the PCS interface mode (MXL862XX_XPCS_IF_*);
+ * neg_mode is the negotiation mode (MXL862XX_XPCS_NEG_*);
+ * permit_pause allows pause to MAC; usx_lane_mode is the USXGMII
+ * lane mode (MXL862XX_XPCS_USX_*); role is the protocol role
+ * (MXL862XX_XPCS_ROLE_*); usx_subport is the sub-port (0-3) within
+ * the XPCS -- despite the name it also identifies the QSGMII
+ * sub-port -- used by the firmware to set MAC pause per sub-port
+ * and ignored for the XPCS-wide bringup, which is idempotent across
+ * slots.
+ * @advertising: AN code word the local end transmits. The active union
+ * member is selected by the interface field (and, for the
+ * asymmetric protocols, by role). Ignored when the local end
+ * does not transmit an AN word (role=MAC for SGMII/QSGMII/
+ * USXGMII, 10GBASE-R, 5GBASE-R) or when neg_mode is not
+ * INBAND_AN_ON. Pass all-zero to keep the firmware default
+ * advertisement.
+ * @result: Firmware result. >0 means the host must follow with an AN
+ * restart, 0 means no host follow-up is needed, <0 is an errno.
+ */
+struct mxl862xx_xpcs_pcs_cfg {
+ __le16 mode;
+ union mxl862xx_xpcs_an_word advertising;
+ __le16 result;
+} __packed;
+
+/* Fields of mxl862xx_xpcs_pcs_state.mode */
+#define MXL862XX_XPCS_ST_PORT_ID GENMASK(1, 0)
+#define MXL862XX_XPCS_ST_INTERFACE GENMASK(7, 2)
+#define MXL862XX_XPCS_ST_USX_LANE_MODE GENMASK(9, 8)
+#define MXL862XX_XPCS_ST_USX_SUBPORT GENMASK(11, 10)
+#define MXL862XX_XPCS_ST_LINK BIT(12)
+#define MXL862XX_XPCS_ST_AN_COMPLETE BIT(13)
+#define MXL862XX_XPCS_ST_DUPLEX BIT(14)
+#define MXL862XX_XPCS_ST_PCS_FAULT BIT(15)
+#define MXL862XX_XPCS_ST_PAUSE GENMASK(17, 16)
+#define MXL862XX_XPCS_ST_LP_EEE_CAP BIT(18)
+#define MXL862XX_XPCS_ST_LP_EEE_CS_CAP BIT(19)
+
+/**
+ * struct mxl862xx_xpcs_pcs_state - PCS link state
+ * @mode: Packed input parameters and firmware status, see
+ * MXL862XX_XPCS_ST_*. The host writes port_id (XPCS port index 0-3),
+ * interface (MXL862XX_XPCS_IF_*), usx_lane_mode
+ * (MXL862XX_XPCS_USX_*) and usx_subport (0-3); the firmware fills in
+ * link, an_complete, duplex (MXL862XX_XPCS_DUPLEX_*), pcs_fault,
+ * pause (bit 0 symmetric, bit 1 asymmetric), lp_eee_cap and
+ * lp_eee_cs_cap.
+ * @speed: Resolved speed in Mbit/s (output)
+ * @lpa: Link partner ability word (output). Same union as
+ * &union mxl862xx_xpcs_an_word; the host picks the member based on
+ * the interface field.
+ */
+struct mxl862xx_xpcs_pcs_state {
+ __le32 mode;
+ __le16 speed; /* Mbit/s */
+ union mxl862xx_xpcs_an_word lpa;
+} __packed;
+
+/**
+ * struct mxl862xx_xpcs_pcs_disable - PCS disable parameters
+ * @port_id: XPCS port index
+ * @__pad: padding
+ * @result: Firmware result. 0 on success, <0 on error.
+ *
+ * Asserts IDDQ + PHY + XPCS resets to power down the SERDES when the
+ * port is admin-down or no module is plugged in. The next PCS config
+ * implicitly powers it back up and reprograms the desired interface.
+ */
+struct mxl862xx_xpcs_pcs_disable {
+ u8 port_id;
+ u8 __pad;
+ __le16 result;
+} __packed;
+
+/* Fields of mxl862xx_xpcs_an_restart.mode */
+#define MXL862XX_XPCS_ANR_PORT_ID GENMASK(1, 0)
+#define MXL862XX_XPCS_ANR_INTERFACE GENMASK(7, 2)
+#define MXL862XX_XPCS_ANR_USX_LANE_MODE GENMASK(9, 8)
+#define MXL862XX_XPCS_ANR_USX_SUBPORT GENMASK(11, 10)
+
+/**
+ * struct mxl862xx_xpcs_an_restart - AN restart parameters
+ * @mode: Packed input parameters, see MXL862XX_XPCS_ANR_*. port_id is the
+ * XPCS port index (0-3); interface is the PCS interface mode
+ * (MXL862XX_XPCS_IF_*); usx_lane_mode is the USX lane mode
+ * (MXL862XX_XPCS_USX_*); usx_subport (0-3) selects the lane whose
+ * AN is restarted for QSGMII and QUSXGMII and is ignored by
+ * single-lane modes.
+ * @result: Firmware result. 0 on success, <0 on error.
+ *
+ * Restarts auto-negotiation on a single sub-port of the XPCS. The
+ * SERDES must already be configured.
+ */
+struct mxl862xx_xpcs_an_restart {
+ __le16 mode;
+ __le16 result;
+} __packed;
+
+/* Fields of mxl862xx_xpcs_pcs_link_up.mode */
+#define MXL862XX_XPCS_LU_PORT_ID GENMASK(1, 0)
+#define MXL862XX_XPCS_LU_INTERFACE GENMASK(7, 2)
+#define MXL862XX_XPCS_LU_DUPLEX BIT(8)
+#define MXL862XX_XPCS_LU_USX_LANE_MODE GENMASK(10, 9)
+#define MXL862XX_XPCS_LU_USX_SUBPORT GENMASK(12, 11)
+
+/**
+ * struct mxl862xx_xpcs_pcs_link_up - PCS link-up parameters
+ * @mode: Packed input parameters, see MXL862XX_XPCS_LU_*. port_id is the
+ * XPCS port index (0-3); interface is the PCS interface mode
+ * (MXL862XX_XPCS_IF_*); duplex is the duplex mode
+ * (MXL862XX_XPCS_DUPLEX_*); usx_lane_mode is the USX lane mode
+ * (USXGMII only, ignored otherwise, MXL862XX_XPCS_USX_*);
+ * usx_subport (0-3) selects the sub-port for QUSXGMII and QSGMII
+ * (despite the name) and is ignored otherwise.
+ * @speed: Resolved speed in Mbit/s
+ * @result: Firmware result. 0 on success, <0 is errno.
+ *
+ * Called once per link-up event after the host has resolved the
+ * line-side speed/duplex (from the PHY's read_status, from a preceding
+ * PCS get-state, or from a fixed-link description).
+ */
+struct mxl862xx_xpcs_pcs_link_up {
+ __le16 mode;
+ __le16 speed; /* Mbit/s */
+ __le16 result;
+} __packed;
+
+#endif /* __MXL862XX_API_H */
diff --git a/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h b/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
new file mode 100644
index 000000000..c87a955c1
--- /dev/null
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
@@ -0,0 +1,85 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#ifndef __MXL862XX_CMD_H
+#define __MXL862XX_CMD_H
+
+#define MXL862XX_MMD_DEV 30
+#define MXL862XX_MMD_REG_CTRL 0
+#define MXL862XX_MMD_REG_LEN_RET 1
+#define MXL862XX_MMD_REG_DATA_FIRST 2
+#define MXL862XX_MMD_REG_DATA_LAST 95
+#define MXL862XX_MMD_REG_DATA_MAX_SIZE \
+ (MXL862XX_MMD_REG_DATA_LAST - MXL862XX_MMD_REG_DATA_FIRST + 1)
+
+#define MXL862XX_COMMON_MAGIC 0x100
+#define MXL862XX_BRDG_MAGIC 0x300
+#define MXL862XX_BRDGPORT_MAGIC 0x400
+#define MXL862XX_CTP_MAGIC 0x500
+#define MXL862XX_QOS_MAGIC 0x600
+#define MXL862XX_RMON_MAGIC 0x700
+#define MXL862XX_SWMAC_MAGIC 0xa00
+#define MXL862XX_EXTVLAN_MAGIC 0xb00
+#define MXL862XX_VLANFILTER_MAGIC 0xc00
+#define MXL862XX_STP_MAGIC 0xf00
+#define MXL862XX_SS_MAGIC 0x1600
+#define GPY_GPY2XX_MAGIC 0x1800
+#define SYS_MISC_MAGIC 0x1900
+#define MXL862XX_XPCS_MAGIC 0x1a00
+
+#define MXL862XX_COMMON_CFGGET (MXL862XX_COMMON_MAGIC + 0x9)
+#define MXL862XX_COMMON_CFGSET (MXL862XX_COMMON_MAGIC + 0xa)
+#define MXL862XX_COMMON_REGISTERMOD (MXL862XX_COMMON_MAGIC + 0x11)
+
+#define MXL862XX_BRIDGE_ALLOC (MXL862XX_BRDG_MAGIC + 0x1)
+#define MXL862XX_BRIDGE_CONFIGSET (MXL862XX_BRDG_MAGIC + 0x2)
+#define MXL862XX_BRIDGE_CONFIGGET (MXL862XX_BRDG_MAGIC + 0x3)
+#define MXL862XX_BRIDGE_FREE (MXL862XX_BRDG_MAGIC + 0x4)
+
+#define MXL862XX_BRIDGEPORT_ALLOC (MXL862XX_BRDGPORT_MAGIC + 0x1)
+#define MXL862XX_BRIDGEPORT_CONFIGSET (MXL862XX_BRDGPORT_MAGIC + 0x2)
+#define MXL862XX_BRIDGEPORT_CONFIGGET (MXL862XX_BRDGPORT_MAGIC + 0x3)
+#define MXL862XX_BRIDGEPORT_FREE (MXL862XX_BRDGPORT_MAGIC + 0x4)
+
+#define MXL862XX_CTP_PORTASSIGNMENTSET (MXL862XX_CTP_MAGIC + 0x3)
+
+#define MXL862XX_QOS_METERCFGSET (MXL862XX_QOS_MAGIC + 0x2)
+#define MXL862XX_QOS_METERALLOC (MXL862XX_QOS_MAGIC + 0x2a)
+
+#define MXL862XX_RMON_PORT_GET (MXL862XX_RMON_MAGIC + 0x1)
+
+#define MXL862XX_MAC_TABLEENTRYADD (MXL862XX_SWMAC_MAGIC + 0x2)
+#define MXL862XX_MAC_TABLEENTRYREAD (MXL862XX_SWMAC_MAGIC + 0x3)
+#define MXL862XX_MAC_TABLEENTRYQUERY (MXL862XX_SWMAC_MAGIC + 0x4)
+#define MXL862XX_MAC_TABLEENTRYREMOVE (MXL862XX_SWMAC_MAGIC + 0x5)
+#define MXL862XX_MAC_TABLECLEARCOND (MXL862XX_SWMAC_MAGIC + 0x8)
+
+#define MXL862XX_EXTENDEDVLAN_ALLOC (MXL862XX_EXTVLAN_MAGIC + 0x1)
+#define MXL862XX_EXTENDEDVLAN_SET (MXL862XX_EXTVLAN_MAGIC + 0x2)
+#define MXL862XX_EXTENDEDVLAN_GET (MXL862XX_EXTVLAN_MAGIC + 0x3)
+#define MXL862XX_EXTENDEDVLAN_FREE (MXL862XX_EXTVLAN_MAGIC + 0x4)
+
+#define MXL862XX_VLANFILTER_ALLOC (MXL862XX_VLANFILTER_MAGIC + 0x1)
+#define MXL862XX_VLANFILTER_SET (MXL862XX_VLANFILTER_MAGIC + 0x2)
+#define MXL862XX_VLANFILTER_GET (MXL862XX_VLANFILTER_MAGIC + 0x3)
+#define MXL862XX_VLANFILTER_FREE (MXL862XX_VLANFILTER_MAGIC + 0x4)
+
+#define MXL862XX_SS_SPTAG_SET (MXL862XX_SS_MAGIC + 0x2)
+
+#define MXL862XX_STP_PORTCFGSET (MXL862XX_STP_MAGIC + 0x2)
+
+#define INT_GPHY_READ (GPY_GPY2XX_MAGIC + 0x1)
+#define INT_GPHY_WRITE (GPY_GPY2XX_MAGIC + 0x2)
+
+#define SYS_MISC_FW_VERSION (SYS_MISC_MAGIC + 0x2)
+
+#define MXL862XX_XPCS_PCS_CONFIG (MXL862XX_XPCS_MAGIC + 0x1)
+#define MXL862XX_XPCS_PCS_GET_STATE (MXL862XX_XPCS_MAGIC + 0x2)
+#define MXL862XX_XPCS_PCS_DISABLE (MXL862XX_XPCS_MAGIC + 0x4)
+#define MXL862XX_XPCS_AN_RESTART (MXL862XX_XPCS_MAGIC + 0x5)
+#define MXL862XX_XPCS_PCS_LINK_UP (MXL862XX_XPCS_MAGIC + 0x7)
+#define MXL862XX_XPCS_LOOPBACK (MXL862XX_XPCS_MAGIC + 0x8)
+#define MXL862XX_XPCS_RESET (MXL862XX_XPCS_MAGIC + 0x9)
+
+#define MMD_API_MAXIMUM_ID 0x7fff
+
+#endif /* __MXL862XX_CMD_H */
diff --git a/drivers/net/dsa/mxl862xx/mxl862xx-host.c b/drivers/net/dsa/mxl862xx/mxl862xx-host.c
new file mode 100644
index 000000000..d55f9dff6
--- /dev/null
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-host.c
@@ -0,0 +1,504 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Based upon the MaxLinear SDK driver
+ *
+ * Copyright (C) 2025 Daniel Golle <daniel@makrotopia.org>
+ * Copyright (C) 2025 John Crispin <john@phrozen.org>
+ * Copyright (C) 2024 MaxLinear Inc.
+ */
+
+#include <linux/bitops.h>
+#include <linux/bits.h>
+#include <linux/crc16.h>
+#include <linux/iopoll.h>
+#include <linux/limits.h>
+#include <net/dsa.h>
+#include "mxl862xx.h"
+#include "mxl862xx-host.h"
+
+#define CTRL_BUSY_MASK BIT(15)
+#define CTRL_CRC_FLAG BIT(14)
+
+#define LEN_RET_LEN_MASK GENMASK(9, 0)
+
+#define MXL862XX_MMD_REG_CTRL 0
+#define MXL862XX_MMD_REG_LEN_RET 1
+#define MXL862XX_MMD_REG_DATA_FIRST 2
+#define MXL862XX_MMD_REG_DATA_LAST 95
+#define MXL862XX_MMD_REG_DATA_MAX_SIZE \
+ (MXL862XX_MMD_REG_DATA_LAST - MXL862XX_MMD_REG_DATA_FIRST + 1)
+
+#define MMD_API_SET_DATA_0 2
+#define MMD_API_GET_DATA_0 5
+#define MMD_API_RST_DATA 8
+
+#define MXL862XX_SWITCH_RESET 0x9907
+
+static void mxl862xx_crc_err_work_fn(struct work_struct *work)
+{
+ struct mxl862xx_priv *priv = container_of(work, struct mxl862xx_priv,
+ crc_err_work);
+ struct dsa_port *dp;
+
+ dev_warn(&priv->mdiodev->dev,
+ "MDIO CRC error detected, shutting down all ports\n");
+
+ rtnl_lock();
+ dsa_switch_for_each_cpu_port(dp, priv->ds)
+ dev_close(dp->conduit);
+ rtnl_unlock();
+
+ clear_bit(MXL862XX_FLAG_CRC_ERR, &priv->flags);
+}
+
+/* Firmware CRC error codes (outside normal Zephyr errno range). */
+#define MXL862XX_FW_CRC6_ERR (-1024)
+#define MXL862XX_FW_CRC16_ERR (-1023)
+
+/* 3GPP CRC-6 lookup table (polynomial 0x6F).
+ * Matches the firmware's default CRC-6 implementation.
+ */
+static const u8 mxl862xx_crc6_table[256] = {
+ 0x00, 0x2f, 0x31, 0x1e, 0x0d, 0x22, 0x3c, 0x13,
+ 0x1a, 0x35, 0x2b, 0x04, 0x17, 0x38, 0x26, 0x09,
+ 0x34, 0x1b, 0x05, 0x2a, 0x39, 0x16, 0x08, 0x27,
+ 0x2e, 0x01, 0x1f, 0x30, 0x23, 0x0c, 0x12, 0x3d,
+ 0x07, 0x28, 0x36, 0x19, 0x0a, 0x25, 0x3b, 0x14,
+ 0x1d, 0x32, 0x2c, 0x03, 0x10, 0x3f, 0x21, 0x0e,
+ 0x33, 0x1c, 0x02, 0x2d, 0x3e, 0x11, 0x0f, 0x20,
+ 0x29, 0x06, 0x18, 0x37, 0x24, 0x0b, 0x15, 0x3a,
+ 0x0e, 0x21, 0x3f, 0x10, 0x03, 0x2c, 0x32, 0x1d,
+ 0x14, 0x3b, 0x25, 0x0a, 0x19, 0x36, 0x28, 0x07,
+ 0x3a, 0x15, 0x0b, 0x24, 0x37, 0x18, 0x06, 0x29,
+ 0x20, 0x0f, 0x11, 0x3e, 0x2d, 0x02, 0x1c, 0x33,
+ 0x09, 0x26, 0x38, 0x17, 0x04, 0x2b, 0x35, 0x1a,
+ 0x13, 0x3c, 0x22, 0x0d, 0x1e, 0x31, 0x2f, 0x00,
+ 0x3d, 0x12, 0x0c, 0x23, 0x30, 0x1f, 0x01, 0x2e,
+ 0x27, 0x08, 0x16, 0x39, 0x2a, 0x05, 0x1b, 0x34,
+ 0x1c, 0x33, 0x2d, 0x02, 0x11, 0x3e, 0x20, 0x0f,
+ 0x06, 0x29, 0x37, 0x18, 0x0b, 0x24, 0x3a, 0x15,
+ 0x28, 0x07, 0x19, 0x36, 0x25, 0x0a, 0x14, 0x3b,
+ 0x32, 0x1d, 0x03, 0x2c, 0x3f, 0x10, 0x0e, 0x21,
+ 0x1b, 0x34, 0x2a, 0x05, 0x16, 0x39, 0x27, 0x08,
+ 0x01, 0x2e, 0x30, 0x1f, 0x0c, 0x23, 0x3d, 0x12,
+ 0x2f, 0x00, 0x1e, 0x31, 0x22, 0x0d, 0x13, 0x3c,
+ 0x35, 0x1a, 0x04, 0x2b, 0x38, 0x17, 0x09, 0x26,
+ 0x12, 0x3d, 0x23, 0x0c, 0x1f, 0x30, 0x2e, 0x01,
+ 0x08, 0x27, 0x39, 0x16, 0x05, 0x2a, 0x34, 0x1b,
+ 0x26, 0x09, 0x17, 0x38, 0x2b, 0x04, 0x1a, 0x35,
+ 0x3c, 0x13, 0x0d, 0x22, 0x31, 0x1e, 0x00, 0x2f,
+ 0x15, 0x3a, 0x24, 0x0b, 0x18, 0x37, 0x29, 0x06,
+ 0x0f, 0x20, 0x3e, 0x11, 0x02, 0x2d, 0x33, 0x1c,
+ 0x21, 0x0e, 0x10, 0x3f, 0x2c, 0x03, 0x1d, 0x32,
+ 0x3b, 0x14, 0x0a, 0x25, 0x36, 0x19, 0x07, 0x28,
+};
+
+/* Compute 3GPP CRC-6 over the ctrl register (16 bits) and the lower
+ * 10 bits of the len_ret register. The 26-bit input is packed as
+ * { len_ret[9:0], ctrl[15:0] } and processed LSB-first through the
+ * lookup table.
+ */
+static u8 mxl862xx_crc6(u16 ctrl, u16 len_ret)
+{
+ u32 data = ((u32)(len_ret & LEN_RET_LEN_MASK) << 16) | ctrl;
+ u8 crc = 0;
+ int i;
+
+ for (i = 0; i < sizeof(data); i++, data >>= 8)
+ crc = mxl862xx_crc6_table[(crc << 2) ^ (data & 0xff)] & 0x3f;
+
+ return crc;
+}
+
+/* Encode CRC-6 into the ctrl and len_ret registers before writing them
+ * to MDIO. The caller must set ctrl = API_ID | CTRL_BUSY_MASK |
+ * CTRL_CRC_FLAG, and len_ret = parameter length (bits 0-9 only).
+ *
+ * After encoding:
+ * ctrl[12:0] = API ID (unchanged)
+ * ctrl[14:13] = CRC-6 bits 5-4
+ * ctrl[15] = busy flag (unchanged)
+ * len_ret[9:0] = parameter length (unchanged)
+ * len_ret[13:10] = CRC-6 bits 3-0
+ * len_ret[14] = original ctrl[14] (CRC check flag, forwarded to FW)
+ * len_ret[15] = original ctrl[13] (magic bit, always 1)
+ */
+static void mxl862xx_crc6_encode(u16 *pctrl, u16 *plen_ret)
+{
+ u16 crc, ctrl, len_ret;
+
+ /* Set magic bit before CRC computation */
+ *pctrl |= BIT(13);
+
+ crc = mxl862xx_crc6(*pctrl, *plen_ret);
+
+ /* Place CRC MSB (bits 5-4) into ctrl bits 13-14 */
+ ctrl = (*pctrl & ~GENMASK(14, 13));
+ ctrl |= (crc & 0x30) << 9;
+
+ /* Place CRC LSB (bits 3-0) into len_ret bits 10-13 */
+ len_ret = *plen_ret | ((crc & 0x0f) << 10);
+
+ /* Forward ctrl[14] (CRC check flag) to len_ret[14],
+ * and ctrl[13] (magic, always 1) to len_ret[15].
+ */
+ len_ret |= (*pctrl & BIT(14)) | ((*pctrl & BIT(13)) << 2);
+
+ *pctrl = ctrl;
+ *plen_ret = len_ret;
+}
+
+/* Verify CRC-6 on a firmware response and extract the return value.
+ *
+ * The firmware encodes the return value as a signed 11-bit integer:
+ * - Sign bit (bit 10) in ctrl[14]
+ * - Magnitude (bits 9-0) in len_ret[9:0]
+ * These are recoverable after CRC-6 verification by restoring the
+ * original ctrl from the auxiliary copies in len_ret[15:14].
+ *
+ * Return: 0 on CRC match (with *result set), or -EIO on mismatch.
+ */
+static int mxl862xx_crc6_verify(u16 ctrl, u16 len_ret, int *result)
+{
+ u16 crc_recv, crc_calc;
+
+ /* Extract the received CRC-6 */
+ crc_recv = ((ctrl >> 9) & 0x30) | ((len_ret >> 10) & 0x0f);
+
+ /* Reconstruct the original ctrl for re-computation:
+ * ctrl[14] = len_ret[14] (sign bit / CRC check flag)
+ * ctrl[13] = len_ret[15] >> 2 (magic bit)
+ */
+ ctrl &= ~GENMASK(14, 13);
+ ctrl |= len_ret & BIT(14);
+ ctrl |= (len_ret & BIT(15)) >> 2;
+
+ crc_calc = mxl862xx_crc6(ctrl, len_ret);
+ if (crc_recv != crc_calc)
+ return -EIO;
+
+ /* Extract signed 11-bit return value:
+ * bit 10 (sign) from ctrl[14], bits 9-0 from len_ret[9:0]
+ */
+ *result = sign_extend32((len_ret & LEN_RET_LEN_MASK) |
+ ((ctrl & CTRL_CRC_FLAG) >> 4), 10);
+
+ return 0;
+}
+
+static int mxl862xx_reg_read(struct mxl862xx_priv *priv, u32 addr)
+{
+ return __mdiodev_c45_read(priv->mdiodev, MDIO_MMD_VEND1, addr);
+}
+
+static int mxl862xx_reg_write(struct mxl862xx_priv *priv, u32 addr, u16 data)
+{
+ return __mdiodev_c45_write(priv->mdiodev, MDIO_MMD_VEND1, addr, data);
+}
+
+static int mxl862xx_ctrl_read(struct mxl862xx_priv *priv)
+{
+ return mxl862xx_reg_read(priv, MXL862XX_MMD_REG_CTRL);
+}
+
+static int mxl862xx_busy_wait(struct mxl862xx_priv *priv)
+{
+ int val;
+
+ return readx_poll_timeout(mxl862xx_ctrl_read, priv, val,
+ !(val & CTRL_BUSY_MASK), 15, 500000);
+}
+
+/* Issue a firmware command with CRC-6 protection on the ctrl and len_ret
+ * registers, wait for completion, and verify the response CRC-6.
+ *
+ * Return: firmware result value (>= 0) on success, or negative errno.
+ */
+static int mxl862xx_issue_cmd(struct mxl862xx_priv *priv, u16 cmd, u16 len)
+{
+ u16 ctrl_enc, len_enc;
+ int ret, fw_result;
+
+ ctrl_enc = cmd | CTRL_BUSY_MASK | CTRL_CRC_FLAG;
+ len_enc = len;
+ mxl862xx_crc6_encode(&ctrl_enc, &len_enc);
+
+ ret = mxl862xx_reg_write(priv, MXL862XX_MMD_REG_LEN_RET, len_enc);
+ if (ret < 0)
+ return ret;
+
+ ret = mxl862xx_reg_write(priv, MXL862XX_MMD_REG_CTRL, ctrl_enc);
+ if (ret < 0)
+ return ret;
+
+ ret = mxl862xx_busy_wait(priv);
+ if (ret < 0)
+ return ret;
+
+ ret = mxl862xx_reg_read(priv, MXL862XX_MMD_REG_CTRL);
+ if (ret < 0)
+ return ret;
+ ctrl_enc = ret;
+
+ ret = mxl862xx_reg_read(priv, MXL862XX_MMD_REG_LEN_RET);
+ if (ret < 0)
+ return ret;
+ len_enc = ret;
+
+ ret = mxl862xx_crc6_verify(ctrl_enc, len_enc, &fw_result);
+ if (ret) {
+ if (!test_and_set_bit(MXL862XX_FLAG_CRC_ERR, &priv->flags))
+ schedule_work(&priv->crc_err_work);
+ return -EIO;
+ }
+
+ return fw_result;
+}
+
+static int mxl862xx_set_data(struct mxl862xx_priv *priv, u16 words)
+{
+ u16 cmd;
+
+ cmd = words / MXL862XX_MMD_REG_DATA_MAX_SIZE - 1;
+ if (!(cmd < 2))
+ return -EINVAL;
+
+ cmd += MMD_API_SET_DATA_0;
+
+ return mxl862xx_issue_cmd(priv, cmd,
+ MXL862XX_MMD_REG_DATA_MAX_SIZE * sizeof(u16));
+}
+
+static int mxl862xx_get_data(struct mxl862xx_priv *priv, u16 words)
+{
+ u16 cmd;
+
+ cmd = words / MXL862XX_MMD_REG_DATA_MAX_SIZE;
+ if (!(cmd > 0 && cmd < 3))
+ return -EINVAL;
+
+ cmd += MMD_API_GET_DATA_0;
+
+ return mxl862xx_issue_cmd(priv, cmd,
+ MXL862XX_MMD_REG_DATA_MAX_SIZE * sizeof(u16));
+}
+
+static int mxl862xx_rst_data(struct mxl862xx_priv *priv)
+{
+ return mxl862xx_issue_cmd(priv, MMD_API_RST_DATA, 0);
+}
+
+/* Minimum number of zero words in the data payload before issuing a
+ * RST_DATA command is worthwhile. RST_DATA costs one full command
+ * round-trip (~5 MDIO transactions), so the threshold must offset that.
+ */
+#define RST_DATA_THRESHOLD 5
+
+static int mxl862xx_send_cmd(struct mxl862xx_priv *priv, u16 cmd, u16 size,
+ bool quiet)
+{
+ int ret;
+
+ ret = mxl862xx_issue_cmd(priv, cmd, size);
+
+ /* Handle errors returned by the firmware as -EIO.
+ * The firmware is based on Zephyr OS and uses the errors as
+ * defined in errno.h of Zephyr OS. See
+ * https://github.com/zephyrproject-rtos/zephyr/blob/v3.7.0/lib/libc/minimal/include/errno.h
+ *
+ * The firmware signals CRC validation failures with dedicated
+ * error codes outside the normal Zephyr errno range:
+ * -1024: CRC-6 mismatch on ctrl/len_ret registers
+ * -1023: CRC-16 mismatch on data payload
+ */
+ if (ret < 0) {
+ if ((ret == MXL862XX_FW_CRC6_ERR ||
+ ret == MXL862XX_FW_CRC16_ERR) &&
+ !test_and_set_bit(MXL862XX_FLAG_CRC_ERR, &priv->flags))
+ schedule_work(&priv->crc_err_work);
+ if (!quiet)
+ dev_err(&priv->mdiodev->dev,
+ "CMD %04x returned error %d\n", cmd, ret);
+ return -EIO;
+ }
+
+ return ret;
+}
+
+int mxl862xx_api_wrap(struct mxl862xx_priv *priv, u16 cmd, void *_data,
+ u16 size, bool read, bool quiet)
+{
+ __le16 *data = _data;
+ bool use_rst = false;
+ unsigned int zeros;
+ int ret, cmd_ret;
+ u16 max, crc, i;
+
+ dev_dbg(&priv->mdiodev->dev, "CMD %04x DATA %*ph\n", cmd, size, data);
+
+ mutex_lock_nested(&priv->mdiodev->bus->mdio_lock, MDIO_MUTEX_NESTED);
+
+ max = (size + 1) / 2;
+
+ ret = mxl862xx_busy_wait(priv);
+ if (ret < 0)
+ goto out;
+
+ /* If the data contains enough zero words, issue RST_DATA to zero
+ * both the firmware buffer and MMD registers, then skip writing
+ * zero words individually.
+ */
+ for (i = 0, zeros = 0; i < size / 2 && zeros < RST_DATA_THRESHOLD; i++)
+ if (!data[i])
+ zeros++;
+
+ if (zeros < RST_DATA_THRESHOLD && (size & 1) && !*(u8 *)&data[i])
+ zeros++;
+
+ if (zeros >= RST_DATA_THRESHOLD) {
+ ret = mxl862xx_rst_data(priv);
+ if (ret < 0)
+ goto out;
+ use_rst = true;
+ }
+
+ /* Compute CRC-16 over the data payload; written as an extra word
+ * after the data so the firmware can verify the transfer.
+ */
+ crc = crc16(0xffff, (const u8 *)data, size);
+
+ for (i = 0; i < max + 1; i++) {
+ u16 off = i % MXL862XX_MMD_REG_DATA_MAX_SIZE;
+ u16 val;
+
+ if (i && off == 0) {
+ /* Send command to set data when every
+ * MXL862XX_MMD_REG_DATA_MAX_SIZE of WORDs are written.
+ */
+ ret = mxl862xx_set_data(priv, i);
+ if (ret < 0)
+ goto out;
+ }
+
+ if (i == max) {
+ /* Even size: full CRC word.
+ * Odd size: only CRC high byte remains (low byte
+ * was packed into the previous word).
+ */
+ val = (size & 1) ? crc >> 8 : crc;
+ } else if ((i * 2 + 1) == size) {
+ /* Special handling for last BYTE if it's not WORD
+ * aligned to avoid reading beyond the allocated data
+ * structure. Pack the CRC low byte into the high
+ * byte of this word so it sits at byte offset 'size'
+ * in the firmware's contiguous buffer.
+ */
+ val = *(u8 *)&data[i] | ((crc & 0xff) << 8);
+ } else {
+ val = le16_to_cpu(data[i]);
+ }
+
+ /* After RST_DATA, skip zero data words as the registers
+ * already contain zeros, but never skip the CRC word at the
+ * final word.
+ */
+ if (use_rst && i < max && val == 0)
+ continue;
+
+ ret = mxl862xx_reg_write(priv,
+ MXL862XX_MMD_REG_DATA_FIRST + off,
+ val);
+ if (ret < 0)
+ goto out;
+ }
+
+ ret = mxl862xx_send_cmd(priv, cmd, size, quiet);
+ if (ret < 0 || !read)
+ goto out;
+
+ /* store result of mxl862xx_send_cmd() */
+ cmd_ret = ret;
+
+ for (i = 0; i < max + 1; i++) {
+ u16 off = i % MXL862XX_MMD_REG_DATA_MAX_SIZE;
+
+ if (i && off == 0) {
+ /* Send command to fetch next batch of data when every
+ * MXL862XX_MMD_REG_DATA_MAX_SIZE of WORDs are read.
+ */
+ ret = mxl862xx_get_data(priv, i);
+ if (ret < 0)
+ goto out;
+ }
+
+ ret = mxl862xx_reg_read(priv, MXL862XX_MMD_REG_DATA_FIRST + off);
+ if (ret < 0)
+ goto out;
+
+ if (i == max) {
+ /* Even size: full CRC word.
+ * Odd size: only CRC high byte remains (low byte
+ * was in the previous word).
+ */
+ if (size & 1)
+ crc = (crc & 0x00ff) |
+ (((u16)ret & 0xff) << 8);
+ else
+ crc = (u16)ret;
+ } else if ((i * 2 + 1) == size) {
+ /* Special handling for last BYTE if it's not WORD
+ * aligned to avoid writing beyond the allocated data
+ * structure. The high byte carries the CRC low byte.
+ */
+ *(uint8_t *)&data[i] = ret & 0xff;
+ crc = (ret >> 8) & 0xff;
+ } else {
+ data[i] = cpu_to_le16((u16)ret);
+ }
+ }
+
+ if (crc16(0xffff, (const u8 *)data, size) != crc) {
+ if (!test_and_set_bit(MXL862XX_FLAG_CRC_ERR, &priv->flags))
+ schedule_work(&priv->crc_err_work);
+ ret = -EIO;
+ goto out;
+ }
+
+ /* on success return the result of the mxl862xx_send_cmd() */
+ ret = cmd_ret;
+
+ dev_dbg(&priv->mdiodev->dev, "RET %d DATA %*ph\n", ret, size, data);
+
+out:
+ mutex_unlock(&priv->mdiodev->bus->mdio_lock);
+
+ return ret;
+}
+
+int mxl862xx_reset(struct mxl862xx_priv *priv)
+{
+ int ret;
+
+ mutex_lock_nested(&priv->mdiodev->bus->mdio_lock, MDIO_MUTEX_NESTED);
+
+ /* Software reset */
+ ret = mxl862xx_reg_write(priv, MXL862XX_MMD_REG_LEN_RET, 0);
+ if (ret)
+ goto out;
+
+ ret = mxl862xx_reg_write(priv, MXL862XX_MMD_REG_CTRL, MXL862XX_SWITCH_RESET);
+out:
+ mutex_unlock(&priv->mdiodev->bus->mdio_lock);
+
+ return ret;
+}
+
+void mxl862xx_host_init(struct mxl862xx_priv *priv)
+{
+ INIT_WORK(&priv->crc_err_work, mxl862xx_crc_err_work_fn);
+}
+
+void mxl862xx_host_shutdown(struct mxl862xx_priv *priv)
+{
+ cancel_work_sync(&priv->crc_err_work);
+}
diff --git a/drivers/net/dsa/mxl862xx/mxl862xx-host.h b/drivers/net/dsa/mxl862xx/mxl862xx-host.h
new file mode 100644
index 000000000..66d6ae198
--- /dev/null
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-host.h
@@ -0,0 +1,22 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#ifndef __MXL862XX_HOST_H
+#define __MXL862XX_HOST_H
+
+#include "mxl862xx.h"
+
+void mxl862xx_host_init(struct mxl862xx_priv *priv);
+void mxl862xx_host_shutdown(struct mxl862xx_priv *priv);
+int mxl862xx_api_wrap(struct mxl862xx_priv *priv, u16 cmd, void *data, u16 size,
+ bool read, bool quiet);
+
+#define MXL862XX_API_WRITE(dev, cmd, data) \
+ mxl862xx_api_wrap(dev, cmd, &(data), sizeof((data)), false, false)
+#define MXL862XX_API_READ(dev, cmd, data) \
+ mxl862xx_api_wrap(dev, cmd, &(data), sizeof((data)), true, false)
+#define MXL862XX_API_READ_QUIET(dev, cmd, data) \
+ mxl862xx_api_wrap(dev, cmd, &(data), sizeof((data)), true, true)
+
+int mxl862xx_reset(struct mxl862xx_priv *priv);
+
+#endif /* __MXL862XX_HOST_H */
diff --git a/drivers/net/dsa/mxl862xx/mxl862xx-phylink.c b/drivers/net/dsa/mxl862xx/mxl862xx-phylink.c
new file mode 100644
index 000000000..b689652aa
--- /dev/null
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-phylink.c
@@ -0,0 +1,446 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Phylink and PCS support for MaxLinear MxL862xx switch family
+ *
+ * Copyright (C) 2024 MaxLinear Inc.
+ * Copyright (C) 2025 John Crispin <john@phrozen.org>
+ * Copyright (C) 2025 Daniel Golle <daniel@makrotopia.org>
+ */
+
+#include <linux/bitfield.h>
+#include <linux/mutex.h>
+#include <linux/phylink.h>
+#include <net/dsa.h>
+
+#include "mxl862xx.h"
+#include "mxl862xx-api.h"
+#include "mxl862xx-cmd.h"
+#include "mxl862xx-host.h"
+#include "mxl862xx-phylink.h"
+
+void mxl862xx_phylink_get_caps(struct dsa_switch *ds, int port,
+ struct phylink_config *config)
+{
+ struct mxl862xx_priv *priv = ds->priv;
+
+ config->mac_capabilities = MAC_ASYM_PAUSE | MAC_SYM_PAUSE | MAC_10 |
+ MAC_100 | MAC_1000 | MAC_2500FD;
+
+ switch (port) {
+ case 1 ... 8:
+ __set_bit(PHY_INTERFACE_MODE_INTERNAL,
+ config->supported_interfaces);
+ break;
+ case 9:
+ case 13:
+ /* Advertised also on old firmware lacking the XPCS API:
+ * there the SerDes runs in its flash-configured mode
+ * without host control (mac_select_pcs returns NULL),
+ * keeping the CPU port working.
+ */
+ __set_bit(PHY_INTERFACE_MODE_SGMII, config->supported_interfaces);
+ __set_bit(PHY_INTERFACE_MODE_1000BASEX, config->supported_interfaces);
+ __set_bit(PHY_INTERFACE_MODE_2500BASEX, config->supported_interfaces);
+ __set_bit(PHY_INTERFACE_MODE_10GBASER, config->supported_interfaces);
+ __set_bit(PHY_INTERFACE_MODE_10GKR, config->supported_interfaces);
+ __set_bit(PHY_INTERFACE_MODE_USXGMII, config->supported_interfaces);
+ fallthrough;
+ case 10 ... 12:
+ case 14 ... 16:
+ if (!MXL862XX_FW_VER_MIN(priv, 1, 0, 84))
+ break;
+ __set_bit(PHY_INTERFACE_MODE_QSGMII, config->supported_interfaces);
+ __set_bit(PHY_INTERFACE_MODE_10G_QXGMII, config->supported_interfaces);
+
+ break;
+ default:
+ break;
+ }
+
+ if (port == 9 || port == 13)
+ config->mac_capabilities |= MAC_10000FD | MAC_5000FD;
+}
+
+static struct mxl862xx_pcs *pcs_to_mxl862xx_pcs(struct phylink_pcs *pcs)
+{
+ return container_of(pcs, struct mxl862xx_pcs, pcs);
+}
+
+static int mxl862xx_xpcs_if_mode(phy_interface_t interface)
+{
+ switch (interface) {
+ case PHY_INTERFACE_MODE_SGMII:
+ return MXL862XX_XPCS_IF_SGMII;
+ case PHY_INTERFACE_MODE_QSGMII:
+ return MXL862XX_XPCS_IF_QSGMII;
+ case PHY_INTERFACE_MODE_1000BASEX:
+ return MXL862XX_XPCS_IF_1000BASEX;
+ case PHY_INTERFACE_MODE_2500BASEX:
+ return MXL862XX_XPCS_IF_2500BASEX;
+ case PHY_INTERFACE_MODE_USXGMII:
+ case PHY_INTERFACE_MODE_10G_QXGMII:
+ return MXL862XX_XPCS_IF_USXGMII;
+ case PHY_INTERFACE_MODE_10GBASER:
+ return MXL862XX_XPCS_IF_10GBASER;
+ case PHY_INTERFACE_MODE_10GKR:
+ return MXL862XX_XPCS_IF_10GKR;
+ default:
+ return -EINVAL;
+ }
+}
+
+static int mxl862xx_xpcs_neg_mode(unsigned int neg_mode)
+{
+ if (!(neg_mode & PHYLINK_PCS_NEG_INBAND))
+ return MXL862XX_XPCS_NEG_NONE;
+ if (neg_mode & PHYLINK_PCS_NEG_ENABLED)
+ return MXL862XX_XPCS_NEG_INBAND_AN_ON;
+ return MXL862XX_XPCS_NEG_INBAND_AN_OFF;
+}
+
+static int mxl862xx_pcs_enable(struct phylink_pcs *pcs)
+{
+ struct mxl862xx_pcs *mpcs = pcs_to_mxl862xx_pcs(pcs);
+
+ /* Bringup is done idempotently by pcs_config; just account this
+ * sub-port so pcs_disable powers the shared XPCS down only after
+ * the last sub-port has been released.
+ */
+ mutex_lock(&mpcs->priv->serdes_lock);
+ mpcs->priv->serdes_refcount[mpcs->serdes_id]++;
+ mutex_unlock(&mpcs->priv->serdes_lock);
+
+ return 0;
+}
+
+static void mxl862xx_pcs_disable(struct phylink_pcs *pcs)
+{
+ struct mxl862xx_pcs *mpcs = pcs_to_mxl862xx_pcs(pcs);
+ struct mxl862xx_xpcs_pcs_disable dis = {};
+ struct mxl862xx_priv *priv = mpcs->priv;
+
+ dis.port_id = mpcs->serdes_id;
+
+ /* The SerDes is shared across QSGMII/QUSXGMII sub-ports; only
+ * power it down once the last active sub-port goes away. Hold
+ * serdes_lock across the count and the power-down so a sibling
+ * sub-port enable cannot race the transition to zero.
+ */
+ mutex_lock(&priv->serdes_lock);
+ if (--priv->serdes_refcount[mpcs->serdes_id] == 0)
+ MXL862XX_API_WRITE(priv, MXL862XX_XPCS_PCS_DISABLE, dis);
+ mutex_unlock(&priv->serdes_lock);
+}
+
+/* The XPCS firmware reports failures in the result field using its own
+ * libc errno values; ENOTSUP (134) in particular has no kernel errno.
+ * Translate the codes the firmware can actually return.
+ */
+static int mxl862xx_xpcs_errno(int result)
+{
+ switch (result) {
+ case -5: /* firmware -EIO */
+ return -EIO;
+ case -134: /* firmware -ENOTSUP */
+ return -EOPNOTSUPP;
+ default: /* firmware -EINVAL and anything unexpected */
+ return -EINVAL;
+ }
+}
+
+static int mxl862xx_pcs_config(struct phylink_pcs *pcs, unsigned int neg_mode,
+ phy_interface_t interface,
+ const unsigned long *advertising,
+ bool permit_pause_to_mac)
+{
+ struct mxl862xx_pcs *mpcs = pcs_to_mxl862xx_pcs(pcs);
+ struct mxl862xx_priv *priv = mpcs->priv;
+ struct mxl862xx_xpcs_pcs_cfg cfg = {};
+ int if_mode, lane, ret, adv;
+
+ if_mode = mxl862xx_xpcs_if_mode(interface);
+ if (if_mode < 0) {
+ dev_err(priv->ds->dev, "unsupported interface: %s\n",
+ phy_modes(interface));
+ return if_mode;
+ }
+
+ /* The XPCS bringup is per-instance and idempotent in the
+ * firmware: every QSGMII/QUSXGMII sub-port may call pcs_config
+ * and the firmware will skip the bringup if the requested mode
+ * matches the cached one, then update MAC pause for the
+ * sub-port indicated by @usx_subport. No serdes_lock is needed
+ * here: the refcount held since pcs_enable keeps a sibling
+ * pcs_disable from powering the XPCS down, and pcs_disable
+ * invalidates the firmware's cached mode so the next pcs_config
+ * redoes the bringup.
+ */
+ lane = (interface == PHY_INTERFACE_MODE_10G_QXGMII) ?
+ MXL862XX_XPCS_USX_QUAD : MXL862XX_XPCS_USX_SINGLE;
+
+ cfg.mode = cpu_to_le16(FIELD_PREP(MXL862XX_XPCS_CFG_PORT_ID,
+ mpcs->serdes_id) |
+ FIELD_PREP(MXL862XX_XPCS_CFG_USX_SUBPORT,
+ mpcs->slot) |
+ FIELD_PREP(MXL862XX_XPCS_CFG_USX_LANE_MODE, lane) |
+ FIELD_PREP(MXL862XX_XPCS_CFG_INTERFACE, if_mode) |
+ FIELD_PREP(MXL862XX_XPCS_CFG_NEG_MODE,
+ mxl862xx_xpcs_neg_mode(neg_mode)) |
+ FIELD_PREP(MXL862XX_XPCS_CFG_ROLE,
+ MXL862XX_XPCS_ROLE_MAC) |
+ FIELD_PREP(MXL862XX_XPCS_CFG_PERMIT_PAUSE,
+ permit_pause_to_mac));
+
+ if (neg_mode & PHYLINK_PCS_NEG_INBAND) {
+ adv = phylink_mii_c22_pcs_encode_advertisement(interface,
+ advertising);
+ if (adv >= 0)
+ cfg.advertising.cl37 = cpu_to_le16(adv);
+ }
+
+ ret = MXL862XX_API_READ(priv, MXL862XX_XPCS_PCS_CONFIG, cfg);
+ if (ret)
+ return ret;
+
+ ret = (s16)le16_to_cpu(cfg.result);
+ if (ret < 0)
+ return mxl862xx_xpcs_errno(ret);
+
+ mpcs->interface = interface;
+ return ret > 0 ? 1 : 0;
+}
+
+static void mxl862xx_pcs_get_state(struct phylink_pcs *pcs,
+ unsigned int neg_mode,
+ struct phylink_link_state *state)
+{
+ struct mxl862xx_pcs *mpcs = pcs_to_mxl862xx_pcs(pcs);
+ struct mxl862xx_priv *priv = mpcs->priv;
+ struct mxl862xx_xpcs_pcs_state st = {};
+ int if_mode, lane, ret;
+ u32 mode;
+ u16 bmsr;
+
+ if_mode = mxl862xx_xpcs_if_mode(state->interface);
+ if (if_mode < 0) {
+ state->link = false;
+ return;
+ }
+
+ lane = (state->interface == PHY_INTERFACE_MODE_10G_QXGMII) ?
+ MXL862XX_XPCS_USX_QUAD : MXL862XX_XPCS_USX_SINGLE;
+
+ st.mode = cpu_to_le32(FIELD_PREP(MXL862XX_XPCS_ST_PORT_ID,
+ mpcs->serdes_id) |
+ FIELD_PREP(MXL862XX_XPCS_ST_INTERFACE, if_mode) |
+ FIELD_PREP(MXL862XX_XPCS_ST_USX_SUBPORT,
+ mpcs->slot) |
+ FIELD_PREP(MXL862XX_XPCS_ST_USX_LANE_MODE, lane));
+
+ ret = MXL862XX_API_READ(priv, MXL862XX_XPCS_PCS_GET_STATE, st);
+ if (ret) {
+ state->link = false;
+ return;
+ }
+
+ mode = le32_to_cpu(st.mode);
+ state->link = FIELD_GET(MXL862XX_XPCS_ST_LINK, mode) &&
+ !FIELD_GET(MXL862XX_XPCS_ST_PCS_FAULT, mode);
+ state->an_complete = FIELD_GET(MXL862XX_XPCS_ST_AN_COMPLETE, mode);
+
+ switch (state->interface) {
+ case PHY_INTERFACE_MODE_1000BASEX:
+ case PHY_INTERFACE_MODE_2500BASEX:
+ case PHY_INTERFACE_MODE_SGMII:
+ case PHY_INTERFACE_MODE_QSGMII:
+ bmsr = (state->link ? BMSR_LSTATUS : 0) |
+ (state->an_complete ? BMSR_ANEGCOMPLETE : 0);
+ phylink_mii_c22_pcs_decode_state(state, neg_mode, bmsr,
+ le16_to_cpu(st.lpa.cl37));
+ break;
+
+ case PHY_INTERFACE_MODE_USXGMII:
+ case PHY_INTERFACE_MODE_10G_QXGMII:
+ if (state->link)
+ phylink_decode_usxgmii_word(state,
+ le16_to_cpu(st.lpa.usx));
+ break;
+
+ case PHY_INTERFACE_MODE_10GBASER:
+ case PHY_INTERFACE_MODE_10GKR:
+ if (state->link) {
+ state->speed = SPEED_10000;
+ state->duplex = DUPLEX_FULL;
+ }
+ break;
+
+ default:
+ state->link = false;
+ break;
+ }
+}
+
+static void mxl862xx_pcs_an_restart(struct phylink_pcs *pcs)
+{
+ struct mxl862xx_pcs *mpcs = pcs_to_mxl862xx_pcs(pcs);
+ struct mxl862xx_priv *priv = mpcs->priv;
+ struct mxl862xx_xpcs_an_restart an = {};
+ int if_mode, lane;
+
+ if_mode = mxl862xx_xpcs_if_mode(mpcs->interface);
+ if (if_mode < 0)
+ return;
+
+ lane = (mpcs->interface == PHY_INTERFACE_MODE_10G_QXGMII) ?
+ MXL862XX_XPCS_USX_QUAD : MXL862XX_XPCS_USX_SINGLE;
+
+ an.mode = cpu_to_le16(FIELD_PREP(MXL862XX_XPCS_ANR_PORT_ID,
+ mpcs->serdes_id) |
+ FIELD_PREP(MXL862XX_XPCS_ANR_INTERFACE, if_mode) |
+ FIELD_PREP(MXL862XX_XPCS_ANR_USX_SUBPORT,
+ mpcs->slot) |
+ FIELD_PREP(MXL862XX_XPCS_ANR_USX_LANE_MODE, lane));
+
+ MXL862XX_API_WRITE(priv, MXL862XX_XPCS_AN_RESTART, an);
+}
+
+static void mxl862xx_pcs_link_up(struct phylink_pcs *pcs, unsigned int neg_mode,
+ phy_interface_t interface, int speed,
+ int duplex)
+{
+ struct mxl862xx_pcs *mpcs = pcs_to_mxl862xx_pcs(pcs);
+ struct mxl862xx_xpcs_pcs_link_up lu = {};
+ struct mxl862xx_priv *priv = mpcs->priv;
+ int if_mode, lane, dup;
+
+ /* With inband-AN enabled (role=MAC), the XPCS auto-resolves
+ * speed/duplex from the partner's AN word and the firmware
+ * short-circuits link_up. Skip the firmware round-trip, same
+ * as pcs-mtk-lynxi.
+ */
+ if (neg_mode == PHYLINK_PCS_NEG_INBAND_ENABLED)
+ return;
+
+ if_mode = mxl862xx_xpcs_if_mode(interface);
+ if (if_mode < 0)
+ return;
+
+ lane = (interface == PHY_INTERFACE_MODE_10G_QXGMII) ?
+ MXL862XX_XPCS_USX_QUAD : MXL862XX_XPCS_USX_SINGLE;
+ dup = (duplex == DUPLEX_FULL) ? MXL862XX_XPCS_DUPLEX_FULL :
+ MXL862XX_XPCS_DUPLEX_HALF;
+
+ lu.mode = cpu_to_le16(FIELD_PREP(MXL862XX_XPCS_LU_PORT_ID,
+ mpcs->serdes_id) |
+ FIELD_PREP(MXL862XX_XPCS_LU_INTERFACE, if_mode) |
+ FIELD_PREP(MXL862XX_XPCS_LU_USX_SUBPORT,
+ mpcs->slot) |
+ FIELD_PREP(MXL862XX_XPCS_LU_USX_LANE_MODE, lane) |
+ FIELD_PREP(MXL862XX_XPCS_LU_DUPLEX, dup));
+ lu.speed = cpu_to_le16(speed);
+
+ MXL862XX_API_WRITE(priv, MXL862XX_XPCS_PCS_LINK_UP, lu);
+}
+
+static unsigned int mxl862xx_pcs_inband_caps(struct phylink_pcs *pcs,
+ phy_interface_t interface)
+{
+ switch (interface) {
+ case PHY_INTERFACE_MODE_SGMII:
+ case PHY_INTERFACE_MODE_QSGMII:
+ case PHY_INTERFACE_MODE_1000BASEX:
+ case PHY_INTERFACE_MODE_2500BASEX:
+ return LINK_INBAND_DISABLE | LINK_INBAND_ENABLE;
+ case PHY_INTERFACE_MODE_USXGMII:
+ case PHY_INTERFACE_MODE_10G_QXGMII:
+ case PHY_INTERFACE_MODE_10GKR:
+ return LINK_INBAND_ENABLE;
+ case PHY_INTERFACE_MODE_10GBASER:
+ return LINK_INBAND_DISABLE;
+ default:
+ return 0;
+ }
+}
+
+static const struct phylink_pcs_ops mxl862xx_pcs_ops = {
+ .pcs_enable = mxl862xx_pcs_enable,
+ .pcs_disable = mxl862xx_pcs_disable,
+ .pcs_config = mxl862xx_pcs_config,
+ .pcs_get_state = mxl862xx_pcs_get_state,
+ .pcs_an_restart = mxl862xx_pcs_an_restart,
+ .pcs_link_up = mxl862xx_pcs_link_up,
+ .pcs_inband_caps = mxl862xx_pcs_inband_caps,
+};
+
+void mxl862xx_setup_pcs(struct mxl862xx_priv *priv, struct mxl862xx_pcs *pcs,
+ int port)
+{
+ pcs->priv = priv;
+ pcs->serdes_id = MXL862XX_SERDES_PORT_ID(port);
+ pcs->slot = MXL862XX_SERDES_SLOT(port);
+ pcs->interface = PHY_INTERFACE_MODE_NA;
+
+ pcs->pcs.ops = &mxl862xx_pcs_ops;
+ pcs->pcs.poll = true;
+
+ __set_bit(PHY_INTERFACE_MODE_QSGMII, pcs->pcs.supported_interfaces);
+ __set_bit(PHY_INTERFACE_MODE_10G_QXGMII, pcs->pcs.supported_interfaces);
+ if (pcs->slot != 0)
+ return;
+
+ __set_bit(PHY_INTERFACE_MODE_SGMII, pcs->pcs.supported_interfaces);
+ __set_bit(PHY_INTERFACE_MODE_1000BASEX, pcs->pcs.supported_interfaces);
+ __set_bit(PHY_INTERFACE_MODE_2500BASEX, pcs->pcs.supported_interfaces);
+ __set_bit(PHY_INTERFACE_MODE_10GBASER, pcs->pcs.supported_interfaces);
+ __set_bit(PHY_INTERFACE_MODE_10GKR, pcs->pcs.supported_interfaces);
+ __set_bit(PHY_INTERFACE_MODE_USXGMII, pcs->pcs.supported_interfaces);
+}
+
+static struct phylink_pcs *
+mxl862xx_phylink_mac_select_pcs(struct phylink_config *config,
+ phy_interface_t interface)
+{
+ struct dsa_port *dp = dsa_phylink_to_port(config);
+ struct mxl862xx_priv *priv = dp->ds->priv;
+ int port = dp->index;
+
+ switch (port) {
+ case 9 ... 16:
+ if (!MXL862XX_FW_VER_MIN(priv, 1, 0, 84)) {
+ dev_warn_once(dp->ds->dev,
+ "SerDes PCS unsupported on old firmware.\n");
+ return NULL;
+ }
+ return &priv->serdes_ports[port - 9].pcs;
+ default:
+ return NULL;
+ }
+}
+
+static void mxl862xx_phylink_mac_config(struct phylink_config *config,
+ unsigned int mode,
+ const struct phylink_link_state *state)
+{
+}
+
+static void mxl862xx_phylink_mac_link_down(struct phylink_config *config,
+ unsigned int mode,
+ phy_interface_t interface)
+{
+}
+
+static void mxl862xx_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)
+{
+}
+
+const struct phylink_mac_ops mxl862xx_phylink_mac_ops = {
+ .mac_config = mxl862xx_phylink_mac_config,
+ .mac_link_down = mxl862xx_phylink_mac_link_down,
+ .mac_link_up = mxl862xx_phylink_mac_link_up,
+ .mac_select_pcs = mxl862xx_phylink_mac_select_pcs,
+};
diff --git a/drivers/net/dsa/mxl862xx/mxl862xx-phylink.h b/drivers/net/dsa/mxl862xx/mxl862xx-phylink.h
new file mode 100644
index 000000000..03bb9caad
--- /dev/null
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-phylink.h
@@ -0,0 +1,21 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#ifndef __MXL862XX_PHYLINK_H
+#define __MXL862XX_PHYLINK_H
+
+#include <linux/phylink.h>
+
+#include "mxl862xx.h"
+
+#define MXL862XX_SERDES_SLOT(port) \
+ (((port) - MXL862XX_FIRST_SERDES_PORT) % MXL862XX_SERDES_SLOTS)
+#define MXL862XX_SERDES_PORT_ID(port) \
+ (((port) - MXL862XX_FIRST_SERDES_PORT) / MXL862XX_SERDES_SLOTS)
+
+extern const struct phylink_mac_ops mxl862xx_phylink_mac_ops;
+void mxl862xx_phylink_get_caps(struct dsa_switch *ds, int port,
+ struct phylink_config *config);
+void mxl862xx_setup_pcs(struct mxl862xx_priv *priv, struct mxl862xx_pcs *pcs,
+ int port);
+
+#endif /* __MXL862XX_PHYLINK_H */
diff --git a/drivers/net/dsa/mxl862xx/mxl862xx.c b/drivers/net/dsa/mxl862xx/mxl862xx.c
new file mode 100644
index 000000000..45d237b3a
--- /dev/null
+++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
@@ -0,0 +1,2213 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Driver for MaxLinear MxL862xx switch family
+ *
+ * Copyright (C) 2024 MaxLinear Inc.
+ * Copyright (C) 2025 John Crispin <john@phrozen.org>
+ * Copyright (C) 2025 Daniel Golle <daniel@makrotopia.org>
+ */
+
+#include <linux/bitfield.h>
+#include <linux/delay.h>
+#include <linux/etherdevice.h>
+#include <linux/if_bridge.h>
+#include <linux/module.h>
+#include <linux/of_device.h>
+#include <linux/of_mdio.h>
+#include <linux/phy.h>
+#include <linux/phylink.h>
+#include <net/dsa.h>
+
+#include "mxl862xx.h"
+#include "mxl862xx-api.h"
+#include "mxl862xx-cmd.h"
+#include "mxl862xx-host.h"
+#include "mxl862xx-phylink.h"
+
+/* Polling interval for RMON counter accumulation. At 2.5 Gbps with
+ * minimum-size (64-byte) frames, a 32-bit packet counter wraps in ~880s.
+ * 2s gives a comfortable margin.
+ */
+#define MXL862XX_STATS_POLL_INTERVAL (2 * HZ)
+
+struct mxl862xx_mib_desc {
+ unsigned int size;
+ unsigned int offset;
+ const char *name;
+};
+
+#define MIB_DESC(_size, _name, _element) \
+{ \
+ .size = _size, \
+ .name = _name, \
+ .offset = offsetof(struct mxl862xx_rmon_port_cnt, _element) \
+}
+
+/* Hardware-specific counters not covered by any standardized stats callback. */
+static const struct mxl862xx_mib_desc mxl862xx_mib[] = {
+ MIB_DESC(1, "TxAcmDroppedPkts", tx_acm_dropped_pkts),
+ MIB_DESC(1, "RxFilteredPkts", rx_filtered_pkts),
+ MIB_DESC(1, "RxExtendedVlanDiscardPkts", rx_extended_vlan_discard_pkts),
+ MIB_DESC(1, "MtuExceedDiscardPkts", mtu_exceed_discard_pkts),
+ MIB_DESC(2, "RxBadBytes", rx_bad_bytes),
+};
+
+static const struct ethtool_rmon_hist_range mxl862xx_rmon_ranges[] = {
+ { 0, 64 },
+ { 65, 127 },
+ { 128, 255 },
+ { 256, 511 },
+ { 512, 1023 },
+ { 1024, 10240 },
+ {}
+};
+
+#define MXL862XX_SDMA_PCTRLP(p) (0xbc0 + ((p) * 0x6))
+#define MXL862XX_SDMA_PCTRL_EN BIT(0)
+
+#define MXL862XX_FDMA_PCTRLP(p) (0xa80 + ((p) * 0x6))
+#define MXL862XX_FDMA_PCTRL_EN BIT(0)
+
+#define MXL862XX_READY_TIMEOUT_MS 10000
+#define MXL862XX_READY_POLL_MS 100
+
+#define MXL862XX_TCM_INST_SEL 0xe00
+#define MXL862XX_TCM_CBS 0xe12
+#define MXL862XX_TCM_EBS 0xe13
+
+static const int mxl862xx_flood_meters[] = {
+ MXL862XX_BRIDGE_PORT_EGRESS_METER_UNKNOWN_UC,
+ MXL862XX_BRIDGE_PORT_EGRESS_METER_UNKNOWN_MC_IP,
+ MXL862XX_BRIDGE_PORT_EGRESS_METER_UNKNOWN_MC_NON_IP,
+ MXL862XX_BRIDGE_PORT_EGRESS_METER_BROADCAST,
+};
+
+enum mxl862xx_evlan_action {
+ EVLAN_ACCEPT, /* pass-through, no tag removal */
+ EVLAN_STRIP_IF_UNTAGGED, /* remove 1 tag if entry's untagged flag set */
+ EVLAN_PVID_OR_DISCARD, /* insert PVID tag or discard if no PVID */
+ EVLAN_STRIP1_AND_PVID_OR_DISCARD,/* strip 1 tag + insert PVID, or discard */
+};
+
+struct mxl862xx_evlan_rule_desc {
+ u8 outer_type; /* enum mxl862xx_extended_vlan_filter_type */
+ u8 inner_type; /* enum mxl862xx_extended_vlan_filter_type */
+ u8 outer_tpid; /* enum mxl862xx_extended_vlan_filter_tpid */
+ u8 inner_tpid; /* enum mxl862xx_extended_vlan_filter_tpid */
+ bool match_vid; /* true: match on VID from the vid parameter */
+ u8 action; /* enum mxl862xx_evlan_action */
+};
+
+/* Shorthand constants for readability */
+#define FT_NORMAL MXL862XX_EXTENDEDVLAN_FILTER_TYPE_NORMAL
+#define FT_NO_FILTER MXL862XX_EXTENDEDVLAN_FILTER_TYPE_NO_FILTER
+#define FT_DEFAULT MXL862XX_EXTENDEDVLAN_FILTER_TYPE_DEFAULT
+#define FT_NO_TAG MXL862XX_EXTENDEDVLAN_FILTER_TYPE_NO_TAG
+#define TP_NONE MXL862XX_EXTENDEDVLAN_FILTER_TPID_NO_FILTER
+#define TP_8021Q MXL862XX_EXTENDEDVLAN_FILTER_TPID_8021Q
+
+/*
+ * VLAN-aware ingress: 7 final catchall rules.
+ *
+ * VLAN Filter handles VID membership for tagged frames, so the
+ * Extended VLAN ingress block only needs to handle:
+ * - Priority-tagged (VID=0): strip + insert PVID
+ * - Untagged: insert PVID or discard
+ * - Standard 802.1Q VID>0: pass through (VF handles membership)
+ * - Non-8021Q TPID (0x88A8 etc.): treat as untagged
+ *
+ * Rule ordering is critical: the EVLAN engine scans entries in
+ * ascending index order and stops at the first match.
+ *
+ * The 802.1Q ACCEPT rules (indices 3--4) must appear BEFORE the
+ * NO_FILTER catchalls (indices 5--6). NO_FILTER matches any tag
+ * regardless of TPID, so without the ACCEPT guard, it would also
+ * catch standard 802.1Q VID>0 frames and corrupt them. With the
+ * guard, 802.1Q VID>0 frames match the ACCEPT rules first and
+ * pass through untouched; only non-8021Q TPID frames pass through
+ * to the NO_FILTER catchalls.
+ */
+static const struct mxl862xx_evlan_rule_desc ingress_aware_final[] = {
+ /* 802.1p / priority-tagged (VID 0): strip + PVID */
+ { FT_NORMAL, FT_NORMAL, TP_8021Q, TP_8021Q, true, EVLAN_STRIP1_AND_PVID_OR_DISCARD },
+ { FT_NORMAL, FT_NO_TAG, TP_8021Q, TP_NONE, true, EVLAN_STRIP1_AND_PVID_OR_DISCARD },
+ /* Untagged: PVID insertion or discard */
+ { FT_NO_TAG, FT_NO_TAG, TP_NONE, TP_NONE, false, EVLAN_PVID_OR_DISCARD },
+ /* 802.1Q VID>0: accept - VF handles membership.
+ * match_vid=false means any VID; VID=0 is already caught above.
+ */
+ { FT_NORMAL, FT_NORMAL, TP_8021Q, TP_8021Q, false, EVLAN_ACCEPT },
+ { FT_NORMAL, FT_NO_TAG, TP_8021Q, TP_NONE, false, EVLAN_ACCEPT },
+ /* Non-8021Q TPID (0x88A8 etc.): treat as untagged - strip + PVID */
+ { FT_NO_FILTER, FT_NO_FILTER, TP_NONE, TP_NONE, false, EVLAN_STRIP1_AND_PVID_OR_DISCARD },
+ { FT_NO_FILTER, FT_NO_TAG, TP_NONE, TP_NONE, false, EVLAN_STRIP1_AND_PVID_OR_DISCARD },
+};
+
+/*
+ * VID-specific accept rules (VLAN-aware, standard tag, 2 per VID).
+ * Outer tag carries the VLAN; inner may or may not be present.
+ */
+static const struct mxl862xx_evlan_rule_desc vid_accept_standard[] = {
+ { FT_NORMAL, FT_NORMAL, TP_8021Q, TP_8021Q, true, EVLAN_STRIP_IF_UNTAGGED },
+ { FT_NORMAL, FT_NO_TAG, TP_8021Q, TP_NONE, true, EVLAN_STRIP_IF_UNTAGGED },
+};
+
+/*
+ * Egress tag-stripping rules for VLAN-unaware mode (2 per untagged VID).
+ * The HW sees the MxL tag as outer; the real VLAN tag, if any, is inner.
+ */
+static const struct mxl862xx_evlan_rule_desc vid_accept_egress_unaware[] = {
+ { FT_NO_FILTER, FT_NORMAL, TP_NONE, TP_8021Q, true, EVLAN_STRIP_IF_UNTAGGED },
+ { FT_NO_FILTER, FT_NO_TAG, TP_NONE, TP_NONE, false, EVLAN_STRIP_IF_UNTAGGED },
+};
+
+static enum dsa_tag_protocol mxl862xx_get_tag_protocol(struct dsa_switch *ds,
+ int port,
+ enum dsa_tag_protocol m)
+{
+ return DSA_TAG_PROTO_MXL862;
+}
+
+/* PHY access via firmware relay */
+static int mxl862xx_phy_read_mmd(struct mxl862xx_priv *priv, int addr,
+ int devadd, int regnum)
+{
+ struct mdio_relay_data param = {
+ .phy = addr,
+ .mmd = devadd,
+ .reg = cpu_to_le16(regnum),
+ };
+ int ret;
+
+ ret = MXL862XX_API_READ(priv, INT_GPHY_READ, param);
+ if (ret)
+ return ret;
+
+ return le16_to_cpu(param.data);
+}
+
+static int mxl862xx_phy_write_mmd(struct mxl862xx_priv *priv, int addr,
+ int devadd, int regnum, u16 data)
+{
+ struct mdio_relay_data param = {
+ .phy = addr,
+ .mmd = devadd,
+ .reg = cpu_to_le16(regnum),
+ .data = cpu_to_le16(data),
+ };
+
+ return MXL862XX_API_WRITE(priv, INT_GPHY_WRITE, param);
+}
+
+static int mxl862xx_phy_read_mii_bus(struct mii_bus *bus, int addr, int regnum)
+{
+ return mxl862xx_phy_read_mmd(bus->priv, addr, 0, regnum);
+}
+
+static int mxl862xx_phy_write_mii_bus(struct mii_bus *bus, int addr,
+ int regnum, u16 val)
+{
+ return mxl862xx_phy_write_mmd(bus->priv, addr, 0, regnum, val);
+}
+
+static int mxl862xx_phy_read_c45_mii_bus(struct mii_bus *bus, int addr,
+ int devadd, int regnum)
+{
+ return mxl862xx_phy_read_mmd(bus->priv, addr, devadd, regnum);
+}
+
+static int mxl862xx_phy_write_c45_mii_bus(struct mii_bus *bus, int addr,
+ int devadd, int regnum, u16 val)
+{
+ return mxl862xx_phy_write_mmd(bus->priv, addr, devadd, regnum, val);
+}
+
+static int mxl862xx_wait_ready(struct dsa_switch *ds)
+{
+ struct mxl862xx_sys_fw_image_version ver = {};
+ unsigned long start = jiffies, timeout;
+ struct mxl862xx_priv *priv = ds->priv;
+ struct mxl862xx_cfg cfg = {};
+ int ret;
+
+ timeout = start + msecs_to_jiffies(MXL862XX_READY_TIMEOUT_MS);
+ msleep(2000); /* it always takes at least 2 seconds */
+ do {
+ ret = MXL862XX_API_READ_QUIET(priv, SYS_MISC_FW_VERSION, ver);
+ if (ret || !ver.iv_major)
+ goto not_ready_yet;
+
+ /* being able to perform CFGGET indicates that
+ * the firmware is ready
+ */
+ ret = MXL862XX_API_READ_QUIET(priv,
+ MXL862XX_COMMON_CFGGET,
+ cfg);
+ if (ret)
+ goto not_ready_yet;
+
+ dev_info(ds->dev, "switch ready after %ums, firmware %u.%u.%u (build %u)\n",
+ jiffies_to_msecs(jiffies - start),
+ ver.iv_major, ver.iv_minor,
+ le16_to_cpu(ver.iv_revision),
+ le32_to_cpu(ver.iv_build_num));
+ priv->fw_version.major = ver.iv_major;
+ priv->fw_version.minor = ver.iv_minor;
+ priv->fw_version.revision = le16_to_cpu(ver.iv_revision);
+ return 0;
+
+not_ready_yet:
+ msleep(MXL862XX_READY_POLL_MS);
+ } while (time_before(jiffies, timeout));
+
+ dev_err(ds->dev, "switch not responding after reset\n");
+ return -ETIMEDOUT;
+}
+
+static int mxl862xx_setup_mdio(struct dsa_switch *ds)
+{
+ struct mxl862xx_priv *priv = ds->priv;
+ struct device *dev = ds->dev;
+ struct device_node *mdio_np;
+ struct mii_bus *bus;
+ int ret;
+
+ bus = devm_mdiobus_alloc(dev);
+ if (!bus)
+ return -ENOMEM;
+
+ bus->priv = priv;
+ bus->name = KBUILD_MODNAME "-mii";
+ snprintf(bus->id, MII_BUS_ID_SIZE, "%s-mii", dev_name(dev));
+ bus->read_c45 = mxl862xx_phy_read_c45_mii_bus;
+ bus->write_c45 = mxl862xx_phy_write_c45_mii_bus;
+ bus->read = mxl862xx_phy_read_mii_bus;
+ bus->write = mxl862xx_phy_write_mii_bus;
+ bus->parent = dev;
+ bus->phy_mask = ~ds->phys_mii_mask;
+
+ mdio_np = of_get_child_by_name(dev->of_node, "mdio");
+ if (!mdio_np)
+ return -ENODEV;
+
+ ret = devm_of_mdiobus_register(dev, bus, mdio_np);
+ of_node_put(mdio_np);
+
+ return ret;
+}
+
+static int mxl862xx_bridge_config_fwd(struct dsa_switch *ds, u16 bridge_id,
+ bool ucast_flood, bool mcast_flood,
+ bool bcast_flood)
+{
+ struct mxl862xx_bridge_config bridge_config = {};
+ struct mxl862xx_priv *priv = ds->priv;
+ int ret;
+
+ bridge_config.mask = cpu_to_le32(MXL862XX_BRIDGE_CONFIG_MASK_FORWARDING_MODE);
+ bridge_config.bridge_id = cpu_to_le16(bridge_id);
+
+ bridge_config.forward_unknown_unicast = cpu_to_le32(ucast_flood ?
+ MXL862XX_BRIDGE_FORWARD_FLOOD : MXL862XX_BRIDGE_FORWARD_DISCARD);
+
+ bridge_config.forward_unknown_multicast_ip = cpu_to_le32(mcast_flood ?
+ MXL862XX_BRIDGE_FORWARD_FLOOD : MXL862XX_BRIDGE_FORWARD_DISCARD);
+ bridge_config.forward_unknown_multicast_non_ip =
+ bridge_config.forward_unknown_multicast_ip;
+
+ bridge_config.forward_broadcast = cpu_to_le32(bcast_flood ?
+ MXL862XX_BRIDGE_FORWARD_FLOOD : MXL862XX_BRIDGE_FORWARD_DISCARD);
+
+ ret = MXL862XX_API_WRITE(priv, MXL862XX_BRIDGE_CONFIGSET, bridge_config);
+ if (ret)
+ dev_err(ds->dev, "failed to configure bridge %u forwarding: %d\n",
+ bridge_id, ret);
+
+ return ret;
+}
+
+/* Allocate a single zero-rate meter shared by all ports and flood types.
+ * All flood-blocking egress sub-meters point to this one meter so that any
+ * packet hitting this meter is unconditionally dropped.
+ *
+ * The firmware API requires CBS >= 64 (its bs2ls encoder clamps smaller
+ * values), so the meter is initially configured with CBS=EBS=64.
+ * A zero-rate bucket starts full at CBS bytes, which would let one packet
+ * through before the bucket empties. To eliminate this one-packet leak we
+ * override CBS and EBS to zero via direct register writes after the API call;
+ * the hardware accepts CBS=0 and immediately flags the bucket as exceeded,
+ * so no traffic can ever pass.
+ */
+static int mxl862xx_setup_drop_meter(struct dsa_switch *ds)
+{
+ struct mxl862xx_qos_meter_cfg meter = {};
+ struct mxl862xx_priv *priv = ds->priv;
+ struct mxl862xx_register_mod reg;
+ int ret;
+
+ /* meter_id=0 means auto-alloc */
+ ret = MXL862XX_API_READ(priv, MXL862XX_QOS_METERALLOC, meter);
+ if (ret)
+ return ret;
+
+ meter.enable = true;
+ meter.cbs = cpu_to_le32(64);
+ meter.ebs = cpu_to_le32(64);
+ snprintf(meter.meter_name, sizeof(meter.meter_name), "drop");
+
+ ret = MXL862XX_API_WRITE(priv, MXL862XX_QOS_METERCFGSET, meter);
+ if (ret)
+ return ret;
+
+ priv->drop_meter = le16_to_cpu(meter.meter_id);
+
+ /* Select the meter instance for subsequent TCM register access. */
+ reg.addr = cpu_to_le16(MXL862XX_TCM_INST_SEL);
+ reg.data = cpu_to_le16(priv->drop_meter);
+ reg.mask = cpu_to_le16(0xffff);
+ ret = MXL862XX_API_WRITE(priv, MXL862XX_COMMON_REGISTERMOD, reg);
+ if (ret)
+ return ret;
+
+ /* Zero CBS so the committed bucket starts empty (exceeded). */
+ reg.addr = cpu_to_le16(MXL862XX_TCM_CBS);
+ reg.data = 0;
+ ret = MXL862XX_API_WRITE(priv, MXL862XX_COMMON_REGISTERMOD, reg);
+ if (ret)
+ return ret;
+
+ /* Zero EBS so the excess bucket starts empty (exceeded). */
+ reg.addr = cpu_to_le16(MXL862XX_TCM_EBS);
+ return MXL862XX_API_WRITE(priv, MXL862XX_COMMON_REGISTERMOD, reg);
+}
+
+static int mxl862xx_set_bridge_port(struct dsa_switch *ds, int port)
+{
+ struct mxl862xx_bridge_port_config br_port_cfg = {};
+ struct dsa_port *dp = dsa_to_port(ds, port);
+ struct mxl862xx_priv *priv = ds->priv;
+ struct mxl862xx_port *p = &priv->ports[port];
+ struct dsa_port *member_dp;
+ u16 bridge_id;
+ u16 vf_scan;
+ bool enable;
+ int i, idx;
+
+ if (dsa_port_is_unused(dp))
+ return 0;
+
+ if (dsa_port_is_cpu(dp)) {
+ dsa_switch_for_each_user_port(member_dp, ds) {
+ if (member_dp->cpu_dp->index != port)
+ continue;
+ mxl862xx_fw_portmap_set_bit(br_port_cfg.bridge_port_map,
+ member_dp->index);
+ }
+ } else if (dp->bridge) {
+ dsa_switch_for_each_bridge_member(member_dp, ds,
+ dp->bridge->dev) {
+ if (member_dp->index == port)
+ continue;
+ mxl862xx_fw_portmap_set_bit(br_port_cfg.bridge_port_map,
+ member_dp->index);
+ }
+ mxl862xx_fw_portmap_set_bit(br_port_cfg.bridge_port_map,
+ dp->cpu_dp->index);
+ } else {
+ mxl862xx_fw_portmap_set_bit(br_port_cfg.bridge_port_map,
+ dp->cpu_dp->index);
+ p->flood_block = 0;
+ p->learning = false;
+ }
+
+ bridge_id = dp->bridge ? priv->bridges[dp->bridge->num] : p->fid;
+
+ br_port_cfg.bridge_port_id = cpu_to_le16(port);
+ br_port_cfg.bridge_id = cpu_to_le16(bridge_id);
+ br_port_cfg.mask = cpu_to_le32(MXL862XX_BRIDGE_PORT_CONFIG_MASK_BRIDGE_ID |
+ MXL862XX_BRIDGE_PORT_CONFIG_MASK_BRIDGE_PORT_MAP |
+ MXL862XX_BRIDGE_PORT_CONFIG_MASK_MC_SRC_MAC_LEARNING |
+ MXL862XX_BRIDGE_PORT_CONFIG_MASK_EGRESS_SUB_METER |
+ MXL862XX_BRIDGE_PORT_CONFIG_MASK_INGRESS_VLAN |
+ MXL862XX_BRIDGE_PORT_CONFIG_MASK_EGRESS_VLAN |
+ MXL862XX_BRIDGE_PORT_CONFIG_MASK_INGRESS_VLAN_FILTER |
+ MXL862XX_BRIDGE_PORT_CONFIG_MASK_EGRESS_VLAN_FILTER1 |
+ MXL862XX_BRIDGE_PORT_CONFIG_MASK_VLAN_BASED_MAC_LEARNING);
+ br_port_cfg.src_mac_learning_disable = !p->learning;
+
+ /* Extended VLAN block assignments.
+ * Ingress: block_size is sent as-is (all entries are finals).
+ * Egress: n_active narrows the scan window to only the
+ * entries actually written by evlan_program_egress.
+ */
+ br_port_cfg.ingress_extended_vlan_enable = p->ingress_evlan.in_use;
+ br_port_cfg.ingress_extended_vlan_block_id =
+ cpu_to_le16(p->ingress_evlan.block_id);
+ br_port_cfg.ingress_extended_vlan_block_size =
+ cpu_to_le16(p->ingress_evlan.block_size);
+ br_port_cfg.egress_extended_vlan_enable = p->egress_evlan.in_use;
+ br_port_cfg.egress_extended_vlan_block_id =
+ cpu_to_le16(p->egress_evlan.block_id);
+ br_port_cfg.egress_extended_vlan_block_size =
+ cpu_to_le16(p->egress_evlan.n_active);
+
+ /* VLAN Filter block assignments (per-port).
+ * The block_size sent to the firmware narrows the HW scan
+ * window to [block_id, block_id + active_count), relying on
+ * discard_unmatched_tagged for frames outside that range.
+ * When active_count=0, send 1 to scan only the DISCARD
+ * sentinel at index 0 (block_size=0 would disable narrowing
+ * and scan the entire allocated block).
+ *
+ * The bridge check ensures VF is disabled when the port
+ * leaves the bridge, without needing to prematurely clear
+ * vlan_filtering (which the DSA framework handles later via
+ * port_vlan_filtering).
+ */
+ if (p->vf.allocated && p->vlan_filtering &&
+ dsa_port_bridge_dev_get(dp)) {
+ vf_scan = max_t(u16, p->vf.active_count, 1);
+ br_port_cfg.ingress_vlan_filter_enable = 1;
+ br_port_cfg.ingress_vlan_filter_block_id =
+ cpu_to_le16(p->vf.block_id);
+ br_port_cfg.ingress_vlan_filter_block_size =
+ cpu_to_le16(vf_scan);
+
+ br_port_cfg.egress_vlan_filter1enable = 1;
+ br_port_cfg.egress_vlan_filter1block_id =
+ cpu_to_le16(p->vf.block_id);
+ br_port_cfg.egress_vlan_filter1block_size =
+ cpu_to_le16(vf_scan);
+ } else {
+ br_port_cfg.ingress_vlan_filter_enable = 0;
+ br_port_cfg.egress_vlan_filter1enable = 0;
+ }
+
+ /* IVL when VLAN-aware: include VID in FDB lookup keys so that
+ * learned entries are per-VID. In VLAN-unaware mode, SVL is
+ * used (VID excluded from key).
+ */
+ br_port_cfg.vlan_src_mac_vid_enable = p->vlan_filtering;
+ br_port_cfg.vlan_dst_mac_vid_enable = p->vlan_filtering;
+
+ for (i = 0; i < ARRAY_SIZE(mxl862xx_flood_meters); i++) {
+ idx = mxl862xx_flood_meters[i];
+ enable = !!(p->flood_block & BIT(idx));
+
+ br_port_cfg.egress_traffic_sub_meter_id[idx] =
+ enable ? cpu_to_le16(priv->drop_meter) : 0;
+ br_port_cfg.egress_sub_metering_enable[idx] = enable;
+ }
+
+ return MXL862XX_API_WRITE(priv, MXL862XX_BRIDGEPORT_CONFIGSET,
+ br_port_cfg);
+}
+
+static int mxl862xx_sync_bridge_members(struct dsa_switch *ds,
+ const struct dsa_bridge *bridge)
+{
+ struct dsa_port *dp;
+ int ret = 0, err;
+
+ dsa_switch_for_each_bridge_member(dp, ds, bridge->dev) {
+ err = mxl862xx_set_bridge_port(ds, dp->index);
+ if (err)
+ ret = err;
+ }
+
+ return ret;
+}
+
+static int mxl862xx_evlan_block_alloc(struct mxl862xx_priv *priv,
+ struct mxl862xx_evlan_block *blk)
+{
+ struct mxl862xx_extendedvlan_alloc param = {};
+ int ret;
+
+ param.number_of_entries = cpu_to_le16(blk->block_size);
+
+ ret = MXL862XX_API_READ(priv, MXL862XX_EXTENDEDVLAN_ALLOC, param);
+ if (ret)
+ return ret;
+
+ blk->block_id = le16_to_cpu(param.extended_vlan_block_id);
+ blk->allocated = true;
+
+ return 0;
+}
+
+static int mxl862xx_vf_block_alloc(struct mxl862xx_priv *priv,
+ u16 size, u16 *block_id)
+{
+ struct mxl862xx_vlanfilter_alloc param = {};
+ int ret;
+
+ param.number_of_entries = cpu_to_le16(size);
+ param.discard_untagged = 0;
+ param.discard_unmatched_tagged = 1;
+
+ ret = MXL862XX_API_READ(priv, MXL862XX_VLANFILTER_ALLOC, param);
+ if (ret)
+ return ret;
+
+ *block_id = le16_to_cpu(param.vlan_filter_block_id);
+ return 0;
+}
+
+static int mxl862xx_vf_entry_discard(struct mxl862xx_priv *priv,
+ u16 block_id, u16 index)
+{
+ struct mxl862xx_vlanfilter_config cfg = {};
+
+ cfg.vlan_filter_block_id = cpu_to_le16(block_id);
+ cfg.entry_index = cpu_to_le16(index);
+ cfg.vlan_filter_mask = cpu_to_le32(MXL862XX_VLAN_FILTER_TCI_MASK_VID);
+ cfg.val = cpu_to_le32(0);
+ cfg.discard_matched = 1;
+
+ return MXL862XX_API_WRITE(priv, MXL862XX_VLANFILTER_SET, cfg);
+}
+
+static int mxl862xx_vf_alloc(struct mxl862xx_priv *priv,
+ struct mxl862xx_vf_block *vf)
+{
+ int ret;
+
+ ret = mxl862xx_vf_block_alloc(priv, vf->block_size, &vf->block_id);
+ if (ret)
+ return ret;
+
+ vf->allocated = true;
+ vf->active_count = 0;
+
+ /* Sentinel: block VID-0 when scan window covers only index 0 */
+ return mxl862xx_vf_entry_discard(priv, vf->block_id, 0);
+}
+
+static int mxl862xx_allocate_bridge(struct mxl862xx_priv *priv)
+{
+ struct mxl862xx_bridge_alloc br_alloc = {};
+ int ret;
+
+ ret = MXL862XX_API_READ(priv, MXL862XX_BRIDGE_ALLOC, br_alloc);
+ if (ret)
+ return ret;
+
+ return le16_to_cpu(br_alloc.bridge_id);
+}
+
+static void mxl862xx_free_bridge(struct dsa_switch *ds,
+ const struct dsa_bridge *bridge)
+{
+ struct mxl862xx_priv *priv = ds->priv;
+ u16 fw_id = priv->bridges[bridge->num];
+ struct mxl862xx_bridge_alloc br_alloc = {
+ .bridge_id = cpu_to_le16(fw_id),
+ };
+ int ret;
+
+ ret = MXL862XX_API_WRITE(priv, MXL862XX_BRIDGE_FREE, br_alloc);
+ if (ret) {
+ dev_err(ds->dev, "failed to free fw bridge %u: %pe\n",
+ fw_id, ERR_PTR(ret));
+ return;
+ }
+
+ priv->bridges[bridge->num] = 0;
+}
+
+static int mxl862xx_setup(struct dsa_switch *ds)
+{
+ struct mxl862xx_priv *priv = ds->priv;
+ int n_user_ports = 0, max_vlans;
+ int ingress_finals, vid_rules;
+ struct dsa_port *dp;
+ int ret, i;
+
+ ret = mxl862xx_reset(priv);
+ if (ret)
+ return ret;
+
+ ret = mxl862xx_wait_ready(ds);
+ if (ret)
+ return ret;
+
+ mutex_init(&priv->serdes_lock);
+ for (i = 0; i < ARRAY_SIZE(priv->serdes_ports); i++)
+ mxl862xx_setup_pcs(priv, &priv->serdes_ports[i],
+ i + MXL862XX_FIRST_SERDES_PORT);
+
+ /* Calculate Extended VLAN block sizes.
+ * With VLAN Filter handling VID membership checks:
+ * Ingress: only final catchall rules (PVID insertion, 802.1Q
+ * accept, non-8021Q TPID handling, discard).
+ * Block sized to exactly fit the finals -- no per-VID
+ * ingress EVLAN rules are needed. (7 entries.)
+ * Egress: 2 rules per VID that needs tag stripping (untagged VIDs).
+ * No egress final catchalls -- VLAN Filter does the discard.
+ * CPU: EVLAN is left disabled on CPU ports -- frames pass
+ * through without EVLAN processing.
+ *
+ * Total EVLAN budget:
+ * n_user_ports * (ingress + egress) <= 1024.
+ * Ingress blocks are small (7 entries), so almost all capacity
+ * goes to egress VID rules.
+ */
+ dsa_switch_for_each_user_port(dp, ds)
+ n_user_ports++;
+
+ if (n_user_ports) {
+ ingress_finals = ARRAY_SIZE(ingress_aware_final);
+ vid_rules = ARRAY_SIZE(vid_accept_standard);
+
+ /* Ingress block: fixed at finals count (7 entries) */
+ priv->evlan_ingress_size = ingress_finals;
+
+ /* Egress block: remaining budget divided equally among
+ * user ports. Each untagged VID needs vid_rules (2)
+ * EVLAN entries for tag stripping. Tagged-only VIDs
+ * need no EVLAN rules at all.
+ */
+ max_vlans = (MXL862XX_TOTAL_EVLAN_ENTRIES -
+ n_user_ports * ingress_finals) /
+ (n_user_ports * vid_rules);
+ priv->evlan_egress_size = vid_rules * max_vlans;
+
+ /* VLAN Filter block: one per user port. The 1024-entry
+ * table is divided equally among user ports. Each port
+ * gets its own VF block for per-port VID membership --
+ * discard_unmatched_tagged handles the rest.
+ */
+ priv->vf_block_size = MXL862XX_TOTAL_VF_ENTRIES / n_user_ports;
+ }
+
+ ret = mxl862xx_setup_drop_meter(ds);
+ if (ret)
+ return ret;
+
+ schedule_delayed_work(&priv->stats_work,
+ MXL862XX_STATS_POLL_INTERVAL);
+
+ return mxl862xx_setup_mdio(ds);
+}
+
+static int mxl862xx_port_state(struct dsa_switch *ds, int port, bool enable)
+{
+ struct mxl862xx_register_mod sdma = {
+ .addr = cpu_to_le16(MXL862XX_SDMA_PCTRLP(port)),
+ .data = cpu_to_le16(enable ? MXL862XX_SDMA_PCTRL_EN : 0),
+ .mask = cpu_to_le16(MXL862XX_SDMA_PCTRL_EN),
+ };
+ struct mxl862xx_register_mod fdma = {
+ .addr = cpu_to_le16(MXL862XX_FDMA_PCTRLP(port)),
+ .data = cpu_to_le16(enable ? MXL862XX_FDMA_PCTRL_EN : 0),
+ .mask = cpu_to_le16(MXL862XX_FDMA_PCTRL_EN),
+ };
+ int ret;
+
+ ret = MXL862XX_API_WRITE(ds->priv, MXL862XX_COMMON_REGISTERMOD, sdma);
+ if (ret)
+ return ret;
+
+ return MXL862XX_API_WRITE(ds->priv, MXL862XX_COMMON_REGISTERMOD, fdma);
+}
+
+static int mxl862xx_port_enable(struct dsa_switch *ds, int port,
+ struct phy_device *phydev)
+{
+ return mxl862xx_port_state(ds, port, true);
+}
+
+static void mxl862xx_port_disable(struct dsa_switch *ds, int port)
+{
+ if (mxl862xx_port_state(ds, port, false))
+ dev_err(ds->dev, "failed to disable port %d\n", port);
+}
+
+static void mxl862xx_port_fast_age(struct dsa_switch *ds, int port)
+{
+ struct mxl862xx_mac_table_clear param = {
+ .type = MXL862XX_MAC_CLEAR_PHY_PORT,
+ .port_id = port,
+ };
+
+ if (MXL862XX_API_WRITE(ds->priv, MXL862XX_MAC_TABLECLEARCOND, param))
+ dev_err(ds->dev, "failed to clear fdb on port %d\n", port);
+}
+
+static int mxl862xx_configure_ctp_port(struct dsa_switch *ds, int port,
+ u16 first_ctp_port_id,
+ u16 number_of_ctp_ports)
+{
+ struct mxl862xx_ctp_port_assignment ctp_assign = {
+ .logical_port_id = port,
+ .first_ctp_port_id = cpu_to_le16(first_ctp_port_id),
+ .number_of_ctp_port = cpu_to_le16(number_of_ctp_ports),
+ .mode = cpu_to_le32(MXL862XX_LOGICAL_PORT_ETHERNET),
+ };
+
+ return MXL862XX_API_WRITE(ds->priv, MXL862XX_CTP_PORTASSIGNMENTSET,
+ ctp_assign);
+}
+
+static int mxl862xx_configure_sp_tag_proto(struct dsa_switch *ds, int port,
+ bool enable)
+{
+ struct mxl862xx_ss_sp_tag tag = {
+ .pid = port,
+ .mask = MXL862XX_SS_SP_TAG_MASK_RX | MXL862XX_SS_SP_TAG_MASK_TX,
+ .rx = enable ? MXL862XX_SS_SP_TAG_RX_TAG_NO_INSERT :
+ MXL862XX_SS_SP_TAG_RX_NO_TAG_INSERT,
+ .tx = enable ? MXL862XX_SS_SP_TAG_TX_TAG_NO_REMOVE :
+ MXL862XX_SS_SP_TAG_TX_TAG_REMOVE,
+ };
+
+ return MXL862XX_API_WRITE(ds->priv, MXL862XX_SS_SPTAG_SET, tag);
+}
+
+static int mxl862xx_evlan_write_rule(struct mxl862xx_priv *priv,
+ u16 block_id, u16 entry_index,
+ const struct mxl862xx_evlan_rule_desc *desc,
+ u16 vid, bool untagged, u16 pvid)
+{
+ struct mxl862xx_extendedvlan_config cfg = {};
+ struct mxl862xx_extendedvlan_filter_vlan *fv;
+
+ cfg.extended_vlan_block_id = cpu_to_le16(block_id);
+ cfg.entry_index = cpu_to_le16(entry_index);
+
+ /* Populate filter */
+ cfg.filter.outer_vlan.type = cpu_to_le32(desc->outer_type);
+ cfg.filter.inner_vlan.type = cpu_to_le32(desc->inner_type);
+ cfg.filter.outer_vlan.tpid = cpu_to_le32(desc->outer_tpid);
+ cfg.filter.inner_vlan.tpid = cpu_to_le32(desc->inner_tpid);
+
+ if (desc->match_vid) {
+ /* For egress unaware: outer=NO_FILTER, match on inner tag */
+ if (desc->outer_type == FT_NO_FILTER)
+ fv = &cfg.filter.inner_vlan;
+ else
+ fv = &cfg.filter.outer_vlan;
+
+ fv->vid_enable = 1;
+ fv->vid_val = cpu_to_le32(vid);
+ }
+
+ /* Populate treatment based on action */
+ switch (desc->action) {
+ case EVLAN_ACCEPT:
+ cfg.treatment.remove_tag =
+ cpu_to_le32(MXL862XX_EXTENDEDVLAN_TREATMENT_NOT_REMOVE_TAG);
+ break;
+
+ case EVLAN_STRIP_IF_UNTAGGED:
+ cfg.treatment.remove_tag = cpu_to_le32(untagged ?
+ MXL862XX_EXTENDEDVLAN_TREATMENT_REMOVE_1_TAG :
+ MXL862XX_EXTENDEDVLAN_TREATMENT_NOT_REMOVE_TAG);
+ break;
+
+ case EVLAN_PVID_OR_DISCARD:
+ if (pvid) {
+ cfg.treatment.remove_tag =
+ cpu_to_le32(MXL862XX_EXTENDEDVLAN_TREATMENT_NOT_REMOVE_TAG);
+ cfg.treatment.add_outer_vlan = 1;
+ cfg.treatment.outer_vlan.vid_mode =
+ cpu_to_le32(MXL862XX_EXTENDEDVLAN_TREATMENT_VID_VAL);
+ cfg.treatment.outer_vlan.vid_val = cpu_to_le32(pvid);
+ cfg.treatment.outer_vlan.tpid =
+ cpu_to_le32(MXL862XX_EXTENDEDVLAN_TREATMENT_8021Q);
+ } else {
+ cfg.treatment.remove_tag =
+ cpu_to_le32(MXL862XX_EXTENDEDVLAN_TREATMENT_DISCARD_UPSTREAM);
+ }
+ break;
+
+ case EVLAN_STRIP1_AND_PVID_OR_DISCARD:
+ if (pvid) {
+ cfg.treatment.remove_tag =
+ cpu_to_le32(MXL862XX_EXTENDEDVLAN_TREATMENT_REMOVE_1_TAG);
+ cfg.treatment.add_outer_vlan = 1;
+ cfg.treatment.outer_vlan.vid_mode =
+ cpu_to_le32(MXL862XX_EXTENDEDVLAN_TREATMENT_VID_VAL);
+ cfg.treatment.outer_vlan.vid_val = cpu_to_le32(pvid);
+ cfg.treatment.outer_vlan.tpid =
+ cpu_to_le32(MXL862XX_EXTENDEDVLAN_TREATMENT_8021Q);
+ } else {
+ cfg.treatment.remove_tag =
+ cpu_to_le32(MXL862XX_EXTENDEDVLAN_TREATMENT_DISCARD_UPSTREAM);
+ }
+ break;
+ }
+
+ return MXL862XX_API_WRITE(priv, MXL862XX_EXTENDEDVLAN_SET, cfg);
+}
+
+static int mxl862xx_evlan_deactivate_entry(struct mxl862xx_priv *priv,
+ u16 block_id, u16 entry_index)
+{
+ struct mxl862xx_extendedvlan_config cfg = {};
+
+ cfg.extended_vlan_block_id = cpu_to_le16(block_id);
+ cfg.entry_index = cpu_to_le16(entry_index);
+
+ /* Use an unreachable filter (DEFAULT+DEFAULT) with DISCARD treatment.
+ * A zeroed entry would have NORMAL+NORMAL filter which matches
+ * real double-tagged traffic and passes it through.
+ */
+ cfg.filter.outer_vlan.type =
+ cpu_to_le32(MXL862XX_EXTENDEDVLAN_FILTER_TYPE_DEFAULT);
+ cfg.filter.inner_vlan.type =
+ cpu_to_le32(MXL862XX_EXTENDEDVLAN_FILTER_TYPE_DEFAULT);
+ cfg.treatment.remove_tag =
+ cpu_to_le32(MXL862XX_EXTENDEDVLAN_TREATMENT_DISCARD_UPSTREAM);
+
+ return MXL862XX_API_WRITE(priv, MXL862XX_EXTENDEDVLAN_SET, cfg);
+}
+
+static int mxl862xx_evlan_write_final_rules(struct mxl862xx_priv *priv,
+ struct mxl862xx_evlan_block *blk,
+ const struct mxl862xx_evlan_rule_desc *rules,
+ int n_rules, u16 pvid)
+{
+ u16 start_idx = blk->block_size - n_rules;
+ int i, ret;
+
+ for (i = 0; i < n_rules; i++) {
+ ret = mxl862xx_evlan_write_rule(priv, blk->block_id,
+ start_idx + i, &rules[i],
+ 0, false, pvid);
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+
+static int mxl862xx_vf_entry_set(struct mxl862xx_priv *priv,
+ u16 block_id, u16 index, u16 vid)
+{
+ struct mxl862xx_vlanfilter_config cfg = {};
+
+ cfg.vlan_filter_block_id = cpu_to_le16(block_id);
+ cfg.entry_index = cpu_to_le16(index);
+ cfg.vlan_filter_mask = cpu_to_le32(MXL862XX_VLAN_FILTER_TCI_MASK_VID);
+ cfg.val = cpu_to_le32(vid);
+ cfg.discard_matched = 0;
+
+ return MXL862XX_API_WRITE(priv, MXL862XX_VLANFILTER_SET, cfg);
+}
+
+static struct mxl862xx_vf_vid *mxl862xx_vf_find_vid(struct mxl862xx_vf_block *vf,
+ u16 vid)
+{
+ struct mxl862xx_vf_vid *ve;
+
+ list_for_each_entry(ve, &vf->vids, list)
+ if (ve->vid == vid)
+ return ve;
+
+ return NULL;
+}
+
+static int mxl862xx_vf_add_vid(struct mxl862xx_priv *priv,
+ struct mxl862xx_vf_block *vf,
+ u16 vid, bool untagged)
+{
+ struct mxl862xx_vf_vid *ve;
+ int ret;
+
+ ve = mxl862xx_vf_find_vid(vf, vid);
+ if (ve) {
+ ve->untagged = untagged;
+ return 0;
+ }
+
+ if (vf->active_count >= vf->block_size)
+ return -ENOSPC;
+
+ ve = kzalloc_obj(*ve);
+ if (!ve)
+ return -ENOMEM;
+
+ ve->vid = vid;
+ ve->index = vf->active_count;
+ ve->untagged = untagged;
+
+ ret = mxl862xx_vf_entry_set(priv, vf->block_id, ve->index, vid);
+ if (ret) {
+ kfree(ve);
+ return ret;
+ }
+
+ list_add_tail(&ve->list, &vf->vids);
+ vf->active_count++;
+
+ return 0;
+}
+
+static int mxl862xx_vf_del_vid(struct mxl862xx_priv *priv,
+ struct mxl862xx_vf_block *vf, u16 vid)
+{
+ struct mxl862xx_vf_vid *ve, *last_ve;
+ u16 gap, last;
+ int ret;
+
+ ve = mxl862xx_vf_find_vid(vf, vid);
+ if (!ve)
+ return 0;
+
+ if (!vf->allocated) {
+ /* Software-only state -- just remove the tracking entry */
+ list_del(&ve->list);
+ kfree(ve);
+ vf->active_count--;
+ return 0;
+ }
+
+ gap = ve->index;
+ last = vf->active_count - 1;
+
+ if (vf->active_count == 1) {
+ /* Last VID -- restore DISCARD sentinel at index 0 */
+ ret = mxl862xx_vf_entry_discard(priv, vf->block_id, 0);
+ if (ret)
+ return ret;
+ } else if (gap < last) {
+ /* Swap: move the last ALLOW entry into the gap */
+ list_for_each_entry(last_ve, &vf->vids, list)
+ if (last_ve->index == last)
+ break;
+
+ if (WARN_ON(list_entry_is_head(last_ve, &vf->vids, list)))
+ return -EINVAL;
+
+ ret = mxl862xx_vf_entry_set(priv, vf->block_id,
+ gap, last_ve->vid);
+ if (ret)
+ return ret;
+
+ last_ve->index = gap;
+ }
+
+ list_del(&ve->list);
+ kfree(ve);
+ vf->active_count--;
+
+ return 0;
+}
+
+static int mxl862xx_evlan_program_ingress(struct mxl862xx_priv *priv, int port)
+{
+ struct mxl862xx_port *p = &priv->ports[port];
+ struct mxl862xx_evlan_block *blk = &p->ingress_evlan;
+
+ if (!p->vlan_filtering)
+ return 0;
+
+ blk->in_use = true;
+ blk->n_active = blk->block_size;
+
+ return mxl862xx_evlan_write_final_rules(priv, blk,
+ ingress_aware_final,
+ ARRAY_SIZE(ingress_aware_final),
+ p->pvid);
+}
+
+static int mxl862xx_evlan_program_egress(struct mxl862xx_priv *priv, int port)
+{
+ struct mxl862xx_port *p = &priv->ports[port];
+ struct mxl862xx_evlan_block *blk = &p->egress_evlan;
+ const struct mxl862xx_evlan_rule_desc *vid_rules;
+ struct mxl862xx_vf_vid *vfv;
+ u16 old_active = blk->n_active;
+ u16 idx = 0, i;
+ int n_vid, ret;
+
+ if (p->vlan_filtering) {
+ vid_rules = vid_accept_standard;
+ n_vid = ARRAY_SIZE(vid_accept_standard);
+ } else {
+ vid_rules = vid_accept_egress_unaware;
+ n_vid = ARRAY_SIZE(vid_accept_egress_unaware);
+ }
+
+ list_for_each_entry(vfv, &p->vf.vids, list) {
+ if (!vfv->untagged)
+ continue;
+
+ if (idx + n_vid > blk->block_size)
+ return -ENOSPC;
+
+ ret = mxl862xx_evlan_write_rule(priv, blk->block_id,
+ idx++, &vid_rules[0],
+ vfv->vid, vfv->untagged,
+ p->pvid);
+ if (ret)
+ return ret;
+
+ if (n_vid > 1) {
+ ret = mxl862xx_evlan_write_rule(priv, blk->block_id,
+ idx++, &vid_rules[1],
+ vfv->vid,
+ vfv->untagged,
+ p->pvid);
+ if (ret)
+ return ret;
+ }
+ }
+
+ /* Deactivate stale entries that are no longer needed.
+ * This closes the brief window between writing the new rules
+ * and set_bridge_port narrowing the scan window.
+ */
+ for (i = idx; i < old_active; i++) {
+ ret = mxl862xx_evlan_deactivate_entry(priv,
+ blk->block_id,
+ i);
+ if (ret)
+ return ret;
+ }
+
+ blk->n_active = idx;
+ blk->in_use = idx > 0;
+
+ return 0;
+}
+
+static int mxl862xx_port_vlan_filtering(struct dsa_switch *ds, int port,
+ bool vlan_filtering,
+ struct netlink_ext_ack *extack)
+{
+ struct mxl862xx_priv *priv = ds->priv;
+ struct mxl862xx_port *p = &priv->ports[port];
+ bool old_vlan_filtering = p->vlan_filtering;
+ bool old_in_use = p->ingress_evlan.in_use;
+ bool changed = (p->vlan_filtering != vlan_filtering);
+ int ret;
+
+ p->vlan_filtering = vlan_filtering;
+
+ if (changed) {
+ /* When leaving VLAN-aware mode, release the ingress HW
+ * block. The firmware passes frames through unchanged
+ * when no ingress EVLAN block is assigned, so the block
+ * is unnecessary in unaware mode.
+ */
+ if (!vlan_filtering)
+ p->ingress_evlan.in_use = false;
+
+ ret = mxl862xx_evlan_program_ingress(priv, port);
+ if (ret)
+ goto err_restore;
+
+ ret = mxl862xx_evlan_program_egress(priv, port);
+ if (ret)
+ goto err_restore;
+ }
+
+ return mxl862xx_set_bridge_port(ds, port);
+
+ /* No HW rollback -- restoring SW state is sufficient for a correct retry. */
+err_restore:
+ p->vlan_filtering = old_vlan_filtering;
+ p->ingress_evlan.in_use = old_in_use;
+ return ret;
+}
+
+static int mxl862xx_port_vlan_add(struct dsa_switch *ds, int port,
+ const struct switchdev_obj_port_vlan *vlan,
+ struct netlink_ext_ack *extack)
+{
+ struct mxl862xx_priv *priv = ds->priv;
+ struct mxl862xx_port *p = &priv->ports[port];
+ bool untagged = !!(vlan->flags & BRIDGE_VLAN_INFO_UNTAGGED);
+ u16 vid = vlan->vid;
+ u16 old_pvid = p->pvid;
+ bool pvid_changed = false;
+ int ret;
+
+ /* CPU port is VLAN-transparent: the SP tag handles port
+ * identification and the host-side DSA tagger manages VLAN
+ * delivery. Egress EVLAN catchalls are set up once in
+ * setup_cpu_bridge; no per-VID VF/EVLAN programming needed.
+ */
+ if (dsa_is_cpu_port(ds, port))
+ return 0;
+
+ /* Update PVID tracking */
+ if (vlan->flags & BRIDGE_VLAN_INFO_PVID) {
+ if (p->pvid != vid) {
+ p->pvid = vid;
+ pvid_changed = true;
+ }
+ } else if (p->pvid == vid) {
+ p->pvid = 0;
+ pvid_changed = true;
+ }
+
+ /* Add/update VID in this port's VLAN Filter block.
+ * VF must be updated before programming egress EVLAN because
+ * evlan_program_egress walks the VF VID list.
+ */
+ ret = mxl862xx_vf_add_vid(priv, &p->vf, vid, untagged);
+ if (ret)
+ goto err_pvid;
+
+ /* Reprogram ingress finals if PVID changed */
+ if (pvid_changed) {
+ ret = mxl862xx_evlan_program_ingress(priv, port);
+ if (ret)
+ goto err_rollback;
+ }
+
+ /* Reprogram egress tag-stripping rules (walks VF VID list) */
+ ret = mxl862xx_evlan_program_egress(priv, port);
+ if (ret)
+ goto err_rollback;
+
+ /* Apply VLAN block IDs and MAC learning flags to bridge port */
+ ret = mxl862xx_set_bridge_port(ds, port);
+ if (ret)
+ goto err_rollback;
+
+ return 0;
+
+err_rollback:
+ /* Best-effort: undo VF add and restore consistent hardware state.
+ * A retry of port_vlan_add will converge since vf_add_vid is
+ * idempotent.
+ */
+ p->pvid = old_pvid;
+ mxl862xx_vf_del_vid(priv, &p->vf, vid);
+ mxl862xx_evlan_program_ingress(priv, port);
+ mxl862xx_evlan_program_egress(priv, port);
+ mxl862xx_set_bridge_port(ds, port);
+ return ret;
+err_pvid:
+ p->pvid = old_pvid;
+ return ret;
+}
+
+static int mxl862xx_port_vlan_del(struct dsa_switch *ds, int port,
+ const struct switchdev_obj_port_vlan *vlan)
+{
+ struct mxl862xx_priv *priv = ds->priv;
+ struct mxl862xx_port *p = &priv->ports[port];
+ struct mxl862xx_vf_vid *ve;
+ bool pvid_changed = false;
+ u16 vid = vlan->vid;
+ bool old_untagged;
+ u16 old_pvid;
+ int ret;
+
+ if (dsa_is_cpu_port(ds, port))
+ return 0;
+
+ ve = mxl862xx_vf_find_vid(&p->vf, vid);
+ if (!ve)
+ return 0;
+ old_untagged = ve->untagged;
+ old_pvid = p->pvid;
+
+ /* Clear PVID if we're deleting it */
+ if (p->pvid == vid) {
+ p->pvid = 0;
+ pvid_changed = true;
+ }
+
+ /* Remove VID from this port's VLAN Filter block.
+ * Must happen before egress reprogram so the VID is no
+ * longer in the list that evlan_program_egress walks.
+ */
+ ret = mxl862xx_vf_del_vid(priv, &p->vf, vid);
+ if (ret)
+ goto err_pvid;
+
+ /* Reprogram egress tag-stripping rules (VID is now gone) */
+ ret = mxl862xx_evlan_program_egress(priv, port);
+ if (ret)
+ goto err_rollback;
+
+ /* If PVID changed, reprogram ingress finals */
+ if (pvid_changed) {
+ ret = mxl862xx_evlan_program_ingress(priv, port);
+ if (ret)
+ goto err_rollback;
+ }
+
+ ret = mxl862xx_set_bridge_port(ds, port);
+ if (ret)
+ goto err_rollback;
+
+ return 0;
+
+err_rollback:
+ /* Best-effort: re-add the VID and restore consistent hardware
+ * state. A retry of port_vlan_del will converge.
+ */
+ p->pvid = old_pvid;
+ mxl862xx_vf_add_vid(priv, &p->vf, vid, old_untagged);
+ mxl862xx_evlan_program_egress(priv, port);
+ mxl862xx_evlan_program_ingress(priv, port);
+ mxl862xx_set_bridge_port(ds, port);
+ return ret;
+err_pvid:
+ p->pvid = old_pvid;
+ return ret;
+}
+
+static int mxl862xx_setup_cpu_bridge(struct dsa_switch *ds, int port)
+{
+ struct mxl862xx_priv *priv = ds->priv;
+ struct mxl862xx_port *p = &priv->ports[port];
+
+ p->fid = MXL862XX_DEFAULT_BRIDGE;
+ p->learning = true;
+
+ /* EVLAN is left disabled on CPU ports -- frames pass through
+ * without EVLAN processing. Only the portmap and bridge
+ * assignment need to be configured.
+ */
+
+ return mxl862xx_set_bridge_port(ds, port);
+}
+
+static int mxl862xx_port_bridge_join(struct dsa_switch *ds, int port,
+ const struct dsa_bridge bridge,
+ bool *tx_fwd_offload,
+ struct netlink_ext_ack *extack)
+{
+ struct mxl862xx_priv *priv = ds->priv;
+ int ret;
+
+ if (!priv->bridges[bridge.num]) {
+ ret = mxl862xx_allocate_bridge(priv);
+ if (ret < 0)
+ return ret;
+
+ priv->bridges[bridge.num] = ret;
+
+ /* Free bridge here on error, DSA rollback won't. */
+ ret = mxl862xx_sync_bridge_members(ds, &bridge);
+ if (ret) {
+ mxl862xx_free_bridge(ds, &bridge);
+ return ret;
+ }
+
+ return 0;
+ }
+
+ return mxl862xx_sync_bridge_members(ds, &bridge);
+}
+
+static void mxl862xx_port_bridge_leave(struct dsa_switch *ds, int port,
+ const struct dsa_bridge bridge)
+{
+ struct mxl862xx_priv *priv = ds->priv;
+ struct mxl862xx_port *p = &priv->ports[port];
+ int err;
+
+ err = mxl862xx_sync_bridge_members(ds, &bridge);
+ if (err)
+ dev_err(ds->dev,
+ "failed to sync bridge members after port %d left: %pe\n",
+ port, ERR_PTR(err));
+
+ /* Revert leaving port, omitted by the sync above, to its
+ * single-port bridge
+ */
+ p->pvid = 0;
+ p->ingress_evlan.in_use = false;
+ p->egress_evlan.in_use = false;
+
+ err = mxl862xx_set_bridge_port(ds, port);
+ if (err)
+ dev_err(ds->dev,
+ "failed to update bridge port %d state: %pe\n", port,
+ ERR_PTR(err));
+
+ if (!dsa_bridge_ports(ds, bridge.dev))
+ mxl862xx_free_bridge(ds, &bridge);
+}
+
+static int mxl862xx_port_setup(struct dsa_switch *ds, int port)
+{
+ struct mxl862xx_priv *priv = ds->priv;
+ struct dsa_port *dp = dsa_to_port(ds, port);
+ bool is_cpu_port = dsa_port_is_cpu(dp);
+ int ret;
+
+ ret = mxl862xx_port_state(ds, port, false);
+ if (ret)
+ return ret;
+
+ mxl862xx_port_fast_age(ds, port);
+
+ if (dsa_port_is_unused(dp))
+ return 0;
+
+ if (dsa_port_is_dsa(dp)) {
+ dev_err(ds->dev, "port %d: DSA links not supported\n", port);
+ return -EOPNOTSUPP;
+ }
+
+ ret = mxl862xx_configure_sp_tag_proto(ds, port, is_cpu_port);
+ if (ret)
+ return ret;
+
+ ret = mxl862xx_configure_ctp_port(ds, port, port,
+ is_cpu_port ? 32 - port : 1);
+ if (ret)
+ return ret;
+
+ if (is_cpu_port)
+ return mxl862xx_setup_cpu_bridge(ds, port);
+
+ /* setup single-port bridge for user ports.
+ * If this fails, the FID is leaked -- but the port then transitions
+ * to unused, and the FID pool is sized to tolerate this.
+ */
+ ret = mxl862xx_allocate_bridge(priv);
+ if (ret < 0) {
+ dev_err(ds->dev, "failed to allocate a bridge for port %d\n", port);
+ return ret;
+ }
+ priv->ports[port].fid = ret;
+ /* Standalone ports should not flood unknown unicast or multicast
+ * towards the CPU by default; only broadcast is needed initially.
+ */
+ ret = mxl862xx_bridge_config_fwd(ds, priv->ports[port].fid,
+ false, false, true);
+ if (ret)
+ return ret;
+ ret = mxl862xx_set_bridge_port(ds, port);
+ if (ret)
+ return ret;
+
+ priv->ports[port].ingress_evlan.block_size = priv->evlan_ingress_size;
+ ret = mxl862xx_evlan_block_alloc(priv, &priv->ports[port].ingress_evlan);
+ if (ret)
+ return ret;
+
+ priv->ports[port].egress_evlan.block_size = priv->evlan_egress_size;
+ ret = mxl862xx_evlan_block_alloc(priv, &priv->ports[port].egress_evlan);
+ if (ret)
+ return ret;
+
+ priv->ports[port].vf.block_size = priv->vf_block_size;
+ INIT_LIST_HEAD(&priv->ports[port].vf.vids);
+ ret = mxl862xx_vf_alloc(priv, &priv->ports[port].vf);
+ if (ret)
+ return ret;
+
+ priv->ports[port].setup_done = true;
+
+ return 0;
+}
+
+static void mxl862xx_port_teardown(struct dsa_switch *ds, int port)
+{
+ struct mxl862xx_priv *priv = ds->priv;
+ struct dsa_port *dp = dsa_to_port(ds, port);
+
+ if (dsa_port_is_unused(dp))
+ return;
+
+ /* Prevent deferred host_flood_work from acting on stale state.
+ * The flag is checked under rtnl_lock() by the worker; since
+ * teardown also runs under RTNL, this is race-free.
+ *
+ * HW EVLAN/VF blocks are not freed here -- the firmware receives
+ * a full reset on the next probe, which reclaims all resources.
+ */
+ priv->ports[port].setup_done = false;
+}
+
+static int mxl862xx_get_fid(struct dsa_switch *ds, struct dsa_db db)
+{
+ struct mxl862xx_priv *priv = ds->priv;
+
+ switch (db.type) {
+ case DSA_DB_PORT:
+ return priv->ports[db.dp->index].fid;
+
+ case DSA_DB_BRIDGE:
+ if (!priv->bridges[db.bridge.num])
+ return -ENOENT;
+ return priv->bridges[db.bridge.num];
+
+ default:
+ return -EOPNOTSUPP;
+ }
+}
+
+static int mxl862xx_port_fdb_add(struct dsa_switch *ds, int port,
+ const unsigned char *addr, u16 vid, struct dsa_db db)
+{
+ struct mxl862xx_mac_table_add param = {};
+ int fid = mxl862xx_get_fid(ds, db), ret;
+ struct mxl862xx_priv *priv = ds->priv;
+
+ if (fid < 0)
+ return fid;
+
+ param.port_id = cpu_to_le32(port);
+ param.static_entry = true;
+ param.fid = cpu_to_le16(fid);
+ param.tci = cpu_to_le16(FIELD_PREP(MXL862XX_TCI_VLAN_ID, vid));
+ ether_addr_copy(param.mac, addr);
+
+ ret = MXL862XX_API_WRITE(priv, MXL862XX_MAC_TABLEENTRYADD, param);
+ if (ret)
+ dev_err(ds->dev, "failed to add FDB entry on port %d\n", port);
+
+ return ret;
+}
+
+static int mxl862xx_port_fdb_del(struct dsa_switch *ds, int port,
+ const unsigned char *addr, u16 vid, const struct dsa_db db)
+{
+ struct mxl862xx_mac_table_remove param = {};
+ int fid = mxl862xx_get_fid(ds, db), ret;
+ struct mxl862xx_priv *priv = ds->priv;
+
+ if (fid < 0)
+ return fid;
+
+ param.fid = cpu_to_le16(fid);
+ param.tci = cpu_to_le16(FIELD_PREP(MXL862XX_TCI_VLAN_ID, vid));
+ ether_addr_copy(param.mac, addr);
+
+ ret = MXL862XX_API_WRITE(priv, MXL862XX_MAC_TABLEENTRYREMOVE, param);
+ if (ret)
+ dev_err(ds->dev, "failed to remove FDB entry on port %d\n", port);
+
+ return ret;
+}
+
+static int mxl862xx_port_fdb_dump(struct dsa_switch *ds, int port,
+ dsa_fdb_dump_cb_t *cb, void *data)
+{
+ struct mxl862xx_mac_table_read param = { .initial = 1 };
+ struct mxl862xx_priv *priv = ds->priv;
+ u32 entry_port_id;
+ int ret;
+
+ while (true) {
+ ret = MXL862XX_API_READ(priv, MXL862XX_MAC_TABLEENTRYREAD, param);
+ if (ret)
+ return ret;
+
+ if (param.last)
+ break;
+
+ entry_port_id = le32_to_cpu(param.port_id);
+
+ if (entry_port_id == port) {
+ ret = cb(param.mac, FIELD_GET(MXL862XX_TCI_VLAN_ID,
+ le16_to_cpu(param.tci)),
+ param.static_entry, data);
+ if (ret)
+ return ret;
+ }
+
+ memset(&param, 0, sizeof(param));
+ }
+
+ return 0;
+}
+
+static int mxl862xx_port_mdb_add(struct dsa_switch *ds, int port,
+ const struct switchdev_obj_port_mdb *mdb,
+ const struct dsa_db db)
+{
+ struct mxl862xx_mac_table_query qparam = {};
+ struct mxl862xx_mac_table_add aparam = {};
+ struct mxl862xx_priv *priv = ds->priv;
+ int fid, ret;
+
+ fid = mxl862xx_get_fid(ds, db);
+ if (fid < 0)
+ return fid;
+
+ ether_addr_copy(qparam.mac, mdb->addr);
+ qparam.fid = cpu_to_le16(fid);
+ qparam.tci = cpu_to_le16(FIELD_PREP(MXL862XX_TCI_VLAN_ID, mdb->vid));
+
+ ret = MXL862XX_API_READ(priv, MXL862XX_MAC_TABLEENTRYQUERY, qparam);
+ if (ret)
+ return ret;
+
+ /* Build the ADD command using portmap mode */
+ ether_addr_copy(aparam.mac, mdb->addr);
+ aparam.fid = cpu_to_le16(fid);
+ aparam.tci = cpu_to_le16(FIELD_PREP(MXL862XX_TCI_VLAN_ID, mdb->vid));
+ aparam.static_entry = true;
+ aparam.port_id = cpu_to_le32(MXL862XX_PORTMAP_FLAG);
+
+ if (qparam.found)
+ memcpy(aparam.port_map, qparam.port_map,
+ sizeof(aparam.port_map));
+
+ mxl862xx_fw_portmap_set_bit(aparam.port_map, port);
+
+ return MXL862XX_API_WRITE(priv, MXL862XX_MAC_TABLEENTRYADD, aparam);
+}
+
+static int mxl862xx_port_mdb_del(struct dsa_switch *ds, int port,
+ const struct switchdev_obj_port_mdb *mdb,
+ const struct dsa_db db)
+{
+ struct mxl862xx_mac_table_remove rparam = {};
+ struct mxl862xx_mac_table_query qparam = {};
+ struct mxl862xx_mac_table_add aparam = {};
+ int fid = mxl862xx_get_fid(ds, db), ret;
+ struct mxl862xx_priv *priv = ds->priv;
+
+ if (fid < 0)
+ return fid;
+
+ qparam.fid = cpu_to_le16(fid);
+ qparam.tci = cpu_to_le16(FIELD_PREP(MXL862XX_TCI_VLAN_ID, mdb->vid));
+ ether_addr_copy(qparam.mac, mdb->addr);
+
+ ret = MXL862XX_API_READ(priv, MXL862XX_MAC_TABLEENTRYQUERY, qparam);
+ if (ret)
+ return ret;
+
+ if (!qparam.found)
+ return 0;
+
+ mxl862xx_fw_portmap_clear_bit(qparam.port_map, port);
+
+ if (mxl862xx_fw_portmap_is_empty(qparam.port_map)) {
+ rparam.fid = cpu_to_le16(fid);
+ rparam.tci = cpu_to_le16(FIELD_PREP(MXL862XX_TCI_VLAN_ID, mdb->vid));
+ ether_addr_copy(rparam.mac, mdb->addr);
+ ret = MXL862XX_API_WRITE(priv, MXL862XX_MAC_TABLEENTRYREMOVE, rparam);
+ } else {
+ /* Write back with reduced portmap */
+ aparam.fid = cpu_to_le16(fid);
+ aparam.tci = cpu_to_le16(FIELD_PREP(MXL862XX_TCI_VLAN_ID, mdb->vid));
+ ether_addr_copy(aparam.mac, mdb->addr);
+ aparam.static_entry = true;
+ aparam.port_id = cpu_to_le32(MXL862XX_PORTMAP_FLAG);
+ memcpy(aparam.port_map, qparam.port_map, sizeof(aparam.port_map));
+ ret = MXL862XX_API_WRITE(priv, MXL862XX_MAC_TABLEENTRYADD, aparam);
+ }
+
+ return ret;
+}
+
+static int mxl862xx_set_ageing_time(struct dsa_switch *ds, unsigned int msecs)
+{
+ struct mxl862xx_cfg param = {};
+ int ret;
+
+ ret = MXL862XX_API_READ(ds->priv, MXL862XX_COMMON_CFGGET, param);
+ if (ret) {
+ dev_err(ds->dev, "failed to read switch config\n");
+ return ret;
+ }
+
+ param.mac_table_age_timer = cpu_to_le32(MXL862XX_AGETIMER_CUSTOM);
+ param.age_timer = cpu_to_le32(msecs / 1000);
+ ret = MXL862XX_API_WRITE(ds->priv, MXL862XX_COMMON_CFGSET, param);
+ if (ret)
+ dev_err(ds->dev, "failed to set ageing\n");
+
+ return ret;
+}
+
+static void mxl862xx_port_stp_state_set(struct dsa_switch *ds, int port,
+ u8 state)
+{
+ struct mxl862xx_stp_port_cfg param = {
+ .port_id = cpu_to_le16(port),
+ };
+ struct mxl862xx_priv *priv = ds->priv;
+ int ret;
+
+ switch (state) {
+ case BR_STATE_DISABLED:
+ param.port_state = cpu_to_le32(MXL862XX_STP_PORT_STATE_DISABLE);
+ break;
+ case BR_STATE_BLOCKING:
+ case BR_STATE_LISTENING:
+ param.port_state = cpu_to_le32(MXL862XX_STP_PORT_STATE_BLOCKING);
+ break;
+ case BR_STATE_LEARNING:
+ param.port_state = cpu_to_le32(MXL862XX_STP_PORT_STATE_LEARNING);
+ break;
+ case BR_STATE_FORWARDING:
+ param.port_state = cpu_to_le32(MXL862XX_STP_PORT_STATE_FORWARD);
+ break;
+ default:
+ dev_err(ds->dev, "invalid STP state: %d\n", state);
+ return;
+ }
+
+ ret = MXL862XX_API_WRITE(priv, MXL862XX_STP_PORTCFGSET, param);
+ if (ret) {
+ dev_err(ds->dev, "failed to set STP state on port %d\n", port);
+ return;
+ }
+
+ /* The firmware may re-enable MAC learning as a side-effect of entering
+ * LEARNING or FORWARDING state (per 802.1D defaults).
+ * Re-apply the driver's intended learning and metering config so that
+ * standalone ports keep learning disabled.
+ */
+ ret = mxl862xx_set_bridge_port(ds, port);
+ if (ret)
+ dev_err(ds->dev, "failed to reapply brport flags on port %d\n",
+ port);
+
+ mxl862xx_port_fast_age(ds, port);
+}
+
+/* Deferred work handler for host flood configuration.
+ *
+ * port_set_host_flood is called from atomic context (under
+ * netif_addr_lock), so firmware calls must be deferred. The worker
+ * acquires rtnl_lock() to serialize with DSA callbacks that access the
+ * same driver state.
+ */
+static void mxl862xx_host_flood_work_fn(struct work_struct *work)
+{
+ struct mxl862xx_port *p = container_of(work, struct mxl862xx_port,
+ host_flood_work);
+ struct mxl862xx_priv *priv = p->priv;
+ struct dsa_switch *ds = priv->ds;
+
+ rtnl_lock();
+
+ /* Port may have been torn down between scheduling and now. */
+ if (!p->setup_done) {
+ rtnl_unlock();
+ return;
+ }
+
+ /* Always write to the standalone FID. When standalone it takes effect
+ * immediately; when bridged the port uses the shared bridge FID so the
+ * write is a no-op for current forwarding, but the state is preserved
+ * in hardware and is ready once the port returns to standalone.
+ */
+ mxl862xx_bridge_config_fwd(ds, p->fid, p->host_flood_uc,
+ p->host_flood_mc, true);
+
+ rtnl_unlock();
+}
+
+static void mxl862xx_port_set_host_flood(struct dsa_switch *ds, int port,
+ bool uc, bool mc)
+{
+ struct mxl862xx_priv *priv = ds->priv;
+ struct mxl862xx_port *p = &priv->ports[port];
+
+ p->host_flood_uc = uc;
+ p->host_flood_mc = mc;
+ schedule_work(&p->host_flood_work);
+}
+
+static int mxl862xx_port_pre_bridge_flags(struct dsa_switch *ds, int port,
+ const struct switchdev_brport_flags flags,
+ struct netlink_ext_ack *extack)
+{
+ if (flags.mask & ~(BR_FLOOD | BR_MCAST_FLOOD | BR_BCAST_FLOOD |
+ BR_LEARNING))
+ return -EINVAL;
+
+ return 0;
+}
+
+static int mxl862xx_port_bridge_flags(struct dsa_switch *ds, int port,
+ const struct switchdev_brport_flags flags,
+ struct netlink_ext_ack *extack)
+{
+ struct mxl862xx_priv *priv = ds->priv;
+ unsigned long old_block = priv->ports[port].flood_block;
+ unsigned long block = old_block;
+ int ret;
+
+ if (flags.mask & BR_FLOOD) {
+ if (flags.val & BR_FLOOD)
+ block &= ~BIT(MXL862XX_BRIDGE_PORT_EGRESS_METER_UNKNOWN_UC);
+ else
+ block |= BIT(MXL862XX_BRIDGE_PORT_EGRESS_METER_UNKNOWN_UC);
+ }
+
+ if (flags.mask & BR_MCAST_FLOOD) {
+ if (flags.val & BR_MCAST_FLOOD) {
+ block &= ~BIT(MXL862XX_BRIDGE_PORT_EGRESS_METER_UNKNOWN_MC_IP);
+ block &= ~BIT(MXL862XX_BRIDGE_PORT_EGRESS_METER_UNKNOWN_MC_NON_IP);
+ } else {
+ block |= BIT(MXL862XX_BRIDGE_PORT_EGRESS_METER_UNKNOWN_MC_IP);
+ block |= BIT(MXL862XX_BRIDGE_PORT_EGRESS_METER_UNKNOWN_MC_NON_IP);
+ }
+ }
+
+ if (flags.mask & BR_BCAST_FLOOD) {
+ if (flags.val & BR_BCAST_FLOOD)
+ block &= ~BIT(MXL862XX_BRIDGE_PORT_EGRESS_METER_BROADCAST);
+ else
+ block |= BIT(MXL862XX_BRIDGE_PORT_EGRESS_METER_BROADCAST);
+ }
+
+ if (flags.mask & BR_LEARNING)
+ priv->ports[port].learning = !!(flags.val & BR_LEARNING);
+
+ if (block != old_block || (flags.mask & BR_LEARNING)) {
+ priv->ports[port].flood_block = block;
+ ret = mxl862xx_set_bridge_port(ds, port);
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+
+static void mxl862xx_get_strings(struct dsa_switch *ds, int port,
+ u32 stringset, u8 *data)
+{
+ int i;
+
+ if (stringset != ETH_SS_STATS)
+ return;
+
+ for (i = 0; i < ARRAY_SIZE(mxl862xx_mib); i++)
+ ethtool_puts(&data, mxl862xx_mib[i].name);
+}
+
+static int mxl862xx_get_sset_count(struct dsa_switch *ds, int port, int sset)
+{
+ if (sset != ETH_SS_STATS)
+ return 0;
+
+ return ARRAY_SIZE(mxl862xx_mib);
+}
+
+static int mxl862xx_read_rmon(struct dsa_switch *ds, int port,
+ struct mxl862xx_rmon_port_cnt *cnt)
+{
+ memset(cnt, 0, sizeof(*cnt));
+ cnt->port_type = cpu_to_le32(MXL862XX_CTP_PORT);
+ cnt->port_id = cpu_to_le16(port);
+
+ return MXL862XX_API_READ(ds->priv, MXL862XX_RMON_PORT_GET, *cnt);
+}
+
+static void mxl862xx_get_ethtool_stats(struct dsa_switch *ds, int port,
+ u64 *data)
+{
+ const struct mxl862xx_mib_desc *mib;
+ struct mxl862xx_rmon_port_cnt cnt;
+ int ret, i;
+ void *field;
+
+ ret = mxl862xx_read_rmon(ds, port, &cnt);
+ if (ret) {
+ dev_err(ds->dev, "failed to read RMON stats on port %d\n", port);
+ return;
+ }
+
+ for (i = 0; i < ARRAY_SIZE(mxl862xx_mib); i++) {
+ mib = &mxl862xx_mib[i];
+ field = (u8 *)&cnt + mib->offset;
+
+ if (mib->size == 1)
+ *data++ = le32_to_cpu(*(__le32 *)field);
+ else
+ *data++ = le64_to_cpu(*(__le64 *)field);
+ }
+}
+
+static void mxl862xx_get_eth_mac_stats(struct dsa_switch *ds, int port,
+ struct ethtool_eth_mac_stats *mac_stats)
+{
+ struct mxl862xx_rmon_port_cnt cnt;
+
+ if (mxl862xx_read_rmon(ds, port, &cnt))
+ return;
+
+ mac_stats->FramesTransmittedOK = le32_to_cpu(cnt.tx_good_pkts);
+ mac_stats->SingleCollisionFrames = le32_to_cpu(cnt.tx_single_coll_count);
+ mac_stats->MultipleCollisionFrames = le32_to_cpu(cnt.tx_mult_coll_count);
+ mac_stats->FramesReceivedOK = le32_to_cpu(cnt.rx_good_pkts);
+ mac_stats->FrameCheckSequenceErrors = le32_to_cpu(cnt.rx_fcserror_pkts);
+ mac_stats->AlignmentErrors = le32_to_cpu(cnt.rx_align_error_pkts);
+ mac_stats->OctetsTransmittedOK = le64_to_cpu(cnt.tx_good_bytes);
+ mac_stats->LateCollisions = le32_to_cpu(cnt.tx_late_coll_count);
+ mac_stats->FramesAbortedDueToXSColls = le32_to_cpu(cnt.tx_excess_coll_count);
+ mac_stats->OctetsReceivedOK = le64_to_cpu(cnt.rx_good_bytes);
+ mac_stats->MulticastFramesXmittedOK = le32_to_cpu(cnt.tx_multicast_pkts);
+ mac_stats->BroadcastFramesXmittedOK = le32_to_cpu(cnt.tx_broadcast_pkts);
+ mac_stats->MulticastFramesReceivedOK = le32_to_cpu(cnt.rx_multicast_pkts);
+ mac_stats->BroadcastFramesReceivedOK = le32_to_cpu(cnt.rx_broadcast_pkts);
+ mac_stats->FrameTooLongErrors = le32_to_cpu(cnt.rx_oversize_error_pkts);
+}
+
+static void mxl862xx_get_eth_ctrl_stats(struct dsa_switch *ds, int port,
+ struct ethtool_eth_ctrl_stats *ctrl_stats)
+{
+ struct mxl862xx_rmon_port_cnt cnt;
+
+ if (mxl862xx_read_rmon(ds, port, &cnt))
+ return;
+
+ ctrl_stats->MACControlFramesTransmitted = le32_to_cpu(cnt.tx_pause_count);
+ ctrl_stats->MACControlFramesReceived = le32_to_cpu(cnt.rx_good_pause_pkts);
+}
+
+static void mxl862xx_get_pause_stats(struct dsa_switch *ds, int port,
+ struct ethtool_pause_stats *pause_stats)
+{
+ struct mxl862xx_rmon_port_cnt cnt;
+
+ if (mxl862xx_read_rmon(ds, port, &cnt))
+ return;
+
+ pause_stats->tx_pause_frames = le32_to_cpu(cnt.tx_pause_count);
+ pause_stats->rx_pause_frames = le32_to_cpu(cnt.rx_good_pause_pkts);
+}
+
+static void mxl862xx_get_rmon_stats(struct dsa_switch *ds, int port,
+ struct ethtool_rmon_stats *rmon_stats,
+ const struct ethtool_rmon_hist_range **ranges)
+{
+ struct mxl862xx_rmon_port_cnt cnt;
+
+ if (mxl862xx_read_rmon(ds, port, &cnt))
+ return;
+
+ rmon_stats->undersize_pkts = le32_to_cpu(cnt.rx_under_size_good_pkts);
+ rmon_stats->oversize_pkts = le32_to_cpu(cnt.rx_oversize_good_pkts);
+ rmon_stats->fragments = le32_to_cpu(cnt.rx_under_size_error_pkts);
+ rmon_stats->jabbers = le32_to_cpu(cnt.rx_oversize_error_pkts);
+
+ rmon_stats->hist[0] = le32_to_cpu(cnt.rx64byte_pkts);
+ rmon_stats->hist[1] = le32_to_cpu(cnt.rx127byte_pkts);
+ rmon_stats->hist[2] = le32_to_cpu(cnt.rx255byte_pkts);
+ rmon_stats->hist[3] = le32_to_cpu(cnt.rx511byte_pkts);
+ rmon_stats->hist[4] = le32_to_cpu(cnt.rx1023byte_pkts);
+ rmon_stats->hist[5] = le32_to_cpu(cnt.rx_max_byte_pkts);
+
+ rmon_stats->hist_tx[0] = le32_to_cpu(cnt.tx64byte_pkts);
+ rmon_stats->hist_tx[1] = le32_to_cpu(cnt.tx127byte_pkts);
+ rmon_stats->hist_tx[2] = le32_to_cpu(cnt.tx255byte_pkts);
+ rmon_stats->hist_tx[3] = le32_to_cpu(cnt.tx511byte_pkts);
+ rmon_stats->hist_tx[4] = le32_to_cpu(cnt.tx1023byte_pkts);
+ rmon_stats->hist_tx[5] = le32_to_cpu(cnt.tx_max_byte_pkts);
+
+ *ranges = mxl862xx_rmon_ranges;
+}
+
+/* Compute the delta between two 32-bit free-running counter snapshots,
+ * handling a single wrap-around correctly via unsigned subtraction.
+ */
+static u64 mxl862xx_delta32(u32 cur, u32 prev)
+{
+ return (u32)(cur - prev);
+}
+
+/**
+ * mxl862xx_stats_poll - Read RMON counters and accumulate into 64-bit stats
+ * @ds: DSA switch
+ * @port: port index
+ *
+ * The firmware RMON counters are free-running 32-bit values (64-bit for
+ * byte counters). This function reads the hardware via MDIO (may sleep),
+ * computes deltas from the previous snapshot, and accumulates them into
+ * 64-bit per-port stats under a spinlock.
+ *
+ * Called only from the stats polling workqueue -- serialized by the
+ * single-threaded delayed_work, so no MDIO locking is needed here.
+ */
+static void mxl862xx_stats_poll(struct dsa_switch *ds, int port)
+{
+ struct mxl862xx_priv *priv = ds->priv;
+ struct mxl862xx_port_stats *s = &priv->ports[port].stats;
+ u32 rx_fcserr, rx_under, rx_over, rx_align, tx_drop;
+ u32 rx_drop, rx_evlan, mtu_exc, tx_acm;
+ struct mxl862xx_rmon_port_cnt cnt;
+ u64 rx_bytes, tx_bytes;
+ u32 rx_mcast, tx_coll;
+ u32 rx_pkts, tx_pkts;
+
+ /* MDIO read -- may sleep, done outside the spinlock. */
+ if (mxl862xx_read_rmon(ds, port, &cnt))
+ return;
+
+ rx_pkts = le32_to_cpu(cnt.rx_good_pkts);
+ tx_pkts = le32_to_cpu(cnt.tx_good_pkts);
+ rx_bytes = le64_to_cpu(cnt.rx_good_bytes);
+ tx_bytes = le64_to_cpu(cnt.tx_good_bytes);
+ rx_fcserr = le32_to_cpu(cnt.rx_fcserror_pkts);
+ rx_under = le32_to_cpu(cnt.rx_under_size_error_pkts);
+ rx_over = le32_to_cpu(cnt.rx_oversize_error_pkts);
+ rx_align = le32_to_cpu(cnt.rx_align_error_pkts);
+ tx_drop = le32_to_cpu(cnt.tx_dropped_pkts);
+ rx_drop = le32_to_cpu(cnt.rx_dropped_pkts);
+ rx_evlan = le32_to_cpu(cnt.rx_extended_vlan_discard_pkts);
+ mtu_exc = le32_to_cpu(cnt.mtu_exceed_discard_pkts);
+ tx_acm = le32_to_cpu(cnt.tx_acm_dropped_pkts);
+ rx_mcast = le32_to_cpu(cnt.rx_multicast_pkts);
+ tx_coll = le32_to_cpu(cnt.tx_coll_count);
+
+ /* Accumulate deltas under spinlock -- .get_stats64 reads these. */
+ spin_lock_bh(&priv->ports[port].stats_lock);
+
+ s->rx_packets += mxl862xx_delta32(rx_pkts, s->prev_rx_good_pkts);
+ s->tx_packets += mxl862xx_delta32(tx_pkts, s->prev_tx_good_pkts);
+ s->rx_bytes += rx_bytes - s->prev_rx_good_bytes;
+ s->tx_bytes += tx_bytes - s->prev_tx_good_bytes;
+
+ s->rx_errors +=
+ mxl862xx_delta32(rx_fcserr, s->prev_rx_fcserror_pkts) +
+ mxl862xx_delta32(rx_under, s->prev_rx_under_size_error_pkts) +
+ mxl862xx_delta32(rx_over, s->prev_rx_oversize_error_pkts) +
+ mxl862xx_delta32(rx_align, s->prev_rx_align_error_pkts);
+ s->tx_errors +=
+ mxl862xx_delta32(tx_drop, s->prev_tx_dropped_pkts);
+
+ s->rx_dropped +=
+ mxl862xx_delta32(rx_drop, s->prev_rx_dropped_pkts) +
+ mxl862xx_delta32(rx_evlan, s->prev_rx_evlan_discard_pkts) +
+ mxl862xx_delta32(mtu_exc, s->prev_mtu_exceed_discard_pkts);
+ s->tx_dropped +=
+ mxl862xx_delta32(tx_drop, s->prev_tx_dropped_pkts) +
+ mxl862xx_delta32(tx_acm, s->prev_tx_acm_dropped_pkts);
+
+ s->multicast += mxl862xx_delta32(rx_mcast, s->prev_rx_multicast_pkts);
+ s->collisions += mxl862xx_delta32(tx_coll, s->prev_tx_coll_count);
+
+ s->rx_length_errors +=
+ mxl862xx_delta32(rx_under, s->prev_rx_under_size_error_pkts) +
+ mxl862xx_delta32(rx_over, s->prev_rx_oversize_error_pkts);
+ s->rx_crc_errors +=
+ mxl862xx_delta32(rx_fcserr, s->prev_rx_fcserror_pkts);
+ s->rx_frame_errors +=
+ mxl862xx_delta32(rx_align, s->prev_rx_align_error_pkts);
+
+ s->prev_rx_good_pkts = rx_pkts;
+ s->prev_tx_good_pkts = tx_pkts;
+ s->prev_rx_good_bytes = rx_bytes;
+ s->prev_tx_good_bytes = tx_bytes;
+ s->prev_rx_fcserror_pkts = rx_fcserr;
+ s->prev_rx_under_size_error_pkts = rx_under;
+ s->prev_rx_oversize_error_pkts = rx_over;
+ s->prev_rx_align_error_pkts = rx_align;
+ s->prev_tx_dropped_pkts = tx_drop;
+ s->prev_rx_dropped_pkts = rx_drop;
+ s->prev_rx_evlan_discard_pkts = rx_evlan;
+ s->prev_mtu_exceed_discard_pkts = mtu_exc;
+ s->prev_tx_acm_dropped_pkts = tx_acm;
+ s->prev_rx_multicast_pkts = rx_mcast;
+ s->prev_tx_coll_count = tx_coll;
+
+ spin_unlock_bh(&priv->ports[port].stats_lock);
+}
+
+static void mxl862xx_stats_work_fn(struct work_struct *work)
+{
+ struct mxl862xx_priv *priv =
+ container_of(work, struct mxl862xx_priv, stats_work.work);
+ struct dsa_switch *ds = priv->ds;
+ struct dsa_port *dp;
+
+ dsa_switch_for_each_available_port(dp, ds)
+ mxl862xx_stats_poll(ds, dp->index);
+
+ if (!test_bit(MXL862XX_FLAG_WORK_STOPPED, &priv->flags))
+ schedule_delayed_work(&priv->stats_work,
+ MXL862XX_STATS_POLL_INTERVAL);
+}
+
+static void mxl862xx_get_stats64(struct dsa_switch *ds, int port,
+ struct rtnl_link_stats64 *s)
+{
+ struct mxl862xx_priv *priv = ds->priv;
+ struct mxl862xx_port_stats *ps = &priv->ports[port].stats;
+
+ spin_lock_bh(&priv->ports[port].stats_lock);
+
+ s->rx_packets = ps->rx_packets;
+ s->tx_packets = ps->tx_packets;
+ s->rx_bytes = ps->rx_bytes;
+ s->tx_bytes = ps->tx_bytes;
+ s->rx_errors = ps->rx_errors;
+ s->tx_errors = ps->tx_errors;
+ s->rx_dropped = ps->rx_dropped;
+ s->tx_dropped = ps->tx_dropped;
+ s->multicast = ps->multicast;
+ s->collisions = ps->collisions;
+ s->rx_length_errors = ps->rx_length_errors;
+ s->rx_crc_errors = ps->rx_crc_errors;
+ s->rx_frame_errors = ps->rx_frame_errors;
+
+ spin_unlock_bh(&priv->ports[port].stats_lock);
+
+ /* Trigger a fresh poll so the next read sees up-to-date counters.
+ * No-op if the work is already pending, running, or teardown started.
+ */
+ if (!test_bit(MXL862XX_FLAG_WORK_STOPPED, &priv->flags))
+ schedule_delayed_work(&priv->stats_work, 0);
+}
+
+static const struct dsa_switch_ops mxl862xx_switch_ops = {
+ .get_tag_protocol = mxl862xx_get_tag_protocol,
+ .setup = mxl862xx_setup,
+ .port_setup = mxl862xx_port_setup,
+ .port_teardown = mxl862xx_port_teardown,
+ .phylink_get_caps = mxl862xx_phylink_get_caps,
+ .port_enable = mxl862xx_port_enable,
+ .port_disable = mxl862xx_port_disable,
+ .port_fast_age = mxl862xx_port_fast_age,
+ .set_ageing_time = mxl862xx_set_ageing_time,
+ .port_bridge_join = mxl862xx_port_bridge_join,
+ .port_bridge_leave = mxl862xx_port_bridge_leave,
+ .port_pre_bridge_flags = mxl862xx_port_pre_bridge_flags,
+ .port_bridge_flags = mxl862xx_port_bridge_flags,
+ .port_stp_state_set = mxl862xx_port_stp_state_set,
+ .port_set_host_flood = mxl862xx_port_set_host_flood,
+ .port_fdb_add = mxl862xx_port_fdb_add,
+ .port_fdb_del = mxl862xx_port_fdb_del,
+ .port_fdb_dump = mxl862xx_port_fdb_dump,
+ .port_mdb_add = mxl862xx_port_mdb_add,
+ .port_mdb_del = mxl862xx_port_mdb_del,
+ .port_vlan_filtering = mxl862xx_port_vlan_filtering,
+ .port_vlan_add = mxl862xx_port_vlan_add,
+ .port_vlan_del = mxl862xx_port_vlan_del,
+ .get_strings = mxl862xx_get_strings,
+ .get_sset_count = mxl862xx_get_sset_count,
+ .get_ethtool_stats = mxl862xx_get_ethtool_stats,
+ .get_eth_mac_stats = mxl862xx_get_eth_mac_stats,
+ .get_eth_ctrl_stats = mxl862xx_get_eth_ctrl_stats,
+ .get_pause_stats = mxl862xx_get_pause_stats,
+ .get_rmon_stats = mxl862xx_get_rmon_stats,
+ .get_stats64 = mxl862xx_get_stats64,
+};
+
+static int mxl862xx_probe(struct mdio_device *mdiodev)
+{
+ struct device *dev = &mdiodev->dev;
+ struct mxl862xx_priv *priv;
+ struct dsa_switch *ds;
+ int err, i;
+
+ priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
+ if (!priv)
+ return -ENOMEM;
+
+ priv->mdiodev = mdiodev;
+
+ ds = devm_kzalloc(dev, sizeof(*ds), GFP_KERNEL);
+ if (!ds)
+ return -ENOMEM;
+
+ priv->ds = ds;
+ ds->dev = dev;
+ ds->priv = priv;
+ ds->ops = &mxl862xx_switch_ops;
+ ds->phylink_mac_ops = &mxl862xx_phylink_mac_ops;
+ ds->num_ports = MXL862XX_MAX_PORTS;
+ ds->fdb_isolation = true;
+ ds->max_num_bridges = MXL862XX_MAX_BRIDGES;
+
+ mxl862xx_host_init(priv);
+
+ for (i = 0; i < MXL862XX_MAX_PORTS; i++) {
+ priv->ports[i].priv = priv;
+ INIT_WORK(&priv->ports[i].host_flood_work,
+ mxl862xx_host_flood_work_fn);
+ spin_lock_init(&priv->ports[i].stats_lock);
+ }
+
+ INIT_DELAYED_WORK(&priv->stats_work, mxl862xx_stats_work_fn);
+
+ dev_set_drvdata(dev, ds);
+
+ err = dsa_register_switch(ds);
+ if (err) {
+ set_bit(MXL862XX_FLAG_WORK_STOPPED, &priv->flags);
+ cancel_delayed_work_sync(&priv->stats_work);
+ mxl862xx_host_shutdown(priv);
+ for (i = 0; i < MXL862XX_MAX_PORTS; i++)
+ cancel_work_sync(&priv->ports[i].host_flood_work);
+ }
+
+ return err;
+}
+
+static void mxl862xx_remove(struct mdio_device *mdiodev)
+{
+ struct dsa_switch *ds = dev_get_drvdata(&mdiodev->dev);
+ struct mxl862xx_priv *priv;
+ int i;
+
+ if (!ds)
+ return;
+
+ priv = ds->priv;
+
+ set_bit(MXL862XX_FLAG_WORK_STOPPED, &priv->flags);
+ cancel_delayed_work_sync(&priv->stats_work);
+
+ dsa_unregister_switch(ds);
+
+ mxl862xx_host_shutdown(priv);
+
+ /* Cancel any pending host flood work. dsa_unregister_switch()
+ * has already called port_teardown (which sets setup_done=false),
+ * but a worker could still be blocked on rtnl_lock(). Since we
+ * are now outside RTNL, cancel_work_sync() will not deadlock.
+ */
+ for (i = 0; i < MXL862XX_MAX_PORTS; i++)
+ cancel_work_sync(&priv->ports[i].host_flood_work);
+}
+
+static void mxl862xx_shutdown(struct mdio_device *mdiodev)
+{
+ struct dsa_switch *ds = dev_get_drvdata(&mdiodev->dev);
+ struct mxl862xx_priv *priv;
+ int i;
+
+ if (!ds)
+ return;
+
+ priv = ds->priv;
+
+ dsa_switch_shutdown(ds);
+
+ set_bit(MXL862XX_FLAG_WORK_STOPPED, &priv->flags);
+ cancel_delayed_work_sync(&priv->stats_work);
+
+ mxl862xx_host_shutdown(priv);
+
+ for (i = 0; i < MXL862XX_MAX_PORTS; i++)
+ cancel_work_sync(&priv->ports[i].host_flood_work);
+
+ dev_set_drvdata(&mdiodev->dev, NULL);
+}
+
+static const struct of_device_id mxl862xx_of_match[] = {
+ { .compatible = "maxlinear,mxl86282" },
+ { .compatible = "maxlinear,mxl86252" },
+ { /* sentinel */ }
+};
+MODULE_DEVICE_TABLE(of, mxl862xx_of_match);
+
+static struct mdio_driver mxl862xx_driver = {
+ .probe = mxl862xx_probe,
+ .remove = mxl862xx_remove,
+ .shutdown = mxl862xx_shutdown,
+ .mdiodrv.driver = {
+ .name = "mxl862xx",
+ .of_match_table = mxl862xx_of_match,
+ },
+};
+
+mdio_module_driver(mxl862xx_driver);
+
+MODULE_DESCRIPTION("Driver for MaxLinear MxL862xx switch family");
+MODULE_LICENSE("GPL");
diff --git a/drivers/net/dsa/mxl862xx/mxl862xx.h b/drivers/net/dsa/mxl862xx/mxl862xx.h
new file mode 100644
index 000000000..432a5f3f2
--- /dev/null
+++ b/drivers/net/dsa/mxl862xx/mxl862xx.h
@@ -0,0 +1,343 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#ifndef __MXL862XX_H
+#define __MXL862XX_H
+
+#include <asm/byteorder.h>
+#include <linux/mdio.h>
+#include <linux/workqueue.h>
+#include <net/dsa.h>
+
+struct mxl862xx_priv;
+
+#define MXL862XX_MAX_PORTS 17
+#define MXL862XX_FIRST_SERDES_PORT 9
+#define MXL862XX_SERDES_SLOTS 4
+
+#define MXL862XX_DEFAULT_BRIDGE 0
+#define MXL862XX_MAX_BRIDGES 48
+#define MXL862XX_MAX_BRIDGE_PORTS 128
+#define MXL862XX_TOTAL_EVLAN_ENTRIES 1024
+#define MXL862XX_TOTAL_VF_ENTRIES 1024
+
+/* Number of __le16 words in a firmware portmap (128-bit bitmap). */
+#define MXL862XX_FW_PORTMAP_WORDS (MXL862XX_MAX_BRIDGE_PORTS / 16)
+
+/**
+ * mxl862xx_fw_portmap_set_bit - set a single port bit in a firmware portmap
+ * @map: firmware portmap array (MXL862XX_FW_PORTMAP_WORDS entries)
+ * @port: port index (0..MXL862XX_MAX_BRIDGE_PORTS-1)
+ */
+static inline void mxl862xx_fw_portmap_set_bit(__le16 *map, int port)
+{
+ map[port / 16] |= cpu_to_le16(BIT(port % 16));
+}
+
+/**
+ * mxl862xx_fw_portmap_clear_bit - clear a single port bit in a firmware portmap
+ * @map: firmware portmap array (MXL862XX_FW_PORTMAP_WORDS entries)
+ * @port: port index (0..MXL862XX_MAX_BRIDGE_PORTS-1)
+ */
+static inline void mxl862xx_fw_portmap_clear_bit(__le16 *map, int port)
+{
+ map[port / 16] &= ~cpu_to_le16(BIT(port % 16));
+}
+
+/**
+ * mxl862xx_fw_portmap_is_empty - check whether a firmware portmap has no
+ * bits set
+ * @map: firmware portmap array (MXL862XX_FW_PORTMAP_WORDS entries)
+ *
+ * Return: true if every word in @map is zero.
+ */
+static inline bool mxl862xx_fw_portmap_is_empty(const __le16 *map)
+{
+ int i;
+
+ for (i = 0; i < MXL862XX_FW_PORTMAP_WORDS; i++)
+ if (map[i])
+ return false;
+ return true;
+}
+
+/**
+ * struct mxl862xx_vf_vid - Per-VID entry within a VLAN Filter block
+ * @list: Linked into &mxl862xx_vf_block.vids
+ * @vid: VLAN ID
+ * @index: Entry index within the VLAN Filter HW block
+ * @untagged: Strip tag on egress for this VID (drives EVLAN tag-stripping)
+ */
+struct mxl862xx_vf_vid {
+ struct list_head list;
+ u16 vid;
+ u16 index;
+ bool untagged;
+};
+
+/**
+ * struct mxl862xx_vf_block - Per-port VLAN Filter block
+ * @allocated: Whether the HW block has been allocated via VLANFILTER_ALLOC
+ * @block_id: HW VLAN Filter block ID from VLANFILTER_ALLOC
+ * @block_size: Total entries allocated in this block
+ * @active_count: Number of ALLOW entries at indices [0, active_count).
+ * The bridge port config sends max(active_count, 1) as
+ * block_size to narrow the HW scan window.
+ * discard_unmatched_tagged handles frames outside this range.
+ * @vids: List of &mxl862xx_vf_vid entries programmed in this block
+ */
+struct mxl862xx_vf_block {
+ bool allocated;
+ u16 block_id;
+ u16 block_size;
+ u16 active_count;
+ struct list_head vids;
+};
+
+/**
+ * struct mxl862xx_evlan_block - Per-port per-direction extended VLAN block
+ * @allocated: Whether the HW block has been allocated via EXTENDEDVLAN_ALLOC.
+ * Guards alloc/free idempotency--the block_id is only valid
+ * while allocated is true.
+ * @in_use: Whether the EVLAN engine should be enabled for this block
+ * on the bridge port (sent as the enable flag in
+ * set_bridge_port). Can be false while allocated is still
+ * true -- e.g. when all egress VIDs are removed (idx == 0 in
+ * evlan_program_egress) the block stays allocated for
+ * potential reuse, but the engine is disabled so an empty
+ * rule set does not discard all traffic.
+ * @block_id: HW block ID from EXTENDEDVLAN_ALLOC
+ * @block_size: Total entries allocated
+ * @n_active: Number of HW entries currently written. The bridge port
+ * config sends this as the egress scan window, so entries
+ * beyond n_active are never scanned. Always equals
+ * block_size for ingress blocks (fixed catchall rules).
+ */
+struct mxl862xx_evlan_block {
+ bool allocated;
+ bool in_use;
+ u16 block_id;
+ u16 block_size;
+ u16 n_active;
+};
+
+/**
+ * struct mxl862xx_port_stats - 64-bit accumulated hardware port statistics
+ * @rx_packets: total received packets
+ * @tx_packets: total transmitted packets
+ * @rx_bytes: total received bytes
+ * @tx_bytes: total transmitted bytes
+ * @rx_errors: total receive errors
+ * @tx_errors: total transmit errors
+ * @rx_dropped: total received packets dropped
+ * @tx_dropped: total transmitted packets dropped
+ * @multicast: total received multicast packets
+ * @collisions: total transmit collisions
+ * @rx_length_errors: received length errors (undersize + oversize)
+ * @rx_crc_errors: received FCS errors
+ * @rx_frame_errors: received alignment errors
+ * @prev_rx_good_pkts: previous snapshot of rx good packet counter
+ * @prev_tx_good_pkts: previous snapshot of tx good packet counter
+ * @prev_rx_good_bytes: previous snapshot of rx good byte counter
+ * @prev_tx_good_bytes: previous snapshot of tx good byte counter
+ * @prev_rx_fcserror_pkts: previous snapshot of rx FCS error counter
+ * @prev_rx_under_size_error_pkts: previous snapshot of rx undersize
+ * error counter
+ * @prev_rx_oversize_error_pkts: previous snapshot of rx oversize
+ * error counter
+ * @prev_rx_align_error_pkts: previous snapshot of rx alignment
+ * error counter
+ * @prev_tx_dropped_pkts: previous snapshot of tx dropped counter
+ * @prev_rx_dropped_pkts: previous snapshot of rx dropped counter
+ * @prev_rx_evlan_discard_pkts: previous snapshot of extended VLAN
+ * discard counter
+ * @prev_mtu_exceed_discard_pkts: previous snapshot of MTU exceed
+ * discard counter
+ * @prev_tx_acm_dropped_pkts: previous snapshot of tx ACM dropped
+ * counter
+ * @prev_rx_multicast_pkts: previous snapshot of rx multicast counter
+ * @prev_tx_coll_count: previous snapshot of tx collision counter
+ *
+ * The firmware RMON counters are 32-bit free-running (64-bit for byte
+ * counters). This structure holds 64-bit accumulators alongside the
+ * previous raw snapshot so that deltas can be computed across polls,
+ * handling 32-bit wrap correctly via unsigned subtraction.
+ */
+struct mxl862xx_port_stats {
+ u64 rx_packets;
+ u64 tx_packets;
+ u64 rx_bytes;
+ u64 tx_bytes;
+ u64 rx_errors;
+ u64 tx_errors;
+ u64 rx_dropped;
+ u64 tx_dropped;
+ u64 multicast;
+ u64 collisions;
+ u64 rx_length_errors;
+ u64 rx_crc_errors;
+ u64 rx_frame_errors;
+ u32 prev_rx_good_pkts;
+ u32 prev_tx_good_pkts;
+ u64 prev_rx_good_bytes;
+ u64 prev_tx_good_bytes;
+ u32 prev_rx_fcserror_pkts;
+ u32 prev_rx_under_size_error_pkts;
+ u32 prev_rx_oversize_error_pkts;
+ u32 prev_rx_align_error_pkts;
+ u32 prev_tx_dropped_pkts;
+ u32 prev_rx_dropped_pkts;
+ u32 prev_rx_evlan_discard_pkts;
+ u32 prev_mtu_exceed_discard_pkts;
+ u32 prev_tx_acm_dropped_pkts;
+ u32 prev_rx_multicast_pkts;
+ u32 prev_tx_coll_count;
+};
+
+/**
+ * struct mxl862xx_port - per-port state tracked by the driver
+ * @priv: back-pointer to switch private data; needed by
+ * deferred work handlers to access ds and priv
+ * @fid: firmware FID for the permanent single-port bridge;
+ * kept alive for the lifetime of the port so traffic is
+ * never forwarded while the port is unbridged
+ * @flood_block: bitmask of firmware meter indices that are currently
+ * rate-limiting flood traffic on this port (zero-rate
+ * meters used to block flooding)
+ * @learning: true when address learning is enabled on this port
+ * @setup_done: set at end of port_setup, cleared at start of
+ * port_teardown; guards deferred work against
+ * acting on torn-down state
+ * @pvid: port VLAN ID (native VLAN) assigned to untagged traffic
+ * @vlan_filtering: true when VLAN filtering is enabled on this port
+ * @vf: per-port VLAN Filter block state
+ * @ingress_evlan: ingress extended VLAN block state
+ * @egress_evlan: egress extended VLAN block state
+ * @host_flood_uc: desired host unicast flood state (true = flood);
+ * updated atomically by port_set_host_flood, consumed
+ * by the deferred host_flood_work
+ * @host_flood_mc: desired host multicast flood state (true = flood)
+ * @host_flood_work: deferred work for applying host flood changes;
+ * port_set_host_flood runs in atomic context (under
+ * netif_addr_lock) so firmware calls must be deferred.
+ * The worker acquires rtnl_lock() to serialize with
+ * DSA callbacks and checks @setup_done to avoid
+ * acting on torn-down ports.
+ * @stats: 64-bit accumulated hardware statistics; updated
+ * periodically by the stats polling work
+ * @stats_lock: protects accumulator reads in .get_stats64 against
+ * concurrent updates from the polling work
+ */
+struct mxl862xx_port {
+ struct mxl862xx_priv *priv;
+ u16 fid;
+ unsigned long flood_block;
+ bool learning;
+ bool setup_done;
+ u16 pvid;
+ bool vlan_filtering;
+ struct mxl862xx_vf_block vf;
+ struct mxl862xx_evlan_block ingress_evlan;
+ struct mxl862xx_evlan_block egress_evlan;
+ bool host_flood_uc;
+ bool host_flood_mc;
+ struct work_struct host_flood_work;
+ struct mxl862xx_port_stats stats;
+ spinlock_t stats_lock; /* protects stats accumulators */
+};
+
+/**
+ * struct mxl862xx_pcs - link SerDes interfaces to bridge ports
+ * @pcs: &struct phylink_pcs instance
+ * @priv: pointer to &struct mxl862xx_priv
+ * @serdes_id: SerDes instance index (0 or 1)
+ * @slot: slot within the SerDes (0-3 for QSGMII/QUSXGMII, 0 otherwise)
+ * @interface: cached PHY interface, last value passed to pcs_config().
+ * %PHY_INTERFACE_MODE_NA before the first successful
+ * pcs_config(). Used by pcs_an_restart() to populate the
+ * firmware command and by pcs_disable() to skip the
+ * firmware power-down for shared (QSGMII/QUSXGMII) modes.
+ */
+struct mxl862xx_pcs {
+ struct phylink_pcs pcs;
+ struct mxl862xx_priv *priv;
+ int serdes_id;
+ int slot;
+ phy_interface_t interface;
+};
+
+/**
+ * struct mxl862xx_fw_version - firmware version for comparison and display
+ * @major: firmware major version
+ * @minor: firmware minor version
+ * @revision: firmware revision number
+ */
+struct mxl862xx_fw_version {
+ u8 major;
+ u8 minor;
+ u16 revision;
+};
+
+#define MXL862XX_FW_VER(maj, min, rev) \
+ (((u32)(maj) << 24) | ((u32)(min) << 16) | (rev))
+#define MXL862XX_FW_VER_MIN(priv, maj, min, rev) \
+ (MXL862XX_FW_VER((priv)->fw_version.major, (priv)->fw_version.minor, \
+ (priv)->fw_version.revision) >= \
+ MXL862XX_FW_VER(maj, min, rev))
+
+/* Bit indices for struct mxl862xx_priv::flags */
+#define MXL862XX_FLAG_CRC_ERR 0
+#define MXL862XX_FLAG_WORK_STOPPED 1
+
+/**
+ * struct mxl862xx_priv - driver private data for an MxL862xx switch
+ * @ds: pointer to the DSA switch instance
+ * @mdiodev: MDIO device used to communicate with the switch firmware
+ * @crc_err_work: deferred work for shutting down all ports on MDIO CRC
+ * errors
+ * @flags: atomic status flags; %MXL862XX_FLAG_CRC_ERR is set
+ * before CRC-triggered shutdown and cleared after;
+ * %MXL862XX_FLAG_WORK_STOPPED is set before cancelling
+ * stats_work to prevent rescheduling during teardown
+ * @drop_meter: index of the single shared zero-rate firmware meter
+ * used to unconditionally drop traffic (used to block
+ * flooding)
+ * @fw_version: cached firmware version, populated at probe and
+ * compared with MXL862XX_FW_VER_MIN()
+ * @serdes_ports: SerDes interfaces incl. sub-interfaces in case of
+ * 10G_QXGMII or QSGMII
+ * @serdes_refcount: per-XPCS count of sub-ports enabled by phylink;
+ * pcs_disable powers an XPCS down when the count
+ * reaches zero. Protected by @serdes_lock.
+ * @serdes_lock: serializes the @serdes_refcount transitions with
+ * the XPCS power-down so a sibling sub-port enable
+ * cannot race a power-down to zero
+ * @ports: per-port state, indexed by switch port number
+ * @bridges: maps DSA bridge number to firmware bridge ID;
+ * zero means no firmware bridge allocated for that
+ * DSA bridge number. Indexed by dsa_bridge.num
+ * (0 .. ds->max_num_bridges).
+ * @evlan_ingress_size: per-port ingress Extended VLAN block size
+ * @evlan_egress_size: per-port egress Extended VLAN block size
+ * @vf_block_size: per-port VLAN Filter block size
+ * @stats_work: periodic work item that polls RMON hardware counters
+ * and accumulates them into 64-bit per-port stats
+ */
+struct mxl862xx_priv {
+ struct dsa_switch *ds;
+ struct mdio_device *mdiodev;
+ struct work_struct crc_err_work;
+ unsigned long flags;
+ u16 drop_meter;
+ struct mxl862xx_fw_version fw_version;
+ struct mxl862xx_pcs serdes_ports[8];
+ int serdes_refcount[2];
+ struct mutex serdes_lock;
+ struct mxl862xx_port ports[MXL862XX_MAX_PORTS];
+ u16 bridges[MXL862XX_MAX_BRIDGES + 1];
+ u16 evlan_ingress_size;
+ u16 evlan_egress_size;
+ u16 vf_block_size;
+ struct delayed_work stats_work;
+};
+
+#endif /* __MXL862XX_H */