// SPDX-License-Identifier: GPL-2.0+ /* Microchip Sparx5 Switch driver * * Copyright (c) 2021 Microchip Technology Inc. and its subsidiaries. */ #include #include #include #include "sparx5_main_regs.h" #include "sparx5_main.h" /* Commands for Mac Table Command register */ #define MAC_CMD_LEARN 0 /* Insert (Learn) 1 entry */ #define MAC_CMD_UNLEARN 1 /* Unlearn (Forget) 1 entry */ #define MAC_CMD_LOOKUP 2 /* Look up 1 entry */ #define MAC_CMD_READ 3 /* Read entry at Mac Table Index */ #define MAC_CMD_WRITE 4 /* Write entry at Mac Table Index */ #define MAC_CMD_SCAN 5 /* Scan (Age or find next) */ #define MAC_CMD_FIND_SMALLEST 6 /* Get next entry */ #define MAC_CMD_CLEAR_ALL 7 /* Delete all entries in table */ /* Commands for MAC_ENTRY_ADDR_TYPE */ #define MAC_ENTRY_ADDR_TYPE_UPSID_PN 0 #define MAC_ENTRY_ADDR_TYPE_UPSID_CPU_OR_INT 1 #define MAC_ENTRY_ADDR_TYPE_GLAG 2 #define MAC_ENTRY_ADDR_TYPE_MC_IDX 3 #define TABLE_UPDATE_SLEEP_US 10 #define TABLE_UPDATE_TIMEOUT_US 100000 struct sparx5_mact_entry { struct list_head list; unsigned char mac[ETH_ALEN]; u32 flags; #define MAC_ENT_ALIVE BIT(0) #define MAC_ENT_MOVED BIT(1) #define MAC_ENT_LOCK BIT(2) u16 vid; u16 port; }; static int sparx5_mact_get_status(struct sparx5 *sparx5) { return spx5_rd(sparx5, LRN_COMMON_ACCESS_CTRL); } static int sparx5_mact_wait_for_completion(struct sparx5 *sparx5) { u32 val; return readx_poll_timeout(sparx5_mact_get_status, sparx5, val, LRN_COMMON_ACCESS_CTRL_MAC_TABLE_ACCESS_SHOT_GET(val) == 0, TABLE_UPDATE_SLEEP_US, TABLE_UPDATE_TIMEOUT_US); } static void sparx5_mact_select(struct sparx5 *sparx5, const unsigned char mac[ETH_ALEN], u16 vid) { u32 macl = 0, mach = 0; /* Set the MAC address to handle and the vlan associated in a format * understood by the hardware. */ mach |= vid << 16; mach |= mac[0] << 8; mach |= mac[1] << 0; macl |= mac[2] << 24; macl |= mac[3] << 16; macl |= mac[4] << 8; macl |= mac[5] << 0; spx5_wr(mach, sparx5, LRN_MAC_ACCESS_CFG_0); spx5_wr(macl, sparx5, LRN_MAC_ACCESS_CFG_1); } int sparx5_mact_learn(struct sparx5 *sparx5, int pgid, const unsigned char mac[ETH_ALEN], u16 vid) { int addr, type, ret; if (pgid < SPX5_PORTS) { type = MAC_ENTRY_ADDR_TYPE_UPSID_PN; addr = pgid % 32; addr += (pgid / 32) << 5; /* Add upsid */ } else { type = MAC_ENTRY_ADDR_TYPE_MC_IDX; addr = pgid - SPX5_PORTS; } mutex_lock(&sparx5->lock); sparx5_mact_select(sparx5, mac, vid); /* MAC entry properties */ spx5_wr(LRN_MAC_ACCESS_CFG_2_MAC_ENTRY_ADDR_SET(addr) | LRN_MAC_ACCESS_CFG_2_MAC_ENTRY_ADDR_TYPE_SET(type) | LRN_MAC_ACCESS_CFG_2_MAC_ENTRY_VLD_SET(1) | LRN_MAC_ACCESS_CFG_2_MAC_ENTRY_LOCKED_SET(1), sparx5, LRN_MAC_ACCESS_CFG_2); spx5_wr(0, sparx5, LRN_MAC_ACCESS_CFG_3); /* Insert/learn new entry */ spx5_wr(LRN_COMMON_ACCESS_CTRL_CPU_ACCESS_CMD_SET(MAC_CMD_LEARN) | LRN_COMMON_ACCESS_CTRL_MAC_TABLE_ACCESS_SHOT_SET(1), sparx5, LRN_COMMON_ACCESS_CTRL); ret = sparx5_mact_wait_for_completion(sparx5); mutex_unlock(&sparx5->lock); return ret; } int sparx5_mc_unsync(struct net_device *dev, const unsigned char *addr) { struct sparx5_port *port = netdev_priv(dev); struct sparx5 *sparx5 = port->sparx5; return sparx5_mact_forget(sparx5, addr, port->pvid); } int sparx5_mc_sync(struct net_device *dev, const unsigned char *addr) { struct sparx5_port *port = netdev_priv(dev); struct sparx5 *sparx5 = port->sparx5; return sparx5_mact_learn(sparx5, PGID_CPU, addr, port->pvid); } static int sparx5_mact_get(struct sparx5 *sparx5, unsigned char mac[ETH_ALEN], u16 *vid, u32 *pcfg2) { u32 mach, macl, cfg2; int ret = -ENOENT; cfg2 = spx5_rd(sparx5, LRN_MAC_ACCESS_CFG_2); if (LRN_MAC_ACCESS_CFG_2_MAC_ENTRY_VLD_GET(cfg2)) { mach = spx5_rd(sparx5, LRN_MAC_ACCESS_CFG_0); macl = spx5_rd(sparx5, LRN_MAC_ACCESS_CFG_1); mac[0] = ((mach >> 8) & 0xff); mac[1] = ((mach >> 0) & 0xff); mac[2] = ((macl >> 24) & 0xff); mac[3] = ((macl >> 16) & 0xff); mac[4] = ((macl >> 8) & 0xff); mac[5] = ((macl >> 0) & 0xff); *vid = mach >> 16; *pcfg2 = cfg2; ret = 0; } return ret; } bool sparx5_mact_getnext(struct sparx5 *sparx5, unsigned char mac[ETH_ALEN], u16 *vid, u32 *pcfg2) { u32 cfg2; int ret; mutex_lock(&sparx5->lock); sparx5_mact_select(sparx5, mac, *vid); spx5_wr(LRN_SCAN_NEXT_CFG_SCAN_NEXT_IGNORE_LOCKED_ENA_SET(1) | LRN_SCAN_NEXT_CFG_SCAN_NEXT_UNTIL_FOUND_ENA_SET(1), sparx5, LRN_SCAN_NEXT_CFG); spx5_wr(LRN_COMMON_ACCESS_CTRL_CPU_ACCESS_CMD_SET (MAC_CMD_FIND_SMALLEST) | LRN_COMMON_ACCESS_CTRL_MAC_TABLE_ACCESS_SHOT_SET(1), sparx5, LRN_COMMON_ACCESS_CTRL); ret = sparx5_mact_wait_for_completion(sparx5); if (ret == 0) { ret = sparx5_mact_get(sparx5, mac, vid, &cfg2); if (ret == 0) *pcfg2 = cfg2; } mutex_unlock(&sparx5->lock); return ret == 0; } static int sparx5_mact_lookup(struct sparx5 *sparx5, const unsigned char mac[ETH_ALEN], u16 vid) { int ret; mutex_lock(&sparx5->lock); sparx5_mact_select(sparx5, mac, vid); /* Issue a lookup command */ spx5_wr(LRN_COMMON_ACCESS_CTRL_CPU_ACCESS_CMD_SET(MAC_CMD_LOOKUP) | LRN_COMMON_ACCESS_CTRL_MAC_TABLE_ACCESS_SHOT_SET(1), sparx5, LRN_COMMON_ACCESS_CTRL); ret = sparx5_mact_wait_for_completion(sparx5); if (ret) goto out; ret = LRN_MAC_ACCESS_CFG_2_MAC_ENTRY_VLD_GET (spx5_rd(sparx5, LRN_MAC_ACCESS_CFG_2)); out: mutex_unlock(&sparx5->lock); return ret; } int sparx5_mact_forget(struct sparx5 *sparx5, const unsigned char mac[ETH_ALEN], u16 vid) { int ret; mutex_lock(&sparx5->lock); sparx5_mact_select(sparx5, mac, vid); /* Issue an unlearn command */ spx5_wr(LRN_COMMON_ACCESS_CTRL_CPU_ACCESS_CMD_SET(MAC_CMD_UNLEARN) | LRN_COMMON_ACCESS_CTRL_MAC_TABLE_ACCESS_SHOT_SET(1), sparx5, LRN_COMMON_ACCESS_CTRL); ret = sparx5_mact_wait_for_completion(sparx5); mutex_unlock(&sparx5->lock); return ret; } static struct sparx5_mact_entry *alloc_mact_entry(struct sparx5 *sparx5, const unsigned char *mac, u16 vid, u16 port_index) { struct sparx5_mact_entry *mact_entry; mact_entry = devm_kzalloc(sparx5->dev, sizeof(*mact_entry), GFP_ATOMIC); if (!mact_entry) return NULL; memcpy(mact_entry->mac, mac, ETH_ALEN); mact_entry->vid = vid; mact_entry->port = port_index; return mact_entry; } static struct sparx5_mact_entry *find_mact_entry(struct sparx5 *sparx5, const unsigned char *mac, u16 vid, u16 port_index) { struct sparx5_mact_entry *mact_entry; struct sparx5_mact_entry *res = NULL; mutex_lock(&sparx5->mact_lock); list_for_each_entry(mact_entry, &sparx5->mact_entries, list) { if (mact_entry->vid == vid && ether_addr_equal(mac, mact_entry->mac) && mact_entry->port == port_index) { res = mact_entry; break; } } mutex_unlock(&sparx5->mact_lock); return res; } static void sparx5_fdb_call_notifiers(enum switchdev_notifier_type type, const char *mac, u16 vid, struct net_device *dev, bool offloaded) { struct switchdev_notifier_fdb_info info; info.addr = mac; info.vid = vid; info.offloaded = offloaded; call_switchdev_notifiers(type, dev, &info.info, NULL); } int sparx5_add_mact_entry(struct sparx5 *sparx5, struct sparx5_port *port, const unsigned char *addr, u16 vid) { struct sparx5_mact_entry *mact_entry; int ret; ret = sparx5_mact_lookup(sparx5, addr, vid); if (ret) return 0; /* In case the entry already exists, don't add it again to SW, * just update HW, but we need to look in the actual HW because * it is possible for an entry to be learn by HW and before the * mact thread to start the frame will reach CPU and the CPU will * add the entry but without the extern_learn flag. */ mact_entry = find_mact_entry(sparx5, addr, vid, port->portno); if (mact_entry) goto update_hw; /* Add the entry in SW MAC table not to get the notification when * SW is pulling again */ mact_entry = alloc_mact_entry(sparx5, addr, vid, port->portno); if (!mact_entry) return -ENOMEM; mutex_lock(&sparx5->mact_lock); list_add_tail(&mact_entry->list, &sparx5->mact_entries); mutex_unlock(&sparx5->mact_lock); update_hw: ret = sparx5_mact_learn(sparx5, port->portno, addr, vid); /* New entry? */ if (mact_entry->flags == 0) { mact_entry->flags |= MAC_ENT_LOCK; /* Don't age this */ sparx5_fdb_call_notifiers(SWITCHDEV_FDB_ADD_TO_BRIDGE, addr, vid, port->ndev, true); } return ret; } int sparx5_del_mact_entry(struct sparx5 *sparx5, const unsigned char *addr, u16 vid) { struct sparx5_mact_entry *mact_entry, *tmp; /* Delete the entry in SW MAC table not to get the notification when * SW is pulling again */ mutex_lock(&sparx5->mact_lock); list_for_each_entry_safe(mact_entry, tmp, &sparx5->mact_entries, list) { if ((vid == 0 || mact_entry->vid == vid) && ether_addr_equal(addr, mact_entry->mac)) { list_del(&mact_entry->list); devm_kfree(sparx5->dev, mact_entry); sparx5_mact_forget(sparx5, addr, mact_entry->vid); } } mutex_unlock(&sparx5->mact_lock); return 0; } static void sparx5_mact_handle_entry(struct sparx5 *sparx5, unsigned char mac[ETH_ALEN], u16 vid, u32 cfg2) { struct sparx5_mact_entry *mact_entry; bool found = false; u16 port; if (LRN_MAC_ACCESS_CFG_2_MAC_ENTRY_ADDR_TYPE_GET(cfg2) != MAC_ENTRY_ADDR_TYPE_UPSID_PN) return; port = LRN_MAC_ACCESS_CFG_2_MAC_ENTRY_ADDR_GET(cfg2); if (port >= SPX5_PORTS) return; if (!test_bit(port, sparx5->bridge_mask)) return; mutex_lock(&sparx5->mact_lock); list_for_each_entry(mact_entry, &sparx5->mact_entries, list) { if (mact_entry->vid == vid && ether_addr_equal(mac, mact_entry->mac)) { found = true; mact_entry->flags |= MAC_ENT_ALIVE; if (mact_entry->port != port) { dev_warn(sparx5->dev, "Entry move: %d -> %d\n", mact_entry->port, port); mact_entry->port = port; mact_entry->flags |= MAC_ENT_MOVED; } /* Entry handled */ break; } } mutex_unlock(&sparx5->mact_lock); if (found && !(mact_entry->flags & MAC_ENT_MOVED)) /* Present, not moved */ return; if (!found) { /* Entry not found - now add */ mact_entry = alloc_mact_entry(sparx5, mac, vid, port); if (!mact_entry) return; mact_entry->flags |= MAC_ENT_ALIVE; mutex_lock(&sparx5->mact_lock); list_add_tail(&mact_entry->list, &sparx5->mact_entries); mutex_unlock(&sparx5->mact_lock); } /* New or moved entry - notify bridge */ sparx5_fdb_call_notifiers(SWITCHDEV_FDB_ADD_TO_BRIDGE, mac, vid, sparx5->ports[port]->ndev, true); } void sparx5_mact_pull_work(struct work_struct *work) { struct delayed_work *del_work = to_delayed_work(work); struct sparx5 *sparx5 = container_of(del_work, struct sparx5, mact_work); struct sparx5_mact_entry *mact_entry, *tmp; unsigned char mac[ETH_ALEN]; u32 cfg2; u16 vid; int ret; /* Reset MAC entry flags */ mutex_lock(&sparx5->mact_lock); list_for_each_entry(mact_entry, &sparx5->mact_entries, list) mact_entry->flags &= MAC_ENT_LOCK; mutex_unlock(&sparx5->mact_lock); /* MAIN mac address processing loop */ vid = 0; memset(mac, 0, sizeof(mac)); do { mutex_lock(&sparx5->lock); sparx5_mact_select(sparx5, mac, vid); spx5_wr(LRN_SCAN_NEXT_CFG_SCAN_NEXT_UNTIL_FOUND_ENA_SET(1), sparx5, LRN_SCAN_NEXT_CFG); spx5_wr(LRN_COMMON_ACCESS_CTRL_CPU_ACCESS_CMD_SET (MAC_CMD_FIND_SMALLEST) | LRN_COMMON_ACCESS_CTRL_MAC_TABLE_ACCESS_SHOT_SET(1), sparx5, LRN_COMMON_ACCESS_CTRL); ret = sparx5_mact_wait_for_completion(sparx5); if (ret == 0) ret = sparx5_mact_get(sparx5, mac, &vid, &cfg2); mutex_unlock(&sparx5->lock); if (ret == 0) sparx5_mact_handle_entry(sparx5, mac, vid, cfg2); } while (ret == 0); mutex_lock(&sparx5->mact_lock); list_for_each_entry_safe(mact_entry, tmp, &sparx5->mact_entries, list) { /* If the entry is in HW or permanent, then skip */ if (mact_entry->flags & (MAC_ENT_ALIVE | MAC_ENT_LOCK)) continue; sparx5_fdb_call_notifiers(SWITCHDEV_FDB_DEL_TO_BRIDGE, mact_entry->mac, mact_entry->vid, sparx5->ports[mact_entry->port]->ndev, true); list_del(&mact_entry->list); devm_kfree(sparx5->dev, mact_entry); } mutex_unlock(&sparx5->mact_lock); queue_delayed_work(sparx5->mact_queue, &sparx5->mact_work, SPX5_MACT_PULL_DELAY); } void sparx5_set_ageing(struct sparx5 *sparx5, int msecs) { int value = max(1, msecs / 10); /* unit 10 ms */ spx5_rmw(LRN_AUTOAGE_CFG_UNIT_SIZE_SET(2) | /* 10 ms */ LRN_AUTOAGE_CFG_PERIOD_VAL_SET(value / 2), /* one bit ageing */ LRN_AUTOAGE_CFG_UNIT_SIZE | LRN_AUTOAGE_CFG_PERIOD_VAL, sparx5, LRN_AUTOAGE_CFG(0)); } void sparx5_mact_init(struct sparx5 *sparx5) { mutex_init(&sparx5->lock); /* Flush MAC table */ spx5_wr(LRN_COMMON_ACCESS_CTRL_CPU_ACCESS_CMD_SET(MAC_CMD_CLEAR_ALL) | LRN_COMMON_ACCESS_CTRL_MAC_TABLE_ACCESS_SHOT_SET(1), sparx5, LRN_COMMON_ACCESS_CTRL); if (sparx5_mact_wait_for_completion(sparx5) != 0) dev_warn(sparx5->dev, "MAC flush error\n"); sparx5_set_ageing(sparx5, BR_DEFAULT_AGEING_TIME / HZ * 1000); }