--- /dev/null
+// SPDX-License-Identifier: GPL-2.0
+/* Texas Instruments PRUETH Switch Driver
+ *
+ * Copyright (C) 2020-2021 Texas Instruments Incorporated - https://www.ti.com
+ */
+#include <linux/etherdevice.h>
+#include <linux/kernel.h>
+#include <linux/remoteproc.h>
+#include <net/switchdev.h>
+#include "icssm_prueth.h"
+#include "icssm_prueth_switch.h"
+#include "icssm_prueth_fdb_tbl.h"
+
+#define FDB_IDX_TBL_ENTRY(n) (&prueth->fdb_tbl->index_a->index_tbl_entry[n])
+
+#define FDB_MAC_TBL_ENTRY(n) (&prueth->fdb_tbl->mac_tbl_a->mac_tbl_entry[n])
+
+#define FLAG_IS_STATIC BIT(0)
+#define FLAG_ACTIVE BIT(1)
+
+void icssm_prueth_sw_free_fdb_table(struct prueth *prueth)
+{
+ if (prueth->emac_configured)
+ return;
+
+ kfree(prueth->fdb_tbl);
+ prueth->fdb_tbl = NULL;
+}
+
+void icssm_prueth_sw_fdb_tbl_init(struct prueth *prueth)
+{
+ struct fdb_tbl *t = prueth->fdb_tbl;
+ void __iomem *sram_base;
+ u8 val;
+
+ sram_base = prueth->mem[PRUETH_MEM_SHARED_RAM].va;
+
+ t->index_a = sram_base + V2_1_FDB_TBL_OFFSET;
+ t->mac_tbl_a = sram_base + FDB_MAC_TBL_OFFSET;
+ t->port1_stp_cfg = sram_base + FDB_PORT1_STP_CFG_OFFSET;
+ t->port2_stp_cfg = sram_base + FDB_PORT2_STP_CFG_OFFSET;
+ t->flood_enable_flags = sram_base + FDB_FLOOD_ENABLE_FLAGS_OFFSET;
+ t->locks = sram_base + FDB_LOCKS_OFFSET;
+
+ val = readb(t->flood_enable_flags);
+ /* host_flood_enable = 1 */
+ val |= BIT(0);
+ /* port1_flood_enable = 1 */
+ val |= BIT(1);
+ /* port2_flood_enable = 1 */
+ val |= BIT(2);
+ writeb(val, t->flood_enable_flags);
+
+ writeb(0, &t->locks->host_lock);
+ t->total_entries = 0;
+}
+
+static u8 icssm_pru_lock_done(struct fdb_tbl *fdb_tbl)
+{
+ return readb(&fdb_tbl->locks->pru_locks);
+}
+
+static int icssm_prueth_sw_fdb_spin_lock(struct fdb_tbl *fdb_tbl)
+{
+ u8 done;
+ int ret;
+
+ /* Take the host lock */
+ writeb(1, &fdb_tbl->locks->host_lock);
+
+ /* Wait for the PRUs to release their locks */
+ ret = read_poll_timeout(icssm_pru_lock_done, done, done == 0,
+ 1, 10, false, fdb_tbl);
+ if (ret == -ETIMEDOUT)
+ writeb(0, &fdb_tbl->locks->host_lock);
+
+ return ret;
+}
+
+static void icssm_prueth_sw_fdb_spin_unlock(struct fdb_tbl *fdb_tbl)
+{
+ writeb(0, &fdb_tbl->locks->host_lock);
+}
+
+static u8 icssm_prueth_sw_fdb_hash(const u8 *mac)
+{
+ return (mac[0] ^ mac[1] ^ mac[2] ^ mac[3] ^ mac[4] ^ mac[5]);
+}
+
+static int
+icssm_prueth_sw_fdb_search(struct fdb_mac_tbl_array __iomem *mac_tbl,
+ struct fdb_index_tbl_entry __iomem *bucket_info,
+ const u8 *mac)
+{
+ unsigned int bucket_entries, mac_tbl_idx;
+ u8 tmp_mac[ETH_ALEN];
+ int i;
+
+ mac_tbl_idx = readw(&bucket_info->bucket_idx);
+ bucket_entries = readw(&bucket_info->bucket_entries);
+ for (i = 0; i < bucket_entries; i++, mac_tbl_idx++) {
+ memcpy_fromio(tmp_mac, mac_tbl->mac_tbl_entry[mac_tbl_idx].mac,
+ ETH_ALEN);
+ if (ether_addr_equal(mac, tmp_mac))
+ return mac_tbl_idx;
+ }
+
+ return -ENODATA;
+}
+
+static int icssm_prueth_sw_fdb_find_open_slot(struct fdb_tbl *fdb_tbl)
+{
+ unsigned int i;
+ u8 flags;
+
+ for (i = 0; i < FDB_MAC_TBL_MAX_ENTRIES; i++) {
+ flags = readb(&fdb_tbl->mac_tbl_a->mac_tbl_entry[i].flags);
+ if (!(flags & FLAG_ACTIVE))
+ break;
+ }
+
+ return i;
+}
+
+static int
+icssm_prueth_sw_find_fdb_insert(struct fdb_tbl *fdb, struct prueth *prueth,
+ struct fdb_index_tbl_entry __iomem *bkt_info,
+ const u8 *mac, const u8 port)
+{
+ struct fdb_mac_tbl_array __iomem *mac_tbl = fdb->mac_tbl_a;
+ unsigned int bucket_entries, mac_tbl_idx;
+ struct fdb_mac_tbl_entry __iomem *e;
+ u8 mac_from_hw[ETH_ALEN];
+ s8 cmp;
+ int i;
+
+ mac_tbl_idx = readw(&bkt_info->bucket_idx);
+ bucket_entries = readw(&bkt_info->bucket_entries);
+
+ for (i = 0; i < bucket_entries; i++, mac_tbl_idx++) {
+ e = &mac_tbl->mac_tbl_entry[mac_tbl_idx];
+ memcpy_fromio(mac_from_hw, e->mac, ETH_ALEN);
+ cmp = memcmp(mac, mac_from_hw, ETH_ALEN);
+ if (cmp < 0) {
+ return mac_tbl_idx;
+ } else if (cmp == 0) {
+ if (readb(&e->port) != port) {
+ /* MAC is already in FDB, only port is
+ * different. So just update the port.
+ * Note: total_entries and bucket_entries
+ * remain the same.
+ */
+ writeb(port, &e->port);
+ }
+
+ /* MAC and port are the same, touch the fdb */
+ writew(0, &e->age);
+ return -EEXIST;
+ }
+ }
+
+ return mac_tbl_idx;
+}
+
+static int
+icssm_prueth_sw_fdb_empty_slot_left(struct fdb_mac_tbl_array __iomem *mac_tbl,
+ unsigned int mac_tbl_idx)
+{
+ u8 flags;
+ int i;
+
+ for (i = mac_tbl_idx - 1; i > -1; i--) {
+ flags = readb(&mac_tbl->mac_tbl_entry[i].flags);
+ if (!(flags & FLAG_ACTIVE))
+ break;
+ }
+
+ return i;
+}
+
+static int
+icssm_prueth_sw_fdb_empty_slot_right(struct fdb_mac_tbl_array __iomem *mac_tbl,
+ unsigned int mac_tbl_idx)
+{
+ u8 flags;
+ int i;
+
+ for (i = mac_tbl_idx; i < FDB_MAC_TBL_MAX_ENTRIES; i++) {
+ flags = readb(&mac_tbl->mac_tbl_entry[i].flags);
+ if (!(flags & FLAG_ACTIVE))
+ return i;
+ }
+
+ return -1;
+}
+
+static void icssm_prueth_sw_fdb_move_range_left(struct prueth *prueth,
+ u16 left, u16 right)
+{
+ struct fdb_mac_tbl_entry entry;
+ u32 sz = 0;
+ u16 i;
+
+ sz = sizeof(struct fdb_mac_tbl_entry);
+ for (i = left; i < right; i++) {
+ memcpy_fromio(&entry, FDB_MAC_TBL_ENTRY(i + 1), sz);
+ memcpy_toio(FDB_MAC_TBL_ENTRY(i), &entry, sz);
+ }
+}
+
+static void icssm_prueth_sw_fdb_move_range_right(struct prueth *prueth,
+ u16 left, u16 right)
+{
+ struct fdb_mac_tbl_entry entry;
+ u32 sz = 0;
+ u16 i;
+
+ sz = sizeof(struct fdb_mac_tbl_entry);
+ for (i = right; i > left; i--) {
+ memcpy_fromio(&entry, FDB_MAC_TBL_ENTRY(i - 1), sz);
+ memcpy_toio(FDB_MAC_TBL_ENTRY(i), &entry, sz);
+ }
+}
+
+static void icssm_prueth_sw_fdb_update_index_tbl(struct prueth *prueth,
+ u16 left, u16 right)
+{
+ unsigned int hash, hash_prev;
+ u8 mac[ETH_ALEN];
+ unsigned int i;
+
+ /* To ensure we don't improperly update the
+ * bucket index, initialize with an invalid
+ * hash in case we are in leftmost slot
+ */
+ hash_prev = 0xff;
+
+ if (left > 0) {
+ memcpy_fromio(mac, FDB_MAC_TBL_ENTRY(left - 1)->mac, ETH_ALEN);
+ hash_prev = icssm_prueth_sw_fdb_hash(mac);
+ }
+
+ /* For each moved element, update the bucket index */
+ for (i = left; i <= right; i++) {
+ memcpy_fromio(mac, FDB_MAC_TBL_ENTRY(i)->mac, ETH_ALEN);
+ hash = icssm_prueth_sw_fdb_hash(mac);
+
+ /* Only need to update buckets once */
+ if (hash != hash_prev)
+ writew(i, &FDB_IDX_TBL_ENTRY(hash)->bucket_idx);
+
+ hash_prev = hash;
+ }
+}
+
+static struct fdb_mac_tbl_entry __iomem *
+icssm_prueth_sw_find_free_mac(struct prueth *prueth, struct fdb_index_tbl_entry
+ __iomem *bucket_info, u8 suggested_mac_tbl_idx,
+ bool *update_indexes, const u8 *mac)
+{
+ s16 empty_slot_idx = 0, left = 0, right = 0;
+ unsigned int mti = suggested_mac_tbl_idx;
+ struct fdb_mac_tbl_array __iomem *mt;
+ struct fdb_tbl *fdb;
+ u8 flags;
+
+ fdb = prueth->fdb_tbl;
+ mt = fdb->mac_tbl_a;
+
+ flags = readb(&FDB_MAC_TBL_ENTRY(mti)->flags);
+ if (!(flags & FLAG_ACTIVE)) {
+ /* Claim the entry */
+ flags |= FLAG_ACTIVE;
+ writeb(flags, &FDB_MAC_TBL_ENTRY(mti)->flags);
+
+ return FDB_MAC_TBL_ENTRY(mti);
+ }
+
+ if (fdb->total_entries == FDB_MAC_TBL_MAX_ENTRIES)
+ return NULL;
+
+ empty_slot_idx = icssm_prueth_sw_fdb_empty_slot_left(mt, mti);
+ if (empty_slot_idx == -1) {
+ /* Nothing available on the left. But table isn't full
+ * so there must be space to the right,
+ */
+ empty_slot_idx = icssm_prueth_sw_fdb_empty_slot_right(mt, mti);
+
+ /* Shift right */
+ left = mti;
+ right = empty_slot_idx;
+ icssm_prueth_sw_fdb_move_range_right(prueth, left, right);
+
+ /* Claim the entry */
+ flags = readb(&FDB_MAC_TBL_ENTRY(mti)->flags);
+ flags |= FLAG_ACTIVE;
+ writeb(flags, &FDB_MAC_TBL_ENTRY(mti)->flags);
+
+ memcpy_toio(FDB_MAC_TBL_ENTRY(mti)->mac, mac, ETH_ALEN);
+
+ /* There is a chance we moved something in a
+ * different bucket, update index table
+ */
+ icssm_prueth_sw_fdb_update_index_tbl(prueth, left, right);
+
+ return FDB_MAC_TBL_ENTRY(mti);
+ }
+
+ if (empty_slot_idx == mti - 1) {
+ /* There is space immediately left of the open slot,
+ * which means the inserted MAC address
+ * must be the lowest-valued MAC address in bucket.
+ * Update bucket pointer accordingly.
+ */
+ writew(empty_slot_idx, &bucket_info->bucket_idx);
+
+ /* Claim the entry */
+ flags = readb(&FDB_MAC_TBL_ENTRY(empty_slot_idx)->flags);
+ flags |= FLAG_ACTIVE;
+ writeb(flags, &FDB_MAC_TBL_ENTRY(empty_slot_idx)->flags);
+
+ return FDB_MAC_TBL_ENTRY(empty_slot_idx);
+ }
+
+ /* There is empty space to the left, shift MAC table entries left */
+ left = empty_slot_idx;
+ right = mti - 1;
+ icssm_prueth_sw_fdb_move_range_left(prueth, left, right);
+
+ /* Claim the entry */
+ flags = readb(&FDB_MAC_TBL_ENTRY(mti - 1)->flags);
+ flags |= FLAG_ACTIVE;
+ writeb(flags, &FDB_MAC_TBL_ENTRY(mti - 1)->flags);
+
+ memcpy_toio(FDB_MAC_TBL_ENTRY(mti - 1)->mac, mac, ETH_ALEN);
+
+ /* There is a chance we moved something in a
+ * different bucket, update index table
+ */
+ icssm_prueth_sw_fdb_update_index_tbl(prueth, left, right);
+
+ return FDB_MAC_TBL_ENTRY(mti - 1);
+}
+
+static int icssm_prueth_sw_insert_fdb_entry(struct prueth_emac *emac,
+ const u8 *mac, u8 is_static)
+{
+ struct fdb_index_tbl_entry __iomem *bucket_info;
+ struct fdb_mac_tbl_entry __iomem *mac_info;
+ struct prueth *prueth = emac->prueth;
+ unsigned int hash_val, mac_tbl_idx;
+ struct prueth_emac *other_emac;
+ enum prueth_port other_port_id;
+ int total_fdb_entries;
+ struct fdb_tbl *fdb;
+ u8 flags;
+ s16 ret;
+ int err;
+ u16 val;
+
+ fdb = prueth->fdb_tbl;
+ other_port_id = (emac->port_id == PRUETH_PORT_MII0) ?
+ PRUETH_PORT_MII1 : PRUETH_PORT_MII0;
+
+ other_emac = prueth->emac[other_port_id - 1];
+ if (!other_emac)
+ return -EINVAL;
+
+ err = icssm_prueth_sw_fdb_spin_lock(fdb);
+ if (err) {
+ dev_err(prueth->dev, "PRU lock timeout %d\n", err);
+ return err;
+ }
+
+ if (fdb->total_entries == FDB_MAC_TBL_MAX_ENTRIES) {
+ icssm_prueth_sw_fdb_spin_unlock(fdb);
+ return -ENOMEM;
+ }
+
+ if (ether_addr_equal(mac, emac->mac_addr) ||
+ (ether_addr_equal(mac, other_emac->mac_addr))) {
+ icssm_prueth_sw_fdb_spin_unlock(fdb);
+ /* Don't insert fdb of own mac addr */
+ return -EINVAL;
+ }
+
+ /* Get the bucket that the mac belongs to */
+ hash_val = icssm_prueth_sw_fdb_hash(mac);
+ bucket_info = FDB_IDX_TBL_ENTRY(hash_val);
+
+ if (!readw(&bucket_info->bucket_entries)) {
+ mac_tbl_idx = icssm_prueth_sw_fdb_find_open_slot(fdb);
+ writew(mac_tbl_idx, &bucket_info->bucket_idx);
+ }
+
+ ret = icssm_prueth_sw_find_fdb_insert(fdb, prueth, bucket_info, mac,
+ emac->port_id - 1);
+ if (ret < 0) {
+ icssm_prueth_sw_fdb_spin_unlock(fdb);
+ /* mac is already in fdb table */
+ return 0;
+ }
+
+ mac_tbl_idx = ret;
+
+ mac_info = icssm_prueth_sw_find_free_mac(prueth, bucket_info,
+ mac_tbl_idx, NULL,
+ mac);
+ if (!mac_info) {
+ /* Should not happen */
+ dev_warn(prueth->dev, "OUT of FDB MEM\n");
+ icssm_prueth_sw_fdb_spin_unlock(fdb);
+ return -ENOMEM;
+ }
+
+ memcpy_toio(mac_info->mac, mac, ETH_ALEN);
+ writew(0, &mac_info->age);
+ writeb(emac->port_id - 1, &mac_info->port);
+
+ flags = readb(&mac_info->flags);
+ if (is_static)
+ flags |= FLAG_IS_STATIC;
+ else
+ flags &= ~FLAG_IS_STATIC;
+
+ /* bit 1 - active */
+ flags |= FLAG_ACTIVE;
+ writeb(flags, &mac_info->flags);
+
+ val = readw(&bucket_info->bucket_entries);
+ val++;
+ writew(val, &bucket_info->bucket_entries);
+
+ fdb->total_entries++;
+
+ total_fdb_entries = fdb->total_entries;
+
+ icssm_prueth_sw_fdb_spin_unlock(fdb);
+
+ dev_dbg(prueth->dev, "added fdb: %pM port=%d total_entries=%u\n",
+ mac, emac->port_id, total_fdb_entries);
+
+ return 0;
+}
+
+static int icssm_prueth_sw_delete_fdb_entry(struct prueth_emac *emac,
+ const u8 *mac, u8 is_static)
+{
+ struct fdb_index_tbl_entry __iomem *bucket_info;
+ struct fdb_mac_tbl_entry __iomem *mac_info;
+ struct fdb_mac_tbl_array __iomem *mt;
+ unsigned int hash_val, mac_tbl_idx;
+ unsigned int idx, entries;
+ struct prueth *prueth;
+ int total_fdb_entries;
+ s16 ret, left, right;
+ struct fdb_tbl *fdb;
+ u8 flags;
+ int err;
+ u16 val;
+
+ prueth = emac->prueth;
+ fdb = prueth->fdb_tbl;
+ mt = fdb->mac_tbl_a;
+
+ err = icssm_prueth_sw_fdb_spin_lock(fdb);
+ if (err) {
+ dev_err(prueth->dev, "PRU lock timeout %d\n", err);
+ return err;
+ }
+
+ if (fdb->total_entries == 0) {
+ icssm_prueth_sw_fdb_spin_unlock(fdb);
+ return 0;
+ }
+
+ /* Get the bucket that the mac belongs to */
+ hash_val = icssm_prueth_sw_fdb_hash(mac);
+ bucket_info = FDB_IDX_TBL_ENTRY(hash_val);
+
+ ret = icssm_prueth_sw_fdb_search(mt, bucket_info, mac);
+ if (ret < 0) {
+ icssm_prueth_sw_fdb_spin_unlock(fdb);
+ return ret;
+ }
+
+ mac_tbl_idx = ret;
+ mac_info = FDB_MAC_TBL_ENTRY(mac_tbl_idx);
+
+ /* Shift all elements in bucket to the left. No need to
+ * update index table since only shifting within bucket.
+ */
+ left = mac_tbl_idx;
+ idx = readw(&bucket_info->bucket_idx);
+ entries = readw(&bucket_info->bucket_entries);
+ right = idx + entries - 1;
+ icssm_prueth_sw_fdb_move_range_left(prueth, left, right);
+
+ /* Remove end of bucket from table */
+ mac_info = FDB_MAC_TBL_ENTRY(right);
+ flags = readb(&mac_info->flags);
+ /* active = 0 */
+ flags &= ~FLAG_ACTIVE;
+ writeb(flags, &mac_info->flags);
+ val = readw(&bucket_info->bucket_entries);
+ val--;
+ writew(val, &bucket_info->bucket_entries);
+ fdb->total_entries--;
+
+ total_fdb_entries = fdb->total_entries;
+
+ icssm_prueth_sw_fdb_spin_unlock(fdb);
+
+ dev_dbg(prueth->dev, "del fdb: %pM total_entries=%u\n",
+ mac, total_fdb_entries);
+
+ return 0;
+}
+
+int icssm_prueth_sw_do_purge_fdb(struct prueth_emac *emac)
+{
+ struct fdb_index_tbl_entry __iomem *bucket_info;
+ struct prueth *prueth = emac->prueth;
+ u8 flags, mac[ETH_ALEN];
+ unsigned int hash_val;
+ struct fdb_tbl *fdb;
+ int ret, i;
+ u16 val;
+
+ fdb = prueth->fdb_tbl;
+
+ ret = icssm_prueth_sw_fdb_spin_lock(fdb);
+ if (ret) {
+ dev_err(prueth->dev, "PRU lock timeout %d\n", ret);
+ return ret;
+ }
+
+ if (fdb->total_entries == 0) {
+ icssm_prueth_sw_fdb_spin_unlock(fdb);
+ return 0;
+ }
+
+ for (i = 0; i < FDB_MAC_TBL_MAX_ENTRIES; i++) {
+ flags = readb(&fdb->mac_tbl_a->mac_tbl_entry[i].flags);
+ if ((flags & FLAG_ACTIVE) && !(flags & FLAG_IS_STATIC)) {
+ /* Get the bucket that the mac belongs to */
+ memcpy_fromio(mac, FDB_MAC_TBL_ENTRY(i)->mac,
+ ETH_ALEN);
+ hash_val = icssm_prueth_sw_fdb_hash(mac);
+ bucket_info = FDB_IDX_TBL_ENTRY(hash_val);
+ flags &= ~FLAG_ACTIVE;
+ writeb(flags,
+ &fdb->mac_tbl_a->mac_tbl_entry[i].flags);
+ val = readw(&bucket_info->bucket_entries);
+ val--;
+ writew(val, &bucket_info->bucket_entries);
+ fdb->total_entries--;
+ }
+ }
+
+ icssm_prueth_sw_fdb_spin_unlock(fdb);
+ return 0;
+}
+
+int icssm_prueth_sw_init_fdb_table(struct prueth *prueth)
+{
+ if (prueth->emac_configured)
+ return 0;
+
+ prueth->fdb_tbl = kmalloc(sizeof(*prueth->fdb_tbl), GFP_KERNEL);
+ if (!prueth->fdb_tbl)
+ return -ENOMEM;
+
+ icssm_prueth_sw_fdb_tbl_init(prueth);
+
+ return 0;
+}
+
+/**
+ * icssm_prueth_sw_fdb_add - insert fdb entry
+ *
+ * @emac: EMAC data structure
+ * @fdb: fdb info
+ *
+ */
+void icssm_prueth_sw_fdb_add(struct prueth_emac *emac,
+ struct switchdev_notifier_fdb_info *fdb)
+{
+ icssm_prueth_sw_insert_fdb_entry(emac, fdb->addr, 1);
+}
+
+/**
+ * icssm_prueth_sw_fdb_del - delete fdb entry
+ *
+ * @emac: EMAC data structure
+ * @fdb: fdb info
+ *
+ */
+void icssm_prueth_sw_fdb_del(struct prueth_emac *emac,
+ struct switchdev_notifier_fdb_info *fdb)
+{
+ icssm_prueth_sw_delete_fdb_entry(emac, fdb->addr, 1);
+}