[PATCH net-next v3 5/8] net: qualcomm: ipqess: add bridge offloading features to the IPQESS driver

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



The IPQ4019 ESS switch is capable of offloading various bridge features.
Add netdev and switchdev notifiers to offload bridge uppers, link state
changes, FDB and MDB accesses and VLANs.

Signed-off-by: Romain Gantois <romain.gantois@xxxxxxxxxxx>
---
 drivers/net/dsa/qca/qca8k-8xxx.c              |   49 +-
 drivers/net/dsa/qca/qca8k-common.c            |   42 +-
 drivers/net/ethernet/qualcomm/Kconfig         |    1 +
 drivers/net/ethernet/qualcomm/ipqess/Makefile |    2 +-
 .../ethernet/qualcomm/ipqess/ipqess_edma.c    |    7 +
 .../qualcomm/ipqess/ipqess_notifiers.c        |  306 +++++
 .../ethernet/qualcomm/ipqess/ipqess_port.c    | 1050 +++++++++++++++--
 .../ethernet/qualcomm/ipqess/ipqess_port.h    |   33 +
 .../ethernet/qualcomm/ipqess/ipqess_switch.c  |   15 +-
 include/linux/dsa/qca8k.h                     |   16 +-
 10 files changed, 1391 insertions(+), 130 deletions(-)
 create mode 100644 drivers/net/ethernet/qualcomm/ipqess/ipqess_notifiers.c

diff --git a/drivers/net/dsa/qca/qca8k-8xxx.c b/drivers/net/dsa/qca/qca8k-8xxx.c
index 210667755b00..7f2bde8ed311 100644
--- a/drivers/net/dsa/qca/qca8k-8xxx.c
+++ b/drivers/net/dsa/qca/qca8k-8xxx.c
@@ -1979,34 +1979,71 @@ qca8k_setup(struct dsa_switch *ds)
 	return 0;
 }
 
+int qca8k_dsa_port_fdb_dump(struct dsa_switch *ds, int port,
+			    dsa_fdb_dump_cb_t *cb, void *data)
+{
+	return qca8k_port_fdb_dump(ds->priv, port, cb, data);
+}
+
+void qca8k_dsa_port_stp_state_set(struct dsa_switch *ds, int port,
+				  u8 state)
+{
+	qca8k_port_stp_state_set(ds->priv, port, state,
+				 dsa_to_port(ds, port)->learning, true);
+}
+
+void qca8k_dsa_port_fast_age(struct dsa_switch *ds, int port)
+{
+	qca8k_port_fast_age(ds->priv, port);
+}
+
+int qca8k_dsa_set_ageing_time(struct dsa_switch *ds, unsigned int msecs)
+{
+	return qca8k_set_ageing_time(ds->priv, msecs);
+}
+
+int qca8k_dsa_port_vlan_filtering(struct dsa_switch *ds, int port,
+				  bool vlan_filtering,
+				  struct netlink_ext_ack *extack)
+{
+	return qca8k_port_vlan_filtering(ds->priv, port, vlan_filtering);
+}
+
+int qca8k_dsa_vlan_add(struct dsa_switch *ds, int port,
+		       const struct switchdev_obj_port_vlan *vlan,
+		       struct netlink_ext_ack *extack)
+{
+	return qca8k_port_vlan_add(ds->priv, port, vlan, extack);
+}
+
 static const struct dsa_switch_ops qca8k_switch_ops = {
 	.get_tag_protocol	= qca8k_get_tag_protocol,
 	.setup			= qca8k_setup,
 	.get_strings		= qca8k_get_strings,
 	.get_ethtool_stats	= qca8k_get_ethtool_stats,
 	.get_sset_count		= qca8k_get_sset_count,
-	.set_ageing_time	= qca8k_set_ageing_time,
+	.set_ageing_time	= qca8k_dsa_set_ageing_time,
 	.get_mac_eee		= qca8k_get_mac_eee,
 	.set_mac_eee		= qca8k_set_mac_eee,
 	.port_enable		= qca8k_port_enable,
 	.port_disable		= qca8k_port_disable,
 	.port_change_mtu	= qca8k_port_change_mtu,
 	.port_max_mtu		= qca8k_port_max_mtu,
-	.port_stp_state_set	= qca8k_port_stp_state_set,
+	.port_stp_state_set	= qca8k_dsa_port_stp_state_set,
 	.port_pre_bridge_flags	= qca8k_port_pre_bridge_flags,
 	.port_bridge_flags	= qca8k_port_bridge_flags,
 	.port_bridge_join	= qca8k_port_bridge_join,
 	.port_bridge_leave	= qca8k_port_bridge_leave,
-	.port_fast_age		= qca8k_port_fast_age,
+	.port_fast_age		= qca8k_dsa_port_fast_age,
 	.port_fdb_add		= qca8k_port_fdb_add,
 	.port_fdb_del		= qca8k_port_fdb_del,
-	.port_fdb_dump		= qca8k_port_fdb_dump,
+	.port_fdb_dump		= qca8k_dsa_port_fdb_dump,
 	.port_mdb_add		= qca8k_port_mdb_add,
 	.port_mdb_del		= qca8k_port_mdb_del,
 	.port_mirror_add	= qca8k_port_mirror_add,
 	.port_mirror_del	= qca8k_port_mirror_del,
-	.port_vlan_filtering	= qca8k_port_vlan_filtering,
-	.port_vlan_add		= qca8k_port_vlan_add,
+	.port_vlan_filtering	= qca8k_dsa_port_vlan_filtering,
+	.port_vlan_add		= qca8k_dsa_vlan_add,
 	.port_vlan_del		= qca8k_port_vlan_del,
 	.phylink_get_caps	= qca8k_phylink_get_caps,
 	.phylink_mac_select_pcs	= qca8k_phylink_mac_select_pcs,
diff --git a/drivers/net/dsa/qca/qca8k-common.c b/drivers/net/dsa/qca/qca8k-common.c
index a66a821ce4d6..3d6ff6f24288 100644
--- a/drivers/net/dsa/qca/qca8k-common.c
+++ b/drivers/net/dsa/qca/qca8k-common.c
@@ -595,11 +595,9 @@ int qca8k_get_mac_eee(struct dsa_switch *ds, int port,
 }
 EXPORT_SYMBOL_GPL(qca8k_get_mac_eee);
 
-static int qca8k_port_configure_learning(struct dsa_switch *ds, int port,
+static int qca8k_port_configure_learning(struct qca8k_priv *priv, int port,
 					 bool learning)
 {
-	struct qca8k_priv *priv = ds->priv;
-
 	if (learning)
 		return regmap_set_bits(priv->regmap,
 				       QCA8K_PORT_LOOKUP_CTRL(port),
@@ -610,10 +608,10 @@ static int qca8k_port_configure_learning(struct dsa_switch *ds, int port,
 					 QCA8K_PORT_LOOKUP_LEARN);
 }
 
-void qca8k_port_stp_state_set(struct dsa_switch *ds, int port, u8 state)
+void qca8k_port_stp_state_set(struct qca8k_priv *priv,
+			      int port, u8 state,
+			      bool port_learning, int set_learning)
 {
-	struct dsa_port *dp = dsa_to_port(ds, port);
-	struct qca8k_priv *priv = ds->priv;
 	bool learning = false;
 	u32 stp_state;
 
@@ -629,10 +627,10 @@ void qca8k_port_stp_state_set(struct dsa_switch *ds, int port, u8 state)
 		break;
 	case BR_STATE_LEARNING:
 		stp_state = QCA8K_PORT_LOOKUP_STATE_LEARNING;
-		learning = dp->learning;
+		learning = port_learning;
 		break;
 	case BR_STATE_FORWARDING:
-		learning = dp->learning;
+		learning = port_learning;
 		fallthrough;
 	default:
 		stp_state = QCA8K_PORT_LOOKUP_STATE_FORWARD;
@@ -642,7 +640,8 @@ void qca8k_port_stp_state_set(struct dsa_switch *ds, int port, u8 state)
 	qca8k_rmw(priv, QCA8K_PORT_LOOKUP_CTRL(port),
 		  QCA8K_PORT_LOOKUP_STATE_MASK, stp_state);
 
-	qca8k_port_configure_learning(ds, port, learning);
+	if (set_learning)
+		qca8k_port_configure_learning(priv, port, learning);
 }
 EXPORT_SYMBOL_GPL(qca8k_port_stp_state_set);
 
@@ -664,7 +663,7 @@ int qca8k_port_bridge_flags(struct dsa_switch *ds, int port,
 	int ret;
 
 	if (flags.mask & BR_LEARNING) {
-		ret = qca8k_port_configure_learning(ds, port,
+		ret = qca8k_port_configure_learning(ds->priv, port,
 						    flags.val & BR_LEARNING);
 		if (ret)
 			return ret;
@@ -740,19 +739,16 @@ void qca8k_port_bridge_leave(struct dsa_switch *ds, int port,
 }
 EXPORT_SYMBOL_GPL(qca8k_port_bridge_leave);
 
-void qca8k_port_fast_age(struct dsa_switch *ds, int port)
+void qca8k_port_fast_age(struct qca8k_priv *priv, int port)
 {
-	struct qca8k_priv *priv = ds->priv;
-
 	mutex_lock(&priv->reg_mutex);
 	qca8k_fdb_access(priv, QCA8K_FDB_FLUSH_PORT, port);
 	mutex_unlock(&priv->reg_mutex);
 }
 EXPORT_SYMBOL_GPL(qca8k_port_fast_age);
 
-int qca8k_set_ageing_time(struct dsa_switch *ds, unsigned int msecs)
+int qca8k_set_ageing_time(struct qca8k_priv *priv, unsigned int msecs)
 {
-	struct qca8k_priv *priv = ds->priv;
 	unsigned int secs = msecs / 1000;
 	u32 val;
 
@@ -877,10 +873,9 @@ int qca8k_port_fdb_del(struct dsa_switch *ds, int port,
 }
 EXPORT_SYMBOL_GPL(qca8k_port_fdb_del);
 
-int qca8k_port_fdb_dump(struct dsa_switch *ds, int port,
+int qca8k_port_fdb_dump(struct qca8k_priv *priv, int port,
 			dsa_fdb_dump_cb_t *cb, void *data)
 {
-	struct qca8k_priv *priv = ds->priv;
 	struct qca8k_fdb _fdb = { 0 };
 	int cnt = QCA8K_NUM_FDB_RECORDS;
 	bool is_static;
@@ -933,8 +928,8 @@ int qca8k_port_mdb_del(struct dsa_switch *ds, int port,
 EXPORT_SYMBOL_GPL(qca8k_port_mdb_del);
 
 int qca8k_port_mirror_add(struct dsa_switch *ds, int port,
-				 struct dsa_mall_mirror_tc_entry *mirror,
-				 bool ingress, struct netlink_ext_ack *extack)
+			  struct dsa_mall_mirror_tc_entry *mirror,
+			  bool ingress, struct netlink_ext_ack *extack)
 {
 	struct qca8k_priv *priv = ds->priv;
 	int monitor_port, ret;
@@ -1025,11 +1020,9 @@ void qca8k_port_mirror_del(struct dsa_switch *ds, int port,
 }
 EXPORT_SYMBOL_GPL(qca8k_port_mirror_del);
 
-int qca8k_port_vlan_filtering(struct dsa_switch *ds, int port,
-			      bool vlan_filtering,
-			      struct netlink_ext_ack *extack)
+int qca8k_port_vlan_filtering(struct qca8k_priv *priv, int port,
+			      bool vlan_filtering)
 {
-	struct qca8k_priv *priv = ds->priv;
 	int ret;
 
 	if (vlan_filtering) {
@@ -1046,13 +1039,12 @@ int qca8k_port_vlan_filtering(struct dsa_switch *ds, int port,
 }
 EXPORT_SYMBOL_GPL(qca8k_port_vlan_filtering);
 
-int qca8k_port_vlan_add(struct dsa_switch *ds, int port,
+int qca8k_port_vlan_add(struct qca8k_priv *priv, int port,
 			const struct switchdev_obj_port_vlan *vlan,
 			struct netlink_ext_ack *extack)
 {
 	bool untagged = vlan->flags & BRIDGE_VLAN_INFO_UNTAGGED;
 	bool pvid = vlan->flags & BRIDGE_VLAN_INFO_PVID;
-	struct qca8k_priv *priv = ds->priv;
 	int ret;
 
 	ret = qca8k_vlan_add(priv, port, vlan->vid, untagged);
diff --git a/drivers/net/ethernet/qualcomm/Kconfig b/drivers/net/ethernet/qualcomm/Kconfig
index 008d20ec9eae..15d120d9e64a 100644
--- a/drivers/net/ethernet/qualcomm/Kconfig
+++ b/drivers/net/ethernet/qualcomm/Kconfig
@@ -66,6 +66,7 @@ config QCOM_IPQ4019_ESS
 	depends on (OF && ARCH_QCOM) || COMPILE_TEST
 	select PHYLINK
 	select NET_DSA
+	select NET_SWITCHDEV
 	select NET_DSA_QCA8K_LIB
 	select PAGE_POOL
 	help
diff --git a/drivers/net/ethernet/qualcomm/ipqess/Makefile b/drivers/net/ethernet/qualcomm/ipqess/Makefile
index 6253f1b0ffd2..b12142bbc7e5 100644
--- a/drivers/net/ethernet/qualcomm/ipqess/Makefile
+++ b/drivers/net/ethernet/qualcomm/ipqess/Makefile
@@ -5,4 +5,4 @@
 
 obj-$(CONFIG_QCOM_IPQ4019_ESS) += ipqess.o
 
-ipqess-objs := ipqess_port.o ipqess_switch.o ipqess_edma.o ipqess_ethtool.o
+ipqess-objs := ipqess_port.o ipqess_switch.o ipqess_edma.o ipqess_ethtool.o ipqess_notifiers.o
diff --git a/drivers/net/ethernet/qualcomm/ipqess/ipqess_edma.c b/drivers/net/ethernet/qualcomm/ipqess/ipqess_edma.c
index 008e92de9eb7..13be40c70750 100644
--- a/drivers/net/ethernet/qualcomm/ipqess/ipqess_edma.c
+++ b/drivers/net/ethernet/qualcomm/ipqess/ipqess_edma.c
@@ -22,6 +22,7 @@
 #include "ipqess_edma.h"
 #include "ipqess_port.h"
 #include "ipqess_switch.h"
+#include "ipqess_notifiers.h"
 
 static void ipqess_edma_w32(struct ipqess_edma *edma, u32 reg, u32 val)
 {
@@ -1152,6 +1153,10 @@ int ipqess_edma_init(struct platform_device *pdev, struct device_node *np)
 			port->edma = edma;
 	}
 
+	err = ipqess_notifiers_register();
+	if (err)
+		goto err_hw_stop;
+
 	return 0;
 
 err_hw_stop:
@@ -1172,6 +1177,8 @@ void ipqess_edma_uninit(struct ipqess_edma *edma)
 	struct qca8k_priv *priv = edma->sw->priv;
 	u32 val;
 
+	ipqess_notifiers_unregister();
+
 	ipqess_edma_irq_disable(edma);
 	ipqess_edma_hw_stop(edma);
 
diff --git a/drivers/net/ethernet/qualcomm/ipqess/ipqess_notifiers.c b/drivers/net/ethernet/qualcomm/ipqess/ipqess_notifiers.c
new file mode 100644
index 000000000000..77f6d79c2ff6
--- /dev/null
+++ b/drivers/net/ethernet/qualcomm/ipqess/ipqess_notifiers.c
@@ -0,0 +1,306 @@
+// SPDX-License-Identifier: GPL-2.0 OR ISC
+/*
+ * Copyright (c) 2023, Romain Gantois <romain.gantois@xxxxxxxxxxx>
+ * Based on net/dsa/slave.c
+ */
+
+#include <net/switchdev.h>
+
+#include <linux/etherdevice.h>
+#include <linux/if_vlan.h>
+#include <linux/if_hsr.h>
+
+#include "ipqess_notifiers.h"
+#include "ipqess_port.h"
+
+static struct workqueue_struct *ipqess_owq;
+
+static bool ipqess_schedule_work(struct work_struct *work)
+{
+	return queue_work(ipqess_owq, work);
+}
+
+void ipqess_flush_workqueue(void)
+{
+	flush_workqueue(ipqess_owq);
+}
+
+/* switchdev */
+
+static int ipqess_port_fdb_event(struct net_device *netdev,
+				 struct net_device *orig_netdev,
+				 unsigned long event, const void *ctx,
+				 const struct switchdev_notifier_fdb_info *fdb_info)
+{
+	struct ipqess_switchdev_event_work *switchdev_work;
+	struct ipqess_port *port = netdev_priv(netdev);
+	bool host_addr = fdb_info->is_local;
+
+	if (ctx && ctx != port)
+		return 0;
+
+	if (!port->bridge)
+		return 0;
+
+	if (switchdev_fdb_is_dynamically_learned(fdb_info) &&
+	    ipqess_port_offloads_bridge_port(port, orig_netdev))
+		return 0;
+
+	/* Also treat FDB entries on foreign interfaces bridged with us as host
+	 * addresses.
+	 */
+	if (ipqess_port_dev_is_foreign(netdev, orig_netdev))
+		host_addr = true;
+
+	switchdev_work = kzalloc(sizeof(*switchdev_work), GFP_ATOMIC);
+	if (!switchdev_work)
+		return -ENOMEM;
+
+	netdev_dbg(netdev, "%s FDB entry towards %s, addr %pM vid %d%s\n",
+		   event == SWITCHDEV_FDB_ADD_TO_DEVICE ? "Adding" : "Deleting",
+		   orig_netdev->name, fdb_info->addr, fdb_info->vid,
+		   host_addr ? " as host address" : "");
+
+	INIT_WORK(&switchdev_work->work, ipqess_port_switchdev_event_work);
+	switchdev_work->event = event;
+	switchdev_work->netdev = netdev;
+	switchdev_work->orig_netdev = orig_netdev;
+
+	ether_addr_copy(switchdev_work->addr, fdb_info->addr);
+	switchdev_work->vid = fdb_info->vid;
+	switchdev_work->host_addr = host_addr;
+
+	ipqess_schedule_work(&switchdev_work->work);
+
+	return 0;
+}
+
+/* Called under rcu_read_lock() */
+static int ipqess_switchdev_event(struct notifier_block *unused,
+				  unsigned long event, void *ptr)
+{
+	struct net_device *netdev = switchdev_notifier_info_to_dev(ptr);
+	int err;
+
+	switch (event) {
+	case SWITCHDEV_PORT_ATTR_SET:
+		err = switchdev_handle_port_attr_set(netdev, ptr,
+						     ipqess_port_recognize_netdev,
+						     ipqess_port_attr_set);
+		return notifier_from_errno(err);
+	case SWITCHDEV_FDB_ADD_TO_DEVICE:
+	case SWITCHDEV_FDB_DEL_TO_DEVICE:
+		err = switchdev_handle_fdb_event_to_device(netdev, event, ptr,
+							   ipqess_port_recognize_netdev,
+							   ipqess_port_dev_is_foreign,
+							   ipqess_port_fdb_event);
+		return notifier_from_errno(err);
+	default:
+		return NOTIFY_DONE;
+	}
+
+	return NOTIFY_OK;
+}
+
+static int ipqess_switchdev_blocking_event(struct notifier_block *unused,
+					   unsigned long event, void *ptr)
+{
+	struct net_device *netdev = switchdev_notifier_info_to_dev(ptr);
+	int err;
+
+	switch (event) {
+	case SWITCHDEV_PORT_OBJ_ADD:
+		err = switchdev_handle_port_obj_add_foreign(netdev, ptr,
+							    ipqess_port_recognize_netdev,
+							    ipqess_port_dev_is_foreign,
+							    ipqess_port_obj_add);
+		return notifier_from_errno(err);
+	case SWITCHDEV_PORT_OBJ_DEL:
+		err = switchdev_handle_port_obj_del_foreign(netdev, ptr,
+							    ipqess_port_recognize_netdev,
+							    ipqess_port_dev_is_foreign,
+							    ipqess_port_obj_del);
+		return notifier_from_errno(err);
+	case SWITCHDEV_PORT_ATTR_SET:
+		err = switchdev_handle_port_attr_set(netdev, ptr,
+						     ipqess_port_recognize_netdev,
+						     ipqess_port_attr_set);
+		return notifier_from_errno(err);
+	}
+
+	return NOTIFY_DONE;
+}
+
+/* netdevice */
+
+static int ipqess_port_changeupper(struct net_device *netdev,
+				   struct netdev_notifier_changeupper_info *info)
+{
+	struct ipqess_port *port = netdev_priv(netdev);
+	struct netlink_ext_ack *extack;
+	int err = NOTIFY_DONE;
+
+	if (!ipqess_port_recognize_netdev(netdev))
+		return err;
+
+	extack = netdev_notifier_info_to_extack(&info->info);
+
+	if (netif_is_bridge_master(info->upper_dev)) {
+		if (info->linking) {
+			err = ipqess_port_bridge_join(port, info->upper_dev, extack);
+			if (err == -EOPNOTSUPP) {
+				NL_SET_ERR_MSG_WEAK_MOD(extack,
+							"Offloading not supported");
+				err = NOTIFY_DONE;
+			}
+			err = notifier_from_errno(err);
+		} else {
+			ipqess_port_bridge_leave(port, info->upper_dev);
+			err = NOTIFY_OK;
+		}
+	} else if (netif_is_lag_master(info->upper_dev)) {
+		/* LAG offloading is not supported by this driver */
+		NL_SET_ERR_MSG_WEAK_MOD(extack,
+					"Offloading not supported");
+		err = NOTIFY_DONE;
+	} else if (is_hsr_master(info->upper_dev)) {
+		if (info->linking) {
+			NL_SET_ERR_MSG_WEAK_MOD(extack,
+						"Offloading not supported");
+			err = NOTIFY_DONE;
+		} else {
+			err = NOTIFY_OK;
+		}
+	}
+
+	return err;
+}
+
+static int ipqess_port_prechangeupper(struct net_device *netdev,
+				      struct netdev_notifier_changeupper_info *info)
+{
+	struct ipqess_port *port = netdev_priv(netdev);
+	struct net_device *brport_dev;
+	int err;
+
+	/* sanity check */
+	if (is_vlan_dev(info->upper_dev)) {
+		err = ipqess_port_check_8021q_upper(netdev, info);
+		if (notifier_to_errno(err))
+			return err;
+	}
+
+	/* prechangeupper */
+	if (netif_is_bridge_master(info->upper_dev) && !info->linking)
+		brport_dev = ipqess_port_get_bridged_netdev(port);
+	else
+		return NOTIFY_DONE;
+
+	if (!brport_dev)
+		return NOTIFY_DONE;
+
+	switchdev_bridge_port_unoffload(brport_dev, port,
+					&ipqess_switchdev_notifier,
+					&ipqess_switchdev_blocking_notifier);
+
+	ipqess_flush_workqueue();
+
+	return NOTIFY_DONE;
+}
+
+static int ipqess_netdevice_event(struct notifier_block *nb,
+				  unsigned long event, void *ptr)
+{
+	struct net_device *netdev = netdev_notifier_info_to_dev(ptr);
+	int err;
+
+	if (!ipqess_port_recognize_netdev(netdev))
+		return NOTIFY_DONE;
+
+	switch (event) {
+	case NETDEV_PRECHANGEUPPER: {
+		err = ipqess_port_prechangeupper(netdev, ptr);
+		if (notifier_to_errno(err))
+			return err;
+
+		break;
+	}
+
+	case NETDEV_CHANGEUPPER: {
+		err = ipqess_port_changeupper(netdev, ptr);
+		if (notifier_to_errno(err))
+			return err;
+
+		break;
+	}
+
+	/* Handling this is only useful for LAG offloading, which this driver
+	 * doesn't support
+	 */
+	case NETDEV_CHANGELOWERSTATE:
+		return NOTIFY_DONE;
+	case NETDEV_CHANGE:
+	case NETDEV_UP:
+	case NETDEV_GOING_DOWN:
+	default:
+		break;
+	}
+
+	return NOTIFY_OK;
+}
+
+struct notifier_block ipqess_switchdev_notifier = {
+	.notifier_call = ipqess_switchdev_event,
+};
+
+struct notifier_block ipqess_switchdev_blocking_notifier = {
+	.notifier_call = ipqess_switchdev_blocking_event,
+};
+
+static struct notifier_block ipqess_nb __read_mostly = {
+	.notifier_call = ipqess_netdevice_event,
+};
+
+int ipqess_notifiers_register(void)
+{
+	int err;
+
+	ipqess_owq = alloc_ordered_workqueue("ipqess_ordered",
+					     WQ_MEM_RECLAIM);
+	if (!ipqess_owq)
+		return -ENOMEM;
+
+	err = register_netdevice_notifier(&ipqess_nb);
+	if (err)
+		goto err_netdev_nb;
+
+	err = register_switchdev_notifier(&ipqess_switchdev_notifier);
+	if (err)
+		goto err_switchdev_nb;
+
+	err = register_switchdev_blocking_notifier(&ipqess_switchdev_blocking_notifier);
+	if (err)
+		goto err_switchdev_blocking_nb;
+
+	return 0;
+
+err_switchdev_blocking_nb:
+	unregister_switchdev_notifier(&ipqess_switchdev_notifier);
+err_switchdev_nb:
+	unregister_netdevice_notifier(&ipqess_nb);
+err_netdev_nb:
+	destroy_workqueue(ipqess_owq);
+
+	return err;
+}
+EXPORT_SYMBOL(ipqess_notifiers_register);
+
+void ipqess_notifiers_unregister(void)
+{
+	unregister_switchdev_blocking_notifier(&ipqess_switchdev_blocking_notifier);
+	unregister_switchdev_notifier(&ipqess_switchdev_notifier);
+	unregister_netdevice_notifier(&ipqess_nb);
+
+	destroy_workqueue(ipqess_owq);
+}
+EXPORT_SYMBOL(ipqess_notifiers_unregister);
diff --git a/drivers/net/ethernet/qualcomm/ipqess/ipqess_port.c b/drivers/net/ethernet/qualcomm/ipqess/ipqess_port.c
index 52d7baa7cae0..29420820c3d8 100644
--- a/drivers/net/ethernet/qualcomm/ipqess/ipqess_port.c
+++ b/drivers/net/ethernet/qualcomm/ipqess/ipqess_port.c
@@ -23,50 +23,50 @@ static struct device_type ipqess_port_type = {
 	.name	= "switch",
 };
 
+struct net_device *ipqess_port_get_bridged_netdev(const struct ipqess_port *port)
+{
+	if (!port->bridge)
+		return NULL;
+
+	return port->netdev;
+}
+
 /* netdev ops */
 
+static void ipqess_port_notify_bridge_fdb_flush(const struct ipqess_port *port,
+						u16 vid)
+{
+	struct net_device *brport_dev = ipqess_port_get_bridged_netdev(port);
+	struct switchdev_notifier_fdb_info info = {
+		.vid = vid,
+	};
+
+	/* When the port becomes standalone it has already left the bridge.
+	 * Don't notify the bridge in that case.
+	 */
+	if (!brport_dev)
+		return;
+
+	call_switchdev_notifiers(SWITCHDEV_FDB_FLUSH_TO_BRIDGE,
+				 brport_dev, &info.info, NULL);
+}
+
 static void ipqess_port_fast_age(const struct ipqess_port *port)
 {
 	struct qca8k_priv *priv = port->sw->priv;
 
-	mutex_lock(&priv->reg_mutex);
-	qca8k_fdb_access(priv, QCA8K_FDB_FLUSH_PORT, port->index);
-	mutex_unlock(&priv->reg_mutex);
+	qca8k_port_fast_age(priv, port->index);
+
+	/* Flush all VLANs */
+	ipqess_port_notify_bridge_fdb_flush(port, 0);
 }
 
 static void ipqess_port_stp_state_set(struct ipqess_port *port,
 				      u8 state)
 {
 	struct qca8k_priv *priv = port->sw->priv;
-	u32 stp_state;
-	int err;
 
-	switch (state) {
-	case BR_STATE_DISABLED:
-		stp_state = QCA8K_PORT_LOOKUP_STATE_DISABLED;
-		break;
-	case BR_STATE_BLOCKING:
-		stp_state = QCA8K_PORT_LOOKUP_STATE_BLOCKING;
-		break;
-	case BR_STATE_LISTENING:
-		stp_state = QCA8K_PORT_LOOKUP_STATE_LISTENING;
-		break;
-	case BR_STATE_LEARNING:
-		stp_state = QCA8K_PORT_LOOKUP_STATE_LEARNING;
-		break;
-	case BR_STATE_FORWARDING:
-	default:
-		stp_state = QCA8K_PORT_LOOKUP_STATE_FORWARD;
-		break;
-	}
-
-	err = qca8k_rmw(priv, QCA8K_PORT_LOOKUP_CTRL(port->index),
-			QCA8K_PORT_LOOKUP_STATE_MASK, stp_state);
-
-	if (err)
-		dev_warn(priv->dev,
-			 "failed to set STP state %d for port %d: err %d\n",
-			 stp_state, port->index, err);
+	qca8k_port_stp_state_set(priv, port->index, state, false, false);
 }
 
 static void ipqess_port_set_state_now(struct ipqess_port *port,
@@ -93,7 +93,8 @@ static int ipqess_port_enable_rt(struct ipqess_port *port,
 
 	phy_support_asym_pause(phy);
 
-	ipqess_port_set_state_now(port, BR_STATE_FORWARDING, false);
+	if (!port->bridge)
+		ipqess_port_set_state_now(port, BR_STATE_FORWARDING, false);
 
 	if (port->pl)
 		phylink_start(port->pl);
@@ -108,7 +109,8 @@ static void ipqess_port_disable_rt(struct ipqess_port *port)
 	if (port->pl)
 		phylink_stop(port->pl);
 
-	ipqess_port_set_state_now(port, BR_STATE_DISABLED, false);
+	if (!port->bridge)
+		ipqess_port_set_state_now(port, BR_STATE_DISABLED, false);
 
 	qca8k_port_set_status(priv, port->index, 0);
 	priv->port_enabled_map &= ~BIT(port->index);
@@ -204,34 +206,9 @@ static int ipqess_port_change_mtu(struct net_device *dev, int new_mtu)
 	return 0;
 }
 
-static int ipqess_port_do_vlan_add(struct qca8k_priv *priv, int port_index,
-				   const struct switchdev_obj_port_vlan *vlan,
-				   struct netlink_ext_ack *extack)
+static inline struct net_device *ipqess_port_bridge_dev_get(struct ipqess_port *port)
 {
-	bool untagged = vlan->flags & BRIDGE_VLAN_INFO_UNTAGGED;
-	bool pvid = vlan->flags & BRIDGE_VLAN_INFO_PVID;
-	int ret;
-
-	ret = qca8k_vlan_add(priv, port_index, vlan->vid, untagged);
-	if (ret) {
-		dev_err(priv->dev, "Failed to add VLAN to port %d (%d)", port_index,
-			ret);
-		return ret;
-	}
-
-	if (pvid) {
-		ret = qca8k_rmw(priv, QCA8K_EGRESS_VLAN(port_index),
-				QCA8K_EGREES_VLAN_PORT_MASK(port_index),
-				QCA8K_EGREES_VLAN_PORT(port_index, vlan->vid));
-		if (ret)
-			return ret;
-
-		ret = qca8k_write(priv, QCA8K_REG_PORT_VLAN_CTRL0(port_index),
-				  QCA8K_PORT_VLAN_CVID(vlan->vid) |
-				  QCA8K_PORT_VLAN_SVID(vlan->vid));
-	}
-
-	return ret;
+	return port->bridge ? port->bridge->netdev : NULL;
 }
 
 static int ipqess_port_vlan_rx_add_vid(struct net_device *dev, __be16 proto,
@@ -248,7 +225,7 @@ static int ipqess_port_vlan_rx_add_vid(struct net_device *dev, __be16 proto,
 	int ret;
 
 	/* User port... */
-	ret = ipqess_port_do_vlan_add(port->sw->priv, port->index, &vlan, &extack);
+	ret = qca8k_port_vlan_add(port->sw->priv, port->index, &vlan, &extack);
 	if (ret) {
 		if (extack._msg)
 			netdev_err(dev, "%s\n", extack._msg);
@@ -256,7 +233,7 @@ static int ipqess_port_vlan_rx_add_vid(struct net_device *dev, __be16 proto,
 	}
 
 	/* And CPU port... */
-	ret = ipqess_port_do_vlan_add(port->sw->priv, 0, &vlan, &extack);
+	ret = qca8k_port_vlan_add(port->sw->priv, 0, &vlan, &extack);
 	if (ret) {
 		if (extack._msg)
 			netdev_err(dev, "CPU port %d: %s\n", 0, extack._msg);
@@ -340,24 +317,13 @@ ipqess_port_fdb_dump(struct sk_buff *skb, struct netlink_callback *cb,
 		.cb = cb,
 		.idx = *idx,
 	};
-	int cnt = QCA8K_NUM_FDB_RECORDS;
-	struct qca8k_fdb _fdb = { 0 };
-	bool is_static;
 	int ret = 0;
 
-	mutex_lock(&priv->reg_mutex);
-	while (cnt-- && !qca8k_fdb_next(priv, &_fdb, port->index)) {
-		if (!_fdb.aging)
-			break;
-		is_static = (_fdb.aging == QCA8K_ATU_STATUS_STATIC);
-		ret = ipqess_port_fdb_do_dump(_fdb.mac, _fdb.vid, is_static, &dump);
-		if (ret)
-			break;
-	}
-	mutex_unlock(&priv->reg_mutex);
-
 	*idx = dump.idx;
 
+	ret = qca8k_port_fdb_dump(priv, port->index, ipqess_port_fdb_do_dump,
+				  &dump);
+
 	return ret;
 }
 
@@ -374,6 +340,882 @@ static const struct net_device_ops ipqess_port_netdev_ops = {
 	.ndo_fdb_dump           = ipqess_port_fdb_dump,
 };
 
+/* Bridge ops */
+
+static int ipqess_port_bridge_alloc(struct ipqess_port *port,
+				    struct net_device *br,
+				    struct netlink_ext_ack *extack)
+{
+	struct ipqess_bridge *bridge;
+
+	bridge = kzalloc(sizeof(*bridge), GFP_KERNEL);
+	if (!bridge)
+		return -ENOMEM;
+
+	refcount_set(&bridge->refcount, 1);
+
+	bridge->netdev = br;
+
+	port->bridge = bridge;
+
+	return 0;
+}
+
+/* Must be called under rcu_read_lock() */
+static bool ipqess_port_can_apply_vlan_filtering(struct ipqess_port *port,
+						 bool vlan_filtering,
+						 struct netlink_ext_ack *extack)
+{
+	int err;
+
+	/* VLAN awareness was off, so the question is "can we turn it on".
+	 * We may have had 8021q uppers, those need to go. Make sure we don't
+	 * enter an inconsistent state: deny changing the VLAN awareness state
+	 * as long as we have 8021q uppers.
+	 */
+	if (vlan_filtering) {
+		struct net_device *br = ipqess_port_bridge_dev_get(port);
+		struct net_device *upper_dev, *netdev = port->netdev;
+		struct list_head *iter;
+
+		netdev_for_each_upper_dev_rcu(netdev, upper_dev, iter) {
+			struct bridge_vlan_info br_info;
+			u16 vid;
+
+			if (!is_vlan_dev(upper_dev))
+				continue;
+
+			vid = vlan_dev_vlan_id(upper_dev);
+
+			/* br_vlan_get_info() returns -EINVAL or -ENOENT if the
+			 * device, respectively the VID is not found, returning
+			 * 0 means success, which is a failure for us here.
+			 */
+			err = br_vlan_get_info(br, vid, &br_info);
+			if (err == 0) {
+				NL_SET_ERR_MSG_MOD(extack,
+						   "Must first remove VLAN uppers having VIDs also present in bridge");
+				return false;
+			}
+		}
+	}
+
+	/* VLAN filtering is not global so we can just return true here */
+	return true;
+}
+
+static int ipqess_port_restore_vlan(struct net_device *vdev, int vid, void *arg)
+{
+	__be16 proto = vdev ? vlan_dev_vlan_proto(vdev) : htons(ETH_P_8021Q);
+
+	return ipqess_port_vlan_rx_add_vid(arg, proto, vid);
+}
+
+static int ipqess_port_clear_vlan(struct net_device *vdev, int vid, void *arg)
+{
+	__be16 proto = vdev ? vlan_dev_vlan_proto(vdev) : htons(ETH_P_8021Q);
+
+	return ipqess_port_vlan_rx_kill_vid(arg, proto, vid);
+}
+
+/* Keep the VLAN RX filtering list in sync with the hardware only if VLAN
+ * filtering is enabled.
+ */
+static int ipqess_port_manage_vlan_filtering(struct net_device *netdev,
+					     bool vlan_filtering)
+{
+	int err;
+
+	if (vlan_filtering) {
+		netdev->features |= NETIF_F_HW_VLAN_CTAG_FILTER;
+
+		err = vlan_for_each(netdev, ipqess_port_restore_vlan, netdev);
+		if (err) {
+			netdev_err(netdev,
+				   "Failed to restore all VLAN's successfully, error %d\n",
+				   err);
+			vlan_for_each(netdev, ipqess_port_clear_vlan, netdev);
+			netdev->features &= ~NETIF_F_HW_VLAN_CTAG_FILTER;
+			return err;
+		}
+	} else {
+		err = vlan_for_each(netdev, ipqess_port_clear_vlan, netdev);
+		if (err)
+			return err;
+
+		netdev->features &= ~NETIF_F_HW_VLAN_CTAG_FILTER;
+	}
+
+	return 0;
+}
+
+static int ipqess_port_vlan_filtering(struct ipqess_port *port,
+				      bool vlan_filtering,
+				      struct netlink_ext_ack *extack)
+{
+	bool old_vlan_filtering = port->vlan_filtering;
+	bool apply;
+	int err;
+
+	/* We are called from ipqess_port_switchdev_blocking_event(),
+	 * which is not under rcu_read_lock(), unlike
+	 * ipqess_port_switchdev_event().
+	 */
+	rcu_read_lock();
+	apply = ipqess_port_can_apply_vlan_filtering(port, vlan_filtering, extack);
+	rcu_read_unlock();
+	if (!apply)
+		return -EINVAL;
+
+	if (old_vlan_filtering == vlan_filtering)
+		return 0;
+
+	err = qca8k_port_vlan_filtering(port->sw->priv, port->index,
+					vlan_filtering);
+
+	if (err)
+		return err;
+
+	port->vlan_filtering = vlan_filtering;
+
+	err = ipqess_port_manage_vlan_filtering(port->netdev,
+						vlan_filtering);
+	if (err)
+		goto restore;
+
+	return 0;
+
+restore:
+	err = qca8k_port_vlan_filtering(port->sw->priv, port->index,
+					old_vlan_filtering);
+	port->vlan_filtering = old_vlan_filtering;
+
+	return err;
+}
+
+static void ipqess_port_reset_vlan_filtering(struct ipqess_port *port,
+					     struct ipqess_bridge *bridge)
+{
+	struct netlink_ext_ack extack = {0};
+	bool change_vlan_filtering = false;
+	bool vlan_filtering;
+	int err;
+
+	if (br_vlan_enabled(bridge->netdev)) {
+		change_vlan_filtering = true;
+		vlan_filtering = false;
+	}
+
+	if (!change_vlan_filtering)
+		return;
+
+	err = ipqess_port_vlan_filtering(port, vlan_filtering, &extack);
+	if (extack._msg) {
+		dev_err(&port->netdev->dev, "port %d: %s\n", port->index,
+			extack._msg);
+	}
+	if (err && err != -EOPNOTSUPP) {
+		dev_err(&port->netdev->dev,
+			"port %d failed to reset VLAN filtering to %d: %pe\n",
+			port->index, vlan_filtering, ERR_PTR(err));
+	}
+}
+
+static int ipqess_port_ageing_time(struct ipqess_port *port,
+				   clock_t ageing_clock)
+{
+	unsigned long ageing_jiffies = clock_t_to_jiffies(ageing_clock);
+	unsigned int ageing_time = jiffies_to_msecs(ageing_jiffies);
+
+	if (ageing_time < IPQESS_SWITCH_AGEING_TIME_MIN ||
+	    ageing_time > IPQESS_SWITCH_AGEING_TIME_MAX)
+		return -ERANGE;
+
+	/* Program the fastest ageing time in case of multiple bridges */
+	ageing_time = ipqess_switch_fastest_ageing_time(port->sw, ageing_time);
+
+	port->ageing_time = ageing_time;
+	return ipqess_set_ageing_time(port->sw, ageing_time);
+}
+
+static int ipqess_port_switchdev_sync_attrs(struct ipqess_port *port,
+					    struct netlink_ext_ack *extack)
+{
+	struct net_device *brport_dev = ipqess_port_get_bridged_netdev(port);
+	struct net_device *br = ipqess_port_bridge_dev_get(port);
+	int err;
+
+	ipqess_port_set_state_now(port, br_port_get_stp_state(brport_dev), false);
+
+	err = ipqess_port_vlan_filtering(port, br_vlan_enabled(br), extack);
+	if (err)
+		return err;
+
+	err = ipqess_port_ageing_time(port, br_get_ageing_time(br));
+	if (err && err != -EOPNOTSUPP)
+		return err;
+
+	return 0;
+}
+
+static void ipqess_port_switchdev_unsync_attrs(struct ipqess_port *port,
+					       struct ipqess_bridge *bridge)
+{
+	/* Port left the bridge, put in BR_STATE_DISABLED by the bridge layer,
+	 * so allow it to be in BR_STATE_FORWARDING to be kept functional
+	 */
+	ipqess_port_set_state_now(port, BR_STATE_FORWARDING, true);
+
+	ipqess_port_reset_vlan_filtering(port, bridge);
+
+	/* Ageing time is global to the switch chip, so don't change it
+	 * here because we have no good reason (or value) to change it to.
+	 */
+}
+
+static inline bool ipqess_port_offloads_bridge(struct ipqess_port *port,
+					       const struct ipqess_bridge *bridge)
+{
+	return ipqess_port_bridge_dev_get(port) == bridge->netdev;
+}
+
+bool ipqess_port_offloads_bridge_port(struct ipqess_port *port,
+				      const struct net_device *netdev)
+{
+	return ipqess_port_get_bridged_netdev(port) == netdev;
+}
+
+static inline bool
+ipqess_port_offloads_bridge_dev(struct ipqess_port *port,
+				const struct net_device *bridge_dev)
+{
+	/* QCA8K ports connected to a bridge, and event was emitted
+	 * for the bridge.
+	 */
+	return ipqess_port_bridge_dev_get(port) == bridge_dev;
+}
+
+static void ipqess_port_bridge_destroy(struct ipqess_port *port,
+				       const struct net_device *br)
+{
+	struct ipqess_bridge *bridge = port->bridge;
+
+	port->bridge = NULL;
+
+	if (!refcount_dec_and_test(&bridge->refcount))
+		return;
+
+	kfree(bridge);
+}
+
+int ipqess_port_bridge_join(struct ipqess_port *port, struct net_device *br,
+			    struct netlink_ext_ack *extack)
+{
+	struct ipqess_switch *sw = port->sw;
+	struct ipqess_bridge *bridge = NULL;
+	struct qca8k_priv *priv = sw->priv;
+	struct ipqess_port *other_port;
+	struct net_device *brport_dev;
+	int port_id = port->index;
+	int port_mask = 0;
+	int i, err;
+
+	/* QCA8K doesn't support MST */
+	if (br_mst_enabled(br)) {
+		err = -EOPNOTSUPP;
+		goto out_err;
+	}
+
+	/* Check if we already registered this bridge with
+	 * another switch port
+	 */
+	for (i = 0; i < IPQESS_SWITCH_MAX_PORTS; i++) {
+		other_port = sw->port_list[i];
+		if (other_port && other_port->bridge &&
+		    other_port->bridge->netdev == br)
+			bridge = other_port->bridge;
+	}
+
+	if (bridge) {
+		refcount_inc(&bridge->refcount);
+		port->bridge = bridge;
+	} else {
+		err = ipqess_port_bridge_alloc(port, br, extack);
+		if (err)
+			goto out_err;
+	}
+	bridge = port->bridge;
+
+	for (i = 1; i <= IPQESS_SWITCH_MAX_PORTS; i++) {
+		other_port = sw->port_list[i - 1];
+		if (!other_port || !ipqess_port_offloads_bridge(other_port, bridge))
+			continue;
+		/* Add this port to the portvlan mask of the other ports
+		 * in the bridge
+		 */
+		err = regmap_set_bits(priv->regmap,
+				      QCA8K_PORT_LOOKUP_CTRL(i),
+				      BIT(port_id));
+		if (err)
+			goto out_rollback;
+		if (i != port_id)
+			port_mask |= BIT(i);
+	}
+	/* Also add the CPU port */
+	err = regmap_set_bits(priv->regmap,
+			      QCA8K_PORT_LOOKUP_CTRL(0),
+			      BIT(port_id));
+	port_mask |= BIT(0);
+
+	/* Add all other ports to this ports portvlan mask */
+	err = qca8k_rmw(priv, QCA8K_PORT_LOOKUP_CTRL(port_id),
+			QCA8K_PORT_LOOKUP_MEMBER, port_mask);
+	if (err)
+		goto out_rollback;
+
+	brport_dev = ipqess_port_get_bridged_netdev(port);
+
+	err = switchdev_bridge_port_offload(brport_dev, port->netdev, port,
+					    &ipqess_switchdev_notifier,
+					    &ipqess_switchdev_blocking_notifier,
+					    false, extack);
+	if (err)
+		goto out_rollback_unbridge;
+
+	err = ipqess_port_switchdev_sync_attrs(port, extack);
+	if (err)
+		goto out_rollback_unoffload;
+
+	return 0;
+
+out_rollback_unoffload:
+	switchdev_bridge_port_unoffload(brport_dev, port,
+					&ipqess_switchdev_notifier,
+					&ipqess_switchdev_blocking_notifier);
+	ipqess_flush_workqueue();
+out_rollback_unbridge:
+	for (i = 1; i <= IPQESS_SWITCH_MAX_PORTS; i++) {
+		other_port = sw->port_list[i - 1];
+		if (!other_port ||
+		    !ipqess_port_offloads_bridge(other_port, port->bridge))
+			continue;
+		/* Remove this port from the portvlan mask of the other ports
+		 * in the bridge
+		 */
+		regmap_clear_bits(priv->regmap,
+				  QCA8K_PORT_LOOKUP_CTRL(i),
+				  BIT(port_id));
+	}
+
+	/* Set the cpu port to be the only one in the portvlan mask of
+	 * this port
+	 */
+	qca8k_rmw(priv, QCA8K_PORT_LOOKUP_CTRL(port_id),
+		  QCA8K_PORT_LOOKUP_MEMBER, BIT(0));
+out_rollback:
+	ipqess_port_bridge_destroy(port, br);
+out_err:
+	dev_err(&port->netdev->dev, "Failed to join bridge: errno %d\n", err);
+	return err;
+}
+
+void ipqess_port_bridge_leave(struct ipqess_port *port, struct net_device *br)
+{
+	struct ipqess_bridge *bridge = port->bridge;
+	struct ipqess_switch *sw = port->sw;
+	struct qca8k_priv *priv = sw->priv;
+	struct ipqess_port *other_port;
+	int port_id = port->index;
+	int i;
+
+	/* If the port could not be offloaded to begin with, then
+	 * there is nothing to do.
+	 */
+	if (!bridge)
+		return;
+
+	for (i = 1; i <= IPQESS_SWITCH_MAX_PORTS; i++) {
+		other_port = sw->port_list[i - 1];
+		if (!other_port || !ipqess_port_offloads_bridge(other_port, bridge))
+			continue;
+		/* Remove this port from the portvlan mask of the other ports
+		 * in the bridge
+		 */
+		regmap_clear_bits(priv->regmap,
+				  QCA8K_PORT_LOOKUP_CTRL(i),
+				  BIT(port_id));
+	}
+
+	/* Set the cpu port to be the only one in the portvlan mask of
+	 * this port
+	 */
+	qca8k_rmw(priv, QCA8K_PORT_LOOKUP_CTRL(port_id),
+		  QCA8K_PORT_LOOKUP_MEMBER, BIT(0));
+
+	ipqess_port_switchdev_unsync_attrs(port, bridge);
+
+	/* Here the port is already unbridged. Reflect the current configuration. */
+
+	ipqess_port_bridge_destroy(port, br);
+}
+
+int ipqess_port_attr_set(struct net_device *dev, const void *ctx,
+			 const struct switchdev_attr *attr,
+			 struct netlink_ext_ack *extack)
+{
+	struct ipqess_port *port = netdev_priv(dev);
+	int ret;
+
+	if (ctx && ctx != port)
+		return 0;
+
+	switch (attr->id) {
+	case SWITCHDEV_ATTR_ID_PORT_STP_STATE:
+		if (!ipqess_port_offloads_bridge_port(port, attr->orig_dev))
+			return -EOPNOTSUPP;
+
+		ipqess_port_set_state_now(port, attr->u.stp_state, true);
+		return 0;
+	case SWITCHDEV_ATTR_ID_BRIDGE_VLAN_FILTERING:
+		if (!ipqess_port_offloads_bridge_dev(port, attr->orig_dev))
+			return -EOPNOTSUPP;
+
+		ret = ipqess_port_vlan_filtering(port, attr->u.vlan_filtering,
+						 extack);
+		break;
+	case SWITCHDEV_ATTR_ID_BRIDGE_AGEING_TIME:
+		if (!ipqess_port_offloads_bridge_dev(port, attr->orig_dev))
+			return -EOPNOTSUPP;
+
+		ret = ipqess_port_ageing_time(port, attr->u.ageing_time);
+		break;
+	case SWITCHDEV_ATTR_ID_PORT_PRE_BRIDGE_FLAGS:
+		if (!ipqess_port_offloads_bridge_port(port, attr->orig_dev))
+			return -EOPNOTSUPP;
+
+		return -EINVAL;
+	case SWITCHDEV_ATTR_ID_BRIDGE_MST:
+	case SWITCHDEV_ATTR_ID_PORT_MST_STATE:
+	case SWITCHDEV_ATTR_ID_PORT_BRIDGE_FLAGS:
+	case SWITCHDEV_ATTR_ID_VLAN_MSTI:
+	default:
+		ret = -EOPNOTSUPP;
+		break;
+	}
+
+	return ret;
+}
+
+static int ipqess_port_vlan_check_for_8021q_uppers(struct net_device *netdev,
+						   const struct switchdev_obj_port_vlan *vlan)
+{
+	struct net_device *upper_dev;
+	struct list_head *iter;
+
+	netdev_for_each_upper_dev_rcu(netdev, upper_dev, iter) {
+		u16 vid;
+
+		if (!is_vlan_dev(upper_dev))
+			continue;
+
+		vid = vlan_dev_vlan_id(upper_dev);
+		if (vid == vlan->vid)
+			return -EBUSY;
+	}
+
+	return 0;
+}
+
+static int ipqess_port_host_vlan_del(struct net_device *netdev,
+				     const struct switchdev_obj *obj)
+{
+	struct ipqess_port *port = netdev_priv(netdev);
+	struct net_device *br = ipqess_port_bridge_dev_get(port);
+	struct switchdev_obj_port_vlan *vlan;
+
+	/* Do nothing if this is a software bridge */
+	if (!port->bridge)
+		return -EOPNOTSUPP;
+
+	if (br && !br_vlan_enabled(br))
+		return 0;
+
+	vlan = SWITCHDEV_OBJ_PORT_VLAN(obj);
+
+	return qca8k_vlan_del(port->sw->priv, 0, vlan->vid);
+}
+
+static int ipqess_port_vlan_del(struct net_device *netdev,
+				const struct switchdev_obj *obj)
+{
+	struct ipqess_port *port = netdev_priv(netdev);
+	struct net_device *br = ipqess_port_bridge_dev_get(port);
+	struct qca8k_priv *priv = port->sw->priv;
+	struct switchdev_obj_port_vlan *vlan;
+	int ret;
+
+	if (br && !br_vlan_enabled(br))
+		return 0;
+
+	vlan = SWITCHDEV_OBJ_PORT_VLAN(obj);
+
+	ret = qca8k_vlan_del(priv, port->index, vlan->vid);
+
+	if (ret)
+		dev_err(priv->dev, "Failed to delete VLAN from port %d (%d)\n",
+			port->index, ret);
+
+	return ret;
+}
+
+static int ipqess_port_host_vlan_add(struct net_device *netdev,
+				     const struct switchdev_obj *obj,
+				     struct netlink_ext_ack *extack)
+{
+	struct ipqess_port *port = netdev_priv(netdev);
+	struct switchdev_obj_port_vlan *vlan;
+	struct net_device *br;
+
+	br = ipqess_port_bridge_dev_get(port);
+	/* Do nothing is this is a software bridge */
+	if (!port->bridge)
+		return -EOPNOTSUPP;
+
+	if (br && !br_vlan_enabled(br)) {
+		NL_SET_ERR_MSG_MOD(extack, "skipping configuration of VLAN");
+		return 0;
+	}
+
+	vlan = SWITCHDEV_OBJ_PORT_VLAN(obj);
+
+	vlan->flags &= ~BRIDGE_VLAN_INFO_PVID;
+
+	/* Add vid to CPU port */
+	return qca8k_port_vlan_add(port->sw->priv, 0, vlan, extack);
+}
+
+static int ipqess_port_vlan_add(struct net_device *netdev,
+				const struct switchdev_obj *obj,
+				struct netlink_ext_ack *extack)
+{
+	struct ipqess_port *port = netdev_priv(netdev);
+	struct net_device *br = ipqess_port_bridge_dev_get(port);
+	struct switchdev_obj_port_vlan *vlan;
+	int err;
+
+	if (br && !br_vlan_enabled(br)) {
+		NL_SET_ERR_MSG_MOD(extack, "skipping configuration of VLAN");
+		return 0;
+	}
+
+	vlan = SWITCHDEV_OBJ_PORT_VLAN(obj);
+
+	/* Deny adding a bridge VLAN when there is already an 802.1Q upper with
+	 * the same VID.
+	 */
+	if (br && br_vlan_enabled(br)) {
+		rcu_read_lock();
+		err = ipqess_port_vlan_check_for_8021q_uppers(netdev, vlan);
+		rcu_read_unlock();
+		if (err) {
+			NL_SET_ERR_MSG_MOD(extack,
+					   "Port already has a VLAN upper with this VID");
+			return err;
+		}
+	}
+
+	err = qca8k_port_vlan_add(port->sw->priv, port->index, vlan, extack);
+	return err;
+}
+
+static int ipqess_port_host_mdb_del(struct ipqess_port *port,
+				    const struct switchdev_obj_port_mdb *mdb)
+{
+	struct qca8k_priv *priv = port->sw->priv;
+	const u8 *addr = mdb->addr;
+	u16 vid = mdb->vid;
+
+	return qca8k_fdb_search_and_del(priv, BIT(0), addr, vid);
+}
+
+static int ipqess_port_host_mdb_add(struct ipqess_port *port,
+				    const struct switchdev_obj_port_mdb *mdb)
+{
+	struct qca8k_priv *priv = port->sw->priv;
+	const u8 *addr = mdb->addr;
+	u16 vid = mdb->vid;
+
+	return qca8k_fdb_search_and_insert(priv, BIT(0), addr, vid,
+					   QCA8K_ATU_STATUS_STATIC);
+}
+
+static int ipqess_port_mdb_del(struct ipqess_port *port,
+			       const struct switchdev_obj_port_mdb *mdb)
+{
+	struct qca8k_priv *priv = port->sw->priv;
+	const u8 *addr = mdb->addr;
+	u16 vid = mdb->vid;
+
+	return qca8k_fdb_search_and_del(priv, BIT(port->index), addr, vid);
+}
+
+static int ipqess_port_mdb_add(struct ipqess_port *port,
+			       const struct switchdev_obj_port_mdb *mdb)
+{
+	struct qca8k_priv *priv = port->sw->priv;
+	const u8 *addr = mdb->addr;
+	u16 vid = mdb->vid;
+
+	return qca8k_fdb_search_and_insert(priv, BIT(port->index), addr, vid,
+					   QCA8K_ATU_STATUS_STATIC);
+}
+
+int ipqess_port_obj_add(struct net_device *netdev, const void *ctx,
+			const struct switchdev_obj *obj,
+			struct netlink_ext_ack *extack)
+{
+	struct ipqess_port *port = netdev_priv(netdev);
+	int err;
+
+	if (ctx && ctx != port)
+		return 0;
+
+	switch (obj->id) {
+	case SWITCHDEV_OBJ_ID_PORT_MDB:
+		if (!ipqess_port_offloads_bridge_port(port, obj->orig_dev))
+			return -EOPNOTSUPP;
+
+		err = ipqess_port_mdb_add(port, SWITCHDEV_OBJ_PORT_MDB(obj));
+		break;
+	case SWITCHDEV_OBJ_ID_HOST_MDB:
+		if (!ipqess_port_offloads_bridge_dev(port, obj->orig_dev))
+			return -EOPNOTSUPP;
+
+		err = ipqess_port_host_mdb_add(port, SWITCHDEV_OBJ_PORT_MDB(obj));
+		break;
+	case SWITCHDEV_OBJ_ID_PORT_VLAN:
+		if (ipqess_port_offloads_bridge_port(port, obj->orig_dev))
+			err = ipqess_port_vlan_add(netdev, obj, extack);
+		else
+			err = ipqess_port_host_vlan_add(netdev, obj, extack);
+		break;
+	case SWITCHDEV_OBJ_ID_MRP:
+	case SWITCHDEV_OBJ_ID_RING_ROLE_MRP:
+	default:
+		err = -EOPNOTSUPP;
+		break;
+	}
+
+	return err;
+}
+
+int ipqess_port_obj_del(struct net_device *netdev, const void *ctx,
+			const struct switchdev_obj *obj)
+{
+	struct ipqess_port *port = netdev_priv(netdev);
+	int err;
+
+	if (ctx && ctx != port)
+		return 0;
+
+	switch (obj->id) {
+	case SWITCHDEV_OBJ_ID_PORT_MDB:
+		if (!ipqess_port_offloads_bridge_port(port, obj->orig_dev))
+			return -EOPNOTSUPP;
+
+		err = ipqess_port_mdb_del(port, SWITCHDEV_OBJ_PORT_MDB(obj));
+		break;
+	case SWITCHDEV_OBJ_ID_HOST_MDB:
+		if (!ipqess_port_offloads_bridge_dev(port, obj->orig_dev))
+			return -EOPNOTSUPP;
+
+		err = ipqess_port_host_mdb_del(port, SWITCHDEV_OBJ_PORT_MDB(obj));
+		break;
+	case SWITCHDEV_OBJ_ID_PORT_VLAN:
+		if (ipqess_port_offloads_bridge_port(port, obj->orig_dev))
+			err = ipqess_port_vlan_del(netdev, obj);
+		else
+			err = ipqess_port_host_vlan_del(netdev, obj);
+		break;
+	case SWITCHDEV_OBJ_ID_MRP:
+	case SWITCHDEV_OBJ_ID_RING_ROLE_MRP:
+	default:
+		err = -EOPNOTSUPP;
+		break;
+	}
+
+	return err;
+}
+
+static int ipqess_cpu_port_fdb_del(struct ipqess_port *port,
+				   const unsigned char *addr, u16 vid)
+{
+	struct ipqess_mac_addr *mac_addr = NULL;
+	struct ipqess_mac_addr *other_mac_addr;
+	struct ipqess_switch *sw = port->sw;
+	int err = 0;
+
+	mutex_lock(&sw->addr_lists_lock);
+
+	list_for_each_entry(other_mac_addr, &sw->fdbs, list)
+		if (ether_addr_equal(other_mac_addr->addr, addr) && other_mac_addr->vid == vid)
+			mac_addr = other_mac_addr;
+
+	if (!mac_addr) {
+		err = -ENOENT;
+		goto out;
+	}
+
+	if (!refcount_dec_and_test(&mac_addr->refcount))
+		goto out;
+
+	err = qca8k_fdb_del(sw->priv, addr, BIT(IPQESS_SWITCH_CPU_PORT), vid);
+	if (err) {
+		refcount_set(&mac_addr->refcount, 1);
+		goto out;
+	}
+
+	list_del(&mac_addr->list);
+	kfree(mac_addr);
+
+out:
+	mutex_unlock(&sw->addr_lists_lock);
+
+	return err;
+}
+
+static int ipqess_cpu_port_fdb_add(struct ipqess_port *port,
+				   const unsigned char *addr, u16 vid)
+{
+	struct ipqess_switch *sw = port->sw;
+	struct ipqess_mac_addr *other_a = NULL;
+	struct ipqess_mac_addr *a = NULL;
+	int err = 0;
+
+	mutex_lock(&sw->addr_lists_lock);
+
+	list_for_each_entry(other_a, &sw->fdbs, list)
+		if (ether_addr_equal(other_a->addr, addr) && other_a->vid == vid)
+			a = other_a;
+
+	if (a) {
+		refcount_inc(&a->refcount);
+		goto out;
+	}
+
+	a = kzalloc(sizeof(*a), GFP_KERNEL);
+	if (!a) {
+		err = -ENOMEM;
+		goto out;
+	}
+
+	err = qca8k_port_fdb_insert(port->sw->priv, addr,
+				    BIT(IPQESS_SWITCH_CPU_PORT), vid);
+	if (err) {
+		kfree(a);
+		goto out;
+	}
+
+	ether_addr_copy(a->addr, addr);
+	a->vid = vid;
+	refcount_set(&a->refcount, 1);
+	list_add_tail(&a->list, &sw->fdbs);
+
+out:
+	mutex_unlock(&sw->addr_lists_lock);
+
+	return err;
+}
+
+static void
+ipqess_fdb_offload_notify(struct ipqess_switchdev_event_work *switchdev_work)
+{
+	struct switchdev_notifier_fdb_info info = {};
+
+	info.addr = switchdev_work->addr;
+	info.vid = switchdev_work->vid;
+	info.offloaded = true;
+	call_switchdev_notifiers(SWITCHDEV_FDB_OFFLOADED,
+				 switchdev_work->orig_netdev, &info.info, NULL);
+}
+
+void ipqess_port_switchdev_event_work(struct work_struct *work)
+{
+	struct ipqess_switchdev_event_work *switchdev_work =
+		container_of(work, struct ipqess_switchdev_event_work, work);
+	struct net_device *netdev = switchdev_work->netdev;
+	const unsigned char *addr = switchdev_work->addr;
+	struct ipqess_port *port = netdev_priv(netdev);
+	struct ipqess_switch *sw = port->sw;
+	struct qca8k_priv *priv = sw->priv;
+	u16 vid = switchdev_work->vid;
+	int err;
+
+	if (!vid)
+		vid = QCA8K_PORT_VID_DEF;
+
+	switch (switchdev_work->event) {
+	case SWITCHDEV_FDB_ADD_TO_DEVICE:
+		if (switchdev_work->host_addr)
+			err = ipqess_cpu_port_fdb_add(port, addr, vid);
+		else
+			err = qca8k_port_fdb_insert(priv, addr, BIT(port->index), vid);
+		if (err) {
+			dev_err(&port->netdev->dev,
+				"port %d failed to add %pM vid %d to fdb: %d\n",
+				port->index, addr, vid, err);
+			break;
+		}
+		ipqess_fdb_offload_notify(switchdev_work);
+		break;
+
+	case SWITCHDEV_FDB_DEL_TO_DEVICE:
+		if (switchdev_work->host_addr)
+			err = ipqess_cpu_port_fdb_del(port, addr, vid);
+		else
+			err = qca8k_fdb_del(priv, addr, BIT(port->index), vid);
+		if (err) {
+			dev_err(&port->netdev->dev,
+				"port %d failed to delete %pM vid %d from fdb: %d\n",
+				port->index, addr, vid, err);
+		}
+
+		break;
+	}
+
+	kfree(switchdev_work);
+}
+
+int ipqess_port_check_8021q_upper(struct net_device *netdev,
+				  struct netdev_notifier_changeupper_info *info)
+{
+	struct ipqess_port *port = netdev_priv(netdev);
+	struct net_device *br = ipqess_port_bridge_dev_get(port);
+	struct bridge_vlan_info br_info;
+	struct netlink_ext_ack *extack;
+	int err = NOTIFY_DONE;
+	u16 vid;
+
+	if (!br || !br_vlan_enabled(br))
+		return NOTIFY_DONE;
+
+	extack = netdev_notifier_info_to_extack(&info->info);
+	vid = vlan_dev_vlan_id(info->upper_dev);
+
+	/* br_vlan_get_info() returns -EINVAL or -ENOENT if the
+	 * device, respectively the VID is not found, returning
+	 * 0 means success, which is a failure for us here.
+	 */
+	err = br_vlan_get_info(br, vid, &br_info);
+	if (err == 0) {
+		NL_SET_ERR_MSG_MOD(extack,
+				   "This VLAN is already configured by the bridge");
+		return notifier_from_errno(-EBUSY);
+	}
+
+	return NOTIFY_DONE;
+}
+
 /* phylink ops */
 
 static void
@@ -669,6 +1511,7 @@ int ipqess_port_register(struct ipqess_switch *sw,
 	port->edma = NULL; /* Assigned during edma initialization */
 	port->qid = port->index - 1;
 	port->sw = sw;
+	port->bridge = NULL;
 
 	of_get_mac_address(port_node, port->mac);
 	if (!is_zero_ether_addr(port->mac))
@@ -756,3 +1599,58 @@ void ipqess_port_unregister(struct ipqess_port *port)
 	free_netdev(netdev);
 }
 
+/* Utilities */
+
+/* Returns true if any port of this switch offloads the given net_device */
+static bool ipqess_switch_offloads_bridge_port(struct ipqess_switch *sw,
+					       const struct net_device *netdev)
+{
+	struct ipqess_port *port;
+	int i;
+
+	for (i = 0; i < IPQESS_SWITCH_MAX_PORTS; i++) {
+		port = sw->port_list[i];
+		if (port && ipqess_port_offloads_bridge_port(port, netdev))
+			return true;
+	}
+
+	return false;
+}
+
+/* Returns true if any port of this switch offloads the given bridge */
+static inline bool
+ipqess_switch_offloads_bridge_dev(struct ipqess_switch *sw,
+				  const struct net_device *bridge_dev)
+{
+	struct ipqess_port *port;
+	int i;
+
+	for (i = 0; i < IPQESS_SWITCH_MAX_PORTS; i++) {
+		port = sw->port_list[i];
+		if (port && ipqess_port_offloads_bridge_dev(port, bridge_dev))
+			return true;
+	}
+
+	return false;
+}
+
+bool ipqess_port_recognize_netdev(const struct net_device *netdev)
+{
+	return netdev->netdev_ops == &ipqess_port_netdev_ops;
+}
+
+bool ipqess_port_dev_is_foreign(const struct net_device *netdev,
+				const struct net_device *foreign_netdev)
+{
+	struct ipqess_port *port = netdev_priv(netdev);
+	struct ipqess_switch *sw = port->sw;
+
+	if (netif_is_bridge_master(foreign_netdev))
+		return !ipqess_switch_offloads_bridge_dev(sw, foreign_netdev);
+
+	if (netif_is_bridge_port(foreign_netdev))
+		return !ipqess_switch_offloads_bridge_port(sw, foreign_netdev);
+
+	/* Everything else is foreign */
+	return true;
+}
diff --git a/drivers/net/ethernet/qualcomm/ipqess/ipqess_port.h b/drivers/net/ethernet/qualcomm/ipqess/ipqess_port.h
index 19d4b5d73625..00f0dff9c39d 100644
--- a/drivers/net/ethernet/qualcomm/ipqess/ipqess_port.h
+++ b/drivers/net/ethernet/qualcomm/ipqess/ipqess_port.h
@@ -9,6 +9,11 @@
 #include "ipqess_edma.h"
 #include "ipqess_switch.h"
 
+struct ipqess_bridge {
+	struct net_device *netdev;
+	refcount_t refcount;
+};
+
 struct ipqess_port {
 	u16 index;
 	u16 qid;
@@ -20,6 +25,7 @@ struct ipqess_port {
 	struct device_node *dn;
 	struct mii_bus *mii_bus;
 	struct net_device *netdev;
+	struct ipqess_bridge *bridge;
 	struct devlink_port devlink_port;
 
 	u8       stp_state;
@@ -62,4 +68,31 @@ void ipqess_port_unregister(struct ipqess_port *port);
 /* Defined in ipqess_ethtool.c */
 void ipqess_port_set_ethtool_ops(struct net_device *netdev);
 
+bool ipqess_port_recognize_netdev(const struct net_device *netdev);
+bool ipqess_port_dev_is_foreign(const struct net_device *netdev,
+				const struct net_device *foreign_netdev);
+
+int ipqess_port_bridge_join(struct ipqess_port *port, struct net_device *br,
+			    struct netlink_ext_ack *extack);
+void ipqess_port_bridge_leave(struct ipqess_port *port, struct net_device *br);
+
+int ipqess_port_attr_set(struct net_device *dev, const void *ctx,
+			 const struct switchdev_attr *attr,
+			 struct netlink_ext_ack *extack);
+
+void ipqess_port_switchdev_event_work(struct work_struct *work);
+
+int ipqess_port_check_8021q_upper(struct net_device *netdev,
+				  struct netdev_notifier_changeupper_info *info);
+
+struct net_device *ipqess_port_get_bridged_netdev(const struct ipqess_port *port);
+
+int ipqess_port_obj_add(struct net_device *netdev, const void *ctx,
+			const struct switchdev_obj *obj,
+			struct netlink_ext_ack *extack);
+int ipqess_port_obj_del(struct net_device *netdev, const void *ctx,
+			const struct switchdev_obj *obj);
+
+bool ipqess_port_offloads_bridge_port(struct ipqess_port *port,
+				      const struct net_device *netdev);
 #endif
diff --git a/drivers/net/ethernet/qualcomm/ipqess/ipqess_switch.c b/drivers/net/ethernet/qualcomm/ipqess/ipqess_switch.c
index 927f834a62bc..d09d0aa8314f 100644
--- a/drivers/net/ethernet/qualcomm/ipqess/ipqess_switch.c
+++ b/drivers/net/ethernet/qualcomm/ipqess/ipqess_switch.c
@@ -80,21 +80,8 @@ unsigned int ipqess_switch_fastest_ageing_time(struct ipqess_switch *sw,
 int ipqess_set_ageing_time(struct ipqess_switch *sw, unsigned int msecs)
 {
 	struct qca8k_priv *priv = sw->priv;
-	unsigned int secs = msecs / 1000;
-	u32 val;
 
-	/* AGE_TIME reg is set in 7s step */
-	val = secs / 7;
-
-	/* Handle case with 0 as val to NOT disable
-	 * learning
-	 */
-	if (!val)
-		val = 1;
-
-	return regmap_update_bits(priv->regmap, QCA8K_REG_ATU_CTRL,
-				  QCA8K_ATU_AGE_TIME_MASK,
-				  QCA8K_ATU_AGE_TIME(val));
+	return qca8k_set_ageing_time(priv, msecs);
 }
 
 static struct qca8k_pcs *pcs_to_qca8k_pcs(struct phylink_pcs *pcs)
diff --git a/include/linux/dsa/qca8k.h b/include/linux/dsa/qca8k.h
index cafb727f4e8b..9ad016f7201e 100644
--- a/include/linux/dsa/qca8k.h
+++ b/include/linux/dsa/qca8k.h
@@ -553,7 +553,8 @@ int qca8k_set_mac_eee(struct dsa_switch *ds, int port, struct ethtool_eee *eee);
 int qca8k_get_mac_eee(struct dsa_switch *ds, int port, struct ethtool_eee *e);
 
 /* Common bridge function */
-void qca8k_port_stp_state_set(struct dsa_switch *ds, int port, u8 state);
+void qca8k_port_stp_state_set(struct qca8k_priv *priv, int port, u8 state,
+			      bool port_learning, int set_learning);
 int qca8k_port_pre_bridge_flags(struct dsa_switch *ds, int port,
 				struct switchdev_brport_flags flags,
 				struct netlink_ext_ack *extack);
@@ -577,8 +578,8 @@ int qca8k_port_change_mtu(struct dsa_switch *ds, int port, int new_mtu);
 int qca8k_port_max_mtu(struct dsa_switch *ds, int port);
 
 /* Common fast age function */
-void qca8k_port_fast_age(struct dsa_switch *ds, int port);
-int qca8k_set_ageing_time(struct dsa_switch *ds, unsigned int msecs);
+void qca8k_port_fast_age(struct qca8k_priv *priv, int port);
+int qca8k_set_ageing_time(struct qca8k_priv *priv, unsigned int msecs);
 
 /* Common FDB function */
 int qca8k_port_fdb_insert(struct qca8k_priv *priv, const u8 *addr,
@@ -589,7 +590,7 @@ int qca8k_port_fdb_add(struct dsa_switch *ds, int port,
 int qca8k_port_fdb_del(struct dsa_switch *ds, int port,
 		       const unsigned char *addr, u16 vid,
 		       struct dsa_db db);
-int qca8k_port_fdb_dump(struct dsa_switch *ds, int port,
+int qca8k_port_fdb_dump(struct qca8k_priv *priv, int port,
 			dsa_fdb_dump_cb_t *cb, void *data);
 int qca8k_fdb_del(struct qca8k_priv *priv, const u8 *mac,
 		  u16 port_mask, u16 vid);
@@ -618,13 +619,12 @@ void qca8k_port_mirror_del(struct dsa_switch *ds, int port,
 			   struct dsa_mall_mirror_tc_entry *mirror);
 
 /* Common port VLAN function */
-int qca8k_port_vlan_filtering(struct dsa_switch *ds, int port,
-			      bool vlan_filtering,
-			      struct netlink_ext_ack *extack);
+int qca8k_port_vlan_filtering(struct qca8k_priv *priv, int port,
+			      bool vlan_filtering);
 int qca8k_vlan_add(struct qca8k_priv *priv, u8 port, u16 vid,
 		   bool untagged);
 int qca8k_vlan_del(struct qca8k_priv *priv, u8 port, u16 vid);
-int qca8k_port_vlan_add(struct dsa_switch *ds, int port,
+int qca8k_port_vlan_add(struct qca8k_priv *priv, int port,
 			const struct switchdev_obj_port_vlan *vlan,
 			struct netlink_ext_ack *extack);
 int qca8k_port_vlan_del(struct dsa_switch *ds, int port,
-- 
2.42.0





[Index of Archives]     [Device Tree Compilter]     [Device Tree Spec]     [Linux Driver Backports]     [Video for Linux]     [Linux USB Devel]     [Linux PCI Devel]     [Linux Audio Users]     [Linux Kernel]     [Linux SCSI]     [XFree86]     [Yosemite Backpacking]


  Powered by Linux