Subject: Re: [PATCH RFC net-next v2] net: dsa: mv88e6xxx: Add partial support for TCAM entries

From: Daniel Golle <[email-masked]>
Date: Tue, 17 Feb 2026 13:36:29 +0000

Hi Cedric,

I've reviewed this submission, find some comments inline below.

On Tue, Feb 17, 2026 at 01:59:27PM +0100, Cedric Jehasse via B4 Relay wrote:
> From: Cedric Jehasse <[email-masked]>
> 
> This patch adds partial Ternary Content Addressable Memory (TCAM) for
> the mv88e6390 and mv88e6393 family of switches. TCAM entries allow the
> switch to match the first 48 or 96 bytes of a frame and take actions on
> matched frames.
> 
> This patch introduces a subset of the available TCAM functionality.
> Matching on ip addresses/protocol and trapping to the cpu.
> 
> Eg. to trap traffic with destination ip 224.0.1.129 to the cpu:
> 
> tc qdisc add dev p1 clsact
> tc filter add dev p1 ingress protocol ip flower skip_sw \
>     dst_ip 224.0.1.129 action trap
> 
> Signed-off-by: Cedric Jehasse <[email-masked]>
> ---
> Changes in RFC-v2:
> - moved tcam_addr to chip info struct and remove tcam_support field
> - added 6393-specific tcam ops that write the TCAM extension register
> - rename the defines for TCAM registers to be consistent
> - Link to v1: https://lore.kernel.org/r/[email-masked]
> ---
>  drivers/net/dsa/mv88e6xxx/Makefile   |   2 +
>  drivers/net/dsa/mv88e6xxx/chip.c     |  30 ++++
>  drivers/net/dsa/mv88e6xxx/chip.h     |  50 ++++++
>  drivers/net/dsa/mv88e6xxx/port.c     |  23 ++-
>  drivers/net/dsa/mv88e6xxx/port.h     |   7 +-
>  drivers/net/dsa/mv88e6xxx/tcam.c     | 310 +++++++++++++++++++++++++++++++++++
>  drivers/net/dsa/mv88e6xxx/tcam.h     |  38 +++++
>  drivers/net/dsa/mv88e6xxx/tcflower.c | 138 ++++++++++++++++
>  drivers/net/dsa/mv88e6xxx/tcflower.h |  13 ++
>  9 files changed, 609 insertions(+), 2 deletions(-)
> 
> diff --git a/drivers/net/dsa/mv88e6xxx/Makefile b/drivers/net/dsa/mv88e6xxx/Makefile
> index dd961081d631..b0b08c6f159c 100644
> --- a/drivers/net/dsa/mv88e6xxx/Makefile
> +++ b/drivers/net/dsa/mv88e6xxx/Makefile
> @@ -21,6 +21,8 @@ mv88e6xxx-objs += serdes.o
>  mv88e6xxx-objs += smi.o
>  mv88e6xxx-objs += switchdev.o
>  mv88e6xxx-objs += trace.o
> +mv88e6xxx-objs += tcflower.o
> +mv88e6xxx-objs += tcam.o
>  
>  # for tracing framework to find trace.h
>  CFLAGS_trace.o := -I$(src)
> diff --git a/drivers/net/dsa/mv88e6xxx/chip.c b/drivers/net/dsa/mv88e6xxx/chip.c
> index 09002c853b78..b95212dc2f75 100644
> --- a/drivers/net/dsa/mv88e6xxx/chip.c
> +++ b/drivers/net/dsa/mv88e6xxx/chip.c
> @@ -43,6 +43,8 @@
>  #include "ptp.h"
>  #include "serdes.h"
>  #include "smi.h"
> +#include "tcam.h"
> +#include "tcflower.h"
>  
>  static void assert_reg_lock(struct mv88e6xxx_chip *chip)
>  {
> @@ -3560,6 +3562,11 @@ static int mv88e6xxx_setup_port(struct mv88e6xxx_chip *chip, int port)
>  		if (err)
>  			return err;
>  	}
> +	if (chip->info->ops->port_enable_tcam) {
> +		err = chip->info->ops->port_enable_tcam(chip, port);
> +		if (err)
> +			return err;
> +	}
>  
>  	if (chip->info->ops->port_tag_remap) {
>  		err = chip->info->ops->port_tag_remap(chip, port);
> @@ -3938,6 +3945,14 @@ static int mv88e6xxx_mdios_register(struct mv88e6xxx_chip *chip)
>  	return 0;
>  }
>  
> +static int mv88e6xxx_tcam_setup(struct mv88e6xxx_chip *chip)
> +{
> +	if (!mv88e6xxx_has_tcam(chip))
> +		return 0;
> +
> +	return chip->info->ops->tcam_ops->flush_tcam(chip);
> +}
> +
>  static void mv88e6xxx_teardown(struct dsa_switch *ds)
>  {
>  	struct mv88e6xxx_chip *chip = ds->priv;
> @@ -4083,6 +4098,10 @@ static int mv88e6xxx_setup(struct dsa_switch *ds)
>  	if (err)
>  		goto unlock;
>  
> +	err = mv88e6xxx_tcam_setup(chip);
> +	if (err)
> +		goto unlock;
> +
>  unlock:
>  	mv88e6xxx_reg_unlock(chip);
>  
> @@ -5134,6 +5153,7 @@ static const struct mv88e6xxx_ops mv88e6290_ops = {
>  	.ptp_ops = &mv88e6390_ptp_ops,
>  	.phylink_get_caps = mv88e6390_phylink_get_caps,
>  	.pcs_ops = &mv88e6390_pcs_ops,
> +	.tcam_ops = &mv88e6390_tcam_ops,
>  };
>  
>  static const struct mv88e6xxx_ops mv88e6320_ops = {
> @@ -5525,6 +5545,7 @@ static const struct mv88e6xxx_ops mv88e6390_ops = {
>  	.serdes_get_regs = mv88e6390_serdes_get_regs,
>  	.phylink_get_caps = mv88e6390_phylink_get_caps,
>  	.pcs_ops = &mv88e6390_pcs_ops,
> +	.tcam_ops = &mv88e6390_tcam_ops,
>  };
>  
>  static const struct mv88e6xxx_ops mv88e6390x_ops = {
> @@ -5621,6 +5642,7 @@ static const struct mv88e6xxx_ops mv88e6393x_ops = {
>  	.port_set_cmode = mv88e6393x_port_set_cmode,
>  	.port_setup_message_port = mv88e6xxx_setup_message_port,
>  	.port_set_upstream_port = mv88e6393x_port_set_upstream_port,
> +	.port_enable_tcam = mv88e6xxx_port_enable_tcam,
>  	.stats_snapshot = mv88e6390_g1_stats_snapshot,
>  	.stats_set_histogram = mv88e6390_g1_stats_set_histogram,
>  	.stats_get_sset_count = mv88e6320_stats_get_sset_count,
> @@ -5652,6 +5674,7 @@ static const struct mv88e6xxx_ops mv88e6393x_ops = {
>  	.ptp_ops = &mv88e6352_ptp_ops,
>  	.phylink_get_caps = mv88e6393x_phylink_get_caps,
>  	.pcs_ops = &mv88e6393x_pcs_ops,
> +	.tcam_ops = &mv88e6393_tcam_ops,
>  };
>  
>  static const struct mv88e6xxx_info mv88e6xxx_table[] = {
> @@ -6132,6 +6155,7 @@ static const struct mv88e6xxx_info mv88e6xxx_table[] = {
>  		.phy_base_addr = 0x0,
>  		.global1_addr = 0x1b,
>  		.global2_addr = 0x1c,
> +		.tcam_addr = 0x1f,
>  		.age_time_coeff = 3750,
>  		.g1_irqs = 10,
>  		.g2_irqs = 14,
> @@ -6233,6 +6257,7 @@ static const struct mv88e6xxx_info mv88e6xxx_table[] = {
>  		.phy_base_addr = 0x0,
>  		.global1_addr = 0x1b,
>  		.global2_addr = 0x1c,
> +		.tcam_addr = 0x1f,
>  		.age_time_coeff = 3750,
>  		.g1_irqs = 9,
>  		.g2_irqs = 14,
> @@ -6445,6 +6470,7 @@ static const struct mv88e6xxx_info mv88e6xxx_table[] = {
>  		.phy_base_addr = 0x0,
>  		.global1_addr = 0x1b,
>  		.global2_addr = 0x1c,
> +		.tcam_addr = 0x1f,
>  		.age_time_coeff = 3750,
>  		.g1_irqs = 9,
>  		.g2_irqs = 14,
> @@ -6497,6 +6523,7 @@ static const struct mv88e6xxx_info mv88e6xxx_table[] = {
>  		.phy_base_addr = 0x0,
>  		.global1_addr = 0x1b,
>  		.global2_addr = 0x1c,
> +		.tcam_addr = 0x1f,
>  		.age_time_coeff = 3750,
>  		.g1_irqs = 10,
>  		.g2_irqs = 14,
> @@ -6589,6 +6616,7 @@ static struct mv88e6xxx_chip *mv88e6xxx_alloc_chip(struct device *dev)
>  	INIT_LIST_HEAD(&chip->mdios);
>  	idr_init(&chip->policies);
>  	INIT_LIST_HEAD(&chip->msts);
> +	INIT_LIST_HEAD(&chip->tcam.entries);
>  
>  	return chip;
>  }
> @@ -7184,6 +7212,8 @@ static const struct dsa_switch_ops mv88e6xxx_switch_ops = {
>  	.port_hwtstamp_get	= mv88e6xxx_port_hwtstamp_get,
>  	.port_txtstamp		= mv88e6xxx_port_txtstamp,
>  	.port_rxtstamp		= mv88e6xxx_port_rxtstamp,
> +	.cls_flower_add		= mv88e6xxx_cls_flower_add,
> +	.cls_flower_del         = mv88e6xxx_cls_flower_del,
>  	.get_ts_info		= mv88e6xxx_get_ts_info,
>  	.devlink_param_get	= mv88e6xxx_devlink_param_get,
>  	.devlink_param_set	= mv88e6xxx_devlink_param_set,
> diff --git a/drivers/net/dsa/mv88e6xxx/chip.h b/drivers/net/dsa/mv88e6xxx/chip.h
> index e073446ee7d0..1734bc4666b0 100644
> --- a/drivers/net/dsa/mv88e6xxx/chip.h
> +++ b/drivers/net/dsa/mv88e6xxx/chip.h
> @@ -141,6 +141,7 @@ struct mv88e6xxx_info {
>  	unsigned int phy_base_addr;
>  	unsigned int global1_addr;
>  	unsigned int global2_addr;
> +	unsigned int tcam_addr;
>  	unsigned int age_time_coeff;
>  	unsigned int g1_irqs;
>  	unsigned int g2_irqs;
> @@ -210,6 +211,7 @@ struct mv88e6xxx_avb_ops;
>  struct mv88e6xxx_ptp_ops;
>  struct mv88e6xxx_pcs_ops;
>  struct mv88e6xxx_cc_coeffs;
> +struct mv88e6xxx_tcam_ops;
>  
>  struct mv88e6xxx_irq {
>  	u16 masked;
> @@ -339,6 +341,10 @@ struct mv88e6xxx_hw_stat {
>  	int type;
>  };
>  
> +struct mv88e6xxx_tcam {
> +	struct list_head entries;
> +};
> +
>  struct mv88e6xxx_chip {
>  	const struct mv88e6xxx_info *info;
>  
> @@ -444,6 +450,35 @@ struct mv88e6xxx_chip {
>  
>  	/* FID map */
>  	DECLARE_BITMAP(fid_bitmap, MV88E6XXX_N_FID);
> +
> +	/* TCAM entries */
> +	struct mv88e6xxx_tcam tcam;
> +};
> +
> +#define TCAM_MATCH_SIZE 96
> +
> +struct mv88e6xxx_tcam_key {
> +	u16 spv;
> +	u16 spv_mask;
> +
> +	u8 frame_data[TCAM_MATCH_SIZE];
> +	u8 frame_mask[TCAM_MATCH_SIZE];
> +};
> +
> +struct mv88e6xxx_tcam_action {
> +	u8 dpv_mode;
> +	u16 dpv;
> +};
> +
> +struct mv88e6xxx_tcam_entry {
> +	struct list_head list;
> +	unsigned long cookie;
> +	u32 prio;
> +	u8 hw_idx;
> +
> +	struct mv88e6xxx_tcam_key key;
> +	struct mv88e6xxx_tcam_action action;
> +
>  };
>  
>  struct mv88e6xxx_bus_ops {
> @@ -678,6 +713,11 @@ struct mv88e6xxx_ops {
>  
>  	/* Max Frame Size */
>  	int (*set_max_frame_size)(struct mv88e6xxx_chip *chip, int mtu);
> +
> +	int (*port_enable_tcam)(struct mv88e6xxx_chip *chip, int port);
> +
> +	/* Ternary Content Addressable Memory operations */
> +	const struct mv88e6xxx_tcam_ops *tcam_ops;
>  };
>  
>  struct mv88e6xxx_irq_ops {
> @@ -752,6 +792,11 @@ struct mv88e6xxx_pcs_ops {
>  
>  };
>  
> +struct mv88e6xxx_tcam_ops {
> +	int (*entry_add)(struct mv88e6xxx_chip *chip, struct mv88e6xxx_tcam_entry *entry, u8 idx);
> +	int (*flush_tcam)(struct mv88e6xxx_chip *chip);
> +};
> +
>  static inline bool mv88e6xxx_has_stu(struct mv88e6xxx_chip *chip)
>  {
>  	return chip->info->max_sid > 0 &&
> @@ -769,6 +814,11 @@ static inline bool mv88e6xxx_has_lag(struct mv88e6xxx_chip *chip)
>  	return !!chip->info->global2_addr;
>  }
>  
> +static inline bool mv88e6xxx_has_tcam(struct mv88e6xxx_chip *chip)
> +{
> +	return !!chip->info->tcam_addr;
> +}
> +
>  static inline unsigned int mv88e6xxx_num_databases(struct mv88e6xxx_chip *chip)
>  {
>  	return chip->info->num_databases;
> diff --git a/drivers/net/dsa/mv88e6xxx/port.c b/drivers/net/dsa/mv88e6xxx/port.c
> index 66b1b7277281..e9d0289f91ad 100644
> --- a/drivers/net/dsa/mv88e6xxx/port.c
> +++ b/drivers/net/dsa/mv88e6xxx/port.c
> @@ -1380,7 +1380,28 @@ int mv88e6xxx_port_disable_learn_limit(struct mv88e6xxx_chip *chip, int port)
>  
>  int mv88e6xxx_port_disable_pri_override(struct mv88e6xxx_chip *chip, int port)
>  {
> -	return mv88e6xxx_port_write(chip, port, MV88E6XXX_PORT_PRI_OVERRIDE, 0);
> +	u16 reg;
> +	int err;
> +
> +	err = mv88e6xxx_port_read(chip, port, MV88E6XXX_PORT_PRI_OVERRIDE, &reg);
> +	if (err)
> +		return err;
> +
> +	reg &= MV88E6XXX_PORT_PRI_OVERRIDE_TCAM_MODE_MASK;
> +	return mv88e6xxx_port_write(chip, port, MV88E6XXX_PORT_PRI_OVERRIDE, reg);
> +}
> +
> +int mv88e6xxx_port_enable_tcam(struct mv88e6xxx_chip *chip, int port)
> +{
> +	u16 reg;
> +	int err;
> +
> +	err = mv88e6xxx_port_read(chip, port, MV88E6XXX_PORT_PRI_OVERRIDE, &reg);
> +	if (err)
> +		return err;
> +
> +	reg |= MV88E6XXX_PORT_PRI_OVERRIDE_TCAM_MODE_96_BYTE;
> +	return mv88e6xxx_port_write(chip, port, MV88E6XXX_PORT_PRI_OVERRIDE, reg);
>  }
>  
>  /* Offset 0x0E: Policy & MGMT Control Register for FAMILY 6191X 6193X 6393X */
> diff --git a/drivers/net/dsa/mv88e6xxx/port.h b/drivers/net/dsa/mv88e6xxx/port.h
> index c1d2f99efb1c..a2492cf4d920 100644
> --- a/drivers/net/dsa/mv88e6xxx/port.h
> +++ b/drivers/net/dsa/mv88e6xxx/port.h
> @@ -254,7 +254,10 @@
>  #define MV88E6XXX_PORT_ATU_CTL		0x0c
>  
>  /* Offset 0x0D: Priority Override Register */
> -#define MV88E6XXX_PORT_PRI_OVERRIDE	0x0d
> +#define MV88E6XXX_PORT_PRI_OVERRIDE			0x0d
> +#define MV88E6XXX_PORT_PRI_OVERRIDE_TCAM_MODE_MASK	0x0003
> +#define MV88E6XXX_PORT_PRI_OVERRIDE_TCAM_MODE_48_BYTE	0x0001
> +#define MV88E6XXX_PORT_PRI_OVERRIDE_TCAM_MODE_96_BYTE	0x0002
>  
>  /* Offset 0x0E: Policy Control Register */
>  #define MV88E6XXX_PORT_POLICY_CTL		0x0e
> @@ -608,4 +611,6 @@ int mv88e6xxx_port_hidden_wait(struct mv88e6xxx_chip *chip);
>  int mv88e6xxx_port_hidden_read(struct mv88e6xxx_chip *chip, int block, int port,
>  			       int reg, u16 *val);
>  
> +int mv88e6xxx_port_enable_tcam(struct mv88e6xxx_chip *chip, int port);
> +
>  #endif /* _MV88E6XXX_PORT_H */
> diff --git a/drivers/net/dsa/mv88e6xxx/tcam.c b/drivers/net/dsa/mv88e6xxx/tcam.c
> new file mode 100644
> index 000000000000..51cf26cdc77f
> --- /dev/null
> +++ b/drivers/net/dsa/mv88e6xxx/tcam.c
> @@ -0,0 +1,310 @@
> +// SPDX-License-Identifier: GPL-2.0-or-later
> +/*
> + * Marvell 88E6xxx Switch TCAM support
> + *
> + * Copyright (c) 2026 Luminex Network Intelligence
> + */
> +
> +#include "linux/list.h"
> +
> +#include "chip.h"
> +#include "tcam.h"
> +
> +/* TCAM operatation register */
> +#define MV88E6XXX_TCAM_OP			0x00
> +#define MV88E6XXX_TCAM_OP_BUSY			0x8000
> +#define MV88E6XXX_TCAM_OP_OP_MASK		0x7000
> +#define MV88E6XXX_TCAM_OP_OP_FLUSH_ALL		0x1000
> +#define MV88E6XXX_TCAM_OP_OP_FLUSH		0x2000
> +#define MV88E6XXX_TCAM_OP_OP_LOAD		0x3000
> +#define MV88E6XXX_TCAM_OP_OP_GET_NEXT		0x4000
> +#define MV88E6XXX_TCAM_OP_OP_READ		0x5000
> +
> +/* TCAM extension register */
> +#define MV88E6XXX_TCAM_EXTENSION		0x01
> +
> +/* TCAM keys register 1 */
> +#define MV88E6XXX_TCAM_KEYS1			0x02
> +#define MV88E6XXX_TCAM_KEYS1_FT_MASK		0xC000
> +#define MV88E6XXX_TCAM_KEYS1_SPV_MASK		0x0007
> +
> +/* TCAM keys register 1 */
> +#define MV88E6XXX_TCAM_KEYS2			0x03
> +#define MV88E6XXX_TCAM_KEYS2_SPV_MASK		0x00ff
> +
> +#define MV88E6XXX_TCAM_ING_ACT3			0x04
> +#define MV88E6XXX_TCAM_ING_ACT3_SF		0x0800
> +#define MV88E6XXX_TCAM_ING_ACT3_DPV_MASK	0x07ff
> +
> +#define MV88E6XXX_TCAM_ING_ACT5			0x06
> +#define MV88E6XXX_TCAM_ING_ACT5_DPV_MODE_MASK	0xc000
> +
> +static int mv88e6xxx_tcam_write(struct mv88e6xxx_chip *chip, int reg, u16 val)
> +{
> +	return mv88e6xxx_write(chip, chip->info->tcam_addr, reg, val);
> +}
> +
> +static int mv88e6xxx_tcam_wait(struct mv88e6xxx_chip *chip)
> +{
> +	int bit = __bf_shf(MV88E6XXX_TCAM_OP_BUSY);
> +
> +	return mv88e6xxx_wait_bit(chip, chip->info->tcam_addr,
> +				  MV88E6XXX_TCAM_OP, bit, 0);
> +}
> +
> +static int mv88e6xxx_tcam_read_page(struct mv88e6xxx_chip *chip, u8 page, u8 entry)
> +
> +{
> +	u16 val = MV88E6XXX_TCAM_OP_BUSY | MV88E6XXX_TCAM_OP_OP_READ | (page & 0x3) << 10 | entry;
> +	int rc;
> +
> +	rc = mv88e6xxx_tcam_write(chip, MV88E6XXX_TCAM_OP, val);
> +	if (rc)
> +		return rc;
> +
> +	return mv88e6xxx_tcam_wait(chip);
> +}
> +
> +static int mv88e6xxx_tcam_load_page(struct mv88e6xxx_chip *chip, u8 page, u8 entry)
> +{
> +	u16 val = MV88E6XXX_TCAM_OP_BUSY | MV88E6XXX_TCAM_OP_OP_LOAD | (page & 0x3) << 10 | entry;
> +	int rc;
> +
> +	rc = mv88e6xxx_tcam_write(chip, MV88E6XXX_TCAM_OP, val);
> +	if (rc)
> +		return rc;
> +
> +	return mv88e6xxx_tcam_wait(chip);
> +}
> +
> +static int mv88e6xxx_tcam_flush_entry(struct mv88e6xxx_chip *chip, u8 entry)
> +{
> +	u16 val = MV88E6XXX_TCAM_OP_BUSY | MV88E6XXX_TCAM_OP_OP_FLUSH | entry;
> +	int rc;
> +
> +	rc = mv88e6xxx_tcam_write(chip, MV88E6XXX_TCAM_OP, val);
> +	if (rc)
> +		return rc;
> +
> +	return mv88e6xxx_tcam_wait(chip);
> +}
> +
> +static int mv88e6xxx_tcam_flush_all(struct mv88e6xxx_chip *chip)
> +{
> +	u16 val = MV88E6XXX_TCAM_OP_BUSY | MV88E6XXX_TCAM_OP_OP_FLUSH_ALL;
> +	int rc;
> +
> +	rc = mv88e6xxx_tcam_write(chip, MV88E6XXX_TCAM_OP, val);
> +	if (rc)
> +		return rc;
> +
> +	return mv88e6xxx_tcam_wait(chip);
> +}
> +
> +struct mv88e6xxx_tcam_entry *mv88e6xxx_tcam_entry_find(struct mv88e6xxx_chip *chip,
> +						       unsigned long cookie)
> +{
> +	struct mv88e6xxx_tcam_entry *entry;
> +
> +	list_for_each_entry(entry, &chip->tcam.entries, list)
> +		if (entry->cookie == cookie)
> +			return entry;
> +
> +	return NULL;
> +}
> +
> +/* insert tcam entry in ordered list and move existing entries if necessary */
> +static int mv88e6xxx_tcam_insert_entry(struct mv88e6xxx_chip *chip,
> +				       struct mv88e6xxx_tcam_entry *entry)
> +{
> +	struct mv88e6xxx_tcam_entry *elem;
> +	struct list_head *hpos;
> +	int rc;
> +
> +	list_for_each_prev(hpos, &chip->tcam.entries) {
> +		elem = list_entry(hpos, struct mv88e6xxx_tcam_entry, list);
> +		if (entry->prio >= elem->prio)
> +			break;
> +
> +		u8 move_idx = elem->hw_idx + 1;
> +
> +		mv88e6xxx_reg_lock(chip);
> +		rc = mv88e6xxx_tcam_flush_entry(chip, move_idx);
> +		mv88e6xxx_reg_unlock(chip);
> +		if (rc)
> +			return rc;
> +		mv88e6xxx_reg_lock(chip);
> +		rc = chip->info->ops->tcam_ops->entry_add(chip, elem, move_idx);
> +		mv88e6xxx_reg_unlock(chip);
> +		if (rc)
> +			return rc;
> +
> +		elem->hw_idx = move_idx;
> +	}
> +
> +	if (list_is_head(hpos, &chip->tcam.entries)) {
> +		entry->hw_idx = 0;
> +	} else {
> +		elem = list_entry(hpos, struct mv88e6xxx_tcam_entry, list);
> +		entry->hw_idx = elem->hw_idx + 1;
> +	}
> +	list_add(&entry->list, hpos);
> +	return 0;
> +}
> +
> +int mv88e6xxx_tcam_entry_add(struct mv88e6xxx_chip *chip, struct mv88e6xxx_tcam_entry *entry)
> +{
> +	int rc;
> +
> +	rc = mv88e6xxx_tcam_insert_entry(chip, entry);
> +	if (rc)
> +		return rc;
> +
> +	mv88e6xxx_reg_lock(chip);
> +	rc = mv88e6xxx_tcam_flush_entry(chip, entry->hw_idx);
> +	mv88e6xxx_reg_unlock(chip);
> +	if (rc)
> +		goto unlink_out;
> +	mv88e6xxx_reg_lock(chip);
> +	rc = chip->info->ops->tcam_ops->entry_add(chip, entry, entry->hw_idx);
> +	mv88e6xxx_reg_unlock(chip);
> +	if (rc)
> +		goto unlink_out;
> +
> +	return 0;
> +
> +unlink_out:
> +	list_del(&entry->list);
> +	return rc;
> +}
> +
> +int mv88e6xxx_tcam_entry_del(struct mv88e6xxx_chip *chip, struct mv88e6xxx_tcam_entry *entry)
> +{
> +	struct mv88e6xxx_tcam_entry *elem = entry;
> +	int rc;
> +	u8 move_idx = entry->hw_idx;
> +
> +	mv88e6xxx_reg_lock(chip);
> +	rc = mv88e6xxx_tcam_flush_entry(chip, entry->hw_idx);
> +	mv88e6xxx_reg_unlock(chip);
> +
> +	/* move entries that come after the deleted entry forward */
> +	list_for_each_entry_continue(elem, &chip->tcam.entries, list) {
> +		u8 tmp_idx = elem->hw_idx;
> +
> +		mv88e6xxx_reg_lock(chip);
> +		rc = chip->info->ops->tcam_ops->entry_add(chip, elem, move_idx);
> +		mv88e6xxx_reg_unlock(chip);
> +
> +		elem->hw_idx =  move_idx;
> +		move_idx = tmp_idx;
> +
> +		/* flush the last entry after moving entries */
> +		if (list_is_last(&elem->list, &chip->tcam.entries)) {
> +			mv88e6xxx_reg_lock(chip);
> +			rc = mv88e6xxx_tcam_flush_entry(chip, tmp_idx);
> +			mv88e6xxx_reg_unlock(chip);
> +		}
> +	}
> +
> +	list_del(&entry->list);
> +	return rc;
> +}
> +
> +static int mv88e6390_tcam_entry_add(struct mv88e6xxx_chip *chip,
> +				    struct mv88e6xxx_tcam_entry *entry, u8 idx)
> +{
> +	int i;
> +	int err = 0;
> +
> +	err = mv88e6xxx_tcam_read_page(chip, 2, idx);
> +	if (err)
> +		return err;
> +	if (entry->action.dpv_mode != 0) {
> +		u16 val = MV88E6XXX_TCAM_ING_ACT3_SF |
> +			 (entry->action.dpv & MV88E6XXX_TCAM_ING_ACT3_DPV_MASK);
> +
> +		err = mv88e6xxx_tcam_write(chip, MV88E6XXX_TCAM_ING_ACT3, val);
> +		if (err)
> +			return err;
> +		val = entry->action.dpv_mode << 14;
> +		err = mv88e6xxx_tcam_write(chip, MV88E6XXX_TCAM_ING_ACT5, val);
> +		if (err)
> +			return err;
> +	}
> +	err = mv88e6xxx_tcam_load_page(chip, 2, idx);
> +	if (err)
> +		return err;
> +
> +	err = mv88e6xxx_tcam_read_page(chip, 1, idx);
> +	if (err)
> +		return err;
> +
> +	for (i = PAGE0_MATCH_SIZE; i < PAGE0_MATCH_SIZE + PAGE1_MATCH_SIZE; i++) {
> +		if (entry->key.frame_mask[i]) {
> +			u16 val = entry->key.frame_mask[i] << 8 | entry->key.frame_data[i];
> +
> +			err = mv88e6xxx_tcam_write(chip, i - PAGE0_MATCH_SIZE + 2, val);
> +			if (err)
> +				return err;
> +		}
> +	}
> +	err = mv88e6xxx_tcam_load_page(chip, 1, idx);
> +	if (err)
> +		return err;
> +
> +	err = mv88e6xxx_tcam_read_page(chip, 0, idx);
> +	if (err)
> +		return err;
> +	for (i = 0; i < PAGE0_MATCH_SIZE; i++) {
> +		if (entry->key.frame_mask[i]) {
> +			u16 val = entry->key.frame_mask[i] << 8 | entry->key.frame_data[i];
> +
> +			err = mv88e6xxx_tcam_write(chip, i + 6, val);
> +			if (err)
> +				return err;
> +		}
> +	}
> +	u16 spv_mask = entry->key.spv_mask & mv88e6xxx_port_mask(chip);
> +	u16 spv = entry->key.spv & mv88e6xxx_port_mask(chip);
> +	// frame type mask bits must be set for a valid entry

Wrong comment style. Use /* ... */ also for single-line comments.

> +	err = mv88e6xxx_tcam_write(chip, MV88E6XXX_TCAM_KEYS1,
> +				   MV88E6XXX_TCAM_KEYS1_FT_MASK | (spv_mask & 0x0700) | (spv >> 8));
> +	if (err)
> +		return err;
> +
> +	err = mv88e6xxx_tcam_write(chip, MV88E6XXX_TCAM_KEYS2,
> +				   (spv_mask << 8) | (spv & 0x00ff));
> +	if (err)
> +		return err;
> +	err = mv88e6xxx_tcam_load_page(chip, 0, idx);
> +	if (err)
> +		return err;
> +
> +	entry->hw_idx = idx;
> +	return 0;
> +}
> +
> +static int mv88e6393_tcam_entry_add(struct mv88e6xxx_chip *chip,
> +				    struct mv88e6xxx_tcam_entry *entry, u8 idx)
> +{
> +	int err;
> +
> +	// select block 0 port 0, then adding an entry is the same as 6390 as other
> +	// blocks aren't used at the moment

Multiline comments also shouldn't use // style

> +	err = mv88e6xxx_tcam_write(chip, MV88E6XXX_TCAM_EXTENSION, 0x00);
> +	if (err)
> +		return err;
> +
> +	return mv88e6390_tcam_entry_add(chip, entry, idx);
> +}
> +
> +const struct mv88e6xxx_tcam_ops mv88e6390_tcam_ops = {
> +	.entry_add = mv88e6390_tcam_entry_add,
> +	.flush_tcam = mv88e6xxx_tcam_flush_all,
> +};
> +
> +const struct mv88e6xxx_tcam_ops mv88e6393_tcam_ops = {
> +	.entry_add = mv88e6393_tcam_entry_add,
> +	.flush_tcam = mv88e6xxx_tcam_flush_all,
> +};
> diff --git a/drivers/net/dsa/mv88e6xxx/tcam.h b/drivers/net/dsa/mv88e6xxx/tcam.h
> new file mode 100644
> index 000000000000..5ef5cb8d69d1
> --- /dev/null
> +++ b/drivers/net/dsa/mv88e6xxx/tcam.h
> @@ -0,0 +1,38 @@
> +/* SPDX-License-Identifier: GPL-2.0-or-later */
> +
> +/*
> + * Copyright (c) 2026 Luminex Network Intelligence
> + */
> +#ifndef _MV88E6XXX_TCAM_H_
> +#define _MV88E6XXX_TCAM_H_
> +
> +#define PAGE0_MATCH_SIZE 22
> +#define PAGE1_MATCH_SIZE 26
> +
> +#define DPV_MODE_NOP		0x0
> +#define DPV_MODE_AND		0x1
> +#define DPV_MODE_OR		0x2
> +#define DPV_MODE_REPLACE	0x3
> +
> +int mv88e6xxx_tcam_entry_add(struct mv88e6xxx_chip *chip, struct mv88e6xxx_tcam_entry *entry);
> +int mv88e6xxx_tcam_entry_del(struct mv88e6xxx_chip *chip, struct mv88e6xxx_tcam_entry *entry);
> +struct mv88e6xxx_tcam_entry *mv88e6xxx_tcam_entry_find(struct mv88e6xxx_chip *chip,
> +						       unsigned long cookie);
> +#define mv88e6xxx_tcam_match_set(key, _offset, data, mask) \
> +	do { \
> +		typeof(_offset) (offset) = (_offset); \
> +		BUILD_BUG_ON((offset) + sizeof((data)) > TCAM_MATCH_SIZE); \
> +		__mv88e6xxx_tcam_match_set(key, offset, sizeof(data), \
> +					   (u8 *)&(data), (u8 *)&(mask)); \
> +	} while (0)
> +
> +static inline void __mv88e6xxx_tcam_match_set(struct mv88e6xxx_tcam_key *key, unsigned int offset,
> +					      size_t size, u8 *data, u8 *mask)
> +{
> +	memcpy(&key->frame_data[offset], data, size);
> +	memcpy(&key->frame_mask[offset], mask, size);
> +}
> +
> +extern const struct mv88e6xxx_tcam_ops mv88e6390_tcam_ops;
> +extern const struct mv88e6xxx_tcam_ops mv88e6393_tcam_ops;
> +#endif
> diff --git a/drivers/net/dsa/mv88e6xxx/tcflower.c b/drivers/net/dsa/mv88e6xxx/tcflower.c
> new file mode 100644
> index 000000000000..4e0cc65759ef
> --- /dev/null
> +++ b/drivers/net/dsa/mv88e6xxx/tcflower.c
> @@ -0,0 +1,138 @@
> +// SPDX-License-Identifier: GPL-2.0-or-later
> +/*
> + * Marvell 88E6xxx Switch flower support
> + *
> + * Copyright (c) 2026 Luminex Network Intelligence
> + */
> +
> +#include "chip.h"
> +#include "tcflower.h"
> +#include "tcam.h"
> +
> +static int mv88e6xx_flower_parse_key(struct mv88e6xxx_chip *chip,
> +				     struct netlink_ext_ack *extack,
> +				     struct flow_cls_offload *cls,
> +				     struct mv88e6xxx_tcam_key *key)
> +{
> +	struct flow_rule *rule = flow_cls_offload_flow_rule(cls);
> +	struct flow_dissector *dissector = rule->match.dissector;
> +	u16 addr_type = 0;
> +
> +	if (dissector->used_keys &
> +	    ~(BIT_ULL(FLOW_DISSECTOR_KEY_BASIC) |
> +	      BIT_ULL(FLOW_DISSECTOR_KEY_CONTROL) |
> +	      BIT_ULL(FLOW_DISSECTOR_KEY_IPV4_ADDRS) |
> +	      BIT_ULL(FLOW_DISSECTOR_KEY_IPV6_ADDRS))) {
> +		NL_SET_ERR_MSG_MOD(extack,
> +				   "Unsupported keys used");
> +		dev_warn(chip->dev, "used_keys: 0x%llx", dissector->used_keys);

Why issue two error messages instead of a single one?

> +		return -EOPNOTSUPP;
> +	}
> +
> +	if (flow_rule_match_key(rule, FLOW_DISSECTOR_KEY_CONTROL)) {
> +		struct flow_match_control match;
> +
> +		flow_rule_match_control(rule, &match);
> +		addr_type = match.key->addr_type;
> +
> +		if (flow_rule_has_control_flags(match.mask->flags,
> +						cls->common.extack))
> +			return -EOPNOTSUPP;
> +	}
> +	if (flow_rule_match_key(rule, FLOW_DISSECTOR_KEY_BASIC)) {
> +		struct flow_match_basic match;
> +
> +		flow_rule_match_basic(rule, &match);
> +		// ethertype

Another //-style comment which should be /* ... */

> +		mv88e6xxx_tcam_match_set(key, 16, match.key->n_proto, match.mask->n_proto);
> +		mv88e6xxx_tcam_match_set(key, 27, match.key->ip_proto, match.mask->ip_proto);
> +	}
> +
> +	if (addr_type == FLOW_DISSECTOR_KEY_IPV4_ADDRS) {
> +		struct flow_match_ipv4_addrs match;
> +
> +		flow_rule_match_ipv4_addrs(cls->rule, &match);
> +		mv88e6xxx_tcam_match_set(key, 30, match.key->src, match.mask->src);
> +		mv88e6xxx_tcam_match_set(key, 34, match.key->dst, match.mask->dst);
> +	}
> +
> +	return 0;
> +}
> +
> +int mv88e6xxx_cls_flower_add(struct dsa_switch *ds, int port,
> +			     struct flow_cls_offload *cls, bool ingress)
> +{
> +	struct mv88e6xxx_chip *chip = ds->priv;
> +	struct flow_rule *rule = flow_cls_offload_flow_rule(cls);
> +	struct netlink_ext_ack *extack = cls->common.extack;
> +	unsigned long cookie = cls->cookie;
> +	const struct flow_action_entry *act;
> +	struct mv88e6xxx_tcam_key key = { 0 };
> +	struct mv88e6xxx_tcam_entry *entry;
> +	int rc, i;
> +
> +	if (!mv88e6xxx_has_tcam(chip)) {
> +		NL_SET_ERR_MSG_MOD(extack, "hardware offload not supported");
> +		return -EOPNOTSUPP;
> +	}
> +
> +	rc = mv88e6xx_flower_parse_key(chip, extack, cls, &key);
> +	if (rc)
> +		return rc;
> +
> +	entry = mv88e6xxx_tcam_entry_find(chip, cookie);
> +	if (entry)
> +		return -EEXIST;
> +
> +	entry = kzalloc(sizeof(*entry), GFP_KERNEL);
> +	if (!entry)
> +		return -ENOMEM;
> +
> +	entry->cookie = cookie;
> +	entry->prio = cls->common.prio;
> +	entry->key = key;
> +
> +	flow_action_for_each(i, act, &rule->action) {
> +		switch (act->id) {
> +		case FLOW_ACTION_TRAP: {
> +			int cpu = dsa_upstream_port(ds, port);
> +
> +			entry->action.dpv_mode = DPV_MODE_REPLACE;
> +			entry->action.dpv = BIT(cpu);
> +			break;
> +		}
> +		default: {
> +			NL_SET_ERR_MSG_MOD(extack, "action not supported");
> +			rc = -EOPNOTSUPP;
> +			goto err_free_entry;
> +		}
> +		}
> +	}
> +
> +	entry->key.spv = BIT(port);
> +	entry->key.spv_mask = mv88e6xxx_port_mask(chip);
> +
> +	rc = mv88e6xxx_tcam_entry_add(chip, entry);
> +	if (rc)
> +		goto err_free_entry;
> +
> +	return  0;
> +
> +err_free_entry:
> +	kfree(entry);
> +	return rc;
> +}
> +
> +int mv88e6xxx_cls_flower_del(struct dsa_switch *ds, int port,
> +			     struct flow_cls_offload *cls, bool ingress)
> +{
> +	struct mv88e6xxx_chip *chip = ds->priv;
> +	struct mv88e6xxx_tcam_entry *entry = mv88e6xxx_tcam_entry_find(chip, cls->cookie);
> +	int rc = 0;
> +
> +	if (entry) {
> +		rc = mv88e6xxx_tcam_entry_del(chip, entry);
> +		kfree(entry);
> +	}
> +	return  rc;
> +}
> diff --git a/drivers/net/dsa/mv88e6xxx/tcflower.h b/drivers/net/dsa/mv88e6xxx/tcflower.h
> new file mode 100644
> index 000000000000..96bb8da80bec
> --- /dev/null
> +++ b/drivers/net/dsa/mv88e6xxx/tcflower.h
> @@ -0,0 +1,13 @@
> +/* SPDX-License-Identifier: GPL-2.0-or-later */
> +
> +/*
> + * Copyright (c) 2026 Luminex Network Intelligence
> + */
> +#ifndef _MV88E6XXX_TCFLOWER_H_
> +#define _MV88E6XXX_TCFLOWER_H_
> +
> +int mv88e6xxx_cls_flower_add(struct dsa_switch *ds, int port,
> +			     struct flow_cls_offload *cls, bool ingress);
> +int mv88e6xxx_cls_flower_del(struct dsa_switch *ds, int port,
> +			     struct flow_cls_offload *cls, bool ingress);
> +#endif
> 
> ---
> base-commit: cd51b40495c09b92beea6893663b3d0ed7605e81
> change-id: 20260213-net-next-mv88e6xxx-tcam-f65be16008fb
> 
> Best regards,
> -- 
> Cedric Jehasse <[email-masked]>
> 
> 
>