[PATCH 18/20] net: dsa: xilinx: offload the bridge FDB to the switch CAM
Nagadheeraj Rottela <[email protected]>
| Newsgroups | org.kernel.vger.linux-devicetree,org.infradead.lists.linux-arm-kernel,org.kernel.vger.linux-kernel,org.kernel.vger.netdev |
|---|---|
| Message-ID | <[email protected]> |
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 <[email protected]> --- 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