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@bootlin.com>
---
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@bootlin.com>
+ * 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
© 2016 - 2025 Red Hat, Inc.