[PATCH 18/20] net: dsa: xilinx: offload the bridge FDB to the switch CAM
Nagadheeraj Rottela
nagadheeraj.rottela at amd.com
Fri Aug 7 03:44:29 PDT 2026
The switch resolves forwarding through a stream-destination lookup
CAM that maps a (destination MAC, VLAN ID) key to an egress port
list. Back the bridge FDB with it.
The CAM is an indirect register block. Load the key and port-list
registers, write the opcode (add, delete, read, or read-key) to the
control register, and wait for the operation to finish. Add a mutex
to serialise these sequences.
The fabric also learns source MACs into the same CAM. Enable the
learning engine in port_bridge_join, and add port_bridge_leave to
disable it once the last user port leaves, so entries accumulate
only while a bridge spans the ports.
port_fdb_add and port_fdb_del read the entry, set or clear the
calling port's bit in the port list, and write it back. Delete the
entry once no port references it. A (MAC, VID) reachable on several
ports then carries a correct multi-port list. The DSA "no VLAN" key
(vid 0) maps to the default native VID that an untagged frame uses.
port_fdb_dump reports a port's learnt entries. The hardware scatters
them across the port's read-key region. Scan all of it and report
every slot that is present. The CPU port has no such region.
port_fast_age flushes a port's learnt addresses from the CAM.
Signed-off-by: Nagadheeraj Rottela <nagadheeraj.rottela at amd.com>
---
drivers/net/dsa/xilinx/xilinx_tsn.c | 301 +++++++++++++++++++++++++++-
drivers/net/dsa/xilinx/xilinx_tsn.h | 93 ++++++++-
2 files changed, 391 insertions(+), 3 deletions(-)
diff --git a/drivers/net/dsa/xilinx/xilinx_tsn.c b/drivers/net/dsa/xilinx/xilinx_tsn.c
index 9a4d4a2fb892..57558b0e2613 100644
--- a/drivers/net/dsa/xilinx/xilinx_tsn.c
+++ b/drivers/net/dsa/xilinx/xilinx_tsn.c
@@ -174,6 +174,31 @@ static int xlnx_tsn_set_port_state(struct xlnx_tsn *sw, int port,
return 0;
}
+static enum tsn_port_state xlnx_tsn_get_port_state(struct xlnx_tsn *sw,
+ int port)
+{
+ u32 mask, chg_bit, reg;
+
+ if (xlnx_tsn_port_state_bits(port, &mask, &chg_bit))
+ return TSN_PORT_STATE_DISABLED;
+
+ reg = sw_ior(sw, TSN_PORT_STATE_CTRL_OFFSET);
+ return (reg & mask) >> __ffs(mask);
+}
+
+static int xlnx_tsn_port_state_cycle(struct xlnx_tsn *sw, int port,
+ enum tsn_port_state state)
+{
+ enum tsn_port_state saved = xlnx_tsn_get_port_state(sw, port);
+ int err;
+
+ err = xlnx_tsn_set_port_state(sw, port, state);
+ if (err)
+ return err;
+
+ return xlnx_tsn_set_port_state(sw, port, saved);
+}
+
static int xlnx_tsn_mdio_wait_ready(struct xlnx_tsn_mac *m)
{
u32 val;
@@ -495,11 +520,32 @@ static int xlnx_tsn_port_set_mac_address(struct dsa_switch *ds, int port,
return 0;
}
+/* Enable or disable the hardware address learning engine globally.
+ * Standalone ports do not learn. Enable when a port joins a bridge,
+ * disable when the last one leaves. The sub-qualifier bits below are
+ * inert while learning is disabled. Caller holds indirect_lock.
+ */
+static void xlnx_tsn_set_global_learning(struct xlnx_tsn *sw, bool on)
+{
+ u32 reg = sw_ior(sw, TSN_SW_ADDR_LEARN_OFFSET);
+
+ if (on)
+ reg &= ~TSN_SW_ADDR_LEARN_DISABLE;
+ else
+ reg |= TSN_SW_ADDR_LEARN_DISABLE;
+ sw_iow(sw, TSN_SW_ADDR_LEARN_OFFSET, reg);
+}
+
static int xlnx_tsn_port_bridge_join(struct dsa_switch *ds, int port,
struct dsa_bridge bridge,
bool *tx_fwd_offload,
struct netlink_ext_ack *extack)
{
+ struct xlnx_tsn *sw = ds->priv;
+
+ scoped_guard(mutex, &sw->indirect_lock)
+ xlnx_tsn_set_global_learning(sw, true);
+
/* The switch fabric replicates flooded frames per egress port
* on its own, so the bridge does not need to clone-and-send.
*/
@@ -508,6 +554,24 @@ static int xlnx_tsn_port_bridge_join(struct dsa_switch *ds, int port,
return 0;
}
+static void xlnx_tsn_port_bridge_leave(struct dsa_switch *ds, int port,
+ struct dsa_bridge bridge)
+{
+ struct xlnx_tsn *sw = ds->priv;
+ struct dsa_port *dp;
+
+ dsa_switch_for_each_user_port(dp, ds) {
+ if (dp->index == port)
+ continue;
+
+ if (dp->bridge)
+ return;
+ }
+
+ scoped_guard(mutex, &sw->indirect_lock)
+ xlnx_tsn_set_global_learning(sw, false);
+}
+
static void xlnx_tsn_port_stp_state_set(struct dsa_switch *ds, int port,
u8 state)
{
@@ -539,6 +603,217 @@ static void xlnx_tsn_port_stp_state_set(struct dsa_switch *ds, int port,
xlnx_tsn_set_port_state(sw, port, hw_state);
}
+/* The CAM status enable bit reads 1 when the block is ready to accept the
+ * next operation.
+ */
+static int xlnx_tsn_cam_wait_ready(struct xlnx_tsn *sw)
+{
+ u32 reg;
+
+ return readl_poll_timeout(sw->sw_base + TSN_CAM_STATUS_OFFSET, reg,
+ reg & TSN_CAM_STATUS_READY, TSN_SW_POLL_DELAY_US,
+ TSN_SW_POLL_TIMEOUT_US);
+}
+
+/* The CAM control enable bit self-clears when the operation completes. */
+static int xlnx_tsn_cam_wait_done(struct xlnx_tsn *sw)
+{
+ u32 reg;
+
+ return readl_poll_timeout(sw->sw_base + TSN_CAM_CTRL_OFFSET, reg,
+ !(reg & TSN_CAM_OP_ENABLE),
+ TSN_SW_POLL_DELAY_US, TSN_SW_POLL_TIMEOUT_US);
+}
+
+static void xlnx_tsn_cam_load_key(struct xlnx_tsn *sw,
+ const unsigned char *addr, u16 vid)
+{
+ sw_iow(sw, TSN_CAM_KEY1_OFFSET,
+ ((u32)addr[0] << 24) | ((u32)addr[1] << 16) |
+ ((u32)addr[2] << 8) | addr[3]);
+ sw_iow(sw, TSN_CAM_KEY2_OFFSET,
+ ((u32)addr[4] << 8) | addr[5] |
+ FIELD_PREP(TSN_CAM_VLAN, vid));
+}
+
+/* Look up a (MAC, VID) key and return its current egress port list, or 0
+ * if the entry is absent. Caller holds indirect_lock.
+ */
+static int xlnx_tsn_cam_read_portlist(struct xlnx_tsn *sw,
+ const unsigned char *addr, u16 vid,
+ u8 *portlist)
+{
+ u32 ctrl;
+ int ret;
+
+ ret = xlnx_tsn_cam_wait_ready(sw);
+ if (ret)
+ return ret;
+
+ xlnx_tsn_cam_load_key(sw, addr, vid);
+ sw_iow(sw, TSN_CAM_CTRL_OFFSET,
+ FIELD_PREP(TSN_CAM_OP_MASK, TSN_CAM_OP_READ) | TSN_CAM_OP_ENABLE);
+
+ ret = xlnx_tsn_cam_wait_done(sw);
+ if (ret)
+ return ret;
+
+ ctrl = sw_ior(sw, TSN_CAM_CTRL_OFFSET);
+ if (!(ctrl & TSN_CAM_FOUND)) {
+ *portlist = 0;
+ return 0;
+ }
+
+ *portlist = FIELD_GET(TSN_CAM_PORT_LIST,
+ sw_ior(sw, TSN_CAM_PORT_ACT_OFFSET));
+ return 0;
+}
+
+/* Add (add=true) or delete (add=false) the (MAC, VID) entry carrying the
+ * given port list. Caller holds indirect_lock.
+ */
+static int xlnx_tsn_cam_write(struct xlnx_tsn *sw, const unsigned char *addr,
+ u16 vid, u8 portlist, bool add)
+{
+ int ret;
+
+ ret = xlnx_tsn_cam_wait_ready(sw);
+ if (ret)
+ return ret;
+
+ xlnx_tsn_cam_load_key(sw, addr, vid);
+ sw_iow(sw, TSN_CAM_TV1_OFFSET, 0);
+ sw_iow(sw, TSN_CAM_TV2_OFFSET, 0);
+ sw_iow(sw, TSN_CAM_PORT_ACT_OFFSET,
+ FIELD_PREP(TSN_CAM_PORT_LIST, portlist));
+ sw_iow(sw, TSN_CAM_CTRL_OFFSET,
+ FIELD_PREP(TSN_CAM_OP_MASK, add ? TSN_CAM_OP_ADD : TSN_CAM_OP_DELETE) |
+ TSN_CAM_OP_ENABLE);
+
+ return xlnx_tsn_cam_wait_done(sw);
+}
+
+static void xlnx_tsn_port_fast_age(struct dsa_switch *ds, int port)
+{
+ struct xlnx_tsn *sw = ds->priv;
+ int err;
+
+ err = xlnx_tsn_port_state_cycle(sw, port, TSN_PORT_STATE_FLUSH);
+ if (err)
+ dev_err(sw->dev, "port %d: fast age failed (%d)\n", port, err);
+}
+
+static int xlnx_tsn_port_fdb_add(struct dsa_switch *ds, int port,
+ const unsigned char *addr, u16 vid,
+ struct dsa_db db)
+{
+ struct xlnx_tsn *sw = ds->priv;
+ u8 portlist;
+ int ret;
+
+ if (!vid)
+ vid = TSN_SW_DEFAULT_VID;
+
+ guard(mutex)(&sw->indirect_lock);
+ ret = xlnx_tsn_cam_read_portlist(sw, addr, vid, &portlist);
+ if (!ret) {
+ portlist |= TSN_PORT_BIT(port);
+ ret = xlnx_tsn_cam_write(sw, addr, vid, portlist, true);
+ }
+
+ return ret;
+}
+
+static int xlnx_tsn_port_fdb_del(struct dsa_switch *ds, int port,
+ const unsigned char *addr, u16 vid,
+ struct dsa_db db)
+{
+ struct xlnx_tsn *sw = ds->priv;
+ u8 portlist;
+ int ret;
+
+ if (!vid)
+ vid = TSN_SW_DEFAULT_VID;
+
+ guard(mutex)(&sw->indirect_lock);
+ ret = xlnx_tsn_cam_read_portlist(sw, addr, vid, &portlist);
+ if (!ret) {
+ if (!portlist)
+ return 0;
+
+ /* Drop the entry once no port references it. Otherwise
+ * rewrite it with the updated port list.
+ */
+ portlist &= ~TSN_PORT_BIT(port);
+ ret = xlnx_tsn_cam_write(sw, addr, vid, portlist, portlist != 0);
+ }
+
+ return ret;
+}
+
+static int xlnx_tsn_port_fdb_dump(struct dsa_switch *ds, int port,
+ dsa_fdb_dump_cb_t *cb, void *data)
+{
+ struct xlnx_tsn *sw = ds->priv;
+ unsigned char addr[ETH_ALEN];
+ u32 base, ctrl, key1, key2;
+ int ret = 0;
+ u16 vid;
+ u32 i;
+
+ /* Learnt entries live in a per-MAC-port read-key region. The CPU
+ * port has no such region.
+ */
+ if (port == XLNX_TSN_CPU_PORT)
+ return 0;
+
+ guard(mutex)(&sw->indirect_lock);
+
+ if (port == XLNX_TSN_PORT_MAC2)
+ base = TSN_CAM_MAC2_READ_KEY_BASE;
+ else
+ base = 0;
+
+ /* Learnt entries occupy non-consecutive slots, so scan the whole
+ * region and report each slot marked found.
+ */
+ for (i = 0; i < TSN_CAM_READ_KEY_COUNT; i++) {
+ ret = xlnx_tsn_cam_wait_ready(sw);
+ if (ret)
+ return ret;
+
+ sw_iow(sw, TSN_CAM_CTRL_OFFSET,
+ FIELD_PREP(TSN_CAM_READ_KEY_ADDR, base + i) |
+ FIELD_PREP(TSN_CAM_OP_MASK, TSN_CAM_OP_READ_KEY) |
+ TSN_CAM_OP_ENABLE);
+
+ ret = xlnx_tsn_cam_wait_done(sw);
+ if (ret)
+ return ret;
+
+ ctrl = sw_ior(sw, TSN_CAM_CTRL_OFFSET);
+ if (!(ctrl & TSN_CAM_FOUND))
+ continue;
+
+ key1 = sw_ior(sw, TSN_CAM_KEY1_OFFSET);
+ key2 = sw_ior(sw, TSN_CAM_KEY2_OFFSET);
+ addr[0] = key1 >> 24;
+ addr[1] = key1 >> 16;
+ addr[2] = key1 >> 8;
+ addr[3] = key1;
+ addr[4] = key2 >> 8;
+ addr[5] = key2;
+ vid = FIELD_GET(TSN_CAM_VLAN, key2);
+ if (vid == TSN_SW_DEFAULT_VID)
+ vid = 0;
+
+ ret = cb(addr, vid, false, data);
+ if (ret)
+ return ret;
+ }
+ return ret;
+}
+
static void xlnx_tsn_phylink_get_caps(struct dsa_switch *ds, int port,
struct phylink_config *config)
{
@@ -643,7 +918,7 @@ static int xlnx_tsn_setup(struct dsa_switch *ds)
struct dsa_port *cpu_dp = dsa_to_port(ds, XLNX_TSN_CPU_PORT);
struct xlnx_tsn *sw = ds->priv;
struct dsa_port *dp;
- u32 mgmt;
+ u32 mgmt, reg;
int ret;
if (!dsa_is_user_port(ds, XLNX_TSN_PORT_MAC1) ||
@@ -656,6 +931,22 @@ static int xlnx_tsn_setup(struct dsa_switch *ds)
sw->conduit = cpu_dp->conduit;
+ /* Pre-arm the learning sub-qualifiers for when a port joins a bridge:
+ * learn untagged frames under their ingress native VID, and allow
+ * learning on VIDs with no membership entry while the bridge is
+ * VLAN-unaware. Both bits are inert while global learning is disabled.
+ */
+ reg = sw_ior(sw, TSN_SW_ADDR_LEARN_OFFSET);
+ reg |= TSN_SW_ADDR_LEARN_DISABLE | TSN_SW_ADDR_LEARN_UNTAGGED_EN |
+ TSN_SW_ADDR_LEARN_NO_VLAN_EN;
+ sw_iow(sw, TSN_SW_ADDR_LEARN_OFFSET, reg);
+
+ /* On a CAM miss flood unknown tagged unicast frames to all ports. */
+ reg = sw_ior(sw, TSN_SW_CTRL_OFFSET);
+ reg &= ~TSN_SW_CTRL_UCAST_MISS_MASK;
+ reg |= FIELD_PREP(TSN_SW_CTRL_UCAST_MISS_MASK, TSN_SW_CTRL_UCAST_MISS_FLOOD);
+ sw_iow(sw, TSN_SW_CTRL_OFFSET, reg);
+
/* Route CPU-originated bridge-group control frames (STP, LLDP) to
* the single wire port whose MAC-nibble field matches the frame's
* source-MAC low nibble, instead of flooding to both.
@@ -743,7 +1034,12 @@ static const struct dsa_switch_ops xlnx_tsn_switch_ops = {
.teardown = xlnx_tsn_teardown,
.port_set_mac_address = xlnx_tsn_port_set_mac_address,
.port_bridge_join = xlnx_tsn_port_bridge_join,
+ .port_bridge_leave = xlnx_tsn_port_bridge_leave,
.port_stp_state_set = xlnx_tsn_port_stp_state_set,
+ .port_fdb_add = xlnx_tsn_port_fdb_add,
+ .port_fdb_del = xlnx_tsn_port_fdb_del,
+ .port_fdb_dump = xlnx_tsn_port_fdb_dump,
+ .port_fast_age = xlnx_tsn_port_fast_age,
.port_hwtstamp_get = xlnx_tsn_port_hwtstamp_get,
.port_hwtstamp_set = xlnx_tsn_port_hwtstamp_set,
.get_ts_info = xlnx_tsn_get_ts_info,
@@ -778,6 +1074,9 @@ static int xlnx_tsn_probe(struct platform_device *pdev)
sw->dev = dev;
sw->mac[XLNX_TSN_PORT_MAC1].sw = sw;
sw->mac[XLNX_TSN_PORT_MAC2].sw = sw;
+ ret = devm_mutex_init(dev, &sw->indirect_lock);
+ if (ret)
+ return ret;
ret = xlnx_tsn_map_reg(pdev, "switch", &sw->sw_base);
if (ret)
diff --git a/drivers/net/dsa/xilinx/xilinx_tsn.h b/drivers/net/dsa/xilinx/xilinx_tsn.h
index d46150535775..a94f5124a33d 100644
--- a/drivers/net/dsa/xilinx/xilinx_tsn.h
+++ b/drivers/net/dsa/xilinx/xilinx_tsn.h
@@ -63,18 +63,104 @@
#define TSN_SW_MGMT_QUEUING_OFFSET 0x00054
#define TSN_SW_MGMT_QUEUING_EP_SA_EGRESS BIT(4)
-/* readl_poll_timeout() parameters (in microseconds): poll until
- * the port-state change-commit bit self-clears.
+/* Shared readl_poll_timeout() parameters (in microseconds) used at
+ * two call sites: the port-state change-commit bit and the CAM
+ * operation enable bit. Both self-clear when the operation completes.
*/
#define TSN_SW_POLL_DELAY_US 10
#define TSN_SW_POLL_TIMEOUT_US 5000
+/* Stream-destination-lookup CAM: maps a (destination MAC, VLAN ID) key
+ * to an egress port list, backing the bridge FDB. Access is indirect:
+ * load the key, value, and port-list registers, write the opcode, then
+ * set the Enable Operation bit in the control register. Poll CAM Init
+ * Done in the status register to 1 before each operation to confirm the
+ * block is ready. Enable Operation self-clears when the operation
+ * finishes.
+ */
+#define TSN_CAM_CTRL_OFFSET 0x01000
+#define TSN_CAM_STATUS_OFFSET 0x01004
+#define TSN_CAM_KEY1_OFFSET 0x01008
+#define TSN_CAM_KEY2_OFFSET 0x0100c
+#define TSN_CAM_TV1_OFFSET 0x01010
+#define TSN_CAM_TV2_OFFSET 0x01014
+#define TSN_CAM_PORT_ACT_OFFSET 0x01018
+
+/* Control register: set to start an operation, self-clears when done. */
+#define TSN_CAM_OP_ENABLE BIT(0)
+/* Status register: reads 1 when the CAM is ready for the next operation. */
+#define TSN_CAM_STATUS_READY BIT(0)
+#define TSN_CAM_OP_MASK GENMASK(2, 1)
+#define TSN_CAM_OP_READ_KEY 0x0
+#define TSN_CAM_OP_ADD 0x1
+#define TSN_CAM_OP_DELETE 0x2
+#define TSN_CAM_OP_READ 0x3
+#define TSN_CAM_FOUND BIT(7)
+#define TSN_CAM_VLAN GENMASK(27, 16)
+#define TSN_CAM_PORT_LIST GENMASK(10, 8)
+#define TSN_CAM_READ_KEY_ADDR GENMASK(19, 8)
+#define TSN_CAM_MAC2_READ_KEY_BASE 0x800
+#define TSN_CAM_READ_KEY_COUNT 2048
+
+/* HW reset-default native VID; DSA's "no VLAN" (vid 0) maps onto it. */
+#define TSN_SW_DEFAULT_VID 1
+
+/* Switch Control Register: switch-wide control fields. */
+#define TSN_SW_CTRL_OFFSET 0x00004
+/* Per-port native-VLAN untag enables: strip the tag on egress when the
+ * frame VID equals the egress port's native VID. Only the wire ports are
+ * untagged; the endpoint port is left tagged so the host keeps the VID for
+ * software classification.
+ */
+#define TSN_SW_CTRL_MAC1_NATIVE_UNTAG_EN BIT(18)
+#define TSN_SW_CTRL_MAC2_NATIVE_UNTAG_EN BIT(19)
+/* Action for an ingress frame whose port is not in its VLAN's member
+ * list: forward to the processor or discard. Pinned to discard. This only
+ * matters once a VID's Port-List-Valid bit is set, which happens only
+ * while VLAN filtering is on.
+ */
+#define TSN_SW_CTRL_MEMBER_VIOL_MASK GENMASK(9, 8)
+#define TSN_SW_CTRL_MEMBER_VIOL_DISCARD 0x1
+
+/* Forwarding action for a tagged unicast or multicast frame that misses
+ * both the CAM and the VLAN membership list: flood, to processor, to MAC,
+ * or discard.
+ */
+#define TSN_SW_CTRL_UCAST_MISS_MASK GENMASK(1, 0)
+#define TSN_SW_CTRL_MCAST_MISS_MASK GENMASK(3, 2)
+#define TSN_SW_CTRL_UCAST_MISS_FLOOD 0x1
+#define TSN_SW_CTRL_MCAST_MISS_FLOOD 0x1
+#define TSN_SW_CTRL_MISS_DISCARD 0x3
+
+/* Hardware Address Learning Control: switch-wide learning behaviour. */
+#define TSN_SW_ADDR_LEARN_OFFSET 0x00048
+/* Disable the global learning engine; set to 1 while every user port is
+ * standalone, cleared to 0 when at least one port joins a bridge. The
+ * sub-qualifier bits below are inert while this bit is set.
+ */
+#define TSN_SW_ADDR_LEARN_DISABLE BIT(0)
+/* Learn the source MAC of untagged and priority-tagged frames against the
+ * ingress port's native VID; the reset default excludes them from learning.
+ */
+#define TSN_SW_ADDR_LEARN_UNTAGGED_EN BIT(1)
+/* Permit learning on a VID that has no member entry programmed in the VLAN
+ * membership memory; the reset default only learns already-configured VIDs.
+ */
+#define TSN_SW_ADDR_LEARN_NO_VLAN_EN BIT(3)
+
+/* Map a DSA port index to its bit in a switch port list. */
+#define TSN_PORT_BIT(port) BIT(port)
+
enum tsn_port_state {
TSN_PORT_STATE_DISABLED = 0,
TSN_PORT_STATE_BLOCKING,
TSN_PORT_STATE_LISTENING,
TSN_PORT_STATE_LEARNING,
TSN_PORT_STATE_FORWARDING,
+ /* Not an STP state. Writing it flushes the port's dynamic learnt
+ * entries and leaves static FDB entries in place.
+ */
+ TSN_PORT_STATE_FLUSH,
};
/* Per-MAC MDIO controller register window, sitting at +0x500 inside
@@ -218,6 +304,8 @@ struct xlnx_tsn_mac {
* @nb: netdev notifier that handles NETDEV_REGISTER on each swpN
* to set its final MAC, and NETDEV_CHANGEADDR on the conduit
* to refresh the shared prefix
+ * @indirect_lock: serialises the CAM and VLAN-membership indirect
+ * register sequences
* @mac: per-MAC state, indexed by user-port number (index 0 unused;
* MAC1 at [1], MAC2 at [2])
* @ptp_timer_irq: 1 PPS / RTC-overflow interrupt
@@ -239,6 +327,7 @@ struct xlnx_tsn {
struct net_device *conduit;
u8 mac_prefix[ETH_ALEN];
struct notifier_block nb;
+ struct mutex indirect_lock; /* serialises CAM indirect access */
struct xlnx_tsn_mac mac[XLNX_TSN_NUM_PORTS];
int ptp_timer_irq;
struct ptp_clock *ptp_clock;
--
2.34.1
More information about the linux-arm-kernel
mailing list