Linux-ARM-Kernel Archive on lore.kernel.org
 help / color / mirror / Atom feed
* Re: [PATCH] net: macb: add TX stall timeout callback to recover from lost TSTART write
From: Andrea della Porta @ 2026-06-12 12:51 UTC (permalink / raw)
  To: Nicolai Buchwitz
  Cc: Andrea della Porta, netdev, Theo Lebrun, Nicolas Ferre,
	Claudiu Beznea, Andrew Lunn, David S . Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni, linux-kernel, linux-arm-kernel,
	linux-rpi-kernel, Lukasz Raczylo, Steffen Jaeckel
In-Reply-To: <dbc25b24865a1ad92555bc826bef4267@tipi-net.de>

Hi Nicolai,

On 14:23 Fri 12 Jun     , Nicolai Buchwitz wrote:
> Hi Andrea
> 
> On 12.6.2026 11:01, Andrea della Porta wrote:
> > From: Lukasz Raczylo <lukasz@raczylo.com>
> > 
> > The MACB found in the Raspberry Pi RP1 suffers from sporadic stalls on
> > the TX queue.
> > While the exact root cause is not yet fully understood, it is likely
> > related to a hardware issue where a TSTART write to the NCR register
> > is missed, preventing the transmission from being kicked off.
> > 
> > Implement a timeout callback to handle TX queue stalls, triggering the
> > existing restart mechanism to recover.
> > 
> > Link:
> > https://lore.kernel.org/all/20260514215459.36109-1-lukasz@raczylo.com/
> > Fixes: dc110d1b23564 ("net: cadence: macb: Add support for Raspberry Pi
> > RP1 ethernet controller")
> > Signed-off-by: Lukasz Raczylo <lukasz@raczylo.com>
> > Co-developed-by: Steffen Jaeckel <sjaeckel@suse.de>
> > Signed-off-by: Steffen Jaeckel <sjaeckel@suse.de>
> > Co-developed-by: Andrea della Porta <andrea.porta@suse.com>
> > Signed-off-by: Andrea della Porta <andrea.porta@suse.com>
> > ---
> >  drivers/net/ethernet/cadence/macb_main.c | 11 +++++++++++
> >  1 file changed, 11 insertions(+)
> > 
> > diff --git a/drivers/net/ethernet/cadence/macb_main.c
> > b/drivers/net/ethernet/cadence/macb_main.c
> > index a12aa21244e83..615da65d5d68d 100644
> > --- a/drivers/net/ethernet/cadence/macb_main.c
> > +++ b/drivers/net/ethernet/cadence/macb_main.c
> > @@ -4522,6 +4522,16 @@ static int macb_setup_tc(struct net_device *dev,
> > enum tc_setup_type type,
> >  	}
> >  }
> > 
> > +static void macb_tx_timeout(struct net_device *dev, unsigned int q)
> > +{
> > +	struct macb *bp = netdev_priv(dev);
> > +
> > +	if (net_ratelimit())
> 
> Do we need the net_ratelimit() check (and message) here? AFAIU the watchdog
> core already prints a message for every timeout.

Correct. I'll drop those two lines.

> 
> > +		netdev_err(dev, "TX stall detected, re-kicking TSTART\n");
> > +	dev->stats.tx_errors++;
> > +	macb_tx_restart(&bp->queues[q]);
> > +}
> > +
> >  static const struct net_device_ops macb_netdev_ops = {
> >  	.ndo_open		= macb_open,
> >  	.ndo_stop		= macb_close,
> > @@ -4540,6 +4550,7 @@ static const struct net_device_ops macb_netdev_ops
> > = {
> >  	.ndo_hwtstamp_set	= macb_hwtstamp_set,
> >  	.ndo_hwtstamp_get	= macb_hwtstamp_get,
> >  	.ndo_setup_tc		= macb_setup_tc,
> > +	.ndo_tx_timeout		= macb_tx_timeout,
> 
> The commit message describes it as RP1 specific, but it gets applied to all
> other variants?

I've seen this issue happening only on RaspberryPi 5, but AFAIK it
could affect also other MACB blocks connected through PCIe, so it
may be widespread (even though it should have probably already been
noticed in the past). In the orginal driver there's no timeout callback
defined and this is much like pretgending the issue causing the timeout
to happen to go away without doing anything (whatever the cause ot the
specific hw are). So in my opinion we can just extend that to all MACB.
Or maybe we should execute the restart conditionally on 
.compatible = "raspberrypi,rp1-gem"?

Thanks,
Andrea

> 
> >  };
> > 
> >  /* Configure peripheral capabilities according to device tree
> 
> Thanks
> Nicolai


^ permalink raw reply

* Re: [PATCH 15/37] drm/display: bridge-connector: allocate the connector dynamically
From: Luca Ceresoli @ 2026-06-12 12:44 UTC (permalink / raw)
  To: Maxime Ripard, Luca Ceresoli
  Cc: Maarten Lankhorst, Thomas Zimmermann, David Airlie, Simona Vetter,
	Andrzej Hajda, Neil Armstrong, Robert Foss, Laurent Pinchart,
	Jonas Karlman, Jernej Skrabec, Inki Dae, Jagan Teki,
	Marek Szyprowski, Marek Vasut, Stefan Agner, Frank Li,
	Sascha Hauer, Pengutronix Kernel Team, Fabio Estevam, Hui Pu,
	Ian Ray, Thomas Petazzoni, dri-devel, linux-kernel, imx,
	linux-arm-kernel
In-Reply-To: <20260608-rustling-infrared-turkey-e09af8@houat>

On Mon Jun 8, 2026 at 1:46 PM CEST, Maxime Ripard wrote:
> On Tue, May 19, 2026 at 12:37:32PM +0200, Luca Ceresoli wrote:
>> Currently the drm_bridge_connector has an embedded drm_connector, so their
>> allocation lifetimes are tied to each other. This is insufficient to
>> support DRM bridge hotplugging, which requires the connector to be added
>> and removed dynamically at runtime multiple times based on hotplug/unplug
>> events while the drm_bridge_connector is persistent.
>>
>> Moreover the drm_connector is exposed to user space and thus an ongoing
>> operation (e.g. an ioctl) might last for an arbitrarily long time even
>> after the hardware gets removed. This means a new connector might have to
>> be added when the previous one is still referenced by user space.
>>
>> In preparation to handle hotplug, allocate the drm-connector dynamically,
>> to allow:
>>
>>  * creating and destroying a connector multiple times during a single
>>    drm_bridge_connector lifetime
>>  * creating a new connector even though the previous one is still in use
>>    and thus still refcounted and not yet freed
>>
>> This commit does not introduce the actions in the two bullets (it will
>> happen in a later commit), it only moves to dynamic APIs for connector
>> allocation and init.
>>
>> Signed-off-by: Luca Ceresoli <luca.ceresoli@bootlin.com>
>
> I think this patch should be split in half, with the switch to using
> destroy first, and then the actual move to the dynamically allocated
> connector API.

Is it doable? drm_connector_dynamic_init() mandates a .destroy callback,
drm_connector_init() forbids it.

Luca

--
Luca Ceresoli, Bootlin
Embedded Linux and Kernel engineering
https://bootlin.com


^ permalink raw reply

* [PATCH net-next 9/9] net: sparx5: add neighbour event handling for L3 routing
From: Jens Emil Schulz Østergaard @ 2026-06-12 12:37 UTC (permalink / raw)
  To: Horatiu Vultur, UNGLinuxDriver, Andrew Lunn, David S. Miller,
	Eric Dumazet, Jakub Kicinski, Paolo Abeni, Daniel Machon,
	Steen Hegelund, Kees Cook, Gustavo A. R. Silva
  Cc: netdev, linux-kernel, linux-arm-kernel, linux-hardening,
	Jens Emil Schulz Østergaard
In-Reply-To: <20260612-sparx5_l3_routing-v1-0-fc3c10160f49@microchip.com>

Register a netevent notifier to handle NETEVENT_NEIGH_UPDATE events,
completing the L3 unicast forwarding data path:

- When ARP/NDP resolves a neighbour, update the hardware ARP table entry
  with the resolved MAC address and notify all linked nexthops to
  refresh their ECMP forwarding state.

- When a neighbour becomes unreachable, tear down the hardware ARP entry
  and mark linked nexthops as unresolved so traffic traps to the CPU.

Reviewed-by: Daniel Machon <daniel.machon@microchip.com>
Reviewed-by: Steen Hegelund <Steen.Hegelund@microchip.com>
Signed-off-by: Jens Emil Schulz Østergaard <jensemil.schulzostergaard@microchip.com>
---
 .../net/ethernet/microchip/sparx5/sparx5_router.c  | 108 ++++++++++++++++++++-
 1 file changed, 107 insertions(+), 1 deletion(-)

diff --git a/drivers/net/ethernet/microchip/sparx5/sparx5_router.c b/drivers/net/ethernet/microchip/sparx5/sparx5_router.c
index 5f6b4288755e..4e8950d6535f 100644
--- a/drivers/net/ethernet/microchip/sparx5/sparx5_router.c
+++ b/drivers/net/ethernet/microchip/sparx5/sparx5_router.c
@@ -2525,6 +2525,104 @@ static int sparx5_rr_fib_event(struct notifier_block *nb, unsigned long event,
 	return NOTIFY_BAD;
 }
 
+static void sparx5_rr_neigh_event_work(struct work_struct *work)
+{
+	struct sparx5_rr_netevent_work *net_work =
+		container_of(work, struct sparx5_rr_netevent_work, work);
+	unsigned char hwaddr[ETH_ALEN] __aligned(2);
+	struct sparx5 *sparx5 = net_work->sparx5;
+	struct neighbour *n = net_work->neigh;
+	struct sparx5_rr_neigh_key key = { };
+	struct sparx5_rr_neigh_entry *entry;
+	bool entry_connected;
+	u8 nud_state, dead;
+
+	sparx5_rr_nb2neigh_key(n, &key);
+
+	/* Frames with link-local dip are trapped, so ignore the neighbour. */
+	if (key.iaddr.version == SPARX5_IPV6 &&
+	    ipv6_addr_type(&key.iaddr.ipv6) & IPV6_ADDR_LINKLOCAL)
+		goto out;
+
+	/* If n changes after this read section, we will get another neigh
+	 * event, which is processed after the current one.
+	 */
+	read_lock_bh(&n->lock);
+	ether_addr_copy(hwaddr, n->ha);
+	nud_state = n->nud_state;
+	dead = n->dead;
+	read_unlock_bh(&n->lock);
+
+	mutex_lock(&sparx5->router->lock);
+
+	entry_connected = nud_state & NUD_VALID && !dead;
+	entry = sparx5_rr_neigh_entry_lookup(sparx5, &key);
+	if (!entry_connected && !entry)
+		goto out_mutex;
+
+	if (!entry) {
+		entry = sparx5_rr_neigh_entry_create(sparx5, &key);
+		if (IS_ERR(entry))
+			goto out_mutex;
+	}
+
+	if (entry->connected && entry_connected &&
+	    ether_addr_equal(entry->hwaddr, hwaddr))
+		goto out_mutex;
+
+	ether_addr_copy(entry->hwaddr, hwaddr);
+	sparx5_rr_neigh_entry_update(sparx5, entry, entry_connected);
+	sparx5_rr_nexthops_update_notify(sparx5, entry, entry_connected);
+	if (!entry_connected)
+		sparx5_rr_neigh_entry_put(sparx5, entry);
+
+out_mutex:
+	mutex_unlock(&sparx5->router->lock);
+out:
+	neigh_release(n);
+	kfree(net_work);
+}
+
+/* Handle neighbour update events. Used to manage neigh_entries. Called in
+ * atomic context, with rcu_read_lock().
+ */
+static int sparx5_rr_netevent_event(struct notifier_block *nb,
+				    unsigned long event, void *ptr)
+{
+	struct sparx5_rr_netevent_work *net_work;
+	struct sparx5_router *router;
+	struct sparx5_port *port;
+	struct neighbour *n;
+
+	router = container_of(nb, struct sparx5_router, netevent_nb);
+
+	switch (event) {
+	case NETEVENT_NEIGH_UPDATE:
+		n = ptr;
+
+		if (n->tbl->family != AF_INET && n->tbl->family != AF_INET6)
+			return NOTIFY_DONE;
+
+		port = sparx5_port_dev_lower_find(n->dev);
+		if (!port)
+			return NOTIFY_DONE;
+
+		net_work = kzalloc_obj(*net_work, GFP_ATOMIC);
+		if (!net_work)
+			return NOTIFY_BAD;
+
+		INIT_WORK(&net_work->work, sparx5_rr_neigh_event_work);
+		net_work->sparx5 = router->sparx5;
+		net_work->neigh = neigh_clone(n);
+		net_work->event = event;
+		sparx5_rr_schedule_work(router->sparx5, &net_work->work);
+
+		return NOTIFY_DONE;
+	}
+
+	return NOTIFY_DONE;
+};
+
 static void sparx5_rr_leg_base_mac_set(struct sparx5 *sparx5,
 				       unsigned char mac[ETH_ALEN])
 {
@@ -2903,10 +3001,15 @@ int sparx5_rr_router_init(struct sparx5 *sparx5)
 		 ANA_ACL_VCAP_S2_MISC_CTRL_ACL_RT_SEL, sparx5,
 		 ANA_ACL_VCAP_S2_MISC_CTRL);
 
+	r->netevent_nb.notifier_call = sparx5_rr_netevent_event;
+	err = register_netevent_notifier(&r->netevent_nb);
+	if (err)
+		goto err_workqueue_destroy;
+
 	r->fib_nb.notifier_call = sparx5_rr_fib_event;
 	err = register_fib_notifier(&init_net, &r->fib_nb, NULL, NULL);
 	if (err)
-		goto err_workqueue_destroy;
+		goto err_unreg_netevent_notifier;
 
 	r->inetaddr_nb.notifier_call = sparx5_rr_inetaddr_event;
 	err = register_inetaddr_notifier(&r->inetaddr_nb);
@@ -2945,6 +3048,8 @@ int sparx5_rr_router_init(struct sparx5 *sparx5)
 	unregister_inetaddr_notifier(&r->inetaddr_nb);
 err_unreg_fib_notifier:
 	unregister_fib_notifier(&init_net, &r->fib_nb);
+err_unreg_netevent_notifier:
+	unregister_netevent_notifier(&r->netevent_nb);
 err_workqueue_destroy:
 	destroy_workqueue(r->sparx5_router_owq);
 err_fib_ht_destroy:
@@ -2979,6 +3084,7 @@ void sparx5_rr_router_deinit(struct sparx5 *sparx5)
 	unregister_inetaddr_validator_notifier(&router->inetaddr_valid_nb);
 	unregister_inetaddr_notifier(&router->inetaddr_nb);
 	unregister_fib_notifier(&init_net, &router->fib_nb);
+	unregister_netevent_notifier(&router->netevent_nb);
 	destroy_workqueue(router->sparx5_router_owq);
 	sparx5_rr_fib_flush(sparx5);
 	rhashtable_destroy(&router->fib_ht);

-- 
2.52.0



^ permalink raw reply related

* [PATCH net-next 8/9] net: sparx5: add L3 FIB, nexthop and neighbour entry management
From: Jens Emil Schulz Østergaard @ 2026-06-12 12:37 UTC (permalink / raw)
  To: Horatiu Vultur, UNGLinuxDriver, Andrew Lunn, David S. Miller,
	Eric Dumazet, Jakub Kicinski, Paolo Abeni, Daniel Machon,
	Steen Hegelund, Kees Cook, Gustavo A. R. Silva
  Cc: netdev, linux-kernel, linux-arm-kernel, linux-hardening,
	Jens Emil Schulz Østergaard
In-Reply-To: <20260612-sparx5_l3_routing-v1-0-fc3c10160f49@microchip.com>

Add the data path for L3 unicast route offloading for IPv4 and IPv6 FIB
and nexthop groups.

* FIB entry lifecycle management. FIB entries are mapped to hardware
  using LPM VCAP rules.

* Nexthop group and nexthop lifecycle management. These are mapped to
  hardware using an ARP table for ECMP, and inline vcap actions for
  single nexthops. Each FIB entry has exactly one nexthop group. As a
  simplification each FIB has a copy of its nexthop group, and we do
  not support nexthop objects.

* Neighbour (ARP/NDP) lifecycle management. These are referenced by
  both nexthops and FIB entries, and are shared amongst them. When a
  neighbour referenced by a nexthop is removed we set the HW mac to
  zero which makes sure traffic for the nexthop is trapped. A
  neighbour will also have an associated /32 or /128 route in the LPM
  VCAP for traffic destined directly to the neighbour.

Neighbour entries are created as a side effect of nexthop resolution
but are not yet updated from ARP/NDP events. Neighbour event handling
to activate MAC resolution is added in the next patch.

The three lifecycles are handled in a single patch because they are
mutually entangled. In particular, sparx5_rr_fib_entry_destroy()
walks fib_entry->neigh_list and calls sparx5_rr_neigh_entry_update(),
sparx5_rr_nexthops_update_notify() and sparx5_rr_neigh_entry_put() on
each entry.

The same neigh_entry and nexthop helpers invoked by the
NETEVENT_NEIGH_UPDATE handler added in the following patch. A FIB
teardown must therefore call into the neighbour path, and a neighbour
update must call into the nexthop path, so the three CRUD layers have to
land together for the patch to compile and for the destroy paths to be
consistent.

Reviewed-by: Daniel Machon <daniel.machon@microchip.com>
Reviewed-by: Steen Hegelund <Steen.Hegelund@microchip.com>
Signed-off-by: Jens Emil Schulz Østergaard <jensemil.schulzostergaard@microchip.com>
---
 .../net/ethernet/microchip/sparx5/sparx5_main.h    |    3 +
 .../net/ethernet/microchip/sparx5/sparx5_router.c  | 1942 +++++++++++++++++++-
 2 files changed, 1943 insertions(+), 2 deletions(-)

diff --git a/drivers/net/ethernet/microchip/sparx5/sparx5_main.h b/drivers/net/ethernet/microchip/sparx5/sparx5_main.h
index 5dc18b8dbed0..662d5e013047 100644
--- a/drivers/net/ethernet/microchip/sparx5/sparx5_main.h
+++ b/drivers/net/ethernet/microchip/sparx5/sparx5_main.h
@@ -529,11 +529,14 @@ struct sparx5_router {
 	struct notifier_block netdevice_nb;
 	struct notifier_block inet6addr_nb;
 	struct notifier_block inet6addr_valid_nb;
+	struct rhashtable neigh_ht;
+	struct rhashtable fib_ht;
 	struct sparx5_rr_hw_route link_local; /* Trap all link-local traffic. */
 	struct net_device *port_dev; /* For VCAP API. */
 
 	struct list_head fib_lpm4_list;
 	struct list_head fib_lpm6_list;
+	struct list_head fib_list; /* All fib entries, for teardown */
 	struct mutex lock; /* Global router lock for all shared data. */
 
 	struct workqueue_struct *sparx5_router_owq;
diff --git a/drivers/net/ethernet/microchip/sparx5/sparx5_router.c b/drivers/net/ethernet/microchip/sparx5/sparx5_router.c
index 03923d91fdfb..5f6b4288755e 100644
--- a/drivers/net/ethernet/microchip/sparx5/sparx5_router.c
+++ b/drivers/net/ethernet/microchip/sparx5/sparx5_router.c
@@ -225,6 +225,7 @@ struct sparx5_rr_neigh_entry {
 		struct net_device *dev;
 		struct sparx5_iaddr iaddr;
 	} key;
+	netdevice_tracker dev_tracker;
 	struct rhash_head ht_node;
 	struct sparx5_rr_fib_entry *fib_entry;
 	struct list_head fib_list_node; /* Fib route for this neighbour */
@@ -282,6 +283,7 @@ struct sparx5_rr_fib_entry {
 	enum sparx5_rr_fib_type type;
 	struct rhash_head ht_node; /* Router member */
 	struct list_head fib_lpm_node; /* Router member */
+	struct list_head fib_node; /* Router member, all fib entries */
 	struct list_head neigh_list; /* Neighbours under this route */
 	struct sparx5_rr_hw_route hw_route;
 	struct sparx5_rr_nexthop_group *nh_grp;
@@ -305,6 +307,133 @@ static void sparx5_rr_schedule_work(struct sparx5 *sparx5,
 	queue_work(sparx5->router->sparx5_router_owq, work);
 }
 
+static void sparx5_rr_fib_info_init(struct sparx5_rr_fib_info *fi,
+				    enum sparx5_rr_l3_version version)
+{
+	fi->version = version;
+
+	switch (version) {
+	case SPARX5_IPV4:
+		fi->fen4_info.fi = NULL;
+		return;
+	case SPARX5_IPV6:
+		fi->fe6_info.nrt6 = 0;
+		fi->fe6_info.rt_arr = NULL;
+		return;
+	}
+}
+
+/* Return number of nexthops. */
+static int sparx5_rr_fib_info_nhs(struct sparx5_rr_fib_info *fi)
+{
+	switch (fi->version) {
+	case SPARX5_IPV4:
+		return fib_info_num_path(fi->fen4_info.fi);
+	case SPARX5_IPV6:
+		return fi->fe6_info.nrt6;
+	default:
+		WARN_ON(1);
+		return 0;
+	}
+}
+
+static struct fib_nh_common *
+sparx5_rr_fib_info_nhc(struct sparx5_rr_fib_info *fi, int nhsel)
+{
+	switch (fi->version) {
+	case SPARX5_IPV4:
+		return fib_info_nhc(fi->fen4_info.fi, nhsel);
+	case SPARX5_IPV6:
+		return &fi->fe6_info.rt_arr[nhsel]->fib6_nh->nh_common;
+	default:
+		WARN_ON(1);
+		return NULL;
+	}
+}
+
+static bool sparx5_rr_fib_info_is_nh_obj(struct sparx5_rr_fib_info *fi)
+{
+	switch (fi->version) {
+	case SPARX5_IPV4:
+		return !!fi->fen4_info.fi->nh;
+	case SPARX5_IPV6:
+		return !!fi->fe6_info.rt_arr[0]->nh;
+	default:
+		WARN_ON(1);
+		return false;
+	}
+}
+
+static u8 sparx5_rr_fib_info_type(struct sparx5_rr_fib_info *fi)
+{
+	switch (fi->version) {
+	case SPARX5_IPV4:
+		return fi->fen4_info.type;
+	case SPARX5_IPV6:
+		return fi->fe6_info.rt_arr[0]->fib6_type;
+	default:
+		WARN_ON(1);
+		return RTN_UNSPEC;
+	}
+}
+
+static u32 sparx5_rr_fib_info_tb_id(struct sparx5_rr_fib_info *fi)
+{
+	switch (fi->version) {
+	case SPARX5_IPV4:
+		return fi->fen4_info.tb_id;
+	case SPARX5_IPV6:
+		return fi->fe6_info.rt_arr[0]->fib6_table->tb6_id;
+	default:
+		WARN_ON(1);
+		return RT_TABLE_UNSPEC;
+	}
+}
+
+static bool sparx5_rr_fib_info_should_ignore(struct sparx5_rr_fib_info *fi)
+{
+	return fi->version == SPARX5_IPV6 &&
+	       ipv6_addr_type(&fi->fe6_info.rt_arr[0]->fib6_dst.addr) &
+		       (IPV6_ADDR_MULTICAST | IPV6_ADDR_LINKLOCAL);
+}
+
+#if IS_ENABLED(CONFIG_IPV6)
+static void sparx5_rr_rt6_release(struct fib6_info *rt)
+{
+	if (!rt->nh)
+		rt->fib6_nh->fib_nh_flags &= ~RTNH_F_OFFLOAD;
+
+	fib6_info_release(rt);
+}
+#else
+static void sparx5_rr_rt6_release(struct fib6_info *rt)
+{
+}
+#endif
+
+static void sparx5_rr_fib6_info_put(struct sparx5_rr_fib6_entry_info *fi)
+{
+	for (int i = 0; i < fi->nrt6; i++)
+		sparx5_rr_rt6_release(fi->rt_arr[i]);
+
+	kfree(fi->rt_arr);
+	fi->nrt6 = 0;
+	fi->rt_arr = NULL;
+}
+
+static void sparx5_rr_fib_info_put(struct sparx5_rr_fib_info *fi)
+{
+	if (fi->version == SPARX5_IPV4) {
+		if (fi->fen4_info.fi) {
+			fib_info_put(fi->fen4_info.fi);
+			fi->fen4_info.fi = NULL;
+		}
+		return;
+	}
+
+	sparx5_rr_fib6_info_put(&fi->fe6_info);
+}
+
 static void sparx5_rr_split_mac(unsigned char mac[ETH_ALEN], u32 split,
 				u32 *msb, u32 *lsb)
 {
@@ -315,6 +444,28 @@ static void sparx5_rr_split_mac(unsigned char mac[ETH_ALEN], u32 split,
 	*msb = m >> split;
 }
 
+static int sparx5_rr_arp_tbl_grp_alloc(struct sparx5 *sparx5,
+				       unsigned int nh_grp_size)
+{
+	int offset;
+
+	offset = bitmap_find_next_zero_area(sparx5->router->arp_tbl_mask,
+					    sparx5->data->consts->arp_tbl_cnt,
+					    0, nh_grp_size, 0);
+	if (offset >= sparx5->data->consts->arp_tbl_cnt)
+		return -ENOMEM;
+
+	bitmap_set(sparx5->router->arp_tbl_mask, offset, nh_grp_size);
+
+	return offset;
+}
+
+static void sparx5_rr_arp_tbl_grp_free(struct sparx5 *sparx5,
+				       unsigned int nh_grp_size, int offset)
+{
+	bitmap_clear(sparx5->router->arp_tbl_mask, offset, nh_grp_size);
+}
+
 static int sparx5_vmid_alloc(struct sparx5 *sparx5)
 {
 	int vmid;
@@ -334,6 +485,104 @@ static void sparx5_vmid_free(struct sparx5 *sparx5, u16 vmid)
 	clear_bit(vmid, sparx5->router->vmid_mask);
 }
 
+static void sparx5_rr_nb2neigh_key(struct neighbour *n,
+				   struct sparx5_rr_neigh_key *key)
+{
+	memset(key, 0, sizeof(*key));
+
+	/* The primary_key, tbl->family and dev are constant for the lifetime of
+	 * the neighbour, so we can read them without n->lock.
+	 */
+	if (n->tbl->family == AF_INET) {
+		key->iaddr.version = SPARX5_IPV4;
+		key->iaddr.ipv4 = *(__be32 *)n->primary_key;
+	} else {
+		key->iaddr.version = SPARX5_IPV6;
+		key->iaddr.ipv6 = *(struct in6_addr *)n->primary_key;
+	}
+
+	key->dev = n->dev;
+}
+
+static void
+sparx5_rr_neigh_entry_offload_mark(struct sparx5_rr_neigh_entry *entry,
+				   bool offloaded)
+{
+	struct neighbour *n;
+
+	if (!entry->neigh_tbl)
+		return;
+
+	n = neigh_lookup(entry->neigh_tbl, &entry->key.iaddr,
+			 entry->key.dev);
+	if (!n)
+		return;
+
+	write_lock_bh(&n->lock);
+	if (offloaded)
+		n->flags |= NTF_OFFLOADED;
+	else
+		n->flags &= ~NTF_OFFLOADED;
+	write_unlock_bh(&n->lock);
+
+	neigh_release(n);
+}
+
+static const struct rhashtable_params sparx5_neigh_ht_params = {
+	.key_offset = offsetof(struct sparx5_rr_neigh_entry, key),
+	.head_offset = offsetof(struct sparx5_rr_neigh_entry, ht_node),
+	.key_len = sizeof(struct sparx5_rr_neigh_key),
+	.automatic_shrinking = true,
+};
+
+static const struct rhashtable_params sparx5_rr_fib_entry_ht_params = {
+	.key_offset = offsetof(struct sparx5_rr_fib_entry, key),
+	.head_offset = offsetof(struct sparx5_rr_fib_entry, ht_node),
+	.key_len = sizeof(struct sparx5_rr_fib_key),
+	.automatic_shrinking = true,
+};
+
+static void sparx5_rr_to_fib4_key(u32 dst, int dst_len, u32 tb_id,
+				  struct sparx5_rr_fib_key *key)
+{
+	memset(key, 0, sizeof(*key));
+	key->addr.version = SPARX5_IPV4;
+	key->addr.ipv4 = cpu_to_be32(dst);
+	key->prefix_len = dst_len;
+	key->tb_id = tb_id;
+}
+
+static void sparx5_rr_to_fib6_key(struct in6_addr *addr, int prefix_len,
+				  u32 tb_id, struct sparx5_rr_fib_key *key)
+{
+	memset(key, 0, sizeof(*key));
+	key->addr.version = SPARX5_IPV6;
+	memcpy(&key->addr.ipv6, addr, sizeof(*addr));
+	key->prefix_len = prefix_len;
+	key->tb_id = tb_id;
+}
+
+static void sparx5_rr_fib_info_to_fib_key(struct sparx5_rr_fib_info *fi,
+					  struct sparx5_rr_fib_key *key)
+{
+	struct fib_entry_notifier_info *fen_info;
+	struct fib6_info *rt;
+
+	switch (fi->version) {
+	case SPARX5_IPV4:
+		fen_info = &fi->fen4_info;
+		sparx5_rr_to_fib4_key(fen_info->dst, fen_info->dst_len,
+				      fen_info->tb_id, key);
+		return;
+	case SPARX5_IPV6:
+		rt = fi->fe6_info.rt_arr[0];
+
+		sparx5_rr_to_fib6_key(&rt->fib6_dst.addr, rt->fib6_dst.plen,
+				      rt->fib6_table->tb6_id, key);
+		return;
+	}
+}
+
 static void sparx5_rr_inet6_make_mask_le(int logmask, u8 *mask)
 {
 	/* Caller must ensure 0 <= logmask <= 128 */
@@ -350,6 +599,81 @@ static void sparx5_rr_inet6_make_mask_le(int logmask, u8 *mask)
 		mask[15 - byte_prefix] = GENMASK(7, 7 - rem + 1);
 }
 
+static bool sparx5_rr_fib_entry_lpm_match(struct sparx5_iaddr *addr,
+					  struct sparx5_rr_fib_entry *fib_entry)
+{
+	__be32 mask;
+
+	switch (addr->version) {
+	case SPARX5_IPV4:
+		mask = inet_make_mask(fib_entry->key.prefix_len);
+		return !((addr->ipv4 ^ fib_entry->key.addr.ipv4) & mask);
+	case SPARX5_IPV6:
+		return ipv6_prefix_equal(&addr->ipv6,
+					 &fib_entry->key.addr.ipv6,
+					 fib_entry->key.prefix_len);
+	default:
+		WARN_ON(1);
+		return false;
+	}
+}
+
+static struct list_head *sparx5_rr_fib_lpm_get(struct sparx5 *sparx5,
+					       struct sparx5_iaddr *addr)
+{
+	return addr->version == SPARX5_IPV6
+		? &sparx5->router->fib_lpm6_list
+		: &sparx5->router->fib_lpm4_list;
+}
+
+static struct sparx5_rr_fib_entry *
+sparx5_rr_fib_lpm_lookup(struct sparx5 *sparx5, struct sparx5_iaddr *addr)
+{
+	struct list_head *lpm_backend = sparx5_rr_fib_lpm_get(sparx5, addr);
+	struct sparx5_rr_fib_entry *iter;
+
+	list_for_each_entry(iter, lpm_backend, fib_lpm_node)
+		if (sparx5_rr_fib_entry_lpm_match(addr, iter))
+			return iter;
+
+	return NULL;
+}
+
+static void sparx5_rr_fib_lpm_insert(struct sparx5 *sparx5,
+				     struct sparx5_rr_fib_entry *fib_entry)
+{
+	struct list_head *lpm_backend =
+		sparx5_rr_fib_lpm_get(sparx5, &fib_entry->key.addr);
+	struct sparx5_rr_fib_entry *iter, *next = NULL;
+
+	/* No need to search through local FIB entries */
+	if (fib_entry->type != SPARX5_RR_FIB_TYPE_UNICAST)
+		return;
+
+	list_for_each_entry(iter, lpm_backend, fib_lpm_node) {
+		if (fib_entry->sort_key < iter->sort_key) {
+			next = iter;
+			break;
+		}
+	}
+
+	if (!next) {
+		list_add_tail(&fib_entry->fib_lpm_node, lpm_backend);
+		return;
+	}
+
+	/* Add before next entry */
+	list_add_tail(&fib_entry->fib_lpm_node, &next->fib_lpm_node);
+}
+
+static void sparx5_rr_fib_lpm_remove(struct sparx5_rr_fib_entry *fib_entry)
+{
+	if (fib_entry->type != SPARX5_RR_FIB_TYPE_UNICAST)
+		return;
+
+	list_del(&fib_entry->fib_lpm_node);
+}
+
 static int sparx5_rr_lpm_rule_xip_add(struct vcap_rule *rule,
 				      struct sparx5_iaddr *addr, u32 prefix_len)
 {
@@ -387,6 +711,668 @@ sparx5_rr_leg_find_by_dev(struct sparx5 *sparx5, struct net_device *dev)
 	return NULL;
 }
 
+static struct sparx5_rr_fib_entry *
+sparx5_rr_fib_entry_lookup(struct sparx5 *sparx5, struct sparx5_rr_fib_key *key)
+{
+	return rhashtable_lookup_fast(&sparx5->router->fib_ht, key,
+				      sparx5_rr_fib_entry_ht_params);
+}
+
+static int sparx5_rr_fib_entry_insert(struct sparx5 *sparx5,
+				      struct sparx5_rr_fib_entry *fib_entry)
+{
+	return rhashtable_insert_fast(&sparx5->router->fib_ht,
+				      &fib_entry->ht_node,
+				      sparx5_rr_fib_entry_ht_params);
+}
+
+static void sparx5_rr_fib_entry_remove(struct sparx5 *sparx5,
+				       struct sparx5_rr_fib_entry *fib_entry)
+{
+	rhashtable_remove_fast(&sparx5->router->fib_ht, &fib_entry->ht_node,
+			       sparx5_rr_fib_entry_ht_params);
+}
+
+static struct sparx5_rr_neigh_entry *
+sparx5_rr_neigh_entry_lookup(struct sparx5 *sparx5,
+			     struct sparx5_rr_neigh_key *key)
+{
+	return rhashtable_lookup_fast(&sparx5->router->neigh_ht, key,
+				      sparx5_neigh_ht_params);
+}
+
+static int sparx5_rr_neigh_entry_insert(struct sparx5 *sparx5,
+					struct sparx5_rr_neigh_entry *entry)
+{
+	return rhashtable_insert_fast(&sparx5->router->neigh_ht,
+				      &entry->ht_node, sparx5_neigh_ht_params);
+}
+
+static void sparx5_rr_neigh_entry_remove(struct sparx5 *sparx5,
+					 struct sparx5_rr_neigh_entry *entry)
+{
+	rhashtable_remove_fast(&sparx5->router->neigh_ht, &entry->ht_node,
+			       sparx5_neigh_ht_params);
+}
+
+static int sparx5_lower_dev_walk(struct net_device *lower_dev,
+				 struct netdev_nested_priv *priv)
+{
+	int ret = 0;
+
+	if (sparx5_netdevice_check(lower_dev)) {
+		priv->data = (void *)netdev_priv(lower_dev);
+		ret = 1;
+	}
+
+	return ret;
+}
+
+static struct sparx5_port *
+sparx5_port_dev_lower_find_rcu(struct net_device *dev)
+{
+	struct netdev_nested_priv priv = {
+		.data = NULL,
+	};
+
+	if (sparx5_netdevice_check(dev))
+		return netdev_priv(dev);
+
+	netdev_walk_all_lower_dev_rcu(dev, sparx5_lower_dev_walk, &priv);
+
+	return priv.data;
+}
+
+static struct sparx5_port *sparx5_port_dev_lower_find(struct net_device *dev)
+{
+	struct sparx5_port *port;
+
+	rcu_read_lock();
+	port = sparx5_port_dev_lower_find_rcu(dev);
+	rcu_read_unlock();
+
+	return port;
+}
+
+static struct sparx5_rr_neigh_entry *
+sparx5_rr_neigh_entry_alloc(struct sparx5 *sparx5,
+			    struct sparx5_rr_neigh_key *key,
+			    struct sparx5_rr_router_leg *leg)
+{
+	struct sparx5_rr_neigh_entry *entry;
+
+	entry = kzalloc_obj(*entry);
+	if (!entry)
+		return NULL;
+
+	memcpy(&entry->key, key, sizeof(*key));
+	entry->vmid = leg->vmid;
+
+	switch (key->iaddr.version) {
+	case SPARX5_IPV4:
+		entry->neigh_tbl = &arp_tbl;
+		break;
+	case SPARX5_IPV6:
+#if IS_ENABLED(CONFIG_IPV6)
+		entry->neigh_tbl = &nd_tbl;
+		break;
+#else
+		kfree(entry);
+		return NULL;
+#endif
+	}
+
+	INIT_LIST_HEAD(&entry->nexthop_list);
+	INIT_LIST_HEAD(&entry->fib_list_node);
+
+	return entry;
+}
+
+static int sparx5_rr_neigh_entry_fib_link(struct sparx5 *sparx5,
+					  struct sparx5_rr_neigh_entry *entry)
+{
+	struct sparx5_rr_fib_entry *fib_entry;
+
+	fib_entry = sparx5_rr_fib_lpm_lookup(sparx5, &entry->key.iaddr);
+	if (!fib_entry)
+		return -ENOENT;
+
+	list_add(&entry->fib_list_node, &fib_entry->neigh_list);
+	entry->fib_entry = fib_entry;
+
+	return 0;
+}
+
+static struct sparx5_rr_neigh_entry *
+sparx5_rr_neigh_entry_create(struct sparx5 *sparx5,
+			     struct sparx5_rr_neigh_key *key)
+{
+	struct sparx5_rr_neigh_entry *entry;
+	struct sparx5_rr_router_leg *leg;
+	struct sparx5_port *port_below;
+	int err;
+
+	port_below = sparx5_port_dev_lower_find(key->dev);
+	if (!port_below)
+		return ERR_PTR(-EINVAL);
+
+	leg = sparx5_rr_leg_find_by_dev(sparx5, key->dev);
+	if (!leg)
+		return ERR_PTR(-EINVAL);
+
+	entry = sparx5_rr_neigh_entry_alloc(sparx5, key, leg);
+	if (!entry)
+		return ERR_PTR(-ENOMEM);
+
+	err = sparx5_rr_neigh_entry_insert(sparx5, entry);
+	if (err)
+		goto err_insert;
+
+	/* Link neigh to the fib which owns the subnet. */
+	err = sparx5_rr_neigh_entry_fib_link(sparx5, entry);
+	if (err)
+		goto err_fib_link;
+
+	netdev_hold(entry->key.dev, &entry->dev_tracker, GFP_KERNEL);
+
+	return entry;
+
+err_fib_link:
+	sparx5_rr_neigh_entry_remove(sparx5, entry);
+err_insert:
+	kfree(entry);
+	return ERR_PTR(err);
+}
+
+static int sparx5_rr_nexthop_neigh_init(struct sparx5 *sparx5,
+					struct sparx5_rr_router_leg *leg,
+					struct sparx5_rr_nexthop *nh)
+{
+	struct sparx5_rr_neigh_entry *neigh_entry;
+	struct net_device *dev = leg->dev;
+	struct sparx5_rr_neigh_key key;
+	struct neighbour *n;
+	int err = 0;
+
+	if (!nh->gateway || nh->neigh_entry || !nh->neigh_tbl)
+		return 0;
+
+	/* Look up neighbor in the global neighbor table. Takes ref to n. */
+	n = neigh_lookup(nh->neigh_tbl, &nh->gw_addr, dev);
+	if (!n) {
+		n = neigh_create(nh->neigh_tbl, &nh->gw_addr, dev);
+		if (IS_ERR(n))
+			return PTR_ERR(n);
+		/* Start arp process */
+		neigh_event_send(n, NULL);
+	}
+
+	sparx5_rr_nb2neigh_key(n, &key);
+
+	neigh_entry = sparx5_rr_neigh_entry_lookup(sparx5, &key);
+	if (!neigh_entry) {
+		neigh_entry = sparx5_rr_neigh_entry_create(sparx5, &key);
+		if (IS_ERR(neigh_entry)) {
+			err = PTR_ERR(neigh_entry);
+			goto out;
+		}
+	}
+
+	nh->neigh_entry = neigh_entry;
+	list_add_tail(&nh->neigh_list_node, &neigh_entry->nexthop_list);
+
+out:
+	neigh_release(n);
+	return err;
+}
+
+static void sparx5_rr_neigh_entry_destroy(struct sparx5 *sparx5,
+					  struct sparx5_rr_neigh_entry *entry)
+{
+	WARN_ON(entry->hw_route.vrule_id_valid);
+
+	if (entry->fib_entry)
+		list_del(&entry->fib_list_node);
+
+	sparx5_rr_neigh_entry_offload_mark(entry, false);
+	sparx5_rr_neigh_entry_remove(sparx5, entry);
+	netdev_put(entry->key.dev, &entry->dev_tracker);
+
+	kfree(entry);
+}
+
+static void sparx5_rr_neigh_entry_put(struct sparx5 *sparx5,
+				      struct sparx5_rr_neigh_entry *neigh_entry)
+{
+	if (neigh_entry && list_empty(&neigh_entry->nexthop_list) &&
+	    !neigh_entry->hw_route.vrule_id_valid)
+		sparx5_rr_neigh_entry_destroy(sparx5, neigh_entry);
+}
+
+static void sparx5_rr_nexthop_deinit(struct sparx5 *sparx5,
+				     struct sparx5_rr_nexthop *nh)
+{
+	struct sparx5_rr_neigh_entry *neigh_entry = nh->neigh_entry;
+
+	if (neigh_entry) {
+		list_del(&nh->neigh_list_node);
+		sparx5_rr_neigh_entry_put(sparx5, neigh_entry);
+	}
+
+	nh->neigh_entry = NULL;
+}
+
+static int sparx5_rr_nexthop_init(struct sparx5 *sparx5,
+				  struct sparx5_rr_nexthop_group *nh_grp,
+				  struct sparx5_rr_nexthop *nh,
+				  struct fib_nh_common *fnhc)
+{
+	struct sparx5_rr_router_leg *leg;
+
+	nh->ifindex = -1;
+	nh->grp = nh_grp;
+	nh->gateway = fnhc->nhc_gw_family != 0;
+	nh->trapped = false;
+	nh->neigh_entry = NULL;
+	nh->neigh_tbl = NULL;
+
+	memset(&nh->gw_addr, 0, sizeof(nh->gw_addr));
+
+	if (!nh->gateway)
+		return 0;
+
+	switch (fnhc->nhc_gw_family) {
+	case AF_INET:
+		nh->gw_addr.version = SPARX5_IPV4;
+		nh->gw_addr.ipv4 = fnhc->nhc_gw.ipv4;
+		nh->neigh_tbl = &arp_tbl;
+		break;
+	case AF_INET6:
+		nh->gw_addr.version = SPARX5_IPV6;
+		nh->gw_addr.ipv6 = fnhc->nhc_gw.ipv6;
+#if IS_ENABLED(CONFIG_IPV6)
+		nh->neigh_tbl = &nd_tbl;
+		break;
+#else
+		return -EINVAL;
+#endif
+	default:
+		WARN_ON_ONCE(1); /* BUG */
+		return 0;
+	}
+
+	/* Blackhole route nexthops have no egress device. */
+	if (!fnhc->nhc_dev)
+		return 0;
+
+	nh->ifindex = fnhc->nhc_dev->ifindex;
+
+	/* When a router leg is removed, all the nexthops with gateway IPs in a
+	 * subnet governed by the leg will receive fib delete events. However,
+	 * these delete events are received one by one. Therefore, this nexthop
+	 * init could have been triggered by a group resize action for such an
+	 * event, where the underlying leg is already removed.
+	 *
+	 * This is not an error. We handle this during offloading by
+	 * trapping nexthops which do not have a neigh_entry. As fib deletion
+	 * events are processed, we converge to the proper state.
+	 */
+	leg = sparx5_rr_leg_find_by_dev(sparx5, fnhc->nhc_dev);
+	if (!leg)
+		return 0;
+
+	return sparx5_rr_nexthop_neigh_init(sparx5, leg, nh);
+}
+
+static int
+sparx5_rr_nexthop_group_info_init(struct sparx5 *sparx5,
+				  struct sparx5_rr_nexthop_group *nh_grp,
+				  struct sparx5_rr_fib_info *fi)
+{
+	unsigned int nhs = sparx5_rr_fib_info_nhs(fi);
+	struct sparx5_rr_nexthop_group_info *nhgi;
+	struct sparx5_rr_nexthop *nh;
+	int err, i;
+
+	nhgi = kzalloc_flex(*nhgi, nexthops, nhs);
+	if (!nhgi)
+		return -ENOMEM;
+
+	nh_grp->nhgi = nhgi;
+	nhgi->grp = nh_grp;
+	nhgi->atbl_offset_valid = false;
+	nhgi->atbl_offset = 0;
+	nhgi->count = nhs;
+
+	for (i = 0; i < nhgi->count; i++) {
+		struct fib_nh_common *fnhc;
+
+		nh = &nhgi->nexthops[i];
+		fnhc = sparx5_rr_fib_info_nhc(fi, i);
+		err = sparx5_rr_nexthop_init(sparx5, nh_grp, nh, fnhc);
+		if (err)
+			goto err_nexthop_init;
+	}
+
+	return 0;
+
+err_nexthop_init:
+	for (i--; i >= 0; i--) {
+		nh = &nhgi->nexthops[i];
+		sparx5_rr_nexthop_deinit(sparx5, nh);
+	}
+	kfree(nhgi);
+	return err;
+}
+
+static void
+sparx5_rr_nexthop_group_info_deinit(struct sparx5 *sparx5,
+				    struct sparx5_rr_nexthop_group *nh_grp)
+{
+	struct sparx5_rr_nexthop_group_info *nhgi = nh_grp->nhgi;
+	struct sparx5_rr_nexthop *nh;
+	int i;
+
+	WARN_ON(!nhgi->count);
+	WARN_ON_ONCE(nhgi->atbl_offset_valid);
+
+	for (i = nhgi->count - 1; i >= 0; i--) {
+		nh = &nhgi->nexthops[i];
+
+		sparx5_rr_nexthop_deinit(sparx5, nh);
+	}
+
+	kfree(nhgi);
+}
+
+static void sparx5_rr_arp_tbl_hw_addr_apply(struct sparx5 *sparx5,
+					    unsigned char mac[ETH_ALEN],
+					    u16 evmid, int offset)
+{
+	u32 mac_msb, mac_lsb;
+
+	sparx5_rr_split_mac(mac, 32, &mac_msb, &mac_lsb);
+
+	spx5_rmw(ANA_L3_ARP_CFG_0_MAC_MSB_SET(mac_msb) |
+		 ANA_L3_ARP_CFG_0_ARP_VMID_SET(evmid) |
+		 ANA_L3_ARP_CFG_0_ARP_ENA_SET(1),
+		 ANA_L3_ARP_CFG_0_ARP_ENA |
+		 ANA_L3_ARP_CFG_0_ARP_VMID |
+		 ANA_L3_ARP_CFG_0_MAC_MSB,
+		 sparx5, ANA_L3_ARP_CFG_0(offset));
+
+	spx5_wr(mac_lsb, sparx5, ANA_L3_ARP_CFG_1(offset));
+}
+
+static void sparx5_rr_arp_tbl_hw_addr_clear(struct sparx5 *sparx5, int offset)
+{
+	spx5_rmw(ANA_L3_ARP_CFG_0_ARP_ENA_SET(0), ANA_L3_ARP_CFG_0_ARP_ENA,
+		 sparx5, ANA_L3_ARP_CFG_0(offset));
+}
+
+static void
+sparx5_rr_nh_grp_arp_tbl_grp_clear(struct sparx5 *sparx5,
+				   struct sparx5_rr_nexthop_group *nh_grp)
+{
+	int offset = nh_grp->nhgi->atbl_offset;
+
+	if (nh_grp->nhgi->atbl_offset_valid) {
+		for (u8 i = 0; i < nh_grp->nhgi->count; i++)
+			sparx5_rr_arp_tbl_hw_addr_clear(sparx5, offset + i);
+		sparx5_rr_arp_tbl_grp_free(sparx5, nh_grp->nhgi->count,
+					   offset);
+	}
+	nh_grp->nhgi->atbl_offset_valid = false;
+}
+
+static void sparx5_rr_nexthop_group_put(struct sparx5 *sparx5,
+					struct sparx5_rr_nexthop_group *nh_grp)
+{
+	sparx5_rr_nh_grp_arp_tbl_grp_clear(sparx5, nh_grp);
+	sparx5_rr_nexthop_group_info_deinit(sparx5, nh_grp);
+	kfree(nh_grp);
+}
+
+static struct sparx5_rr_nexthop_group *
+sparx5_rr_nexthop_group_create(struct sparx5 *sparx5,
+			       struct sparx5_rr_fib_entry *fib_entry)
+{
+	struct sparx5_rr_nexthop_group *nh_grp;
+	int err;
+
+	nh_grp = kzalloc_obj(*nh_grp);
+	if (!nh_grp)
+		return ERR_PTR(-ENOMEM);
+
+	err = sparx5_rr_nexthop_group_info_init(sparx5, nh_grp, &fib_entry->fi);
+	if (err)
+		goto err_group_info_init;
+
+	return nh_grp;
+
+err_group_info_init:
+	kfree(nh_grp);
+	return ERR_PTR(err);
+}
+
+static enum sparx5_rr_fib_type sparx5_rr_rtm_type2fib_type(u8 type)
+{
+	switch (type) {
+	case RTN_UNICAST:
+		return SPARX5_RR_FIB_TYPE_UNICAST;
+	case RTN_LOCAL:
+		return SPARX5_RR_FIB_TYPE_LOCAL;
+	case RTN_MULTICAST:
+		return SPARX5_RR_FIB_TYPE_MULTICAST;
+	case RTN_BLACKHOLE:
+		return SPARX5_RR_FIB_TYPE_BLACKHOLE;
+	case RTN_PROHIBIT:
+		return SPARX5_RR_FIB_TYPE_PROHIBIT;
+	case RTN_UNREACHABLE:
+		return SPARX5_RR_FIB_TYPE_UNREACHABLE;
+	default:
+		return SPARX5_RR_FIB_TYPE_INVALID;
+	}
+}
+
+static void
+sparx5_rr_fib_entry_fib4_info_set(struct sparx5_rr_fib_entry *fib_entry,
+				  struct fib_entry_notifier_info *fen4_info)
+{
+	/* Prevent the fib_info from being deleted while we store the
+	 * fen_info
+	 */
+	fib_info_hold(fen4_info->fi);
+	memcpy(&fib_entry->fi.fen4_info, fen4_info, sizeof(*fen4_info));
+}
+
+static int
+sparx5_rr_fib_entry_fib6_info_add(struct sparx5_rr_fib_entry *fib_entry,
+				  struct sparx5_rr_fib6_entry_info *fib6_info)
+{
+	struct sparx5_rr_fib6_entry_info *f6i = &fib_entry->fi.fe6_info;
+	unsigned int old_ntr6, new_ntr6;
+	struct fib6_info **rt_arr;
+
+	old_ntr6 = f6i->nrt6;
+	new_ntr6 = old_ntr6 + fib6_info->nrt6;
+
+	rt_arr = kzalloc_objs(struct fib6_info *, new_ntr6);
+	if (!rt_arr)
+		return -ENOMEM;
+
+	/* Copy existing */
+	for (int i = 0; i < old_ntr6; i++)
+		rt_arr[i] = f6i->rt_arr[i];
+
+	/* Copy new and hold fib6_info */
+	for (int i = 0; i < fib6_info->nrt6; i++) {
+		struct fib6_info *rt = fib6_info->rt_arr[i];
+
+		rt_arr[old_ntr6 + i] = rt;
+		fib6_info_hold(rt);
+	}
+
+	/* Free old fib6_info */
+	kfree(f6i->rt_arr);
+	f6i->rt_arr = rt_arr;
+	f6i->nrt6 = new_ntr6;
+
+	WARN_ON(!fib_entry->fi.fe6_info.rt_arr);
+	WARN_ON(!fib_entry->fi.fe6_info.nrt6);
+
+	return 0;
+}
+
+static int
+sparx5_rr_fib_entry_fib_info_add(struct sparx5_rr_fib_entry *fib_entry,
+				 struct sparx5_rr_fib_info *fi)
+{
+	switch (fi->version) {
+	case SPARX5_IPV4:
+		/* IPv4 nexthops can not be added/removed piecemeal similar to
+		 * IPv6, so this is a replace in practice.
+		 */
+		sparx5_rr_fib_entry_fib4_info_set(fib_entry, &fi->fen4_info);
+		return 0;
+	case SPARX5_IPV6:
+		return sparx5_rr_fib_entry_fib6_info_add(fib_entry,
+							 &fi->fe6_info);
+	default:
+		WARN_ON(1);
+		return 0;
+	}
+}
+
+static struct sparx5_rr_fib_entry *
+sparx5_rr_fib_entry_create(struct sparx5 *sparx5, struct sparx5_rr_fib_key *key,
+			   struct sparx5_rr_fib_info *fi)
+{
+	struct sparx5_rr_nexthop_group *nh_grp;
+	u8 type = sparx5_rr_fib_info_type(fi);
+	struct sparx5_rr_fib_entry *fib_entry;
+	int err;
+
+	fib_entry = kzalloc_obj(*fib_entry);
+	if (!fib_entry)
+		return ERR_PTR(-ENOMEM);
+
+	memcpy(&fib_entry->key, key, sizeof(*key));
+	sparx5_rr_fib_info_init(&fib_entry->fi, fi->version);
+	fib_entry->type = sparx5_rr_rtm_type2fib_type(type);
+	fib_entry->sort_key = LPM_SORT_KEY(key->prefix_len);
+
+	err = sparx5_rr_fib_entry_fib_info_add(fib_entry, fi);
+	if (err)
+		goto err_fib_info_set;
+
+	err = sparx5_rr_fib_entry_insert(sparx5, fib_entry);
+	if (err)
+		goto err_fib_entry_insert;
+
+	nh_grp = sparx5_rr_nexthop_group_create(sparx5, fib_entry);
+	if (IS_ERR(nh_grp)) {
+		err = PTR_ERR(nh_grp);
+		goto err_nexthop_group_create;
+	}
+
+	fib_entry->nh_grp = nh_grp;
+	nh_grp->fib_entry = fib_entry;
+	INIT_LIST_HEAD(&fib_entry->neigh_list);
+
+	list_add(&fib_entry->fib_node, &sparx5->router->fib_list);
+	sparx5_rr_fib_lpm_insert(sparx5, fib_entry);
+
+	return fib_entry;
+
+err_nexthop_group_create:
+	sparx5_rr_fib_entry_remove(sparx5, fib_entry);
+err_fib_entry_insert:
+	sparx5_rr_fib_info_put(&fib_entry->fi);
+err_fib_info_set:
+	kfree(fib_entry);
+
+	return ERR_PTR(err);
+}
+
+#if IS_ENABLED(CONFIG_IPV6)
+static void
+sparx5_rr_fib6_entry_offload_mark(struct sparx5 *sparx5,
+				  struct sparx5_rr_fib6_entry_info *fen6_info,
+				  bool offload,
+				  bool trap,
+				  bool offload_failed)
+{
+	for (int i = 0; i < fen6_info->nrt6; i++)
+		fib6_info_hw_flags_set(&init_net, fen6_info->rt_arr[i], offload,
+				       trap, offload_failed);
+}
+#else
+static void
+sparx5_rr_fib6_entry_offload_mark(struct sparx5 *sparx5,
+				  struct sparx5_rr_fib6_entry_info *fen6_info,
+				  bool offload,
+				  bool trap,
+				  bool offload_failed)
+{
+}
+#endif
+
+static void
+sparx5_rr_fib4_entry_offload_mark(struct sparx5 *sparx5,
+				  struct fib_entry_notifier_info *fen4_info,
+				  bool offload,
+				  bool trap,
+				  bool offload_failed)
+{
+	struct fib_rt_info fri;
+
+	fri.fi = fen4_info->fi;
+	fri.tb_id = fen4_info->tb_id;
+	fri.dst = cpu_to_be32(fen4_info->dst);
+	fri.dst_len = fen4_info->dst_len;
+	fri.dscp = fen4_info->dscp;
+	fri.type = fen4_info->type;
+	fri.offload = offload;
+	fri.trap = trap;
+	fri.offload_failed = offload_failed;
+
+	fib_alias_hw_flags_set(&init_net, &fri);
+}
+
+static void sparx5_rr_fib_info_offload_mark(struct sparx5 *sparx5,
+					    struct sparx5_rr_fib_info *fi,
+					    bool offload, bool trap,
+					    bool offload_failed)
+{
+	switch (fi->version) {
+	case SPARX5_IPV4:
+		return sparx5_rr_fib4_entry_offload_mark(sparx5,
+							 &fi->fen4_info,
+							 offload, trap,
+							 offload_failed);
+	case SPARX5_IPV6:
+		return sparx5_rr_fib6_entry_offload_mark(sparx5,
+							 &fi->fe6_info,
+							 offload, trap,
+							 offload_failed);
+	}
+}
+
+static void
+sparx5_rr_fib_entry_offload_mark(struct sparx5 *sparx5,
+				 struct sparx5_rr_fib_entry *fib_entry)
+{
+	bool offload, trap, offload_failed;
+
+	offload_failed = fib_entry->offload_fail;
+	offload = !fib_entry->offload_fail;
+	trap = !fib_entry->offload_fail && fib_entry->trap;
+
+	sparx5_rr_fib_info_offload_mark(sparx5, &fib_entry->fi, offload, trap,
+					offload_failed);
+}
+
 static int
 sparx5_rr_lpm_arp_entry_create(struct sparx5 *sparx5,
 			       struct sparx5_iaddr *addr,
@@ -424,6 +1410,472 @@ sparx5_rr_lpm_arp_entry_create(struct sparx5 *sparx5,
 	return err;
 }
 
+static int sparx5_rr_lpm_arp_entry_mod(struct sparx5 *sparx5,
+				       unsigned char mac[ETH_ALEN], u16 evmid,
+				       u32 vrule_id)
+{
+	struct vcap_control *vctrl = sparx5->vcap_ctrl;
+	struct vcap_rule *vrule;
+	u32 mac_msb, mac_lsb;
+	int err;
+
+	sparx5_rr_split_mac(mac, 32, &mac_msb, &mac_lsb);
+
+	vrule = vcap_get_rule(vctrl, vrule_id);
+	if (IS_ERR(vrule))
+		return -EINVAL;
+
+	err = vcap_rule_mod_action_u32(vrule, VCAP_AF_MAC_MSB, mac_msb);
+	err |= vcap_rule_mod_action_u32(vrule, VCAP_AF_MAC_LSB, mac_lsb);
+	err |= vcap_rule_mod_action_u32(vrule, VCAP_AF_ARP_VMID, evmid);
+	err |= vcap_rule_mod_action_bit(vrule, VCAP_AF_ARP_ENA, VCAP_BIT_1);
+
+	err = err ? -EINVAL : vcap_mod_rule(vrule);
+
+	vcap_free_rule(vrule);
+	return err;
+}
+
+static int
+sparx5_rr_fib_entry_update_arp_entry(struct sparx5 *sparx5,
+				     struct sparx5_rr_fib_entry *fib_entry,
+				     unsigned char mac[ETH_ALEN], u16 evmid)
+{
+	struct net_device *pdev = sparx5->router->port_dev;
+	struct vcap_control *vctrl = sparx5->vcap_ctrl;
+	u32 vrule_id = fib_entry->hw_route.vrule_id;
+	struct vcap_rule *vrule;
+	u32 mac_msb, mac_lsb;
+	int err;
+
+	sparx5_rr_split_mac(mac, 32, &mac_msb, &mac_lsb);
+
+	vrule = vcap_get_rule(vctrl, vrule_id);
+	if (IS_ERR(vrule)) {
+		fib_entry->hw_route.vrule_id_valid = false;
+		return PTR_ERR(vrule);
+	}
+
+	switch (vrule->actionset) {
+	case VCAP_AFS_ARP_ENTRY:
+		err = vcap_rule_mod_action_u32(vrule, VCAP_AF_MAC_MSB,
+					       mac_msb);
+		err |= vcap_rule_mod_action_u32(vrule, VCAP_AF_MAC_LSB,
+						mac_lsb);
+
+		err = err ? -EINVAL : vcap_mod_rule(vrule);
+		goto free_rule;
+	case VCAP_AFS_ARP_PTR:
+		/* Convert arp_ptr to arp_entry */
+		err = sparx5_rr_lpm_arp_entry_create(sparx5,
+						     &fib_entry->key.addr,
+						     fib_entry->key.prefix_len,
+						     mac, evmid,
+						     &fib_entry->hw_route);
+		if (err)
+			goto free_rule;
+
+		sparx5_rr_nh_grp_arp_tbl_grp_clear(sparx5, fib_entry->nh_grp);
+		err = vcap_del_rule(vctrl, pdev, vrule_id);
+		goto free_rule;
+	default:
+		err = -EINVAL;
+		WARN_ON(1); /* BUG */
+	}
+
+free_rule:
+	vcap_free_rule(vrule);
+
+	return err;
+}
+
+static int sparx5_rr_lpm_arp_ptr_create(struct sparx5 *sparx5,
+					struct sparx5_iaddr *addr,
+					u32 prefix_len, u32 arp_offset_addr,
+					u8 ecmp_size,
+					struct sparx5_rr_hw_route *hw_route)
+{
+	struct net_device *pdev = sparx5->router->port_dev;
+	struct vcap_control *vctrl = sparx5->vcap_ctrl;
+	u32 priority = LPM_SORT_KEY(prefix_len);
+	struct vcap_rule *rule;
+	int err;
+
+	rule = vcap_alloc_rule(vctrl, pdev, VCAP_CID_PREROUTING_L0,
+			       VCAP_USER_L3, priority, 0);
+	if (IS_ERR(rule))
+		return PTR_ERR(rule);
+
+	err = sparx5_rr_lpm_rule_xip_add(rule, addr, prefix_len);
+	err |= vcap_rule_add_key_u32(rule, VCAP_KF_AFFIX, 0, 0);
+	err |= vcap_rule_add_key_bit(rule, VCAP_KF_DST_FLAG, VCAP_BIT_1);
+	err |= vcap_rule_add_action_u32(rule, VCAP_AF_ARP_PTR, arp_offset_addr);
+	err |= vcap_rule_add_action_bit(rule, VCAP_AF_ARP_PTR_REMAP_ENA,
+				       VCAP_BIT_0);
+	err |= vcap_rule_add_action_u32(rule, VCAP_AF_ECMP_CNT, ecmp_size - 1);
+	err |= vcap_rule_add_action_u32(rule, VCAP_AF_RGID, 0);
+
+	err = err ? -EINVAL : vcap_val_add_rule(rule, LPM_PROTO(addr));
+	if (!err) {
+		hw_route->vrule_id_valid = true;
+		hw_route->vrule_id = rule->id;
+	}
+
+	vcap_free_rule(rule);
+	return err;
+}
+
+/* Get egress mac and vmid for nexthop. */
+static void sparx5_rr_nexthop_egress_derive(struct sparx5_rr_nexthop *nh,
+					    u8 *mac, u16 *vmid)
+{
+	struct sparx5_rr_neigh_entry *nh_neigh = nh->neigh_entry;
+
+	nh->trapped = !nh_neigh || is_zero_ether_addr(nh_neigh->hwaddr);
+
+	if (nh_neigh) {
+		memcpy(mac, nh_neigh->hwaddr, ETH_ALEN);
+		*vmid = nh_neigh->vmid;
+		return;
+	}
+
+	eth_zero_addr(mac);
+	*vmid = 0;
+}
+
+static int
+sparx5_rr_fib_entry_ecmp_hw_apply(struct sparx5 *sparx5,
+				  struct sparx5_rr_fib_entry *fib_entry)
+{
+	struct sparx5_rr_nexthop_group_info *nhgi = fib_entry->nh_grp->nhgi;
+	unsigned char mac[ETH_ALEN] __aligned(2);
+	struct sparx5_rr_nexthop *nh;
+	int err, i, offset;
+	u16 vmid;
+
+	offset = sparx5_rr_arp_tbl_grp_alloc(sparx5, nhgi->count);
+
+	if (offset < 0) {
+		fib_entry->offload_fail = true;
+		return offset;
+	}
+
+	for (i = 0; i < nhgi->count; i++) {
+		nh = &nhgi->nexthops[i];
+
+		sparx5_rr_nexthop_egress_derive(nh, mac, &vmid);
+
+		sparx5_rr_arp_tbl_hw_addr_apply(sparx5, mac, vmid, offset + i);
+	}
+
+	err = sparx5_rr_lpm_arp_ptr_create(sparx5,
+					   &fib_entry->key.addr,
+					   fib_entry->key.prefix_len, offset,
+					   nhgi->count, &fib_entry->hw_route);
+	if (err)
+		goto err_arp_ptr_create;
+
+	nhgi->atbl_offset = offset;
+	nhgi->atbl_offset_valid = true;
+	fib_entry->offload_fail = false;
+
+	return 0;
+
+err_arp_ptr_create:
+	for (i--; i >= 0; i--)
+		sparx5_rr_arp_tbl_hw_addr_clear(sparx5, offset + i);
+	sparx5_rr_arp_tbl_grp_free(sparx5, nhgi->count, offset);
+	fib_entry->offload_fail = true;
+	nhgi->atbl_offset_valid = false;
+
+	return err;
+}
+
+static int
+sparx5_rr_fib_blackhole_hw_apply(struct sparx5 *sparx5,
+				 struct sparx5_rr_fib_entry *fib_entry)
+{
+	unsigned char mac[ETH_ALEN];
+
+	/* Hardware blackholes are implemented by:
+	 *
+	 * 1) Making sure traffic is not trapped with non-zero dmac.
+	 * 2) Using reserved router leg vmid for egress.
+	 * 3) This router leg is attached to a VLAN id > 4095.
+	 * 4) The port-mask for this VLAN is all zero.
+	 *
+	 * The hardware VLAN table has more than 4096 entries. The specific
+	 * size depends on the chip. LAN969x has 4608 and Sparx5 has 5120
+	 * entries. These additional VLAN entries can be used for internal
+	 * logic.
+	 *
+	 * The port-mask for the blackhole VLAN is zero. Therefore, frames
+	 * routed to the blackhole leg will not egress on any ports.
+	 */
+	eth_zero_addr(mac);
+	mac[5] = 0xff;
+
+	return sparx5_rr_lpm_arp_entry_create(sparx5, &fib_entry->key.addr,
+					      fib_entry->key.prefix_len, mac,
+					      SPARX5_BLACKHOLE_VMID(sparx5),
+					      &fib_entry->hw_route);
+}
+
+static int sparx5_rr_fib_entry_hw_apply(struct sparx5 *sparx5,
+					struct sparx5_rr_fib_entry *fib_entry)
+{
+	struct sparx5_rr_nexthop_group_info *nhgi = fib_entry->nh_grp->nhgi;
+	unsigned char mac[ETH_ALEN] __aligned(2);
+	struct sparx5_rr_nexthop *nh;
+	int err = 0;
+	u16 vmid;
+
+	switch (fib_entry->type) {
+	case SPARX5_RR_FIB_TYPE_UNREACHABLE:
+		fallthrough;
+	case SPARX5_RR_FIB_TYPE_PROHIBIT:
+		/* Ensure kernel can respond with correct ICMP packets. */
+		fallthrough;
+	case SPARX5_RR_FIB_TYPE_LOCAL:
+		/* Trap traffic destined for device itself, to ensure
+		 * device can receive traffic even when default gateways are
+		 * configured.
+		 */
+		fib_entry->trap = true;
+
+		/* Trap frames with zero mac, VMID does not matter */
+		eth_zero_addr(mac);
+		err = sparx5_rr_lpm_arp_entry_create(sparx5,
+						     &fib_entry->key.addr,
+						     fib_entry->key.prefix_len,
+						     mac, 0,
+						     &fib_entry->hw_route);
+		goto out;
+
+	case SPARX5_RR_FIB_TYPE_UNICAST:
+		fib_entry->trap = false;
+
+		if (!nhgi->nexthops->gateway) {
+			/* Directly connected subnet. Trap traffic so kernel
+			 * can perform ARP/NDP on our behalf.
+			 */
+			eth_zero_addr(mac);
+			err = sparx5_rr_lpm_arp_entry_create(sparx5,
+							     &fib_entry->key.addr,
+							     fib_entry->key.prefix_len,
+							     mac, 0,
+							     &fib_entry->hw_route);
+			goto out;
+		}
+
+		if (nhgi->count == 1) { /* Use arp_entry */
+			nh = &nhgi->nexthops[0];
+			sparx5_rr_nexthop_egress_derive(nh, mac, &vmid);
+			err = sparx5_rr_lpm_arp_entry_create(sparx5,
+							     &fib_entry->key.addr,
+							     fib_entry->key.prefix_len,
+							     mac, vmid,
+							     &fib_entry->hw_route);
+			goto out;
+		}
+
+		/* Multiple nexthops so we use the HW arp table. */
+		err = sparx5_rr_fib_entry_ecmp_hw_apply(sparx5,
+							fib_entry);
+		goto out;
+
+	case SPARX5_RR_FIB_TYPE_BLACKHOLE:
+		fib_entry->trap = false;
+		err = sparx5_rr_fib_blackhole_hw_apply(sparx5, fib_entry);
+		goto out;
+
+	default:
+		dev_warn(sparx5->dev, "Fib entry offload, unhandled type=%d\n",
+			 fib_entry->type);
+		return -EINVAL;
+	}
+
+out:
+	fib_entry->offload_fail = !!err;
+
+	return err;
+}
+
+static void sparx5_rr_nexthop_neigh_update(struct sparx5 *sparx5,
+					   struct sparx5_rr_nexthop *nh,
+					   bool entry_connected)
+{
+	unsigned char mac[ETH_ALEN] __aligned(2);
+	int err, nh_offset, grp_idx;
+	u16 vmid;
+
+	if (!nh->gateway)
+		return;
+
+	vmid = nh->neigh_entry->vmid;
+
+	/* Trap traffic with zero mac */
+	eth_zero_addr(mac);
+
+	if (entry_connected)
+		ether_addr_copy(mac, nh->neigh_entry->hwaddr);
+
+	if (nh->trapped && !entry_connected)
+		return;
+
+	nh->trapped = !entry_connected;
+
+	if (nh->grp->nhgi->count == 1) {
+		err = sparx5_rr_fib_entry_update_arp_entry(sparx5,
+							   nh->grp->fib_entry,
+							   mac, vmid);
+		if (err)
+			dev_err(sparx5->dev,
+				"Nexthop fib entry update failed\n");
+
+		return;
+	}
+
+	if (!nh->grp->nhgi->atbl_offset_valid)
+		return;
+
+	nh_offset = (int)(ptrdiff_t)(nh - nh->grp->nhgi->nexthops);
+	grp_idx = nh->grp->nhgi->atbl_offset;
+
+	sparx5_rr_arp_tbl_hw_addr_apply(sparx5, mac, vmid, grp_idx + nh_offset);
+}
+
+static void
+sparx5_rr_nexthops_update_notify(struct sparx5 *sparx5,
+				 struct sparx5_rr_neigh_entry *neigh_entry,
+				 bool entry_connected)
+{
+	struct sparx5_rr_nexthop *nh;
+
+	list_for_each_entry(nh, &neigh_entry->nexthop_list, neigh_list_node)
+		sparx5_rr_nexthop_neigh_update(sparx5, nh, entry_connected);
+}
+
+static int sparx5_rr_neigh_entry_hw_apply(struct sparx5 *sparx5,
+					  struct sparx5_rr_neigh_entry *entry)
+{
+	u32 prefix_len = SPARX5_IADDR_LEN(entry->key.iaddr.version);
+
+	if (!entry->hw_route.vrule_id_valid)
+		return sparx5_rr_lpm_arp_entry_create(sparx5,
+						      &entry->key.iaddr,
+						      prefix_len,
+						      entry->hwaddr,
+						      entry->vmid,
+						      &entry->hw_route);
+
+	return sparx5_rr_lpm_arp_entry_mod(sparx5, entry->hwaddr, entry->vmid,
+					    entry->hw_route.vrule_id);
+}
+
+static void sparx5_rr_neigh_entry_update(struct sparx5 *sparx5,
+					 struct sparx5_rr_neigh_entry *entry,
+					 bool adding)
+{
+	struct net_device *pdev = sparx5->router->port_dev;
+	bool offloaded = adding;
+	int err;
+
+	if (!adding && !entry->connected && !entry->hw_route.vrule_id_valid)
+		return;
+
+	entry->connected = adding;
+
+	if (adding) {
+		err = sparx5_rr_neigh_entry_hw_apply(sparx5, entry);
+		if (err)
+			offloaded = false;
+	} else if (entry->hw_route.vrule_id_valid) {
+		vcap_del_rule(sparx5->vcap_ctrl, pdev,
+			      entry->hw_route.vrule_id);
+		entry->hw_route.vrule_id_valid = false;
+	}
+
+	return sparx5_rr_neigh_entry_offload_mark(entry, offloaded);
+}
+
+static void sparx5_rr_fib_entry_destroy(struct sparx5 *sparx5,
+					struct sparx5_rr_fib_entry *fib_entry)
+{
+	struct net_device *pdev = sparx5->router->port_dev;
+	struct sparx5_rr_neigh_entry *neigh_entry, *tmp;
+	struct vcap_control *vctrl = sparx5->vcap_ctrl;
+
+	list_del(&fib_entry->fib_node);
+	sparx5_rr_fib_lpm_remove(fib_entry);
+
+	list_for_each_entry_safe(neigh_entry, tmp, &fib_entry->neigh_list,
+				 fib_list_node) {
+		list_del(&neigh_entry->fib_list_node);
+		neigh_entry->fib_entry = NULL;
+
+		/* Remove LPM VCAP entry for neighbour, if used */
+		sparx5_rr_neigh_entry_update(sparx5, neigh_entry, false);
+		sparx5_rr_nexthops_update_notify(sparx5, neigh_entry, false);
+		sparx5_rr_neigh_entry_put(sparx5, neigh_entry);
+	}
+
+	sparx5_rr_fib_entry_remove(sparx5, fib_entry);
+	sparx5_rr_nexthop_group_put(sparx5, fib_entry->nh_grp);
+	if (fib_entry->hw_route.vrule_id_valid)
+		vcap_del_rule(vctrl, pdev, fib_entry->hw_route.vrule_id);
+	sparx5_rr_fib_info_put(&fib_entry->fi);
+	kfree(fib_entry);
+}
+
+/* Update nexthop group based on current fib_info state. */
+static int
+sparx5_rr_entry_nexthop_group_update(struct sparx5 *sparx5,
+				     struct sparx5_rr_fib_entry *fib_entry)
+{
+	struct net_device *pdev = sparx5->router->port_dev;
+	struct vcap_control *vctrl = sparx5->vcap_ctrl;
+	struct sparx5_rr_nexthop_group *new_nh_grp;
+	struct sparx5_rr_nexthop_group *old_nh_grp;
+	bool old_vrule_id_valid;
+	u32 old_vrule_id;
+	int err;
+
+	old_nh_grp = fib_entry->nh_grp;
+	old_vrule_id = fib_entry->hw_route.vrule_id;
+	old_vrule_id_valid = fib_entry->hw_route.vrule_id_valid;
+
+	/* Prepare new group in SW representation */
+	new_nh_grp = sparx5_rr_nexthop_group_create(sparx5, fib_entry);
+	if (IS_ERR(new_nh_grp)) {
+		dev_warn(sparx5->dev, "Failed to create nexthop group\n");
+		return PTR_ERR(new_nh_grp);
+	}
+
+	fib_entry->nh_grp = new_nh_grp;
+	new_nh_grp->fib_entry = fib_entry;
+
+	/* Write new rule to HW */
+	err = sparx5_rr_fib_entry_hw_apply(sparx5, fib_entry);
+	if (err)
+		goto hw_apply_err;
+
+	/* Clean up old rule and start routing traffic according to new rule */
+	if (old_vrule_id_valid && fib_entry->hw_route.vrule_id != old_vrule_id)
+		vcap_del_rule(vctrl, pdev, old_vrule_id);
+
+	/* Remove old unused group */
+	sparx5_rr_nexthop_group_put(sparx5, old_nh_grp);
+
+	return 0;
+
+hw_apply_err:
+	fib_entry->nh_grp = old_nh_grp;
+	new_nh_grp->fib_entry = NULL;
+	sparx5_rr_nexthop_group_put(sparx5, new_nh_grp);
+	return err;
+}
+
 static void sparx5_rr_leg_hw_init(struct sparx5 *sparx5,
 				  struct sparx5_rr_router_leg *leg)
 {
@@ -599,6 +2051,21 @@ sparx5_rr_router_leg_create(struct sparx5 *sparx5, struct net_device *dev,
 	return leg;
 }
 
+static void sparx5_rr_fib4_del(struct sparx5 *sparx5,
+			       struct sparx5_rr_fib_info *fi)
+{
+	struct sparx5_rr_fib_entry *fib_entry;
+	struct sparx5_rr_fib_key key;
+
+	sparx5_rr_fib_info_to_fib_key(fi, &key);
+
+	fib_entry = sparx5_rr_fib_entry_lookup(sparx5, &key);
+	if (!fib_entry)
+		return;
+
+	sparx5_rr_fib_entry_destroy(sparx5, fib_entry);
+}
+
 static bool sparx5_rr_dev_real_is_vlan_aware(struct net_device *dev)
 {
 	struct net_device *vlan_rdev;
@@ -620,6 +2087,444 @@ static bool sparx5_rr_dev_real_is_vlan_aware(struct net_device *dev)
 	return false;
 }
 
+static bool sparx5_rr_fib_info_should_offload(struct sparx5 *sparx5,
+					      struct sparx5_rr_fib_info *fi)
+{
+	u32 tb_id = sparx5_rr_fib_info_tb_id(fi);
+	u8 type = sparx5_rr_fib_info_type(fi);
+	int nhs = sparx5_rr_fib_info_nhs(fi);
+
+	if (!(type == RTN_UNICAST ||
+	      type == RTN_LOCAL ||
+	      type == RTN_BLACKHOLE ||
+	      type == RTN_PROHIBIT ||
+	      type == RTN_UNREACHABLE))
+		return false;
+
+	if (!(tb_id == RT_TABLE_MAIN ||
+	      tb_id == RT_TABLE_LOCAL))
+		return false;
+
+	/* No support for nexthop objects (optimization for larger scale
+	 * routing). Instead each route has a copy of it's nexthops.
+	 */
+	if (sparx5_rr_fib_info_is_nh_obj(fi))
+		return false;
+
+	/* For IPv4 the nexthops of these route types have NULL egress device.
+	 * However, for IPv6 the nexthops use the loopback interface, so accept
+	 * early.
+	 */
+	if (type == RTN_BLACKHOLE ||
+	    type == RTN_PROHIBIT ||
+	    type == RTN_UNREACHABLE)
+		return true;
+
+	if (nhs > SPARX5_MAX_ECMP_SIZE)
+		return false;
+
+	for (int i = 0; i < nhs; i++) {
+		struct fib_nh_common *nhc = sparx5_rr_fib_info_nhc(fi, i);
+
+		if (nhc->nhc_dev &&
+		    !sparx5_rr_dev_real_is_vlan_aware(nhc->nhc_dev))
+			return false;
+
+		/* HW only supports equal weight nexthops */
+		if (nhc->nhc_weight != 1)
+			return false;
+	}
+
+	return true;
+}
+
+static int sparx5_rr_fib_replace(struct sparx5 *sparx5,
+				 struct sparx5_rr_fib_info *fi)
+{
+	u8 fi_type = sparx5_rr_fib_info_type(fi);
+	struct sparx5_rr_fib_entry *fib_entry;
+	struct sparx5_rr_fib_info old_fi;
+	struct sparx5_rr_fib_key key;
+	int err = 0;
+
+	if (sparx5_rr_fib_info_should_ignore(fi))
+		return 0;
+
+	sparx5_rr_fib_info_to_fib_key(fi, &key);
+
+	fib_entry = sparx5_rr_fib_entry_lookup(sparx5, &key);
+
+	if (!sparx5_rr_fib_info_should_offload(sparx5, fi)) {
+		/* A previously offloadable fib, is modified to unoffloadable
+		 * state, so we must remove it.
+		 */
+		if (fib_entry)
+			sparx5_rr_fib_entry_destroy(sparx5, fib_entry);
+		return 0;
+	}
+
+	if (!fib_entry) {
+		/* Holds refs to kernel fib_info */
+		fib_entry = sparx5_rr_fib_entry_create(sparx5, &key, fi);
+		if (IS_ERR(fib_entry)) {
+			dev_warn(sparx5->dev, "Failed to create fib entry\n");
+			sparx5_rr_fib_info_offload_mark(sparx5, fi, false,
+							false, true);
+			return PTR_ERR(fib_entry);
+		}
+
+		err = sparx5_rr_fib_entry_hw_apply(sparx5, fib_entry);
+		goto out_fib_mark_offload;
+	}
+
+	/* Save old fib_info, add new one, then release old. This ordering
+	 * ensures fib_entry retains valid fi on allocation failure.
+	 */
+	old_fi = fib_entry->fi;
+
+	/* Clear fib_entry fi */
+	sparx5_rr_fib_info_init(&fib_entry->fi, fi->version);
+
+	/* Hold and replace with new fib_info */
+	err = sparx5_rr_fib_entry_fib_info_add(fib_entry, fi);
+	if (err) {
+		fib_entry->fi = old_fi;
+		dev_err(sparx5->dev, "Failed to replace fib info\n");
+		sparx5_rr_fib_info_offload_mark(sparx5, fi, false, false, true);
+		sparx5_rr_fib_entry_destroy(sparx5, fib_entry);
+		return err;
+	}
+
+	/* Release and allow any previous fib_info to be deleted */
+	sparx5_rr_fib_info_put(&old_fi);
+
+	fib_entry->type = sparx5_rr_rtm_type2fib_type(fi_type);
+
+	err = sparx5_rr_entry_nexthop_group_update(sparx5, fib_entry);
+
+out_fib_mark_offload:
+	fib_entry->offload_fail = !!err;
+	sparx5_rr_fib_entry_offload_mark(sparx5, fib_entry);
+	if (err)
+		sparx5_rr_fib_entry_destroy(sparx5, fib_entry);
+	return err;
+}
+
+static void sparx5_rr_fib4_event_work(struct work_struct *work)
+{
+	struct sparx5_fib_event_work *fib_work =
+		container_of(work, struct sparx5_fib_event_work, work);
+	struct sparx5 *sparx5 = fib_work->sparx5;
+	int err;
+
+	mutex_lock(&sparx5->router->lock);
+
+	switch (fib_work->event) {
+	case FIB_EVENT_ENTRY_REPLACE:
+		err = sparx5_rr_fib_replace(sparx5, &fib_work->fi);
+		if (err)
+			dev_warn(sparx5->dev, "FIB replace failed, ip=%pI4l\n",
+				 &fib_work->fi.fen4_info.dst);
+
+		break;
+	case FIB_EVENT_ENTRY_DEL:
+		sparx5_rr_fib4_del(sparx5, &fib_work->fi);
+		break;
+	default:
+		/* FIB_EVENT_ENTRY_APPEND only occurs for IPv6. */
+		WARN_ON_ONCE(1); /* BUG */
+		break;
+	}
+
+	/* Release fib_info hold for workqueue. */
+	sparx5_rr_fib_info_put(&fib_work->fi);
+	mutex_unlock(&sparx5->router->lock);
+	kfree(fib_work);
+}
+
+static int sparx5_rr_fib6_append(struct sparx5 *sparx5,
+				 struct sparx5_rr_fib_info *fi)
+{
+	struct sparx5_rr_fib_entry *fib_entry;
+	struct sparx5_rr_fib_key key;
+	int err = 0;
+
+	if (sparx5_rr_fib_info_should_ignore(fi))
+		return 0;
+
+	sparx5_rr_fib_info_to_fib_key(fi, &key);
+
+	fib_entry = sparx5_rr_fib_entry_lookup(sparx5, &key);
+	if (!fib_entry)
+		return 0;
+
+	/* Are we adding new nexthops which can not be offloaded */
+	if (!sparx5_rr_fib_info_should_offload(sparx5, fi)) {
+		err = -EINVAL;
+		goto out_fib_mark_offload;
+	}
+
+	/* Append new rt_arr data to fen6_info rt data */
+	err = sparx5_rr_fib_entry_fib_info_add(fib_entry, fi);
+	if (err)
+		goto out_fib_mark_offload;
+
+	/* Realloc nexthop group and apply to hw. */
+	err = sparx5_rr_entry_nexthop_group_update(sparx5, fib_entry);
+
+out_fib_mark_offload:
+	fib_entry->offload_fail = !!err;
+	sparx5_rr_fib_entry_offload_mark(sparx5, fib_entry);
+	if (err)
+		sparx5_rr_fib_entry_destroy(sparx5, fib_entry);
+
+	return err;
+}
+
+static bool sparx5_rr_fib6_rt_exists(struct sparx5_rr_fib6_entry_info *f6i,
+				     struct fib6_info *rt)
+{
+	for (int i = 0; i < f6i->nrt6; i++)
+		if (f6i->rt_arr[i] == rt)
+			return true;
+
+	return false;
+}
+
+static int sparx5_rr_fib6_nexthop_prune(struct sparx5 *sparx5,
+					struct sparx5_rr_fib_entry *fib_entry,
+					struct sparx5_rr_fib6_entry_info *f6i)
+{
+	struct fib6_info **old_rt_arr = fib_entry->fi.fe6_info.rt_arr;
+	unsigned int old_nrt6, new_nrt6;
+	struct fib6_info **rt_arr;
+	int j = 0;
+
+	old_nrt6 = fib_entry->fi.fe6_info.nrt6;
+	new_nrt6 = old_nrt6 >= f6i->nrt6 ? old_nrt6 - f6i->nrt6 : 0;
+
+	rt_arr = kzalloc_objs(struct fib6_info *, new_nrt6);
+	if (!rt_arr)
+		return -ENOMEM;
+
+	for (int i = 0; i < old_nrt6; i++) {
+		struct fib6_info *fi = old_rt_arr[i];
+
+		if (sparx5_rr_fib6_rt_exists(f6i, fi)) {
+			sparx5_rr_rt6_release(fi);
+			continue;
+		}
+
+		rt_arr[j++] = fi;
+	}
+
+	/* Assume incoming f6i only contain live nexthops, and no duplicates. */
+	WARN_ON_ONCE(j != new_nrt6);
+
+	kfree(fib_entry->fi.fe6_info.rt_arr);
+	fib_entry->fi.fe6_info.nrt6 = new_nrt6;
+	fib_entry->fi.fe6_info.rt_arr = rt_arr;
+	return 0;
+}
+
+static int sparx5_rr_fib6_del(struct sparx5 *sparx5,
+			      struct sparx5_rr_fib_info *fi)
+{
+	struct sparx5_rr_fib_entry *fib_entry;
+	int nhs = sparx5_rr_fib_info_nhs(fi);
+	struct sparx5_rr_fib_key key;
+	int err;
+
+	sparx5_rr_fib_info_to_fib_key(fi, &key);
+
+	fib_entry = sparx5_rr_fib_entry_lookup(sparx5, &key);
+	if (!fib_entry)
+		return 0;
+
+	/* Full delete. */
+	if (nhs == sparx5_rr_fib_info_nhs(&fib_entry->fi)) {
+		sparx5_rr_fib_entry_destroy(sparx5, fib_entry);
+		return 0;
+	}
+
+	/* Partial delete. Remove fi nexthops from fib_entry. */
+	err = sparx5_rr_fib6_nexthop_prune(sparx5, fib_entry, &fi->fe6_info);
+	if (err)
+		goto err_nexthop_prune;
+
+	/* Realloc nexthop group and apply to hw. */
+	err = sparx5_rr_entry_nexthop_group_update(sparx5, fib_entry);
+
+err_nexthop_prune:
+	fib_entry->offload_fail = !!err;
+	sparx5_rr_fib_entry_offload_mark(sparx5, fib_entry);
+	if (err)
+		sparx5_rr_fib_entry_destroy(sparx5, fib_entry);
+
+	return err;
+}
+
+static void sparx5_rr_fib6_event_work(struct work_struct *work)
+{
+	struct sparx5_fib_event_work *fib_work =
+		container_of(work, struct sparx5_fib_event_work, work);
+	struct sparx5_rr_fib_info *fi = &fib_work->fi;
+	struct sparx5 *sparx5 = fib_work->sparx5;
+	int err;
+
+	mutex_lock(&sparx5->router->lock);
+
+	switch (fib_work->event) {
+	case FIB_EVENT_ENTRY_REPLACE:
+		err = sparx5_rr_fib_replace(sparx5, fi);
+		if (err)
+			dev_warn(sparx5->dev, "FIB 6 replace failed.\n");
+
+		break;
+
+	case FIB_EVENT_ENTRY_APPEND:
+		/* Netlink API for IPv6 is different from IPV4. It is
+		 * possible to do partial update/deletes of nexthops on a
+		 * route. In this case fi only contains the nexthops to
+		 * add/remove, and must be merged with the existing nexthops
+		 * on the route. Therefore, we only share fib_replace between
+		 * IPv6 and IPv4 logic.
+		 */
+		err = sparx5_rr_fib6_append(sparx5, fi);
+		if (err)
+			dev_warn(sparx5->dev, "FIB 6 append failed.\n");
+
+		break;
+
+	case FIB_EVENT_ENTRY_DEL:
+		err = sparx5_rr_fib6_del(sparx5, fi);
+		if (err)
+			dev_warn(sparx5->dev, "FIB 6 delete failed.\n");
+
+		break;
+
+	default:
+		WARN_ON_ONCE(1); /* BUG */
+		break;
+	}
+
+	/* Release fib6_info holds for workqueue. */
+	sparx5_rr_fib_info_put(fi);
+	mutex_unlock(&sparx5->router->lock);
+	kfree(fib_work);
+}
+
+static int sparx5_rr_fib6_work_init(struct sparx5_fib_event_work *fib_work,
+				    struct fib6_entry_notifier_info *fen6_info)
+{
+	struct sparx5_rr_fib6_entry_info *fib6_info = &fib_work->fi.fe6_info;
+	struct fib6_info *rt = fen6_info->rt;
+	struct fib6_info **rt_arr;
+	struct fib6_info *iter;
+	unsigned int nrt6;
+	int i = 0;
+
+	nrt6 = fen6_info->nsiblings + 1;
+
+	rt_arr = kzalloc_objs(struct fib6_info *, nrt6, GFP_ATOMIC);
+	if (!rt_arr)
+		return -ENOMEM;
+
+	fib6_info->rt_arr = rt_arr;
+	fib6_info->nrt6 = nrt6;
+
+	rt_arr[0] = rt;
+	fib6_info_hold(rt);
+
+	if (!fen6_info->nsiblings)
+		return 0;
+
+	list_for_each_entry(iter, &rt->fib6_siblings, fib6_siblings) {
+		if (i == fen6_info->nsiblings)
+			break;
+
+		rt_arr[i + 1] = iter;
+		fib6_info_hold(iter);
+		i++;
+	}
+
+	return 0;
+}
+
+/* Handle fib events, which manage fib_entries. Called in atomic context, with
+ * rcu_read_lock().
+ */
+static int sparx5_rr_fib_event(struct notifier_block *nb, unsigned long event,
+			       void *ptr)
+{
+	struct fib6_entry_notifier_info *fen6_info;
+	struct fib_entry_notifier_info *fen_info;
+	struct sparx5_fib_event_work *fib_work;
+	struct fib_notifier_info *info = ptr;
+	struct sparx5_router *router;
+	int err;
+
+	/* Handle IPv4 and IPv6  */
+	if (info->family != AF_INET && info->family != AF_INET6)
+		return NOTIFY_DONE;
+
+	if (event != FIB_EVENT_ENTRY_REPLACE &&
+	    event != FIB_EVENT_ENTRY_DEL &&
+	    event != FIB_EVENT_ENTRY_APPEND)
+		return NOTIFY_DONE;
+
+	router = container_of(nb, struct sparx5_router, fib_nb);
+
+	fib_work = kzalloc_obj(*fib_work, GFP_ATOMIC);
+	if (!fib_work)
+		return NOTIFY_BAD;
+
+	fib_work->sparx5 = router->sparx5;
+	fib_work->event = event;
+
+	switch (info->family) {
+	case AF_INET:
+		INIT_WORK(&fib_work->work, sparx5_rr_fib4_event_work);
+
+		fen_info = container_of(info, struct fib_entry_notifier_info,
+					info);
+		fib_work->fi.fen4_info = *fen_info;
+		fib_work->fi.version = SPARX5_IPV4;
+
+		/* Hold fib_info while item is queued */
+		fib_info_hold(fib_work->fi.fen4_info.fi);
+
+		sparx5_rr_schedule_work(router->sparx5, &fib_work->work);
+		break;
+	case AF_INET6:
+		INIT_WORK(&fib_work->work, sparx5_rr_fib6_event_work);
+
+		/* Copy and hold fib6_info for route and all nhs while item is
+		 * queued.
+		 */
+		fen6_info = container_of(info, struct fib6_entry_notifier_info,
+					 info);
+		err = sparx5_rr_fib6_work_init(fib_work, fen6_info);
+		if (err)
+			goto err_fib6;
+
+		fib_work->fi.version = SPARX5_IPV6;
+
+		sparx5_rr_schedule_work(router->sparx5, &fib_work->work);
+		break;
+	default:
+		goto err_fam_unhandled;
+	}
+
+	return NOTIFY_DONE;
+
+err_fam_unhandled:
+	WARN_ON_ONCE(1); /* BUG */
+err_fib6:
+	kfree(fib_work);
+	return NOTIFY_BAD;
+}
+
 static void sparx5_rr_leg_base_mac_set(struct sparx5 *sparx5,
 				       unsigned char mac[ETH_ALEN])
 {
@@ -922,16 +2827,25 @@ int sparx5_rr_router_init(struct sparx5 *sparx5)
 	INIT_LIST_HEAD(&r->leg_list);
 	INIT_LIST_HEAD(&r->fib_lpm4_list);
 	INIT_LIST_HEAD(&r->fib_lpm6_list);
+	INIT_LIST_HEAD(&r->fib_list);
 
 	/* Add reserved leg for blackhole routes. */
 	err = sparx5_rr_blackhole_leg_create(sparx5);
 	if (err)
 		goto err_free_router;
 
+	err = rhashtable_init(&r->neigh_ht, &sparx5_neigh_ht_params);
+	if (err)
+		goto err_blackhole_destroy;
+
+	err = rhashtable_init(&r->fib_ht, &sparx5_rr_fib_entry_ht_params);
+	if (err)
+		goto err_neigh_ht_destroy;
+
 	r->sparx5_router_owq = alloc_ordered_workqueue("sparx5_router_owq", 0);
 	if (!r->sparx5_router_owq) {
 		err = -ENOMEM;
-		goto err_blackhole_destroy;
+		goto err_fib_ht_destroy;
 	}
 
 	atomic_set(&r->legs_count, 0);
@@ -989,10 +2903,15 @@ int sparx5_rr_router_init(struct sparx5 *sparx5)
 		 ANA_ACL_VCAP_S2_MISC_CTRL_ACL_RT_SEL, sparx5,
 		 ANA_ACL_VCAP_S2_MISC_CTRL);
 
+	r->fib_nb.notifier_call = sparx5_rr_fib_event;
+	err = register_fib_notifier(&init_net, &r->fib_nb, NULL, NULL);
+	if (err)
+		goto err_workqueue_destroy;
+
 	r->inetaddr_nb.notifier_call = sparx5_rr_inetaddr_event;
 	err = register_inetaddr_notifier(&r->inetaddr_nb);
 	if (err)
-		goto err_workqueue_destroy;
+		goto err_unreg_fib_notifier;
 
 	r->inetaddr_valid_nb.notifier_call = sparx5_rr_inetaddr_valid_event;
 	err = register_inetaddr_validator_notifier(&r->inetaddr_valid_nb);
@@ -1024,8 +2943,14 @@ int sparx5_rr_router_init(struct sparx5 *sparx5)
 	unregister_inetaddr_validator_notifier(&r->inetaddr_valid_nb);
 err_unreg_inet_notifier:
 	unregister_inetaddr_notifier(&r->inetaddr_nb);
+err_unreg_fib_notifier:
+	unregister_fib_notifier(&init_net, &r->fib_nb);
 err_workqueue_destroy:
 	destroy_workqueue(r->sparx5_router_owq);
+err_fib_ht_destroy:
+	rhashtable_destroy(&r->fib_ht);
+err_neigh_ht_destroy:
+	rhashtable_destroy(&r->neigh_ht);
 err_blackhole_destroy:
 	sparx5_rr_router_legs_flush(sparx5);
 err_free_router:
@@ -1035,6 +2960,15 @@ int sparx5_rr_router_init(struct sparx5 *sparx5)
 	return err;
 }
 
+static void sparx5_rr_fib_flush(struct sparx5 *sparx5)
+{
+	struct sparx5_rr_fib_entry *fib_entry, *tmp;
+
+	list_for_each_entry_safe(fib_entry, tmp, &sparx5->router->fib_list,
+				 fib_node)
+		sparx5_rr_fib_entry_destroy(sparx5, fib_entry);
+}
+
 void sparx5_rr_router_deinit(struct sparx5 *sparx5)
 {
 	struct sparx5_router *router = sparx5->router;
@@ -1044,7 +2978,11 @@ void sparx5_rr_router_deinit(struct sparx5 *sparx5)
 	unregister_netdevice_notifier(&router->netdevice_nb);
 	unregister_inetaddr_validator_notifier(&router->inetaddr_valid_nb);
 	unregister_inetaddr_notifier(&router->inetaddr_nb);
+	unregister_fib_notifier(&init_net, &router->fib_nb);
 	destroy_workqueue(router->sparx5_router_owq);
+	sparx5_rr_fib_flush(sparx5);
+	rhashtable_destroy(&router->fib_ht);
+	rhashtable_destroy(&router->neigh_ht);
 	sparx5_rr_router_legs_flush(sparx5);
 	mutex_destroy(&router->lock);
 	kfree(router);

-- 
2.52.0



^ permalink raw reply related

* [PATCH net-next 7/9] net: sparx5: add L3 router infrastructure and leg management
From: Jens Emil Schulz Østergaard @ 2026-06-12 12:37 UTC (permalink / raw)
  To: Horatiu Vultur, UNGLinuxDriver, Andrew Lunn, David S. Miller,
	Eric Dumazet, Jakub Kicinski, Paolo Abeni, Daniel Machon,
	Steen Hegelund, Kees Cook, Gustavo A. R. Silva
  Cc: netdev, linux-kernel, linux-arm-kernel, linux-hardening,
	Jens Emil Schulz Østergaard
In-Reply-To: <20260612-sparx5_l3_routing-v1-0-fc3c10160f49@microchip.com>

Add event handling for netdevices and iaddrs to intercept VLAN uppers of
a bridge device, and their IP addresses. Add all the basic types we will
use for l3 routing management. Add lifecycle management for router legs.

The chips can only route in VLAN aware mode, and routing happens between
router legs (RLEG). To each VLAN upper of a VLAN aware bridge, we
associate a router leg. The indices to the router leg table are called
VMIDs in the datasheet.

There is a global router leg base MAC used for MAC rewrites by the chip.
Here we use the bridge MAC.

A specific leg is reserved to enable blackhole routes. The blackhole
route uses an reserved egress VLAN id for which the port mask is empty,
resulting in all frames being dropped.

Reviewed-by: Daniel Machon <daniel.machon@microchip.com>
Reviewed-by: Steen Hegelund <Steen.Hegelund@microchip.com>
Signed-off-by: Jens Emil Schulz Østergaard <jensemil.schulzostergaard@microchip.com>
---
 drivers/net/ethernet/microchip/sparx5/Makefile     |    2 +-
 .../ethernet/microchip/sparx5/lan969x/lan969x.c    |    2 +
 .../net/ethernet/microchip/sparx5/sparx5_main.c    |   13 +-
 .../net/ethernet/microchip/sparx5/sparx5_main.h    |   43 +
 .../net/ethernet/microchip/sparx5/sparx5_router.c  | 1051 ++++++++++++++++++++
 5 files changed, 1109 insertions(+), 2 deletions(-)

diff --git a/drivers/net/ethernet/microchip/sparx5/Makefile b/drivers/net/ethernet/microchip/sparx5/Makefile
index d447f9e84d92..d917734d9f12 100644
--- a/drivers/net/ethernet/microchip/sparx5/Makefile
+++ b/drivers/net/ethernet/microchip/sparx5/Makefile
@@ -11,7 +11,7 @@ sparx5-switch-y  := sparx5_main.o sparx5_packet.o \
  sparx5_ptp.o sparx5_pgid.o sparx5_tc.o sparx5_qos.o \
  sparx5_vcap_impl.o sparx5_vcap_ag_api.o sparx5_tc_flower.o \
  sparx5_tc_matchall.o sparx5_pool.o sparx5_sdlb.o sparx5_police.o \
- sparx5_psfp.o sparx5_mirror.o sparx5_regs.o
+ sparx5_psfp.o sparx5_mirror.o sparx5_regs.o sparx5_router.o
 
 sparx5-switch-$(CONFIG_SPARX5_DCB) += sparx5_dcb.o
 sparx5-switch-$(CONFIG_DEBUG_FS) += sparx5_vcap_debugfs.o
diff --git a/drivers/net/ethernet/microchip/sparx5/lan969x/lan969x.c b/drivers/net/ethernet/microchip/sparx5/lan969x/lan969x.c
index f3a9c71bea36..f8d1741c264f 100644
--- a/drivers/net/ethernet/microchip/sparx5/lan969x/lan969x.c
+++ b/drivers/net/ethernet/microchip/sparx5/lan969x/lan969x.c
@@ -322,6 +322,8 @@ static const struct sparx5_consts lan969x_consts = {
 	.qres_max_prio_idx   = 315,
 	.qres_max_colour_idx = 323,
 	.tod_pin             = 4,
+	.vmid_cnt            = 127,
+	.arp_tbl_cnt         = 1024,
 	.vcaps               = lan969x_vcaps,
 	.vcap_stats          = &lan969x_vcap_stats,
 	.vcaps_cfg           = lan969x_vcap_inst_cfg,
diff --git a/drivers/net/ethernet/microchip/sparx5/sparx5_main.c b/drivers/net/ethernet/microchip/sparx5/sparx5_main.c
index dad713e9ddd5..24acdbc20dc8 100644
--- a/drivers/net/ethernet/microchip/sparx5/sparx5_main.c
+++ b/drivers/net/ethernet/microchip/sparx5/sparx5_main.c
@@ -994,14 +994,22 @@ static int mchp_sparx5_probe(struct platform_device *pdev)
 		goto cleanup_ptp;
 	}
 
+	err = sparx5_rr_router_init(sparx5);
+	if (err) {
+		dev_err(sparx5->dev, "Router initialization failed\n");
+		goto cleanup_netdevs;
+	}
+
 	err = sparx5_register_notifier_blocks(sparx5);
 	if (err) {
 		dev_err(sparx5->dev, "Failed to register notifier blocks\n");
-		goto cleanup_netdevs;
+		goto cleanup_router;
 	}
 
 	goto cleanup_config;
 
+cleanup_router:
+	sparx5_rr_router_deinit(sparx5);
 cleanup_netdevs:
 	sparx5_unregister_netdevs(sparx5);
 cleanup_ptp:
@@ -1031,6 +1039,7 @@ static void mchp_sparx5_remove(struct platform_device *pdev)
 	sparx5_unregister_notifier_blocks(sparx5);
 	sparx5_unregister_netdevs(sparx5);
 	sparx5_ptp_deinit(sparx5);
+	sparx5_rr_router_deinit(sparx5);
 	sparx5_frame_io_deinit(sparx5);
 	sparx5_stats_deinit(sparx5);
 	sparx5_mact_deinit(sparx5);
@@ -1067,6 +1076,8 @@ static const struct sparx5_consts sparx5_consts = {
 	.qres_max_prio_idx   = 630,
 	.qres_max_colour_idx = 638,
 	.tod_pin             = 4,
+	.vmid_cnt            = 511,
+	.arp_tbl_cnt         = 2048,
 	.vcaps               = sparx5_vcaps,
 	.vcaps_cfg           = sparx5_vcap_inst_cfg,
 	.vcap_stats          = &sparx5_vcap_stats,
diff --git a/drivers/net/ethernet/microchip/sparx5/sparx5_main.h b/drivers/net/ethernet/microchip/sparx5/sparx5_main.h
index eb57b86fbe22..5dc18b8dbed0 100644
--- a/drivers/net/ethernet/microchip/sparx5/sparx5_main.h
+++ b/drivers/net/ethernet/microchip/sparx5/sparx5_main.h
@@ -136,6 +136,10 @@ enum sparx5_feature {
 
 #define SPARX5_MAX_PTP_ID	512
 
+/* Must be maximum values across all L3 enabled platforms */
+#define SPARX5_ROUTER_LEG_N_VMID 511
+#define SPARX5_ARP_TBL_SIZE 2048
+
 struct sparx5;
 
 struct sparx5_calendar_data {
@@ -320,6 +324,8 @@ struct sparx5_consts {
 	u32 qres_max_prio_idx;   /* Maximum QRES prio index */
 	u32 qres_max_colour_idx; /* Maximum QRES colour index */
 	u32 tod_pin;             /* PTP TOD pin */
+	u32 vmid_cnt;            /* Number of router leg VMID's */
+	u32 arp_tbl_cnt;         /* Number of ARP table entries */
 	const struct sparx5_vcap_inst *vcaps_cfg;
 	const struct vcap_info *vcaps;
 	const struct vcap_statistics *vcap_stats;
@@ -434,6 +440,8 @@ struct sparx5 {
 	/* Common root for debugfs */
 	struct dentry *debugfs_root;
 	const struct sparx5_match_data *data;
+	/* L3 Forwarding */
+	struct sparx5_router *router;
 };
 
 /* sparx5_main.c */
@@ -503,6 +511,41 @@ int sparx5_vlan_vid_add(struct sparx5_port *port, u16 vid, bool pvid,
 int sparx5_vlan_vid_del(struct sparx5_port *port, u16 vid);
 void sparx5_vlan_port_apply(struct sparx5 *sparx5, struct sparx5_port *port);
 
+/* sparx5_router.c */
+int sparx5_rr_router_init(struct sparx5 *sparx5);
+void sparx5_rr_router_deinit(struct sparx5 *sparx5);
+
+struct sparx5_rr_hw_route {
+	u32 vrule_id;
+	bool vrule_id_valid;
+};
+
+struct sparx5_router {
+	struct sparx5 *sparx5;
+	struct notifier_block fib_nb;
+	struct notifier_block netevent_nb;
+	struct notifier_block inetaddr_nb;
+	struct notifier_block inetaddr_valid_nb;
+	struct notifier_block netdevice_nb;
+	struct notifier_block inet6addr_nb;
+	struct notifier_block inet6addr_valid_nb;
+	struct sparx5_rr_hw_route link_local; /* Trap all link-local traffic. */
+	struct net_device *port_dev; /* For VCAP API. */
+
+	struct list_head fib_lpm4_list;
+	struct list_head fib_lpm6_list;
+	struct mutex lock; /* Global router lock for all shared data. */
+
+	struct workqueue_struct *sparx5_router_owq;
+
+	atomic_t legs_count;
+	struct list_head leg_list;
+	/* Track allocated router leg indices in hw */
+	DECLARE_BITMAP(vmid_mask, SPARX5_ROUTER_LEG_N_VMID);
+	/* Track allocated arp table indices in hw */
+	DECLARE_BITMAP(arp_tbl_mask, SPARX5_ARP_TBL_SIZE);
+};
+
 /* sparx5_calendar.c */
 int sparx5_calendar_init(struct sparx5 *sparx5);
 int sparx5_dsm_calendar_calc(struct sparx5 *sparx5, u32 taxi,
diff --git a/drivers/net/ethernet/microchip/sparx5/sparx5_router.c b/drivers/net/ethernet/microchip/sparx5/sparx5_router.c
new file mode 100644
index 000000000000..03923d91fdfb
--- /dev/null
+++ b/drivers/net/ethernet/microchip/sparx5/sparx5_router.c
@@ -0,0 +1,1051 @@
+// SPDX-License-Identifier: GPL-2.0+
+/* Microchip Sparx5 Switch driver
+ *
+ * Copyright (c) 2026 Microchip Technology Inc. and its subsidiaries.
+ */
+
+#include <linux/etherdevice.h>
+#include <linux/if_bridge.h>
+#include <linux/if_ether.h>
+#include <linux/inet.h>
+#include <linux/inetdevice.h>
+#include <linux/list.h>
+#include <linux/rhashtable.h>
+#include <net/addrconf.h>
+#include <net/arp.h>
+#include <net/fib_notifier.h>
+#include <net/ip6_fib.h>
+#include <net/ipv6.h>
+#include <net/ndisc.h>
+#include <net/neighbour.h>
+#include <net/netevent.h>
+#include <net/nexthop.h>
+
+#include "sparx5_main.h"
+#include "sparx5_port.h"
+#include "sparx5_vcap_ag_api.h"
+#include "sparx5_vcap_impl.h"
+
+/* The main routing objects are
+ *
+ * 1) Router legs, which correspond to IP interfaces. They can have multiple IPs
+ *    attached and are associated with a VLAN. Only routes with nexthops that
+ *    egress on ports that are part of a router leg are considered for
+ *    offloading.
+ *
+ * 2) Fib entries, which correspond to entries in the routing table.
+ *    > ip route add 6.6.0.0/16 nexthop via 1.0.10.10
+ *    will create fib entry for 6.6.0.0/16, if it is offloadable.
+ *
+ * 3) Nexthop groups and nexthops. Each fib entry has exactly one nexthop hop.
+ *    To simplify the SW representation, we duplicate nexthops and nexthops
+ *    groups for each fib entry, even when the same nexthop is used for many
+ *    different routes.
+ *
+ * 4) Neigh entries, which correspond to directly connected neighbours.
+ *    These are created mainly by ARP events. Their job is to maintain the
+ *    association of L3 and L2 addresses.
+ *
+ *    The neigh entries are referenced by nexthops and fib entries. They are
+ *    shared so we must keep track of the objects that are referencing them.
+ *
+ *    If a neighbour which is used in a nexthop group dies, we will set the mac
+ *    to zero, so traffic for this nexthop is trapped.
+ *
+ *
+ * Both fib_entry and neigh_entry can trigger writes to the LPM VCAP, and own
+ * entries in HW. Fib entries own the corresponding route associated with them.
+ * Neigh entries own a /32 route, for traffic destined directly to the
+ * neighbour.
+ *
+ * We have 3 main cases for routing:
+ *
+ * 1) Routes for directly connected subnets. E.g. router has IP 1.0.10.1, and
+ *    routes subnet 1.0.10.0/24.
+ *
+ *    In this case, we have a fib_entry, with a non-gateway nexthop group. We
+ *    install a LPM VCAP route, with action type arp entry, for 1.0.10.0/24 with
+ *    a zero mac, to ensure frames destined for this subnet will be sent to the
+ *    CPU and start the ARP process.
+ *
+ *    When we get the arp reply, we create a neigh_entry for each neighbour and
+ *    install a direct route in the LPM VCAP for this neighbour. For example,
+ *    1.0.10.11 is sent to DMAC 0x1111110001. This is how we route directly
+ *    connected subnets.
+ *
+ *    Moreover, the fib_entry maintains a list of all neighbours discovered in
+ *    the subnet it is routing. These neighbours hold a reference back to the
+ *    fib_entry.
+ *
+ *                   |-> neigh_entry 1.0.10.10
+ *        neigh_list |-> neigh_entry 1.0.10.11
+ *                   |-> neigh_entry 1.0.10.12
+ *                   |
+ *   +-------------+ |  +--------------+
+ *   |  fib_entry  |-+  | nexthop group|
+ *   | 1.0.10.1/24 |----+              |
+ *   +-------------+    |  nexthop --------> NULL (Write zero mac in VCAP)
+ *                      +--------------+
+ *
+ *
+ * 2) Routes for non-connected subnets. E.g. we are routing subnet 6.6.0.0/16,
+ *    but we have no IP in this subnet. We are routing via nexthops, which are
+ *    directly connected. Say we have >=2 nexthops.
+ *
+ *    In this case, have a fib_entry with a gateway nexthop group. Each nexthop
+ *    points to a neigh_entry, corresponding to the gateway used for routing.
+ *
+ *    Say we use the nexthops:
+ *
+ *    - 1.0.11.10
+ *    - 1.0.10.10
+ *    - 1.0.9.10
+ *
+ *    We install a LPM VCAP route for 6.6.0.0/16, which contains a pointer
+ *    to the hw arp table, and the size of the group. The ARP table
+ *    contains the Mac addresses of the nexthops. The Mac addresses are
+ *    supplied by neigh_entries.
+ *
+ *
+ *   +-------------+    +--------------+
+ *   |  fib_entry  |    | nexthop group|
+ *   | 6.6.0.0/164 |----+              |
+ *   +-------------+    |  nexthop --------> neigh_entry 1.0.11.10
+ *                      |  nexthop --------> neigh_entry 1.0.10.10
+ *                      |  nexthop --------> neigh_entry 1.0.9.10
+ *                      +--------------+
+ *
+ *
+ * 3) Local routes for traffic destined to the router. If the router has an IP
+ *    1.0.10.1, then we must ensure this traffic is sent to the CPU.
+ *    Therefore, we install a direct route for 1.0.10.1/32 in the LPM VCAP, with
+ *    zero mac.
+ *
+ * 4) All IPv6 link-local traffic is explicitly trapped.
+ *
+ * On the hardware side, we use the VCAP LPM and ARP table for UC IPv4
+ * routing. HW picks the match found at the highest address in the VCAP LPM.
+ * To ensure the longest prefix match we make sure to order the entries
+ * according to mask length, with longer masks at higher addresses.
+ *
+ * It is possible to store ARP data, such as DMAC, directly in the VCAP LPM
+ * using ARP entry actions. We do this whenever possible, so the ARP table
+ * is only used when a route has multiple nexthops.
+ *
+ * With the above breakdown in mind, cases 1) and 3) use arp entries, and
+ * case 2) use the arp table if the number of nexthops is >1.
+ *
+ * If the DMAC written to HW is all zero, the chip will trap the frame,
+ * redirecting it to the CPU. This is how we get the kernel to perform ARP
+ * requests on our behalf.
+ *
+ * The nexthop group must be laid out at contiguous addresses in the ARP table.
+ * The VCAP LPM stores a pointer to the bottom address in the group, and the
+ * group size. We do not use the arp pointer remap table.
+ *
+ * The layout of nexthops in a nexthop group matches the layout in HW, e.g.
+ *
+ * nhgi->nexthops[0] -> arp table address n
+ * ...
+ * nhgi->nexthops[k] -> arp table address n+k
+ *
+ * where the n is the ARP table offset (atbl_offset) for the group.
+ */
+
+#define SPARX5_MAX_ECMP_SIZE		16
+#define SPARX5_RLEG_USE_GLOBAL_BASE_MAC	2
+#define SPARX5_LINK_LOCAL_PREFIX_LEN	64
+#define SPARX5_BLACKHOLE_VMID(spx5)	((spx5)->data->consts->vmid_cnt - 1)
+/* The rewriter field REW_RLEG_CTRL_RLEG_EVID will be written with the
+ * blackhole vid, but it is only 12 bits wide. However, this is only used for
+ * routing based rewrites on egress, which is not used for the blackhole.
+ * The important field is ANA_L3_RLEG_CTRL_RLEG_EVID which is 13 bits wide.
+ * Therefore, it is safe to use VIDs wider than 12 bits for the blackhole
+ * vid, frames will not be forwarded into VLAN 0s port mask.
+ */
+#define SPARX5_BLACKHOLE_VID		VLAN_N_VID
+
+struct sparx5_rr_fib6_entry_info {
+	struct fib6_info **rt_arr;
+	unsigned int nrt6;
+};
+
+enum sparx5_rr_l3_version {
+	SPARX5_IPV4 = 0,
+	SPARX5_IPV6,
+};
+
+#define SPARX5_IADDR_LEN(v)		((v) == SPARX5_IPV4 ? 32 : 128)
+/* Order longer prefixes at high addresses. */
+#define LPM_SORT_KEY(plen)		(SPARX5_IADDR_LEN(SPARX5_IPV6) - (plen))
+
+struct sparx5_rr_fib_info {
+	union {
+		struct fib_entry_notifier_info fen4_info;
+		struct sparx5_rr_fib6_entry_info fe6_info;
+	};
+	enum sparx5_rr_l3_version version;
+};
+
+struct sparx5_fib_event_work {
+	struct work_struct work;
+	struct sparx5_rr_fib_info fi;
+	struct sparx5 *sparx5;
+	unsigned long event;
+};
+
+struct sparx5_rr_netevent_work {
+	struct work_struct work;
+	struct sparx5 *sparx5;
+	struct neighbour *neigh;
+	unsigned long event;
+};
+
+struct sparx5_rr_router_leg {
+	struct net_device *dev;
+	netdevice_tracker dev_tracker;
+	struct sparx5 *sparx5;
+	struct list_head leg_list_node; /* Router member */
+	u16 vmid; /* Internal id */
+	u32 vid; /* VLAN id */
+};
+
+struct sparx5_iaddr {
+	union {
+		__be32 ipv4;
+		struct in6_addr ipv6;
+	}; /* Must be first */
+	enum sparx5_rr_l3_version version;
+};
+
+#define LPM_PROTO(iaddr) ((iaddr)->version ? ETH_P_IPV6 : ETH_P_IP)
+
+struct sparx5_rr_neigh_entry {
+	struct sparx5_rr_neigh_key {
+		struct net_device *dev;
+		struct sparx5_iaddr iaddr;
+	} key;
+	struct rhash_head ht_node;
+	struct sparx5_rr_fib_entry *fib_entry;
+	struct list_head fib_list_node; /* Fib route for this neighbour */
+	struct neigh_table *neigh_tbl; /* Kernel neighbour table */
+	struct list_head nexthop_list; /* Nexthops using this neigh entry */
+	struct sparx5_rr_hw_route hw_route;
+	unsigned char hwaddr[ETH_ALEN];
+	u16 vmid;
+	bool connected;
+};
+
+struct sparx5_rr_nexthop {
+	struct sparx5_rr_neigh_entry *neigh_entry;
+	struct sparx5_rr_nexthop_group *grp;
+	struct list_head neigh_list_node; /* Neigh entry member */
+	struct list_head leg_list_node; /* Router leg member */
+	struct neigh_table *neigh_tbl; /* Kernel neighbour table */
+	struct sparx5_iaddr gw_addr;
+	int ifindex;
+	bool gateway;
+	bool trapped;
+};
+
+struct sparx5_rr_nexthop_group_info {
+	struct sparx5_rr_nexthop_group *grp;
+	u16 atbl_offset;
+	bool atbl_offset_valid;
+	u8 count; /* HW allows up to 16 nexthops */
+	struct sparx5_rr_nexthop nexthops[] __counted_by(count);
+};
+
+struct sparx5_rr_nexthop_group {
+	struct sparx5_rr_fib_entry *fib_entry;
+	struct sparx5_rr_nexthop_group_info *nhgi;
+};
+
+enum sparx5_rr_fib_type {
+	SPARX5_RR_FIB_TYPE_INVALID = 0,
+	SPARX5_RR_FIB_TYPE_LOCAL,
+	SPARX5_RR_FIB_TYPE_UNICAST,
+	SPARX5_RR_FIB_TYPE_MULTICAST,
+	SPARX5_RR_FIB_TYPE_BLACKHOLE,
+	SPARX5_RR_FIB_TYPE_PROHIBIT,
+	SPARX5_RR_FIB_TYPE_UNREACHABLE,
+};
+
+struct sparx5_rr_fib_key {
+	struct sparx5_iaddr addr;
+	u32 prefix_len;
+	u32 tb_id; /* Routing table type: RT_TABLE_* */
+};
+
+struct sparx5_rr_fib_entry {
+	struct sparx5_rr_fib_key key;
+	enum sparx5_rr_fib_type type;
+	struct rhash_head ht_node; /* Router member */
+	struct list_head fib_lpm_node; /* Router member */
+	struct list_head neigh_list; /* Neighbours under this route */
+	struct sparx5_rr_hw_route hw_route;
+	struct sparx5_rr_nexthop_group *nh_grp;
+	struct sparx5_rr_fib_info fi;
+	u64 sort_key; /* For sw lpm lookup */
+	bool trap;
+	bool offload_fail;
+};
+
+struct sparx5_rr_inet6addr_event_work {
+	struct work_struct work;
+	struct sparx5 *sparx5;
+	struct net_device *dev;
+	netdevice_tracker dev_tracker;
+	unsigned long event;
+};
+
+static void sparx5_rr_schedule_work(struct sparx5 *sparx5,
+				    struct work_struct *work)
+{
+	queue_work(sparx5->router->sparx5_router_owq, work);
+}
+
+static void sparx5_rr_split_mac(unsigned char mac[ETH_ALEN], u32 split,
+				u32 *msb, u32 *lsb)
+{
+	u32 mask = GENMASK(split - 1, 0);
+	u64 m = ether_addr_to_u64(mac);
+
+	*lsb = m & mask;
+	*msb = m >> split;
+}
+
+static int sparx5_vmid_alloc(struct sparx5 *sparx5)
+{
+	int vmid;
+
+	vmid = find_first_zero_bit(sparx5->router->vmid_mask,
+				   sparx5->data->consts->vmid_cnt);
+	if (vmid >= sparx5->data->consts->vmid_cnt)
+		return -ENOMEM;
+
+	set_bit(vmid, sparx5->router->vmid_mask);
+
+	return vmid;
+}
+
+static void sparx5_vmid_free(struct sparx5 *sparx5, u16 vmid)
+{
+	clear_bit(vmid, sparx5->router->vmid_mask);
+}
+
+static void sparx5_rr_inet6_make_mask_le(int logmask, u8 *mask)
+{
+	/* Caller must ensure 0 <= logmask <= 128 */
+	int rem, byte_prefix = logmask;
+
+	rem = do_div(byte_prefix, BITS_PER_BYTE);
+
+	memset(mask, 0, 16);
+
+	for (int i = 0; i < byte_prefix; i++)
+		mask[15 - i] = 0xff;
+
+	if (rem)
+		mask[15 - byte_prefix] = GENMASK(7, 7 - rem + 1);
+}
+
+static int sparx5_rr_lpm_rule_xip_add(struct vcap_rule *rule,
+				      struct sparx5_iaddr *addr, u32 prefix_len)
+{
+	struct vcap_u128_key addr_key;
+	u32 mask, iaddr;
+
+	switch (addr->version) {
+	case SPARX5_IPV4:
+		mask = ntohl(inet_make_mask(prefix_len));
+		iaddr = ntohl(addr->ipv4);
+
+		return vcap_rule_add_key_u32(rule, VCAP_KF_IP4_XIP, iaddr,
+					     mask);
+	case SPARX5_IPV6:
+		sparx5_rr_inet6_make_mask_le(prefix_len, addr_key.mask);
+		vcap_netbytes_copy(addr_key.value, addr->ipv6.s6_addr, 16);
+
+		return vcap_rule_add_key_u128(rule, VCAP_KF_IP6_XIP, &addr_key);
+	default:
+		WARN_ON(1);
+		return -EINVAL;
+	}
+}
+
+static struct sparx5_rr_router_leg *
+sparx5_rr_leg_find_by_dev(struct sparx5 *sparx5, struct net_device *dev)
+{
+	struct sparx5_rr_router_leg *leg;
+
+	list_for_each_entry(leg, &sparx5->router->leg_list, leg_list_node) {
+		if (leg->dev == dev)
+			return leg;
+	}
+
+	return NULL;
+}
+
+static int
+sparx5_rr_lpm_arp_entry_create(struct sparx5 *sparx5,
+			       struct sparx5_iaddr *addr,
+			       u32 prefix_len, unsigned char mac[ETH_ALEN],
+			       u16 evmid, struct sparx5_rr_hw_route *hw_route)
+{
+	struct net_device *pdev = sparx5->router->port_dev;
+	struct vcap_control *vctrl = sparx5->vcap_ctrl;
+	u32 priority = LPM_SORT_KEY(prefix_len);
+	struct vcap_rule *rule;
+	u32 mac_msb, mac_lsb;
+	int err;
+
+	sparx5_rr_split_mac(mac, 32, &mac_msb, &mac_lsb);
+
+	rule = vcap_alloc_rule(vctrl, pdev, VCAP_CID_PREROUTING_L0,
+			       VCAP_USER_L3, priority, 0);
+	if (IS_ERR(rule))
+		return PTR_ERR(rule);
+
+	err = sparx5_rr_lpm_rule_xip_add(rule, addr, prefix_len);
+	err |= vcap_rule_add_key_u32(rule, VCAP_KF_AFFIX, 0, 0);
+	err |= vcap_rule_add_key_bit(rule, VCAP_KF_DST_FLAG, VCAP_BIT_1);
+	err |= vcap_rule_add_action_u32(rule, VCAP_AF_MAC_MSB, mac_msb);
+	err |= vcap_rule_add_action_u32(rule, VCAP_AF_MAC_LSB, mac_lsb);
+	err |= vcap_rule_add_action_u32(rule, VCAP_AF_ARP_VMID, evmid);
+	err |= vcap_rule_add_action_bit(rule, VCAP_AF_ARP_ENA, VCAP_BIT_1);
+
+	err = err ? -EINVAL : vcap_val_add_rule(rule, LPM_PROTO(addr));
+	if (!err) {
+		hw_route->vrule_id = rule->id;
+		hw_route->vrule_id_valid = true;
+	}
+	vcap_free_rule(rule);
+	return err;
+}
+
+static void sparx5_rr_leg_hw_init(struct sparx5 *sparx5,
+				  struct sparx5_rr_router_leg *leg)
+{
+	/* Associate Router leg VMID to VLAN */
+	spx5_rmw(ANA_L3_VMID_CFG_VMID_SET(leg->vmid), ANA_L3_VMID_CFG_VMID,
+		 sparx5, ANA_L3_VMID_CFG(leg->vid));
+
+	/* Enable Router leg for VLAN */
+	spx5_rmw(ANA_L3_VLAN_CFG_VLAN_RLEG_ENA_SET(1),
+		 ANA_L3_VLAN_CFG_VLAN_RLEG_ENA, sparx5,
+		 ANA_L3_VLAN_CFG(leg->vid));
+
+	/* Configure router leg */
+
+#if IS_ENABLED(CONFIG_IPV6)
+	spx5_rmw(ANA_L3_RLEG_CTRL_RLEG_IP4_UC_ENA_SET(1) |
+		 ANA_L3_RLEG_CTRL_RLEG_EVID_SET(leg->vid) |
+		 ANA_L3_RLEG_CTRL_RLEG_IP6_UC_ENA_SET(1),
+		 ANA_L3_RLEG_CTRL_RLEG_IP4_UC_ENA |
+		 ANA_L3_RLEG_CTRL_RLEG_EVID |
+		 ANA_L3_RLEG_CTRL_RLEG_IP6_UC_ENA, sparx5,
+		 ANA_L3_RLEG_CTRL(leg->vmid));
+#else
+	spx5_rmw(ANA_L3_RLEG_CTRL_RLEG_IP4_UC_ENA_SET(1) |
+		 ANA_L3_RLEG_CTRL_RLEG_EVID_SET(leg->vid),
+		 ANA_L3_RLEG_CTRL_RLEG_IP4_UC_ENA |
+		 ANA_L3_RLEG_CTRL_RLEG_EVID, sparx5,
+		 ANA_L3_RLEG_CTRL(leg->vmid));
+#endif
+
+	/* Configure egress VLAN in rewriter */
+	spx5_rmw(REW_RLEG_CTRL_RLEG_EVID_SET(leg->vid), REW_RLEG_CTRL_RLEG_EVID,
+		 sparx5, REW_RLEG_CTRL(leg->vmid));
+}
+
+static void sparx5_rr_leg_hw_deinit(struct sparx5 *sparx5,
+				    struct sparx5_rr_router_leg *leg)
+{
+	/* Disable Router leg for VLAN */
+	spx5_rmw(ANA_L3_VLAN_CFG_VLAN_RLEG_ENA_SET(0),
+		 ANA_L3_VLAN_CFG_VLAN_RLEG_ENA, sparx5,
+		 ANA_L3_VLAN_CFG(leg->vid));
+
+	/* Disable IP UC routing on leg */
+	spx5_rmw(ANA_L3_RLEG_CTRL_RLEG_IP4_UC_ENA_SET(0) |
+		 ANA_L3_RLEG_CTRL_RLEG_IP6_UC_ENA_SET(0),
+		 ANA_L3_RLEG_CTRL_RLEG_IP4_UC_ENA |
+		 ANA_L3_RLEG_CTRL_RLEG_IP6_UC_ENA, sparx5,
+		 ANA_L3_RLEG_CTRL(leg->vmid));
+}
+
+static int sparx5_rr_lpm_link_local_create(struct sparx5 *sparx5)
+{
+	struct sparx5_iaddr addr __aligned(2) = { };
+	unsigned char zero_mac[ETH_ALEN];
+
+	eth_zero_addr(zero_mac);
+
+	/* Trap traffic to fe80::/64 */
+	addr.version = SPARX5_IPV6;
+	addr.ipv6.in6_u.u6_addr8[0] = 0xfe;
+	addr.ipv6.in6_u.u6_addr8[1] = 0x80;
+
+	return sparx5_rr_lpm_arp_entry_create(sparx5, &addr,
+					      SPARX5_LINK_LOCAL_PREFIX_LEN,
+					      zero_mac, 0,
+					      &sparx5->router->link_local);
+}
+
+static void sparx5_rr_lpm_link_local_destroy(struct sparx5 *sparx5)
+{
+	struct sparx5_rr_hw_route *llocal = &sparx5->router->link_local;
+	struct net_device *pdev = sparx5->router->port_dev;
+	struct vcap_control *vctrl = sparx5->vcap_ctrl;
+
+	if (!llocal->vrule_id_valid)
+		return;
+
+	vcap_del_rule(vctrl, pdev, llocal->vrule_id);
+	llocal->vrule_id_valid = false;
+}
+
+static struct sparx5_rr_router_leg *
+__sparx5_rr_leg_alloc(struct sparx5 *sparx5, struct net_device *dev, u16 vmid,
+		      u16 vid)
+{
+	struct sparx5_rr_router_leg *leg;
+
+	leg = kzalloc_obj(*leg);
+	if (!leg)
+		return NULL;
+
+	INIT_LIST_HEAD(&leg->leg_list_node);
+	leg->dev = dev;
+	leg->vmid = vmid;
+	leg->vid = vid;
+	leg->sparx5 = sparx5;
+
+	return leg;
+}
+
+/* Router legs are identified by their VMID in hw */
+static struct sparx5_rr_router_leg *
+sparx5_rr_leg_alloc(struct sparx5 *sparx5, struct net_device *dev, u16 vid)
+{
+	struct sparx5_rr_router_leg *leg;
+	int next_vmid;
+
+	next_vmid = sparx5_vmid_alloc(sparx5);
+	if (next_vmid < 0)
+		return NULL;
+
+	leg = __sparx5_rr_leg_alloc(sparx5, dev, next_vmid, vid);
+	if (!leg)
+		goto err_kzalloc;
+
+	return leg;
+
+err_kzalloc:
+	sparx5_vmid_free(sparx5, next_vmid);
+
+	return NULL;
+}
+
+static void sparx5_rr_router_leg_destroy(struct sparx5_rr_router_leg *leg)
+{
+	struct sparx5 *sparx5 = leg->sparx5;
+
+	dev_dbg(sparx5->dev, "Leg destroy vid=%u vmid=%u dev=%s\n", leg->vid,
+		leg->vmid, leg->dev ? netdev_name(leg->dev) : "blackhole");
+
+	sparx5_rr_leg_hw_deinit(sparx5, leg);
+	sparx5_vmid_free(leg->sparx5, leg->vmid);
+	list_del(&leg->leg_list_node);
+
+	if (leg->dev) {
+		if (atomic_dec_return(&sparx5->router->legs_count) == 0)
+			sparx5_rr_lpm_link_local_destroy(sparx5);
+
+		netdev_put(leg->dev, &leg->dev_tracker);
+	}
+	kfree(leg);
+}
+
+static struct sparx5_rr_router_leg *
+sparx5_rr_router_leg_create(struct sparx5 *sparx5, struct net_device *dev,
+			    u16 vid)
+{
+	struct sparx5_rr_router_leg *leg;
+
+	leg = sparx5_rr_leg_alloc(sparx5, dev, vid);
+	if (!leg)
+		return ERR_PTR(-ENOMEM);
+
+	/* Prevent net device from being freed while we have added it to a
+	 * router leg.
+	 */
+	netdev_hold(dev, &leg->dev_tracker, GFP_KERNEL);
+
+	/* While a router leg exists, add route to trap link-local traffic. */
+	if (atomic_inc_return(&sparx5->router->legs_count) == 1) {
+		if (sparx5_rr_lpm_link_local_create(sparx5))
+			dev_warn(sparx5->dev,
+				 "Failed to create link-local route\n");
+	}
+
+	list_add(&leg->leg_list_node, &sparx5->router->leg_list);
+	sparx5_rr_leg_hw_init(sparx5, leg);
+
+	dev_dbg(sparx5->dev, "Leg create dev=%s vid=%u vmid=%u\n", dev->name,
+		leg->vid, leg->vmid);
+
+	return leg;
+}
+
+static bool sparx5_rr_dev_real_is_vlan_aware(struct net_device *dev)
+{
+	struct net_device *vlan_rdev;
+	/* Support l3 offloading for:
+	 *	1) upper vlan interfaces for the bridge.
+	 */
+	if (is_vlan_dev(dev)) {
+		if (netif_is_bridge_port(dev))
+			return false;
+
+		vlan_rdev = vlan_dev_real_dev(dev);
+		if (sparx5_netdevice_check(vlan_rdev))
+			return false;
+
+		return netif_is_bridge_master(vlan_rdev) &&
+		       br_vlan_enabled(vlan_rdev);
+	}
+
+	return false;
+}
+
+static void sparx5_rr_leg_base_mac_set(struct sparx5 *sparx5,
+				       unsigned char mac[ETH_ALEN])
+{
+	u8 rleg_type_sel = SPARX5_RLEG_USE_GLOBAL_BASE_MAC;
+	u32 mac_msb, mac_lsb;
+
+	sparx5_rr_split_mac(mac, 24, &mac_msb, &mac_lsb);
+
+	dev_dbg(sparx5->dev, "Router leg base MAC=%pM\n", mac);
+
+	/* The global router leg MAC must be set consistently across ANA_L3, REW
+	 * and EACL.
+	 */
+	spx5_wr(ANA_L3_RLEG_CFG_0_RLEG_MAC_LSB_SET(mac_lsb), sparx5,
+		ANA_L3_RLEG_CFG_0);
+
+	spx5_rmw(ANA_L3_RLEG_CFG_1_RLEG_MAC_MSB_SET(mac_msb) |
+		 ANA_L3_RLEG_CFG_1_RLEG_MAC_TYPE_SEL_SET(rleg_type_sel),
+		 ANA_L3_RLEG_CFG_1_RLEG_MAC_MSB |
+		 ANA_L3_RLEG_CFG_1_RLEG_MAC_TYPE_SEL,
+		 sparx5, ANA_L3_RLEG_CFG_1);
+
+	/* Set global Router leg MAC (REW) */
+	spx5_wr(REW_RLEG_CFG_0_RLEG_MAC_LSB_SET(mac_lsb), sparx5,
+		REW_RLEG_CFG_0);
+
+	spx5_rmw(REW_RLEG_CFG_1_RLEG_MAC_MSB_SET(mac_msb) |
+		 REW_RLEG_CFG_1_RLEG_MAC_TYPE_SEL_SET(rleg_type_sel),
+		 REW_RLEG_CFG_1_RLEG_MAC_MSB | REW_RLEG_CFG_1_RLEG_MAC_TYPE_SEL,
+		 sparx5, REW_RLEG_CFG_1);
+
+	/* Set global Router leg MAC (EACL) */
+	spx5_wr(EACL_RLEG_CFG_0_RLEG_MAC_LSB_SET(mac_lsb), sparx5,
+		EACL_RLEG_CFG_0);
+
+	spx5_rmw(EACL_RLEG_CFG_1_RLEG_MAC_MSB_SET(mac_msb) |
+		 EACL_RLEG_CFG_1_RLEG_MAC_TYPE_SEL_SET(rleg_type_sel),
+		 EACL_RLEG_CFG_1_RLEG_MAC_MSB |
+		 EACL_RLEG_CFG_1_RLEG_MAC_TYPE_SEL,
+		 sparx5, EACL_RLEG_CFG_1);
+}
+
+static bool
+sparx5_rr_router_leg_addr_list_empty_rcu(struct sparx5_rr_router_leg *leg)
+{
+	struct inet6_dev *inet6_dev;
+	struct in_device *in_dev;
+
+	in_dev = __in_dev_get_rcu(leg->dev);
+	if (in_dev && in_dev->ifa_list)
+		return false;
+
+	inet6_dev = __in6_dev_get(leg->dev);
+	if (inet6_dev && !list_empty(&inet6_dev->addr_list))
+		return false;
+
+	return true;
+}
+
+static bool
+sparx5_rr_router_leg_addr_list_empty(struct sparx5_rr_router_leg *leg)
+{
+	bool addr_list_empty;
+
+	rcu_read_lock();
+	addr_list_empty = sparx5_rr_router_leg_addr_list_empty_rcu(leg);
+	rcu_read_unlock();
+
+	return addr_list_empty;
+}
+
+static int __sparx5_rr_inetaddr_event(struct sparx5 *sparx5,
+				      struct net_device *dev,
+				      unsigned long event)
+{
+	struct sparx5_rr_router_leg *leg;
+	u16 vid;
+
+	if (!sparx5_rr_dev_real_is_vlan_aware(dev))
+		return 0;
+
+	/* Our basic case: ip addr/subnet added to vlan upper of
+	 * bridge dev.
+	 */
+	switch (event) {
+	case NETDEV_UP:
+		leg = sparx5_rr_leg_find_by_dev(sparx5, dev);
+		if (leg)
+			return 0;
+
+		/* HW allows at most 1 leg per VLAN, but we do not need to
+		 * lookup leg by vid, since the kernel does not allow multiple
+		 * vlan devs with the same vid on top of a given device.
+		 */
+		vid = vlan_dev_vlan_id(dev);
+
+		leg = sparx5_rr_router_leg_create(sparx5, dev, vid);
+		if (IS_ERR(leg))
+			return PTR_ERR(leg);
+		break;
+	case NETDEV_DOWN:
+		leg = sparx5_rr_leg_find_by_dev(sparx5, dev);
+		if (!leg || !sparx5_rr_router_leg_addr_list_empty(leg))
+			return 0;
+
+		sparx5_rr_router_leg_destroy(leg);
+		break;
+	}
+
+	return 0;
+}
+
+static int sparx5_rr_inetaddr_event_handle(struct sparx5 *sparx5,
+					   struct net_device *dev,
+					   unsigned long event)
+{
+	int err;
+
+	mutex_lock(&sparx5->router->lock);
+	err = __sparx5_rr_inetaddr_event(sparx5, dev, event);
+	mutex_unlock(&sparx5->router->lock);
+
+	return notifier_from_errno(err);
+}
+
+/* Called with RTNL. */
+static int sparx5_rr_inet6addr_valid_event(struct notifier_block *nb,
+					   unsigned long event, void *ptr)
+{
+	struct in6_validator_info *i6vi = (struct in6_validator_info *)ptr;
+	struct net_device *dev = i6vi->i6vi_dev->dev;
+	struct sparx5_router *router;
+
+	ASSERT_RTNL();
+
+	if (event != NETDEV_UP)
+		return NOTIFY_DONE;
+
+	router = container_of(nb, struct sparx5_router, inet6addr_valid_nb);
+
+	return sparx5_rr_inetaddr_event_handle(router->sparx5, dev, event);
+}
+
+static void sparx5_rr_inet6addr_event_work(struct work_struct *work)
+{
+	struct sparx5_rr_inet6addr_event_work *addr_work =
+		container_of(work, struct sparx5_rr_inet6addr_event_work, work);
+	struct sparx5_router *router = addr_work->sparx5->router;
+
+	rtnl_lock();
+	mutex_lock(&router->lock);
+
+	__sparx5_rr_inetaddr_event(addr_work->sparx5, addr_work->dev,
+				   addr_work->event);
+
+	mutex_unlock(&router->lock);
+	rtnl_unlock();
+	netdev_put(addr_work->dev, &addr_work->dev_tracker);
+	kfree(addr_work);
+}
+
+/* Called in atomic context. */
+static int sparx5_rr_inet6addr_event(struct notifier_block *nb,
+				     unsigned long event, void *ptr)
+{
+	struct inet6_ifaddr *if6 = (struct inet6_ifaddr *)ptr;
+	struct sparx5_rr_inet6addr_event_work *work;
+	struct net_device *dev = if6->idev->dev;
+	struct sparx5_router *router;
+
+	if (event != NETDEV_DOWN)
+		return NOTIFY_DONE;
+
+	work = kzalloc_obj(*work, GFP_ATOMIC);
+	if (!work)
+		return NOTIFY_BAD;
+
+	router = container_of(nb, struct sparx5_router, inet6addr_nb);
+	INIT_WORK(&work->work, sparx5_rr_inet6addr_event_work);
+	work->sparx5 = router->sparx5;
+	work->dev = dev;
+	work->event = event;
+	netdev_hold(dev, &work->dev_tracker, GFP_ATOMIC);
+	sparx5_rr_schedule_work(router->sparx5, &work->work);
+
+	return NOTIFY_DONE;
+}
+
+/* Handle events for ip address changes on ifs. Used to manage router legs.
+ * Called with RTNL.
+ */
+static int sparx5_rr_inetaddr_event(struct notifier_block *nb,
+				    unsigned long event, void *ptr)
+{
+	struct in_ifaddr *ifa = (struct in_ifaddr *)ptr;
+	struct net_device *dev = ifa->ifa_dev->dev;
+	struct sparx5_router *router;
+
+	ASSERT_RTNL();
+
+	if (event != NETDEV_DOWN)
+		return NOTIFY_DONE;
+
+	router = container_of(nb, struct sparx5_router, inetaddr_nb);
+
+	return sparx5_rr_inetaddr_event_handle(router->sparx5, dev, event);
+}
+
+/* Called with RTNL. */
+static int sparx5_rr_inetaddr_valid_event(struct notifier_block *nb,
+					  unsigned long event, void *ptr)
+{
+	struct in_validator_info *ivi = (struct in_validator_info *)ptr;
+	struct net_device *dev = ivi->ivi_dev->dev;
+	struct sparx5_router *router;
+
+	ASSERT_RTNL();
+
+	if (event != NETDEV_UP)
+		return NOTIFY_DONE;
+
+	router = container_of(nb, struct sparx5_router, inetaddr_valid_nb);
+
+	return sparx5_rr_inetaddr_event_handle(router->sparx5, dev, event);
+}
+
+/* Called with RTNL. */
+static int sparx5_rr_netdevice_event(struct notifier_block *nb,
+				     unsigned long event, void *ptr)
+{
+	struct net_device *dev = netdev_notifier_info_to_dev(ptr);
+	unsigned char mac[ETH_ALEN] __aligned(2);
+	struct sparx5_router *router;
+	struct sparx5 *sparx5;
+
+	ASSERT_RTNL();
+
+	router = container_of(nb, struct sparx5_router, netdevice_nb);
+	sparx5 = router->sparx5;
+
+	/* Allow single bridge. Global router leg MAC tracks bridge mac. */
+	if (!netif_is_bridge_master(dev))
+		return NOTIFY_OK;
+
+	switch (event) {
+	case NETDEV_CHANGEADDR:
+		ether_addr_copy(mac, dev->dev_addr);
+		sparx5_rr_leg_base_mac_set(sparx5, mac);
+		break;
+	}
+
+	return NOTIFY_OK;
+}
+
+static int sparx5_rr_blackhole_leg_create(struct sparx5 *sparx5)
+{
+	struct sparx5_rr_router_leg *leg;
+	u16 vmid, vid;
+
+	vmid = SPARX5_BLACKHOLE_VMID(sparx5);
+	vid = SPARX5_BLACKHOLE_VID;
+
+	leg = __sparx5_rr_leg_alloc(sparx5, NULL, vmid, vid);
+	if (!leg)
+		return -ENOMEM;
+
+	set_bit(vmid, sparx5->router->vmid_mask);
+
+	list_add(&leg->leg_list_node, &sparx5->router->leg_list);
+	sparx5_rr_leg_hw_init(sparx5, leg);
+
+	dev_dbg(sparx5->dev, "Blackhole leg create vid=%u vmid=%u\n",
+		leg->vid, leg->vmid);
+
+	return 0;
+}
+
+static void sparx5_rr_router_legs_flush(struct sparx5 *sparx5)
+{
+	struct sparx5_rr_router_leg *leg, *tmp;
+
+	list_for_each_entry_safe(leg, tmp, &sparx5->router->leg_list,
+				 leg_list_node)
+		sparx5_rr_router_leg_destroy(leg);
+}
+
+int sparx5_rr_router_init(struct sparx5 *sparx5)
+{
+	struct sparx5_router *r;
+	int err;
+
+	r = kzalloc_obj(*sparx5->router);
+	if (!r)
+		return -ENOMEM;
+
+	mutex_init(&r->lock);
+	sparx5->router = r;
+	r->sparx5 = sparx5;
+
+	INIT_LIST_HEAD(&r->leg_list);
+	INIT_LIST_HEAD(&r->fib_lpm4_list);
+	INIT_LIST_HEAD(&r->fib_lpm6_list);
+
+	/* Add reserved leg for blackhole routes. */
+	err = sparx5_rr_blackhole_leg_create(sparx5);
+	if (err)
+		goto err_free_router;
+
+	r->sparx5_router_owq = alloc_ordered_workqueue("sparx5_router_owq", 0);
+	if (!r->sparx5_router_owq) {
+		err = -ENOMEM;
+		goto err_blackhole_destroy;
+	}
+
+	atomic_set(&r->legs_count, 0);
+	r->link_local.vrule_id = 0;
+	r->link_local.vrule_id_valid = false;
+	/* VCAP API requires a port net_device, to get a sparx5 reference.
+	 * Fetch any valid port.
+	 */
+	for (int i = 0; i < sparx5->data->consts->n_ports; i++) {
+		if (!sparx5->ports[i])
+			continue;
+
+		r->port_dev = sparx5->ports[i]->ndev;
+		if (r->port_dev)
+			break;
+	}
+	if (!r->port_dev) {
+		err = -ENXIO;
+		goto err_workqueue_destroy;
+	}
+
+	/* Enable L3 UC routing on all ports. */
+	spx5_wr(~0, sparx5, ANA_L3_L3_UC_ENA);
+	if (is_sparx5(sparx5)) {
+		spx5_wr(~0, sparx5, ANA_L3_L3_UC_ENA1);
+		spx5_wr(~0, sparx5, ANA_L3_L3_UC_ENA2);
+	}
+
+	/* Enable routing and global router options */
+	spx5_rmw(ANA_L3_ROUTING_CFG_L3_ENA_MODE_SET(1) |
+		 ANA_L3_ROUTING_CFG_RT_SMAC_UPDATE_ENA_SET(1) |
+		 ANA_L3_ROUTING_CFG_CPU_RLEG_IP_HDR_FAIL_REDIR_ENA_SET(1) |
+		 ANA_L3_ROUTING_CFG_CPU_IP4_OPTIONS_REDIR_ENA_SET(1) |
+		 ANA_L3_ROUTING_CFG_CPU_IP6_HOPBYHOP_REDIR_ENA_SET(1) |
+		 ANA_L3_ROUTING_CFG_IP6_HC_REDIR_ENA_SET(1) |
+		 ANA_L3_ROUTING_CFG_IP4_TTL_REDIR_ENA_SET(1),
+		 ANA_L3_ROUTING_CFG_L3_ENA_MODE |
+		 ANA_L3_ROUTING_CFG_RT_SMAC_UPDATE_ENA |
+		 ANA_L3_ROUTING_CFG_CPU_RLEG_IP_HDR_FAIL_REDIR_ENA |
+		 ANA_L3_ROUTING_CFG_CPU_IP4_OPTIONS_REDIR_ENA |
+		 ANA_L3_ROUTING_CFG_CPU_IP6_HOPBYHOP_REDIR_ENA |
+		 ANA_L3_ROUTING_CFG_IP6_HC_REDIR_ENA |
+		 ANA_L3_ROUTING_CFG_IP4_TTL_REDIR_ENA,
+		 sparx5, ANA_L3_ROUTING_CFG);
+
+	/* By default, routing related frame edits are done in REW, but when
+	 * combining routing with PTP, ANA_ACL must be configured to change DMAC
+	 * to next-hop DMAC in order to allow other information to be stored in
+	 * the IFH.
+	 *
+	 * This enables routing related frame edits independently of VCAP_S2
+	 * action ACL_RT_MODE.
+	 */
+	spx5_rmw(ANA_ACL_VCAP_S2_MISC_CTRL_ACL_RT_SEL_SET(1),
+		 ANA_ACL_VCAP_S2_MISC_CTRL_ACL_RT_SEL, sparx5,
+		 ANA_ACL_VCAP_S2_MISC_CTRL);
+
+	r->inetaddr_nb.notifier_call = sparx5_rr_inetaddr_event;
+	err = register_inetaddr_notifier(&r->inetaddr_nb);
+	if (err)
+		goto err_workqueue_destroy;
+
+	r->inetaddr_valid_nb.notifier_call = sparx5_rr_inetaddr_valid_event;
+	err = register_inetaddr_validator_notifier(&r->inetaddr_valid_nb);
+	if (err)
+		goto err_unreg_inet_notifier;
+
+	r->netdevice_nb.notifier_call = sparx5_rr_netdevice_event;
+	err = register_netdevice_notifier(&r->netdevice_nb);
+	if (err)
+		goto err_unreg_inet_addr_val_notifier;
+
+	r->inet6addr_valid_nb.notifier_call = sparx5_rr_inet6addr_valid_event;
+	err = register_inet6addr_validator_notifier(&r->inet6addr_valid_nb);
+	if (err)
+		goto err_unreg_netdev_notifier;
+
+	r->inet6addr_nb.notifier_call = sparx5_rr_inet6addr_event;
+	err = register_inet6addr_notifier(&r->inet6addr_nb);
+	if (err)
+		goto err_unreg_inet6_addr_val_notifier;
+
+	return 0;
+
+err_unreg_inet6_addr_val_notifier:
+	unregister_inet6addr_validator_notifier(&r->inet6addr_valid_nb);
+err_unreg_netdev_notifier:
+	unregister_netdevice_notifier(&r->netdevice_nb);
+err_unreg_inet_addr_val_notifier:
+	unregister_inetaddr_validator_notifier(&r->inetaddr_valid_nb);
+err_unreg_inet_notifier:
+	unregister_inetaddr_notifier(&r->inetaddr_nb);
+err_workqueue_destroy:
+	destroy_workqueue(r->sparx5_router_owq);
+err_blackhole_destroy:
+	sparx5_rr_router_legs_flush(sparx5);
+err_free_router:
+	mutex_destroy(&r->lock);
+	kfree(r);
+
+	return err;
+}
+
+void sparx5_rr_router_deinit(struct sparx5 *sparx5)
+{
+	struct sparx5_router *router = sparx5->router;
+
+	unregister_inet6addr_notifier(&router->inet6addr_nb);
+	unregister_inet6addr_validator_notifier(&router->inet6addr_valid_nb);
+	unregister_netdevice_notifier(&router->netdevice_nb);
+	unregister_inetaddr_validator_notifier(&router->inetaddr_valid_nb);
+	unregister_inetaddr_notifier(&router->inetaddr_nb);
+	destroy_workqueue(router->sparx5_router_owq);
+	sparx5_rr_router_legs_flush(sparx5);
+	mutex_destroy(&router->lock);
+	kfree(router);
+}

-- 
2.52.0



^ permalink raw reply related

* [PATCH net-next 6/9] net: sparx5: vcap: add lpm vcap implementation
From: Jens Emil Schulz Østergaard @ 2026-06-12 12:37 UTC (permalink / raw)
  To: Horatiu Vultur, UNGLinuxDriver, Andrew Lunn, David S. Miller,
	Eric Dumazet, Jakub Kicinski, Paolo Abeni, Daniel Machon,
	Steen Hegelund, Kees Cook, Gustavo A. R. Silva
  Cc: netdev, linux-kernel, linux-arm-kernel, linux-hardening,
	Jens Emil Schulz Østergaard
In-Reply-To: <20260612-sparx5_l3_routing-v1-0-fc3c10160f49@microchip.com>

Add the implementation for the LPM VCAP for lan969x and sparx5. The LPM
VCAP has a static keyset configuration, and a single lookup dedicated to
LPM, so this is straight forward, and many functions can use existing
functionality.

The LPM VCAP is allocated from the Super VCAP Blocks, which is shared
amongst different VCAPs. All blocks are used, so we steal a block from
IS0, which had 2 assigned. Since we statically assign blocks, this means
IS0 has half the available address space for vcap rules, compared to
before.

Reviewed-by: Daniel Machon <daniel.machon@microchip.com>
Reviewed-by: Steen Hegelund <Steen.Hegelund@microchip.com>
Signed-off-by: Jens Emil Schulz Østergaard <jensemil.schulzostergaard@microchip.com>
---
 .../microchip/sparx5/lan969x/lan969x_vcap_impl.c   |  12 ++
 .../ethernet/microchip/sparx5/sparx5_vcap_impl.c   | 159 ++++++++++++++++++++-
 .../ethernet/microchip/sparx5/sparx5_vcap_impl.h   |   5 +
 3 files changed, 174 insertions(+), 2 deletions(-)

diff --git a/drivers/net/ethernet/microchip/sparx5/lan969x/lan969x_vcap_impl.c b/drivers/net/ethernet/microchip/sparx5/lan969x/lan969x_vcap_impl.c
index 543a1f2bf6bd..6e6a1f833dea 100644
--- a/drivers/net/ethernet/microchip/sparx5/lan969x/lan969x_vcap_impl.c
+++ b/drivers/net/ethernet/microchip/sparx5/lan969x/lan969x_vcap_impl.c
@@ -82,4 +82,16 @@ const struct sparx5_vcap_inst lan969x_vcap_inst_cfg[] = {
 		.count = 1024,
 		.ingress = false,
 	},
+	{
+		.vtype = VCAP_TYPE_LPM,
+		.vinst = 0,
+		.map_id = 6,
+		.lookups = SPARX5_LPM_LOOKUPS,
+		.lookups_per_instance = SPARX5_LPM_LOOKUPS,
+		.first_cid = SPARX5_VCAP_CID_LPM_L0,
+		.last_cid = SPARX5_VCAP_CID_LPM_MAX,
+		.blockno = 5,
+		.blocks = 1,
+		.ingress = true,
+	},
 };
diff --git a/drivers/net/ethernet/microchip/sparx5/sparx5_vcap_impl.c b/drivers/net/ethernet/microchip/sparx5/sparx5_vcap_impl.c
index 95b93e46a41d..e25e759c24f6 100644
--- a/drivers/net/ethernet/microchip/sparx5/sparx5_vcap_impl.c
+++ b/drivers/net/ethernet/microchip/sparx5/sparx5_vcap_impl.c
@@ -54,8 +54,8 @@ const struct sparx5_vcap_inst sparx5_vcap_inst_cfg[] = {
 		.lookups_per_instance = SPARX5_IS0_LOOKUPS / 3,
 		.first_cid = SPARX5_VCAP_CID_IS0_L0,
 		.last_cid = SPARX5_VCAP_CID_IS0_L2 - 1,
-		.blockno = 8, /* Maps block 8-9 */
-		.blocks = 2,
+		.blockno = 8, /* Maps block 8 */
+		.blocks = 1,
 		.ingress = true,
 	},
 	{
@@ -124,6 +124,18 @@ const struct sparx5_vcap_inst sparx5_vcap_inst_cfg[] = {
 		.count = 12288, /* Addresses according to datasheet */
 		.ingress = false,
 	},
+	{
+		.vtype = VCAP_TYPE_LPM,
+		.vinst = 0,
+		.map_id = 6,
+		.lookups = SPARX5_LPM_LOOKUPS,
+		.lookups_per_instance = SPARX5_LPM_LOOKUPS,
+		.first_cid = SPARX5_VCAP_CID_LPM_L0,
+		.last_cid = SPARX5_VCAP_CID_LPM_MAX,
+		.blockno = 9,
+		.blocks = 1,
+		.ingress = true,
+	},
 };
 
 /* These protocols have dedicated keysets in IS0 and a TC dissector */
@@ -149,6 +161,12 @@ static u16 sparx5_vcap_es2_known_etypes[] = {
 	ETH_P_IPV6,
 };
 
+static u16 sparx5_vcap_lpm_known_etypes[] = {
+	ETH_P_ALL,
+	ETH_P_IP,
+	ETH_P_IPV6,
+};
+
 static void sparx5_vcap_type_err(struct sparx5 *sparx5,
 				 struct vcap_admin *admin,
 				 const char *fname)
@@ -195,6 +213,7 @@ static void _sparx5_vcap_range_init(struct sparx5 *sparx5,
 	u32 size = count - 1;
 
 	switch (admin->vtype) {
+	case VCAP_TYPE_LPM:
 	case VCAP_TYPE_IS0:
 	case VCAP_TYPE_IS2:
 		spx5_wr(VCAP_SUPER_CFG_MV_NUM_POS_SET(0) |
@@ -386,6 +405,11 @@ static int sparx5_vcap_es2_cid_to_lookup(int cid)
 	return lookup;
 }
 
+static int sparx5_vcap_lpm_cid_to_lookup(int cid)
+{
+	return 0;
+}
+
 /* Add ethernet type IS0 keyset to a list */
 static void
 sparx5_vcap_is0_get_port_etype_keysets(struct vcap_keyset_list *keysetlist,
@@ -401,6 +425,28 @@ sparx5_vcap_is0_get_port_etype_keysets(struct vcap_keyset_list *keysetlist,
 	}
 }
 
+static int sparx5_vcap_lpm_get_port_keysets(struct net_device *ndev,
+					    int lookup,
+					    struct vcap_keyset_list *keysetlist,
+					    u16 l3_proto)
+{
+	/* LPM keysets are static. */
+	if (l3_proto == ETH_P_ALL) {
+		vcap_keyset_list_add(keysetlist, VCAP_KFS_DBL_IP4);
+		vcap_keyset_list_add(keysetlist, VCAP_KFS_SGL_IP4);
+		vcap_keyset_list_add(keysetlist, VCAP_KFS_DBL_IP6);
+		vcap_keyset_list_add(keysetlist, VCAP_KFS_SGL_IP6);
+	} else if (l3_proto == ETH_P_IP) {
+		vcap_keyset_list_add(keysetlist, VCAP_KFS_DBL_IP4);
+		vcap_keyset_list_add(keysetlist, VCAP_KFS_SGL_IP4);
+	} else if (l3_proto == ETH_P_IPV6) {
+		vcap_keyset_list_add(keysetlist, VCAP_KFS_DBL_IP6);
+		vcap_keyset_list_add(keysetlist, VCAP_KFS_SGL_IP6);
+	}
+
+	return 0;
+}
+
 /* Return the list of keysets for the vcap port configuration */
 static int sparx5_vcap_is0_get_port_keysets(struct net_device *ndev,
 					    int lookup,
@@ -683,6 +729,11 @@ int sparx5_vcap_get_port_keyset(struct net_device *ndev,
 	struct sparx5_port *port;
 
 	switch (admin->vtype) {
+	case VCAP_TYPE_LPM:
+		lookup = sparx5_vcap_lpm_cid_to_lookup(cid);
+		err = sparx5_vcap_lpm_get_port_keysets(ndev, lookup, kslist,
+						       l3_proto);
+		break;
 	case VCAP_TYPE_IS0:
 		lookup = sparx5_vcap_is0_cid_to_lookup(cid);
 		err = sparx5_vcap_is0_get_port_keysets(ndev, lookup, kslist,
@@ -716,6 +767,10 @@ bool sparx5_vcap_is_known_etype(struct vcap_admin *admin, u16 etype)
 	int size, idx;
 
 	switch (admin->vtype) {
+	case VCAP_TYPE_LPM:
+		known_etypes = sparx5_vcap_lpm_known_etypes;
+		size = ARRAY_SIZE(sparx5_vcap_lpm_known_etypes);
+		break;
 	case VCAP_TYPE_IS0:
 		known_etypes = sparx5_vcap_is0_known_etypes;
 		size = ARRAY_SIZE(sparx5_vcap_is0_known_etypes);
@@ -760,6 +815,11 @@ sparx5_vcap_validate_keyset(struct net_device *ndev,
 
 	/* Get a list of currently configured keysets in the lookups */
 	switch (admin->vtype) {
+	case VCAP_TYPE_LPM:
+		lookup = sparx5_vcap_lpm_cid_to_lookup(rule->vcap_chain_id);
+		sparx5_vcap_lpm_get_port_keysets(ndev, lookup, &keysetlist,
+						 l3_proto);
+		break;
 	case VCAP_TYPE_IS0:
 		lookup = sparx5_vcap_is0_cid_to_lookup(rule->vcap_chain_id);
 		sparx5_vcap_is0_get_port_keysets(ndev, lookup, &keysetlist,
@@ -873,6 +933,9 @@ static void sparx5_vcap_add_default_fields(struct net_device *ndev,
 
 	/* add the lookup bit */
 	switch (admin->vtype) {
+	case VCAP_TYPE_LPM:
+		/* LPM VCAP has no default fields */
+		break;
 	case VCAP_TYPE_IS0:
 	case VCAP_TYPE_IS2:
 		sparx5_vcap_ingress_add_default_fields(ndev, admin, rule);
@@ -1083,6 +1146,46 @@ static void sparx5_vcap_es2_cache_write(struct sparx5 *sparx5,
 	}
 }
 
+static void sparx5_vcap_lpm_cache_write(struct sparx5 *sparx5,
+					struct vcap_admin *admin,
+					enum vcap_selection sel,
+					u32 start,
+					u32 count)
+{
+	u32 *keystr, *mskstr, *actstr;
+	int idx;
+
+	keystr = &admin->cache.keystream[start];
+	mskstr = &admin->cache.maskstream[start];
+	actstr = &admin->cache.actionstream[start];
+
+	switch (sel) {
+	case VCAP_SEL_ENTRY:
+		for (idx = 0; idx < count; ++idx) {
+			/* Avoid 'match-off' by setting value & mask */
+			spx5_wr(keystr[idx] & mskstr[idx], sparx5,
+				VCAP_SUPER_VCAP_ENTRY_DAT(idx));
+			spx5_wr(~mskstr[idx], sparx5,
+				VCAP_SUPER_VCAP_MASK_DAT(idx));
+		}
+		break;
+	case VCAP_SEL_ACTION:
+		for (idx = 0; idx < count; ++idx)
+			spx5_wr(actstr[idx], sparx5,
+				VCAP_SUPER_VCAP_ACTION_DAT(idx));
+		break;
+	case VCAP_SEL_ALL:
+		pr_err("%s:%d: cannot write all streams at once\n", __func__,
+		       __LINE__);
+		break;
+	default:
+		break;
+	}
+	if (sel & VCAP_SEL_COUNTER)
+		spx5_wr(admin->cache.counter, sparx5,
+			VCAP_SUPER_VCAP_CNT_DAT(0));
+}
+
 /* API callback used for writing to the VCAP cache */
 static void sparx5_vcap_cache_write(struct net_device *ndev,
 				    struct vcap_admin *admin,
@@ -1094,6 +1197,9 @@ static void sparx5_vcap_cache_write(struct net_device *ndev,
 	struct sparx5 *sparx5 = port->sparx5;
 
 	switch (admin->vtype) {
+	case VCAP_TYPE_LPM:
+		sparx5_vcap_lpm_cache_write(sparx5, admin, sel, start, count);
+		break;
 	case VCAP_TYPE_IS0:
 		sparx5_vcap_is0_cache_write(sparx5, admin, sel, start, count);
 		break;
@@ -1273,6 +1379,41 @@ static void sparx5_vcap_es2_cache_read(struct sparx5 *sparx5,
 	}
 }
 
+static void sparx5_vcap_lpm_cache_read(struct sparx5 *sparx5,
+				       struct vcap_admin *admin,
+				       enum vcap_selection sel,
+				       u32 start,
+				       u32 count)
+{
+	u32 *keystr, *mskstr, *actstr;
+	int idx;
+
+	keystr = &admin->cache.keystream[start];
+	mskstr = &admin->cache.maskstream[start];
+	actstr = &admin->cache.actionstream[start];
+
+	if (sel & VCAP_SEL_ENTRY) {
+		for (idx = 0; idx < count; ++idx) {
+			keystr[idx] = spx5_rd(sparx5,
+					      VCAP_SUPER_VCAP_ENTRY_DAT(idx));
+			mskstr[idx] = ~spx5_rd(sparx5,
+					       VCAP_SUPER_VCAP_MASK_DAT(idx));
+		}
+	}
+
+	if (sel & VCAP_SEL_ACTION)
+		for (idx = 0; idx < count; ++idx)
+			actstr[idx] = spx5_rd(sparx5,
+					      VCAP_SUPER_VCAP_ACTION_DAT(idx));
+
+	if (sel & VCAP_SEL_COUNTER) {
+		u32 val = spx5_rd(sparx5, VCAP_SUPER_VCAP_CNT_DAT(0));
+
+		admin->cache.counter = val;
+		admin->cache.sticky = !!val;
+	}
+}
+
 /* API callback used for reading from the VCAP into the VCAP cache */
 static void sparx5_vcap_cache_read(struct net_device *ndev,
 				   struct vcap_admin *admin,
@@ -1284,6 +1425,9 @@ static void sparx5_vcap_cache_read(struct net_device *ndev,
 	struct sparx5 *sparx5 = port->sparx5;
 
 	switch (admin->vtype) {
+	case VCAP_TYPE_LPM:
+		sparx5_vcap_lpm_cache_read(sparx5, admin, sel, start, count);
+		break;
 	case VCAP_TYPE_IS0:
 		sparx5_vcap_is0_cache_read(sparx5, admin, sel, start, count);
 		break;
@@ -1379,6 +1523,7 @@ static void sparx5_vcap_update(struct net_device *ndev,
 	struct sparx5 *sparx5 = port->sparx5;
 
 	switch (admin->vtype) {
+	case VCAP_TYPE_LPM:
 	case VCAP_TYPE_IS0:
 	case VCAP_TYPE_IS2:
 		sparx5_vcap_super_update(sparx5, cmd, sel, addr);
@@ -1475,6 +1620,7 @@ static void sparx5_vcap_move(struct net_device *ndev, struct vcap_admin *admin,
 	}
 
 	switch (admin->vtype) {
+	case VCAP_TYPE_LPM:
 	case VCAP_TYPE_IS0:
 	case VCAP_TYPE_IS2:
 		sparx5_vcap_super_move(sparx5, addr, cmd, mv_num_pos, mv_size);
@@ -1743,6 +1889,8 @@ void sparx5_vcap_set_port_keyset(struct net_device *ndev,
 	int lookup;
 
 	switch (admin->vtype) {
+	case VCAP_TYPE_LPM:
+		break;
 	case VCAP_TYPE_IS0:
 		lookup = sparx5_vcap_is0_cid_to_lookup(cid);
 		if (orig)
@@ -1879,6 +2027,9 @@ static void sparx5_vcap_port_key_selection(struct sparx5 *sparx5,
 	case VCAP_TYPE_ES2:
 		sparx5_vcap_es2_port_key_selection(sparx5, admin);
 		break;
+	case VCAP_TYPE_LPM:
+		/* VCAP LPM key selection is static */
+		break;
 	default:
 		sparx5_vcap_type_err(sparx5, admin, __func__);
 		break;
@@ -1920,6 +2071,9 @@ static void sparx5_vcap_port_key_deselection(struct sparx5 *sparx5,
 					 sparx5,
 					 EACL_VCAP_ES2_KEY_SEL(portno, lookup));
 		break;
+	case VCAP_TYPE_LPM:
+		/* LPM key selection is static */
+		break;
 	default:
 		sparx5_vcap_type_err(sparx5, admin, __func__);
 		break;
@@ -1980,6 +2134,7 @@ static void sparx5_vcap_block_alloc(struct sparx5 *sparx5,
 	int idx, cores;
 
 	switch (admin->vtype) {
+	case VCAP_TYPE_LPM:
 	case VCAP_TYPE_IS0:
 	case VCAP_TYPE_IS2:
 		/* Super VCAP block mapping and address configuration. Block 0
diff --git a/drivers/net/ethernet/microchip/sparx5/sparx5_vcap_impl.h b/drivers/net/ethernet/microchip/sparx5/sparx5_vcap_impl.h
index d0a42406bf26..94c77a60e4f8 100644
--- a/drivers/net/ethernet/microchip/sparx5/sparx5_vcap_impl.h
+++ b/drivers/net/ethernet/microchip/sparx5/sparx5_vcap_impl.h
@@ -20,6 +20,7 @@
 #define SPARX5_IS0_LOOKUPS 6
 #define SPARX5_ES0_LOOKUPS 1
 #define SPARX5_ES2_LOOKUPS 2
+#define SPARX5_LPM_LOOKUPS 1
 
 #define SPARX5_VCAP_CID_IS0_L0 VCAP_CID_INGRESS_L0 /* IS0/CLM lookup 0 */
 #define SPARX5_VCAP_CID_IS0_L1 VCAP_CID_INGRESS_L1 /* IS0/CLM lookup 1 */
@@ -30,6 +31,10 @@
 #define SPARX5_VCAP_CID_IS0_MAX \
 	(VCAP_CID_INGRESS_L5 + VCAP_CID_LOOKUP_SIZE - 1) /* IS0/CLM Max */
 
+#define SPARX5_VCAP_CID_LPM_L0 VCAP_CID_PREROUTING_L0 /* LPM lookup 0 */
+#define SPARX5_VCAP_CID_LPM_MAX \
+	(VCAP_CID_PREROUTING_L0 + VCAP_CID_LOOKUP_SIZE - 1) /* LPM Max */
+
 #define SPARX5_VCAP_CID_IS2_L0 VCAP_CID_INGRESS_STAGE2_L0 /* IS2 lookup 0 */
 #define SPARX5_VCAP_CID_IS2_L1 VCAP_CID_INGRESS_STAGE2_L1 /* IS2 lookup 1 */
 #define SPARX5_VCAP_CID_IS2_L2 VCAP_CID_INGRESS_STAGE2_L2 /* IS2 lookup 2 */

-- 
2.52.0



^ permalink raw reply related

* [PATCH net-next 5/9] net: sparx5: add l3 routing registers
From: Jens Emil Schulz Østergaard @ 2026-06-12 12:37 UTC (permalink / raw)
  To: Horatiu Vultur, UNGLinuxDriver, Andrew Lunn, David S. Miller,
	Eric Dumazet, Jakub Kicinski, Paolo Abeni, Daniel Machon,
	Steen Hegelund, Kees Cook, Gustavo A. R. Silva
  Cc: netdev, linux-kernel, linux-arm-kernel, linux-hardening,
	Jens Emil Schulz Østergaard
In-Reply-To: <20260612-sparx5_l3_routing-v1-0-fc3c10160f49@microchip.com>

Add autogenerated register macros for L3 routing on lan969x and sparx5.

Reviewed-by: Daniel Machon <daniel.machon@microchip.com>
Reviewed-by: Steen Hegelund <Steen.Hegelund@microchip.com>
Signed-off-by: Jens Emil Schulz Østergaard <jensemil.schulzostergaard@microchip.com>
---
 .../microchip/sparx5/lan969x/lan969x_regs.c        |  20 +-
 .../ethernet/microchip/sparx5/sparx5_main_regs.h   | 691 ++++++++++++++++++++-
 .../net/ethernet/microchip/sparx5/sparx5_regs.c    |  20 +-
 .../net/ethernet/microchip/sparx5/sparx5_regs.h    |  20 +-
 4 files changed, 739 insertions(+), 12 deletions(-)

diff --git a/drivers/net/ethernet/microchip/sparx5/lan969x/lan969x_regs.c b/drivers/net/ethernet/microchip/sparx5/lan969x/lan969x_regs.c
index ace4ba21eec4..63a6253a1218 100644
--- a/drivers/net/ethernet/microchip/sparx5/lan969x/lan969x_regs.c
+++ b/drivers/net/ethernet/microchip/sparx5/lan969x/lan969x_regs.c
@@ -1,11 +1,11 @@
 // SPDX-License-Identifier: GPL-2.0+
 /* Microchip lan969x Switch driver
  *
- * Copyright (c) 2024 Microchip Technology Inc.
+ * Copyright (c) 2026 Microchip Technology Inc.
  */
 
-/* This file is autogenerated by cml-utils 2024-09-30 11:48:29 +0200.
- * Commit ID: 9d07b8d19363f3cd3590ddb3f7a2e2768e16524b
+/* This file is autogenerated by cml-utils 2026-02-16 21:50:29 +0100.
+ * Commit ID: 09d52195beeeb9682063ef0bd6eec50eebc137e2
  */
 
 #include "lan969x.h"
@@ -77,12 +77,16 @@ const unsigned int lan969x_gaddr[GADDR_LAST] = {
 	[GA_ANA_CL_COMMON] = 87040,
 	[GA_ANA_L2_COMMON] = 561928,
 	[GA_ANA_L3_COMMON] = 370752,
+	[GA_ANA_L3_VMID] = 360448,
+	[GA_ANA_L3_ARP_PTR_REMAP] = 370944,
+	[GA_ANA_L3_ARP] = 294912,
 	[GA_ANA_L3_VLAN_ARP_L3MC_STICKY] = 368580,
 	[GA_ASM_CFG] = 18304,
 	[GA_ASM_PFC_TIMER_CFG] = 15568,
 	[GA_ASM_LBK_WM_CFG] = 15596,
 	[GA_ASM_LBK_MISC_CFG] = 15608,
 	[GA_ASM_RAM_CTRL] = 15684,
+	[GA_EACL_COMMON] = 37408,
 	[GA_EACL_ES2_KEY_SELECT_PROFILE] = 36864,
 	[GA_EACL_CNT_TBL] = 30720,
 	[GA_EACL_POL_CFG] = 38400,
@@ -102,6 +106,7 @@ const unsigned int lan969x_gaddr[GADDR_LAST] = {
 	[GA_QSYS_RAM_CTRL] = 2204,
 	[GA_REW_COMMON] = 98304,
 	[GA_REW_PORT] = 49152,
+	[GA_REW_VMID] = 96768,
 	[GA_REW_VOE_PORT_LM_CNT] = 90112,
 	[GA_REW_RAM_CTRL] = 93992,
 	[GA_VOP_RAM_CTRL] = 16368,
@@ -123,6 +128,8 @@ const unsigned int lan969x_gcnt[GCNT_LAST] = {
 	[GC_ANA_L2_ISDX_LIMIT] = 256,
 	[GC_ANA_L2_ISDX] = 1024,
 	[GC_ANA_L3_VLAN] = 4608,
+	[GC_ANA_L3_VMID] = 127,
+	[GC_ANA_L3_ARP] = 1024,
 	[GC_ASM_DEV_STATISTICS] = 30,
 	[GC_EACL_ES2_KEY_SELECT_PROFILE] = 68,
 	[GC_EACL_CNT_TBL] = 512,
@@ -132,6 +139,7 @@ const unsigned int lan969x_gcnt[GCNT_LAST] = {
 	[GC_PTP_PTP_PINS] = 8,
 	[GC_PTP_PHASE_DETECTOR_CTRL] = 8,
 	[GC_REW_PORT] = 35,
+	[GC_REW_VMID] = 127,
 	[GC_REW_VOE_PORT_LM_CNT] = 240,
 };
 
@@ -188,7 +196,12 @@ const unsigned int lan969x_fsize[FSIZE_LAST] = {
 	[FW_ANA_L2_AUTO_LRN_CFG_AUTO_LRN_ENA] = 30,
 	[FW_ANA_L2_DLB_CFG_DLB_IDX] = 9,
 	[FW_ANA_L2_TSN_CFG_TSN_SFID] = 8,
+	[FW_ANA_L3_L3_UC_ENA_L3_UC_ENA] = 30,
+	[FW_ANA_L3_SIP_SECURE_ENA1_SIP_CMP_ENA1] = 3,
+	[FW_ANA_L3_DIP_SECURE_ENA_DIP_CMP_ENA] = 30,
+	[FW_ANA_L3_VMID_CFG_VMID] = 7,
 	[FW_ANA_L3_VLAN_MASK_CFG_VLAN_PORT_MASK] = 30,
+	[FW_ANA_L3_ARP_CFG_0_ARP_VMID] = 7,
 	[FW_FDMA_CH_CFG_CH_DCB_DB_CNT] = 2,
 	[FW_GCB_HW_SGPIO_TO_SD_MAP_CFG_SGPIO_TO_SD_SEL] = 7,
 	[FW_HSCH_SE_CFG_SE_DWRR_CNT] = 5,
@@ -214,6 +227,7 @@ const unsigned int lan969x_fsize[FSIZE_LAST] = {
 	[FW_QSYS_ATOP_ATOP] = 11,
 	[FW_QSYS_ATOP_TOT_CFG_ATOP_TOT] = 11,
 	[FW_REW_RTAG_ETAG_CTRL_IPE_TBL] = 6,
+	[FW_REW_RLEG_CTRL_DECAP_IRLEG] = 7,
 	[FW_XQS_STAT_CFG_STAT_VIEW] = 10,
 	[FW_XQS_QLIMIT_SHR_TOP_CFG_QLIMIT_SHR_TOP] = 14,
 	[FW_XQS_QLIMIT_SHR_ATOP_CFG_QLIMIT_SHR_ATOP] = 14,
diff --git a/drivers/net/ethernet/microchip/sparx5/sparx5_main_regs.h b/drivers/net/ethernet/microchip/sparx5/sparx5_main_regs.h
index d9ef4ef137b8..9d34750416eb 100644
--- a/drivers/net/ethernet/microchip/sparx5/sparx5_main_regs.h
+++ b/drivers/net/ethernet/microchip/sparx5/sparx5_main_regs.h
@@ -1,11 +1,11 @@
 /* SPDX-License-Identifier: GPL-2.0+
  * Microchip Sparx5 Switch driver
  *
- * Copyright (c) 2024 Microchip Technology Inc.
+ * Copyright (c) 2026 Microchip Technology Inc.
  */
 
-/* This file is autogenerated by cml-utils 2024-10-04 10:40:40 +0200.
- * Commit ID: 9d07b8d19363f3cd3590ddb3f7a2e2768e16524b
+/* This file is autogenerated by cml-utils 2026-02-16 21:50:30 +0100.
+ * Commit ID: 09d52195beeeb9682063ef0bd6eec50eebc137e2
  */
 
 #ifndef _SPARX5_MAIN_REGS_H_
@@ -711,6 +711,124 @@ extern const struct sparx5_regs *regs;
 #define ANA_ACL_SWAP_IP_CTRL_IP_SWAP_IP4_TTL_ENA_GET(x)\
 	FIELD_GET(ANA_ACL_SWAP_IP_CTRL_IP_SWAP_IP4_TTL_ENA, x)
 
+/* ANA_ACL:COMMON:VCAP_S2_MISC_CTRL */
+#define ANA_ACL_VCAP_S2_MISC_CTRL                                              \
+	__REG(TARGET_ANA_ACL, 0, 1, regs->gaddr[GA_ANA_ACL_COMMON], 0, 1, 592, \
+	      416, 0, 1, 4)
+
+#define ANA_ACL_VCAP_S2_MISC_CTRL_MAPPED_PORT_ENA GENMASK(27, 24)
+#define ANA_ACL_VCAP_S2_MISC_CTRL_MAPPED_PORT_ENA_SET(x)\
+	FIELD_PREP(ANA_ACL_VCAP_S2_MISC_CTRL_MAPPED_PORT_ENA, x)
+#define ANA_ACL_VCAP_S2_MISC_CTRL_MAPPED_PORT_ENA_GET(x)\
+	FIELD_GET(ANA_ACL_VCAP_S2_MISC_CTRL_MAPPED_PORT_ENA, x)
+
+#define ANA_ACL_VCAP_S2_MISC_CTRL_AFFIX_OVERLOAD_ENA GENMASK(23, 20)
+#define ANA_ACL_VCAP_S2_MISC_CTRL_AFFIX_OVERLOAD_ENA_SET(x)\
+	FIELD_PREP(ANA_ACL_VCAP_S2_MISC_CTRL_AFFIX_OVERLOAD_ENA, x)
+#define ANA_ACL_VCAP_S2_MISC_CTRL_AFFIX_OVERLOAD_ENA_GET(x)\
+	FIELD_GET(ANA_ACL_VCAP_S2_MISC_CTRL_AFFIX_OVERLOAD_ENA, x)
+
+#define ANA_ACL_VCAP_S2_MISC_CTRL_ISDX_OVERLOAD_ENA GENMASK(19, 16)
+#define ANA_ACL_VCAP_S2_MISC_CTRL_ISDX_OVERLOAD_ENA_SET(x)\
+	FIELD_PREP(ANA_ACL_VCAP_S2_MISC_CTRL_ISDX_OVERLOAD_ENA, x)
+#define ANA_ACL_VCAP_S2_MISC_CTRL_ISDX_OVERLOAD_ENA_GET(x)\
+	FIELD_GET(ANA_ACL_VCAP_S2_MISC_CTRL_ISDX_OVERLOAD_ENA, x)
+
+#define ANA_ACL_VCAP_S2_MISC_CTRL_PAG_FORCE_VID_ENA GENMASK(15, 12)
+#define ANA_ACL_VCAP_S2_MISC_CTRL_PAG_FORCE_VID_ENA_SET(x)\
+	FIELD_PREP(ANA_ACL_VCAP_S2_MISC_CTRL_PAG_FORCE_VID_ENA, x)
+#define ANA_ACL_VCAP_S2_MISC_CTRL_PAG_FORCE_VID_ENA_GET(x)\
+	FIELD_GET(ANA_ACL_VCAP_S2_MISC_CTRL_PAG_FORCE_VID_ENA, x)
+
+#define ANA_ACL_VCAP_S2_MISC_CTRL_VLAN_PIPELINE_ACT_ENA BIT(11)
+#define ANA_ACL_VCAP_S2_MISC_CTRL_VLAN_PIPELINE_ACT_ENA_SET(x)\
+	FIELD_PREP(ANA_ACL_VCAP_S2_MISC_CTRL_VLAN_PIPELINE_ACT_ENA, x)
+#define ANA_ACL_VCAP_S2_MISC_CTRL_VLAN_PIPELINE_ACT_ENA_GET(x)\
+	FIELD_GET(ANA_ACL_VCAP_S2_MISC_CTRL_VLAN_PIPELINE_ACT_ENA, x)
+
+#define ANA_ACL_VCAP_S2_MISC_CTRL_ACL_RT_IGR_RLEG_STAT_MODE BIT(10)
+#define ANA_ACL_VCAP_S2_MISC_CTRL_ACL_RT_IGR_RLEG_STAT_MODE_SET(x)\
+	FIELD_PREP(ANA_ACL_VCAP_S2_MISC_CTRL_ACL_RT_IGR_RLEG_STAT_MODE, x)
+#define ANA_ACL_VCAP_S2_MISC_CTRL_ACL_RT_IGR_RLEG_STAT_MODE_GET(x)\
+	FIELD_GET(ANA_ACL_VCAP_S2_MISC_CTRL_ACL_RT_IGR_RLEG_STAT_MODE, x)
+
+#define ANA_ACL_VCAP_S2_MISC_CTRL_ACL_RT_EGR_RLEG_STAT_MODE BIT(9)
+#define ANA_ACL_VCAP_S2_MISC_CTRL_ACL_RT_EGR_RLEG_STAT_MODE_SET(x)\
+	FIELD_PREP(ANA_ACL_VCAP_S2_MISC_CTRL_ACL_RT_EGR_RLEG_STAT_MODE, x)
+#define ANA_ACL_VCAP_S2_MISC_CTRL_ACL_RT_EGR_RLEG_STAT_MODE_GET(x)\
+	FIELD_GET(ANA_ACL_VCAP_S2_MISC_CTRL_ACL_RT_EGR_RLEG_STAT_MODE, x)
+
+#define ANA_ACL_VCAP_S2_MISC_CTRL_ACL_RT_FORCE_ES0_VID_ENA BIT(8)
+#define ANA_ACL_VCAP_S2_MISC_CTRL_ACL_RT_FORCE_ES0_VID_ENA_SET(x)\
+	FIELD_PREP(ANA_ACL_VCAP_S2_MISC_CTRL_ACL_RT_FORCE_ES0_VID_ENA, x)
+#define ANA_ACL_VCAP_S2_MISC_CTRL_ACL_RT_FORCE_ES0_VID_ENA_GET(x)\
+	FIELD_GET(ANA_ACL_VCAP_S2_MISC_CTRL_ACL_RT_FORCE_ES0_VID_ENA, x)
+
+#define ANA_ACL_VCAP_S2_MISC_CTRL_ACL_RT_UPDATE_CL_VID_ENA BIT(7)
+#define ANA_ACL_VCAP_S2_MISC_CTRL_ACL_RT_UPDATE_CL_VID_ENA_SET(x)\
+	FIELD_PREP(ANA_ACL_VCAP_S2_MISC_CTRL_ACL_RT_UPDATE_CL_VID_ENA, x)
+#define ANA_ACL_VCAP_S2_MISC_CTRL_ACL_RT_UPDATE_CL_VID_ENA_GET(x)\
+	FIELD_GET(ANA_ACL_VCAP_S2_MISC_CTRL_ACL_RT_UPDATE_CL_VID_ENA, x)
+
+#define ANA_ACL_VCAP_S2_MISC_CTRL_ACL_RT_SEL     GENMASK(6, 5)
+#define ANA_ACL_VCAP_S2_MISC_CTRL_ACL_RT_SEL_SET(x)\
+	FIELD_PREP(ANA_ACL_VCAP_S2_MISC_CTRL_ACL_RT_SEL, x)
+#define ANA_ACL_VCAP_S2_MISC_CTRL_ACL_RT_SEL_GET(x)\
+	FIELD_GET(ANA_ACL_VCAP_S2_MISC_CTRL_ACL_RT_SEL, x)
+
+#define ANA_ACL_VCAP_S2_MISC_CTRL_LBK_IGR_MASK_SEL3_ENA BIT(4)
+#define ANA_ACL_VCAP_S2_MISC_CTRL_LBK_IGR_MASK_SEL3_ENA_SET(x)\
+	FIELD_PREP(ANA_ACL_VCAP_S2_MISC_CTRL_LBK_IGR_MASK_SEL3_ENA, x)
+#define ANA_ACL_VCAP_S2_MISC_CTRL_LBK_IGR_MASK_SEL3_ENA_GET(x)\
+	FIELD_GET(ANA_ACL_VCAP_S2_MISC_CTRL_LBK_IGR_MASK_SEL3_ENA, x)
+
+#define ANA_ACL_VCAP_S2_MISC_CTRL_MASQ_IGR_MASK_ENA BIT(3)
+#define ANA_ACL_VCAP_S2_MISC_CTRL_MASQ_IGR_MASK_ENA_SET(x)\
+	FIELD_PREP(ANA_ACL_VCAP_S2_MISC_CTRL_MASQ_IGR_MASK_ENA, x)
+#define ANA_ACL_VCAP_S2_MISC_CTRL_MASQ_IGR_MASK_ENA_GET(x)\
+	FIELD_GET(ANA_ACL_VCAP_S2_MISC_CTRL_MASQ_IGR_MASK_ENA, x)
+
+#define ANA_ACL_VCAP_S2_MISC_CTRL_FP_VS2_IGR_MASK_ENA BIT(2)
+#define ANA_ACL_VCAP_S2_MISC_CTRL_FP_VS2_IGR_MASK_ENA_SET(x)\
+	FIELD_PREP(ANA_ACL_VCAP_S2_MISC_CTRL_FP_VS2_IGR_MASK_ENA, x)
+#define ANA_ACL_VCAP_S2_MISC_CTRL_FP_VS2_IGR_MASK_ENA_GET(x)\
+	FIELD_GET(ANA_ACL_VCAP_S2_MISC_CTRL_FP_VS2_IGR_MASK_ENA, x)
+
+#define ANA_ACL_VCAP_S2_MISC_CTRL_VD_IGR_MASK_ENA BIT(1)
+#define ANA_ACL_VCAP_S2_MISC_CTRL_VD_IGR_MASK_ENA_SET(x)\
+	FIELD_PREP(ANA_ACL_VCAP_S2_MISC_CTRL_VD_IGR_MASK_ENA, x)
+#define ANA_ACL_VCAP_S2_MISC_CTRL_VD_IGR_MASK_ENA_GET(x)\
+	FIELD_GET(ANA_ACL_VCAP_S2_MISC_CTRL_VD_IGR_MASK_ENA, x)
+
+#define ANA_ACL_VCAP_S2_MISC_CTRL_CPU_IGR_MASK_ENA BIT(0)
+#define ANA_ACL_VCAP_S2_MISC_CTRL_CPU_IGR_MASK_ENA_SET(x)\
+	FIELD_PREP(ANA_ACL_VCAP_S2_MISC_CTRL_CPU_IGR_MASK_ENA, x)
+#define ANA_ACL_VCAP_S2_MISC_CTRL_CPU_IGR_MASK_ENA_GET(x)\
+	FIELD_GET(ANA_ACL_VCAP_S2_MISC_CTRL_CPU_IGR_MASK_ENA, x)
+
+/* ANA_ACL:COMMON:VCAP_S2_MISC_CTRL2 */
+#define ANA_ACL_VCAP_S2_MISC_CTRL2                                             \
+	__REG(TARGET_ANA_ACL, 0, 1, regs->gaddr[GA_ANA_ACL_COMMON], 0, 1, 592, \
+	      420, 0, 1, 4)
+
+#define ANA_ACL_VCAP_S2_MISC_CTRL2_TWAMP_PIPELINE_PT GENMASK(12, 8)
+#define ANA_ACL_VCAP_S2_MISC_CTRL2_TWAMP_PIPELINE_PT_SET(x)\
+	FIELD_PREP(ANA_ACL_VCAP_S2_MISC_CTRL2_TWAMP_PIPELINE_PT, x)
+#define ANA_ACL_VCAP_S2_MISC_CTRL2_TWAMP_PIPELINE_PT_GET(x)\
+	FIELD_GET(ANA_ACL_VCAP_S2_MISC_CTRL2_TWAMP_PIPELINE_PT, x)
+
+#define ANA_ACL_VCAP_S2_MISC_CTRL2_EGR_ACTION_ENA GENMASK(7, 4)
+#define ANA_ACL_VCAP_S2_MISC_CTRL2_EGR_ACTION_ENA_SET(x)\
+	FIELD_PREP(ANA_ACL_VCAP_S2_MISC_CTRL2_EGR_ACTION_ENA, x)
+#define ANA_ACL_VCAP_S2_MISC_CTRL2_EGR_ACTION_ENA_GET(x)\
+	FIELD_GET(ANA_ACL_VCAP_S2_MISC_CTRL2_EGR_ACTION_ENA, x)
+
+#define ANA_ACL_VCAP_S2_MISC_CTRL2_EGR_KEY_ENA   GENMASK(3, 0)
+#define ANA_ACL_VCAP_S2_MISC_CTRL2_EGR_KEY_ENA_SET(x)\
+	FIELD_PREP(ANA_ACL_VCAP_S2_MISC_CTRL2_EGR_KEY_ENA, x)
+#define ANA_ACL_VCAP_S2_MISC_CTRL2_EGR_KEY_ENA_GET(x)\
+	FIELD_GET(ANA_ACL_VCAP_S2_MISC_CTRL2_EGR_KEY_ENA, x)
+
 /* ANA_ACL:COMMON:VCAP_S2_RLEG_STAT */
 #define ANA_ACL_VCAP_S2_RLEG_STAT(r)                                           \
 	__REG(TARGET_ANA_ACL, 0, 1, regs->gaddr[GA_ANA_ACL_COMMON], 0, 1, 592, \
@@ -1812,6 +1930,338 @@ extern const struct sparx5_regs *regs;
 #define ANA_L3_VLAN_CTRL_VLAN_ENA_GET(x)\
 	FIELD_GET(ANA_L3_VLAN_CTRL_VLAN_ENA, x)
 
+/* ANA_L3:COMMON:L3_UC_ENA */
+#define ANA_L3_L3_UC_ENA                                                       \
+	__REG(TARGET_ANA_L3, 0, 1, regs->gaddr[GA_ANA_L3_COMMON], 0, 1, 184, 8,\
+	      0, 1, 4)
+
+/* SPARX5 ONLY */
+/* ANA_L3:COMMON:L3_UC_ENA1 */
+#define ANA_L3_L3_UC_ENA1                                                      \
+	__REG(TARGET_ANA_L3, 0, 1, regs->gaddr[GA_ANA_L3_COMMON], 0, 1, 184,   \
+	      12, 0, 1, 4)
+
+/* SPARX5 ONLY */
+/* ANA_L3:COMMON:L3_UC_ENA2 */
+#define ANA_L3_L3_UC_ENA2                                                      \
+	__REG(TARGET_ANA_L3, 0, 1, regs->gaddr[GA_ANA_L3_COMMON], 0, 1, 184,   \
+	      16, 0, 1, 4)
+
+#define ANA_L3_L3_UC_ENA2_L3_UC_ENA2             BIT(0)
+#define ANA_L3_L3_UC_ENA2_L3_UC_ENA2_SET(x)\
+	FIELD_PREP(ANA_L3_L3_UC_ENA2_L3_UC_ENA2, x)
+#define ANA_L3_L3_UC_ENA2_L3_UC_ENA2_GET(x)\
+	FIELD_GET(ANA_L3_L3_UC_ENA2_L3_UC_ENA2, x)
+
+/* ANA_L3:COMMON:ROUTING_CFG */
+#define ANA_L3_ROUTING_CFG                                                     \
+	__REG(TARGET_ANA_L3, 0, 1, regs->gaddr[GA_ANA_L3_COMMON], 0, 1, 184,   \
+	      104, 0, 1, 4)
+
+#define ANA_L3_ROUTING_CFG_L3_ENA_MODE           BIT(30)
+#define ANA_L3_ROUTING_CFG_L3_ENA_MODE_SET(x)\
+	FIELD_PREP(ANA_L3_ROUTING_CFG_L3_ENA_MODE, x)
+#define ANA_L3_ROUTING_CFG_L3_ENA_MODE_GET(x)\
+	FIELD_GET(ANA_L3_ROUTING_CFG_L3_ENA_MODE, x)
+
+#define ANA_L3_ROUTING_CFG_IP6_MC_DIP_FWD_ENA    BIT(29)
+#define ANA_L3_ROUTING_CFG_IP6_MC_DIP_FWD_ENA_SET(x)\
+	FIELD_PREP(ANA_L3_ROUTING_CFG_IP6_MC_DIP_FWD_ENA, x)
+#define ANA_L3_ROUTING_CFG_IP6_MC_DIP_FWD_ENA_GET(x)\
+	FIELD_GET(ANA_L3_ROUTING_CFG_IP6_MC_DIP_FWD_ENA, x)
+
+#define ANA_L3_ROUTING_CFG_IP4_MC_DIP_FWD_ENA    BIT(28)
+#define ANA_L3_ROUTING_CFG_IP4_MC_DIP_FWD_ENA_SET(x)\
+	FIELD_PREP(ANA_L3_ROUTING_CFG_IP4_MC_DIP_FWD_ENA, x)
+#define ANA_L3_ROUTING_CFG_IP4_MC_DIP_FWD_ENA_GET(x)\
+	FIELD_GET(ANA_L3_ROUTING_CFG_IP4_MC_DIP_FWD_ENA, x)
+
+#define ANA_L3_ROUTING_CFG_RT_SMAC_UPDATE_ENA    BIT(27)
+#define ANA_L3_ROUTING_CFG_RT_SMAC_UPDATE_ENA_SET(x)\
+	FIELD_PREP(ANA_L3_ROUTING_CFG_RT_SMAC_UPDATE_ENA, x)
+#define ANA_L3_ROUTING_CFG_RT_SMAC_UPDATE_ENA_GET(x)\
+	FIELD_GET(ANA_L3_ROUTING_CFG_RT_SMAC_UPDATE_ENA, x)
+
+#define ANA_L3_ROUTING_CFG_RLEG_NONIP_UC_REDIR_MODE GENMASK(26, 25)
+#define ANA_L3_ROUTING_CFG_RLEG_NONIP_UC_REDIR_MODE_SET(x)\
+	FIELD_PREP(ANA_L3_ROUTING_CFG_RLEG_NONIP_UC_REDIR_MODE, x)
+#define ANA_L3_ROUTING_CFG_RLEG_NONIP_UC_REDIR_MODE_GET(x)\
+	FIELD_GET(ANA_L3_ROUTING_CFG_RLEG_NONIP_UC_REDIR_MODE, x)
+
+#define ANA_L3_ROUTING_CFG_IP6_LEN_REDIR         BIT(22)
+#define ANA_L3_ROUTING_CFG_IP6_LEN_REDIR_SET(x)\
+	FIELD_PREP(ANA_L3_ROUTING_CFG_IP6_LEN_REDIR, x)
+#define ANA_L3_ROUTING_CFG_IP6_LEN_REDIR_GET(x)\
+	FIELD_GET(ANA_L3_ROUTING_CFG_IP6_LEN_REDIR, x)
+
+#define ANA_L3_ROUTING_CFG_IP4_LEN_REDIR         BIT(21)
+#define ANA_L3_ROUTING_CFG_IP4_LEN_REDIR_SET(x)\
+	FIELD_PREP(ANA_L3_ROUTING_CFG_IP4_LEN_REDIR, x)
+#define ANA_L3_ROUTING_CFG_IP4_LEN_REDIR_GET(x)\
+	FIELD_GET(ANA_L3_ROUTING_CFG_IP4_LEN_REDIR, x)
+
+#define ANA_L3_ROUTING_CFG_IP6_L2_BC_COPY_ENA    BIT(20)
+#define ANA_L3_ROUTING_CFG_IP6_L2_BC_COPY_ENA_SET(x)\
+	FIELD_PREP(ANA_L3_ROUTING_CFG_IP6_L2_BC_COPY_ENA, x)
+#define ANA_L3_ROUTING_CFG_IP6_L2_BC_COPY_ENA_GET(x)\
+	FIELD_GET(ANA_L3_ROUTING_CFG_IP6_L2_BC_COPY_ENA, x)
+
+#define ANA_L3_ROUTING_CFG_IP4_L2_BC_COPY_ENA    BIT(19)
+#define ANA_L3_ROUTING_CFG_IP4_L2_BC_COPY_ENA_SET(x)\
+	FIELD_PREP(ANA_L3_ROUTING_CFG_IP4_L2_BC_COPY_ENA, x)
+#define ANA_L3_ROUTING_CFG_IP4_L2_BC_COPY_ENA_GET(x)\
+	FIELD_GET(ANA_L3_ROUTING_CFG_IP4_L2_BC_COPY_ENA, x)
+
+#define ANA_L3_ROUTING_CFG_RLEG_IP6_SIP_RPF_REDIR_ENA BIT(18)
+#define ANA_L3_ROUTING_CFG_RLEG_IP6_SIP_RPF_REDIR_ENA_SET(x)\
+	FIELD_PREP(ANA_L3_ROUTING_CFG_RLEG_IP6_SIP_RPF_REDIR_ENA, x)
+#define ANA_L3_ROUTING_CFG_RLEG_IP6_SIP_RPF_REDIR_ENA_GET(x)\
+	FIELD_GET(ANA_L3_ROUTING_CFG_RLEG_IP6_SIP_RPF_REDIR_ENA, x)
+
+#define ANA_L3_ROUTING_CFG_RLEG_IP4_SIP_RPF_REDIR_ENA BIT(17)
+#define ANA_L3_ROUTING_CFG_RLEG_IP4_SIP_RPF_REDIR_ENA_SET(x)\
+	FIELD_PREP(ANA_L3_ROUTING_CFG_RLEG_IP4_SIP_RPF_REDIR_ENA, x)
+#define ANA_L3_ROUTING_CFG_RLEG_IP4_SIP_RPF_REDIR_ENA_GET(x)\
+	FIELD_GET(ANA_L3_ROUTING_CFG_RLEG_IP4_SIP_RPF_REDIR_ENA, x)
+
+#define ANA_L3_ROUTING_CFG_IP6_DIP_ADDR_VIOLATION_REDIR_ENA GENMASK(16, 15)
+#define ANA_L3_ROUTING_CFG_IP6_DIP_ADDR_VIOLATION_REDIR_ENA_SET(x)\
+	FIELD_PREP(ANA_L3_ROUTING_CFG_IP6_DIP_ADDR_VIOLATION_REDIR_ENA, x)
+#define ANA_L3_ROUTING_CFG_IP6_DIP_ADDR_VIOLATION_REDIR_ENA_GET(x)\
+	FIELD_GET(ANA_L3_ROUTING_CFG_IP6_DIP_ADDR_VIOLATION_REDIR_ENA, x)
+
+#define ANA_L3_ROUTING_CFG_IP4_DIP_ADDR_VIOLATION_REDIR_ENA GENMASK(13, 11)
+#define ANA_L3_ROUTING_CFG_IP4_DIP_ADDR_VIOLATION_REDIR_ENA_SET(x)\
+	FIELD_PREP(ANA_L3_ROUTING_CFG_IP4_DIP_ADDR_VIOLATION_REDIR_ENA, x)
+#define ANA_L3_ROUTING_CFG_IP4_DIP_ADDR_VIOLATION_REDIR_ENA_GET(x)\
+	FIELD_GET(ANA_L3_ROUTING_CFG_IP4_DIP_ADDR_VIOLATION_REDIR_ENA, x)
+
+#define ANA_L3_ROUTING_CFG_IP6_SIP_ADDR_VIOLATION_REDIR_ENA GENMASK(10, 8)
+#define ANA_L3_ROUTING_CFG_IP6_SIP_ADDR_VIOLATION_REDIR_ENA_SET(x)\
+	FIELD_PREP(ANA_L3_ROUTING_CFG_IP6_SIP_ADDR_VIOLATION_REDIR_ENA, x)
+#define ANA_L3_ROUTING_CFG_IP6_SIP_ADDR_VIOLATION_REDIR_ENA_GET(x)\
+	FIELD_GET(ANA_L3_ROUTING_CFG_IP6_SIP_ADDR_VIOLATION_REDIR_ENA, x)
+
+#define ANA_L3_ROUTING_CFG_IP4_SIP_ADDR_VIOLATION_REDIR_ENA GENMASK(7, 5)
+#define ANA_L3_ROUTING_CFG_IP4_SIP_ADDR_VIOLATION_REDIR_ENA_SET(x)\
+	FIELD_PREP(ANA_L3_ROUTING_CFG_IP4_SIP_ADDR_VIOLATION_REDIR_ENA, x)
+#define ANA_L3_ROUTING_CFG_IP4_SIP_ADDR_VIOLATION_REDIR_ENA_GET(x)\
+	FIELD_GET(ANA_L3_ROUTING_CFG_IP4_SIP_ADDR_VIOLATION_REDIR_ENA, x)
+
+#define ANA_L3_ROUTING_CFG_CPU_RLEG_IP_HDR_FAIL_REDIR_ENA BIT(4)
+#define ANA_L3_ROUTING_CFG_CPU_RLEG_IP_HDR_FAIL_REDIR_ENA_SET(x)\
+	FIELD_PREP(ANA_L3_ROUTING_CFG_CPU_RLEG_IP_HDR_FAIL_REDIR_ENA, x)
+#define ANA_L3_ROUTING_CFG_CPU_RLEG_IP_HDR_FAIL_REDIR_ENA_GET(x)\
+	FIELD_GET(ANA_L3_ROUTING_CFG_CPU_RLEG_IP_HDR_FAIL_REDIR_ENA, x)
+
+#define ANA_L3_ROUTING_CFG_CPU_IP6_HOPBYHOP_REDIR_ENA BIT(3)
+#define ANA_L3_ROUTING_CFG_CPU_IP6_HOPBYHOP_REDIR_ENA_SET(x)\
+	FIELD_PREP(ANA_L3_ROUTING_CFG_CPU_IP6_HOPBYHOP_REDIR_ENA, x)
+#define ANA_L3_ROUTING_CFG_CPU_IP6_HOPBYHOP_REDIR_ENA_GET(x)\
+	FIELD_GET(ANA_L3_ROUTING_CFG_CPU_IP6_HOPBYHOP_REDIR_ENA, x)
+
+#define ANA_L3_ROUTING_CFG_CPU_IP4_OPTIONS_REDIR_ENA BIT(2)
+#define ANA_L3_ROUTING_CFG_CPU_IP4_OPTIONS_REDIR_ENA_SET(x)\
+	FIELD_PREP(ANA_L3_ROUTING_CFG_CPU_IP4_OPTIONS_REDIR_ENA, x)
+#define ANA_L3_ROUTING_CFG_CPU_IP4_OPTIONS_REDIR_ENA_GET(x)\
+	FIELD_GET(ANA_L3_ROUTING_CFG_CPU_IP4_OPTIONS_REDIR_ENA, x)
+
+#define ANA_L3_ROUTING_CFG_IP6_HC_REDIR_ENA      BIT(1)
+#define ANA_L3_ROUTING_CFG_IP6_HC_REDIR_ENA_SET(x)\
+	FIELD_PREP(ANA_L3_ROUTING_CFG_IP6_HC_REDIR_ENA, x)
+#define ANA_L3_ROUTING_CFG_IP6_HC_REDIR_ENA_GET(x)\
+	FIELD_GET(ANA_L3_ROUTING_CFG_IP6_HC_REDIR_ENA, x)
+
+#define ANA_L3_ROUTING_CFG_IP4_TTL_REDIR_ENA     BIT(0)
+#define ANA_L3_ROUTING_CFG_IP4_TTL_REDIR_ENA_SET(x)\
+	FIELD_PREP(ANA_L3_ROUTING_CFG_IP4_TTL_REDIR_ENA, x)
+#define ANA_L3_ROUTING_CFG_IP4_TTL_REDIR_ENA_GET(x)\
+	FIELD_GET(ANA_L3_ROUTING_CFG_IP4_TTL_REDIR_ENA, x)
+
+/* ANA_L3:COMMON:ROUTING_CFG2 */
+#define ANA_L3_ROUTING_CFG2                                                    \
+	__REG(TARGET_ANA_L3, 0, 1, regs->gaddr[GA_ANA_L3_COMMON], 0, 1, 184,   \
+	      108, 0, 1, 4)
+
+#define ANA_L3_ROUTING_CFG2_IP6_SIP_LOOKUP_ENA   BIT(4)
+#define ANA_L3_ROUTING_CFG2_IP6_SIP_LOOKUP_ENA_SET(x)\
+	FIELD_PREP(ANA_L3_ROUTING_CFG2_IP6_SIP_LOOKUP_ENA, x)
+#define ANA_L3_ROUTING_CFG2_IP6_SIP_LOOKUP_ENA_GET(x)\
+	FIELD_GET(ANA_L3_ROUTING_CFG2_IP6_SIP_LOOKUP_ENA, x)
+
+#define ANA_L3_ROUTING_CFG2_IP4_SIP_LOOKUP_ENA   BIT(3)
+#define ANA_L3_ROUTING_CFG2_IP4_SIP_LOOKUP_ENA_SET(x)\
+	FIELD_PREP(ANA_L3_ROUTING_CFG2_IP4_SIP_LOOKUP_ENA, x)
+#define ANA_L3_ROUTING_CFG2_IP4_SIP_LOOKUP_ENA_GET(x)\
+	FIELD_GET(ANA_L3_ROUTING_CFG2_IP4_SIP_LOOKUP_ENA, x)
+
+#define ANA_L3_ROUTING_CFG2_SIP_IP6PFX_ENA       BIT(1)
+#define ANA_L3_ROUTING_CFG2_SIP_IP6PFX_ENA_SET(x)\
+	FIELD_PREP(ANA_L3_ROUTING_CFG2_SIP_IP6PFX_ENA, x)
+#define ANA_L3_ROUTING_CFG2_SIP_IP6PFX_ENA_GET(x)\
+	FIELD_GET(ANA_L3_ROUTING_CFG2_SIP_IP6PFX_ENA, x)
+
+#define ANA_L3_ROUTING_CFG2_DIP_IP6PFX_ENA       BIT(0)
+#define ANA_L3_ROUTING_CFG2_DIP_IP6PFX_ENA_SET(x)\
+	FIELD_PREP(ANA_L3_ROUTING_CFG2_DIP_IP6PFX_ENA, x)
+#define ANA_L3_ROUTING_CFG2_DIP_IP6PFX_ENA_GET(x)\
+	FIELD_GET(ANA_L3_ROUTING_CFG2_DIP_IP6PFX_ENA, x)
+
+#define ANA_L3_ROUTING_CFG2_IP4_DECAP_REDIR_ENA  BIT(2)
+#define ANA_L3_ROUTING_CFG2_IP4_DECAP_REDIR_ENA_SET(x)\
+	FIELD_PREP(ANA_L3_ROUTING_CFG2_IP4_DECAP_REDIR_ENA, x)
+#define ANA_L3_ROUTING_CFG2_IP4_DECAP_REDIR_ENA_GET(x)\
+	FIELD_GET(ANA_L3_ROUTING_CFG2_IP4_DECAP_REDIR_ENA, x)
+
+/* ANA_L3:COMMON:RLEG_CFG_0 */
+#define ANA_L3_RLEG_CFG_0                                                      \
+	__REG(TARGET_ANA_L3, 0, 1, regs->gaddr[GA_ANA_L3_COMMON], 0, 1, 184,   \
+	      112, 0, 1, 4)
+
+#define ANA_L3_RLEG_CFG_0_RLEG_MAC_LSB           GENMASK(31, 8)
+#define ANA_L3_RLEG_CFG_0_RLEG_MAC_LSB_SET(x)\
+	FIELD_PREP(ANA_L3_RLEG_CFG_0_RLEG_MAC_LSB, x)
+#define ANA_L3_RLEG_CFG_0_RLEG_MAC_LSB_GET(x)\
+	FIELD_GET(ANA_L3_RLEG_CFG_0_RLEG_MAC_LSB, x)
+
+/* ANA_L3:COMMON:RLEG_CFG_1 */
+#define ANA_L3_RLEG_CFG_1                                                      \
+	__REG(TARGET_ANA_L3, 0, 1, regs->gaddr[GA_ANA_L3_COMMON], 0, 1, 184,   \
+	      116, 0, 1, 4)
+
+#define ANA_L3_RLEG_CFG_1_RLEG_MAC_TYPE_SEL      GENMASK(25, 24)
+#define ANA_L3_RLEG_CFG_1_RLEG_MAC_TYPE_SEL_SET(x)\
+	FIELD_PREP(ANA_L3_RLEG_CFG_1_RLEG_MAC_TYPE_SEL, x)
+#define ANA_L3_RLEG_CFG_1_RLEG_MAC_TYPE_SEL_GET(x)\
+	FIELD_GET(ANA_L3_RLEG_CFG_1_RLEG_MAC_TYPE_SEL, x)
+
+#define ANA_L3_RLEG_CFG_1_RLEG_MAC_MSB           GENMASK(23, 0)
+#define ANA_L3_RLEG_CFG_1_RLEG_MAC_MSB_SET(x)\
+	FIELD_PREP(ANA_L3_RLEG_CFG_1_RLEG_MAC_MSB, x)
+#define ANA_L3_RLEG_CFG_1_RLEG_MAC_MSB_GET(x)\
+	FIELD_GET(ANA_L3_RLEG_CFG_1_RLEG_MAC_MSB, x)
+
+/* ANA_L3:COMMON:CPU_QU_CFG */
+#define ANA_L3_CPU_QU_CFG                                                      \
+	__REG(TARGET_ANA_L3, 0, 1, regs->gaddr[GA_ANA_L3_COMMON], 0, 1, 184,   \
+	      120, 0, 1, 4)
+
+#define ANA_L3_CPU_QU_CFG_CPU_RLEG_QU            GENMASK(30, 28)
+#define ANA_L3_CPU_QU_CFG_CPU_RLEG_QU_SET(x)\
+	FIELD_PREP(ANA_L3_CPU_QU_CFG_CPU_RLEG_QU, x)
+#define ANA_L3_CPU_QU_CFG_CPU_RLEG_QU_GET(x)\
+	FIELD_GET(ANA_L3_CPU_QU_CFG_CPU_RLEG_QU, x)
+
+#define ANA_L3_CPU_QU_CFG_CPU_RLEG_IP_OPT_QU     GENMASK(26, 24)
+#define ANA_L3_CPU_QU_CFG_CPU_RLEG_IP_OPT_QU_SET(x)\
+	FIELD_PREP(ANA_L3_CPU_QU_CFG_CPU_RLEG_IP_OPT_QU, x)
+#define ANA_L3_CPU_QU_CFG_CPU_RLEG_IP_OPT_QU_GET(x)\
+	FIELD_GET(ANA_L3_CPU_QU_CFG_CPU_RLEG_IP_OPT_QU, x)
+
+#define ANA_L3_CPU_QU_CFG_CPU_RLEG_IP_HDR_FAIL_QU GENMASK(22, 20)
+#define ANA_L3_CPU_QU_CFG_CPU_RLEG_IP_HDR_FAIL_QU_SET(x)\
+	FIELD_PREP(ANA_L3_CPU_QU_CFG_CPU_RLEG_IP_HDR_FAIL_QU, x)
+#define ANA_L3_CPU_QU_CFG_CPU_RLEG_IP_HDR_FAIL_QU_GET(x)\
+	FIELD_GET(ANA_L3_CPU_QU_CFG_CPU_RLEG_IP_HDR_FAIL_QU, x)
+
+#define ANA_L3_CPU_QU_CFG_CPU_SIP_RPF_QU         GENMASK(18, 16)
+#define ANA_L3_CPU_QU_CFG_CPU_SIP_RPF_QU_SET(x)\
+	FIELD_PREP(ANA_L3_CPU_QU_CFG_CPU_SIP_RPF_QU, x)
+#define ANA_L3_CPU_QU_CFG_CPU_SIP_RPF_QU_GET(x)\
+	FIELD_GET(ANA_L3_CPU_QU_CFG_CPU_SIP_RPF_QU, x)
+
+#define ANA_L3_CPU_QU_CFG_CPU_IP_LEN_QU          GENMASK(14, 12)
+#define ANA_L3_CPU_QU_CFG_CPU_IP_LEN_QU_SET(x)\
+	FIELD_PREP(ANA_L3_CPU_QU_CFG_CPU_IP_LEN_QU, x)
+#define ANA_L3_CPU_QU_CFG_CPU_IP_LEN_QU_GET(x)\
+	FIELD_GET(ANA_L3_CPU_QU_CFG_CPU_IP_LEN_QU, x)
+
+#define ANA_L3_CPU_QU_CFG_CPU_MC_FAIL_QU         GENMASK(10, 8)
+#define ANA_L3_CPU_QU_CFG_CPU_MC_FAIL_QU_SET(x)\
+	FIELD_PREP(ANA_L3_CPU_QU_CFG_CPU_MC_FAIL_QU, x)
+#define ANA_L3_CPU_QU_CFG_CPU_MC_FAIL_QU_GET(x)\
+	FIELD_GET(ANA_L3_CPU_QU_CFG_CPU_MC_FAIL_QU, x)
+
+#define ANA_L3_CPU_QU_CFG_CPU_UC_FAIL_QU         GENMASK(6, 4)
+#define ANA_L3_CPU_QU_CFG_CPU_UC_FAIL_QU_SET(x)\
+	FIELD_PREP(ANA_L3_CPU_QU_CFG_CPU_UC_FAIL_QU, x)
+#define ANA_L3_CPU_QU_CFG_CPU_UC_FAIL_QU_GET(x)\
+	FIELD_GET(ANA_L3_CPU_QU_CFG_CPU_UC_FAIL_QU, x)
+
+#define ANA_L3_CPU_QU_CFG_CPU_IP_TTL_FAIL_QU     GENMASK(2, 0)
+#define ANA_L3_CPU_QU_CFG_CPU_IP_TTL_FAIL_QU_SET(x)\
+	FIELD_PREP(ANA_L3_CPU_QU_CFG_CPU_IP_TTL_FAIL_QU, x)
+#define ANA_L3_CPU_QU_CFG_CPU_IP_TTL_FAIL_QU_GET(x)\
+	FIELD_GET(ANA_L3_CPU_QU_CFG_CPU_IP_TTL_FAIL_QU, x)
+
+/* ANA_L3:COMMON:CPU_QU_CFG2 */
+#define ANA_L3_CPU_QU_CFG2                                                     \
+	__REG(TARGET_ANA_L3, 0, 1, regs->gaddr[GA_ANA_L3_COMMON], 0, 1, 184,   \
+	      124, 0, 1, 4)
+
+#define ANA_L3_CPU_QU_CFG2_CPU_IP_DECAP_QU       GENMASK(2, 0)
+#define ANA_L3_CPU_QU_CFG2_CPU_IP_DECAP_QU_SET(x)\
+	FIELD_PREP(ANA_L3_CPU_QU_CFG2_CPU_IP_DECAP_QU, x)
+#define ANA_L3_CPU_QU_CFG2_CPU_IP_DECAP_QU_GET(x)\
+	FIELD_GET(ANA_L3_CPU_QU_CFG2_CPU_IP_DECAP_QU, x)
+
+/* ANA_L3:COMMON:SIP_SECURE_ENA */
+#define ANA_L3_SIP_SECURE_ENA                                                  \
+	__REG(TARGET_ANA_L3, 0, 1, regs->gaddr[GA_ANA_L3_COMMON], 0, 1, 184,   \
+	      144, 0, 1, 4)
+
+/* ANA_L3:COMMON:SIP_SECURE_ENA1 */
+#define ANA_L3_SIP_SECURE_ENA1                                                 \
+	__REG(TARGET_ANA_L3, 0, 1, regs->gaddr[GA_ANA_L3_COMMON], 0, 1, 184,   \
+	      148, 0, 1, 4)
+
+/* SPARX5 ONLY */
+/* ANA_L3:COMMON:SIP_SECURE_ENA2 */
+#define ANA_L3_SIP_SECURE_ENA2                                                 \
+	__REG(TARGET_ANA_L3, 0, 1, regs->gaddr[GA_ANA_L3_COMMON], 0, 1, 184,   \
+	      152, 0, 1, 4)
+
+#define ANA_L3_SIP_SECURE_ENA2_SIP_CMP_ENA2      GENMASK(5, 0)
+#define ANA_L3_SIP_SECURE_ENA2_SIP_CMP_ENA2_SET(x)\
+	FIELD_PREP(ANA_L3_SIP_SECURE_ENA2_SIP_CMP_ENA2, x)
+#define ANA_L3_SIP_SECURE_ENA2_SIP_CMP_ENA2_GET(x)\
+	FIELD_GET(ANA_L3_SIP_SECURE_ENA2_SIP_CMP_ENA2, x)
+
+/* ANA_L3:COMMON:DIP_SECURE_ENA */
+#define ANA_L3_DIP_SECURE_ENA                                                  \
+	__REG(TARGET_ANA_L3, 0, 1, regs->gaddr[GA_ANA_L3_COMMON], 0, 1, 184,   \
+	      156, 0, 1, 4)
+
+/* SPARX5 ONLY */
+/* ANA_L3:COMMON:DIP_SECURE_ENA1 */
+#define ANA_L3_DIP_SECURE_ENA1                                                 \
+	__REG(TARGET_ANA_L3, 0, 1, regs->gaddr[GA_ANA_L3_COMMON], 0, 1, 184,   \
+	      160, 0, 1, 4)
+
+/* SPARX5 ONLY */
+/* ANA_L3:COMMON:DIP_SECURE_ENA2 */
+#define ANA_L3_DIP_SECURE_ENA2                                                 \
+	__REG(TARGET_ANA_L3, 0, 1, regs->gaddr[GA_ANA_L3_COMMON], 0, 1, 184,   \
+	      164, 0, 1, 4)
+
+#define ANA_L3_DIP_SECURE_ENA2_DIP_CMP_ENA2      BIT(0)
+#define ANA_L3_DIP_SECURE_ENA2_DIP_CMP_ENA2_SET(x)\
+	FIELD_PREP(ANA_L3_DIP_SECURE_ENA2_DIP_CMP_ENA2, x)
+#define ANA_L3_DIP_SECURE_ENA2_DIP_CMP_ENA2_GET(x)\
+	FIELD_GET(ANA_L3_DIP_SECURE_ENA2_DIP_CMP_ENA2, x)
+
+/* ANA_L3:VLAN:VMID_CFG */
+#define ANA_L3_VMID_CFG(g)                                                     \
+	__REG(TARGET_ANA_L3, 0, 1, 0, g, regs->gcnt[GC_ANA_L3_VLAN], 64, 0, 0, \
+	      1, 4)
+
+#define ANA_L3_VMID_CFG_VMID\
+	GENMASK(regs->fsize[FW_ANA_L3_VMID_CFG_VMID] + 0 - 1, 0)
+#define ANA_L3_VMID_CFG_VMID_SET(x)\
+	spx5_field_prep(ANA_L3_VMID_CFG_VMID, x)
+#define ANA_L3_VMID_CFG_VMID_GET(x)\
+	spx5_field_get(ANA_L3_VMID_CFG_VMID, x)
+
 /* ANA_L3:VLAN:VLAN_CFG */
 #define ANA_L3_VLAN_CFG(g)                                                     \
 	__REG(TARGET_ANA_L3, 0, 1, 0, g, regs->gcnt[GC_ANA_L3_VLAN], 64, 8, 0, \
@@ -1871,6 +2321,13 @@ extern const struct sparx5_regs *regs;
 #define ANA_L3_VLAN_CFG_VLAN_MIRROR_ENA_GET(x)\
 	FIELD_GET(ANA_L3_VLAN_CFG_VLAN_MIRROR_ENA, x)
 
+/* LAN969X ONLY */
+#define ANA_L3_VLAN_CFG_VLAN_PGID_CPU_DIS        BIT(31)
+#define ANA_L3_VLAN_CFG_VLAN_PGID_CPU_DIS_SET(x)\
+	FIELD_PREP(ANA_L3_VLAN_CFG_VLAN_PGID_CPU_DIS, x)
+#define ANA_L3_VLAN_CFG_VLAN_PGID_CPU_DIS_GET(x)\
+	FIELD_GET(ANA_L3_VLAN_CFG_VLAN_PGID_CPU_DIS, x)
+
 /* ANA_L3:VLAN:VLAN_MASK_CFG */
 #define ANA_L3_VLAN_MASK_CFG(g)                                                \
 	__REG(TARGET_ANA_L3, 0, 1, 0, g, regs->gcnt[GC_ANA_L3_VLAN], 64, 16, 0,\
@@ -1894,6 +2351,154 @@ extern const struct sparx5_regs *regs;
 #define ANA_L3_VLAN_MASK_CFG2_VLAN_PORT_MASK2_GET(x)\
 	FIELD_GET(ANA_L3_VLAN_MASK_CFG2_VLAN_PORT_MASK2, x)
 
+/* ANA_L3:VMID:RLEG_CTRL */
+#define ANA_L3_RLEG_CTRL(g)                                                    \
+	__REG(TARGET_ANA_L3, 0, 1, regs->gaddr[GA_ANA_L3_VMID], g,             \
+	      regs->gcnt[GC_ANA_L3_VMID], 64, 0, 0, 1, 4)
+
+#define ANA_L3_RLEG_CTRL_RLEG_EVID               GENMASK(31, 19)
+#define ANA_L3_RLEG_CTRL_RLEG_EVID_SET(x)\
+	FIELD_PREP(ANA_L3_RLEG_CTRL_RLEG_EVID, x)
+#define ANA_L3_RLEG_CTRL_RLEG_EVID_GET(x)\
+	FIELD_GET(ANA_L3_RLEG_CTRL_RLEG_EVID, x)
+
+#define ANA_L3_RLEG_CTRL_RLEG_IP6_STAT_IP_ONLY_ENA BIT(18)
+#define ANA_L3_RLEG_CTRL_RLEG_IP6_STAT_IP_ONLY_ENA_SET(x)\
+	FIELD_PREP(ANA_L3_RLEG_CTRL_RLEG_IP6_STAT_IP_ONLY_ENA, x)
+#define ANA_L3_RLEG_CTRL_RLEG_IP6_STAT_IP_ONLY_ENA_GET(x)\
+	FIELD_GET(ANA_L3_RLEG_CTRL_RLEG_IP6_STAT_IP_ONLY_ENA, x)
+
+#define ANA_L3_RLEG_CTRL_RLEG_IP4_STAT_IP_ONLY_ENA BIT(17)
+#define ANA_L3_RLEG_CTRL_RLEG_IP4_STAT_IP_ONLY_ENA_SET(x)\
+	FIELD_PREP(ANA_L3_RLEG_CTRL_RLEG_IP4_STAT_IP_ONLY_ENA, x)
+#define ANA_L3_RLEG_CTRL_RLEG_IP4_STAT_IP_ONLY_ENA_GET(x)\
+	FIELD_GET(ANA_L3_RLEG_CTRL_RLEG_IP4_STAT_IP_ONLY_ENA, x)
+
+#define ANA_L3_RLEG_CTRL_RLEG_IP6_SIP_RPF_MODE   GENMASK(15, 14)
+#define ANA_L3_RLEG_CTRL_RLEG_IP6_SIP_RPF_MODE_SET(x)\
+	FIELD_PREP(ANA_L3_RLEG_CTRL_RLEG_IP6_SIP_RPF_MODE, x)
+#define ANA_L3_RLEG_CTRL_RLEG_IP6_SIP_RPF_MODE_GET(x)\
+	FIELD_GET(ANA_L3_RLEG_CTRL_RLEG_IP6_SIP_RPF_MODE, x)
+
+#define ANA_L3_RLEG_CTRL_RLEG_IP4_SIP_RPF_MODE   GENMASK(13, 12)
+#define ANA_L3_RLEG_CTRL_RLEG_IP4_SIP_RPF_MODE_SET(x)\
+	FIELD_PREP(ANA_L3_RLEG_CTRL_RLEG_IP4_SIP_RPF_MODE, x)
+#define ANA_L3_RLEG_CTRL_RLEG_IP4_SIP_RPF_MODE_GET(x)\
+	FIELD_GET(ANA_L3_RLEG_CTRL_RLEG_IP4_SIP_RPF_MODE, x)
+
+#define ANA_L3_RLEG_CTRL_RLEG_IP6_TTL_DECR_DIS   BIT(9)
+#define ANA_L3_RLEG_CTRL_RLEG_IP6_TTL_DECR_DIS_SET(x)\
+	FIELD_PREP(ANA_L3_RLEG_CTRL_RLEG_IP6_TTL_DECR_DIS, x)
+#define ANA_L3_RLEG_CTRL_RLEG_IP6_TTL_DECR_DIS_GET(x)\
+	FIELD_GET(ANA_L3_RLEG_CTRL_RLEG_IP6_TTL_DECR_DIS, x)
+
+#define ANA_L3_RLEG_CTRL_RLEG_IP4_TTL_DECR_DIS   BIT(8)
+#define ANA_L3_RLEG_CTRL_RLEG_IP4_TTL_DECR_DIS_SET(x)\
+	FIELD_PREP(ANA_L3_RLEG_CTRL_RLEG_IP4_TTL_DECR_DIS, x)
+#define ANA_L3_RLEG_CTRL_RLEG_IP4_TTL_DECR_DIS_GET(x)\
+	FIELD_GET(ANA_L3_RLEG_CTRL_RLEG_IP4_TTL_DECR_DIS, x)
+
+#define ANA_L3_RLEG_CTRL_RLEG_IP6_UC_ENA         BIT(7)
+#define ANA_L3_RLEG_CTRL_RLEG_IP6_UC_ENA_SET(x)\
+	FIELD_PREP(ANA_L3_RLEG_CTRL_RLEG_IP6_UC_ENA, x)
+#define ANA_L3_RLEG_CTRL_RLEG_IP6_UC_ENA_GET(x)\
+	FIELD_GET(ANA_L3_RLEG_CTRL_RLEG_IP6_UC_ENA, x)
+
+#define ANA_L3_RLEG_CTRL_RLEG_IP4_UC_ENA         BIT(6)
+#define ANA_L3_RLEG_CTRL_RLEG_IP4_UC_ENA_SET(x)\
+	FIELD_PREP(ANA_L3_RLEG_CTRL_RLEG_IP4_UC_ENA, x)
+#define ANA_L3_RLEG_CTRL_RLEG_IP4_UC_ENA_GET(x)\
+	FIELD_GET(ANA_L3_RLEG_CTRL_RLEG_IP4_UC_ENA, x)
+
+#define ANA_L3_RLEG_CTRL_RLEG_IP6_MC_ENA         BIT(5)
+#define ANA_L3_RLEG_CTRL_RLEG_IP6_MC_ENA_SET(x)\
+	FIELD_PREP(ANA_L3_RLEG_CTRL_RLEG_IP6_MC_ENA, x)
+#define ANA_L3_RLEG_CTRL_RLEG_IP6_MC_ENA_GET(x)\
+	FIELD_GET(ANA_L3_RLEG_CTRL_RLEG_IP6_MC_ENA, x)
+
+#define ANA_L3_RLEG_CTRL_RLEG_IP4_MC_ENA         BIT(4)
+#define ANA_L3_RLEG_CTRL_RLEG_IP4_MC_ENA_SET(x)\
+	FIELD_PREP(ANA_L3_RLEG_CTRL_RLEG_IP4_MC_ENA, x)
+#define ANA_L3_RLEG_CTRL_RLEG_IP4_MC_ENA_GET(x)\
+	FIELD_GET(ANA_L3_RLEG_CTRL_RLEG_IP4_MC_ENA, x)
+
+#define ANA_L3_RLEG_CTRL_RLEG_IP6_ICMP_REDIR_ENA BIT(3)
+#define ANA_L3_RLEG_CTRL_RLEG_IP6_ICMP_REDIR_ENA_SET(x)\
+	FIELD_PREP(ANA_L3_RLEG_CTRL_RLEG_IP6_ICMP_REDIR_ENA, x)
+#define ANA_L3_RLEG_CTRL_RLEG_IP6_ICMP_REDIR_ENA_GET(x)\
+	FIELD_GET(ANA_L3_RLEG_CTRL_RLEG_IP6_ICMP_REDIR_ENA, x)
+
+#define ANA_L3_RLEG_CTRL_RLEG_IP4_ICMP_REDIR_ENA BIT(2)
+#define ANA_L3_RLEG_CTRL_RLEG_IP4_ICMP_REDIR_ENA_SET(x)\
+	FIELD_PREP(ANA_L3_RLEG_CTRL_RLEG_IP4_ICMP_REDIR_ENA, x)
+#define ANA_L3_RLEG_CTRL_RLEG_IP4_ICMP_REDIR_ENA_GET(x)\
+	FIELD_GET(ANA_L3_RLEG_CTRL_RLEG_IP4_ICMP_REDIR_ENA, x)
+
+#define ANA_L3_RLEG_CTRL_RLEG_IP6_VRID_ENA       BIT(1)
+#define ANA_L3_RLEG_CTRL_RLEG_IP6_VRID_ENA_SET(x)\
+	FIELD_PREP(ANA_L3_RLEG_CTRL_RLEG_IP6_VRID_ENA, x)
+#define ANA_L3_RLEG_CTRL_RLEG_IP6_VRID_ENA_GET(x)\
+	FIELD_GET(ANA_L3_RLEG_CTRL_RLEG_IP6_VRID_ENA, x)
+
+#define ANA_L3_RLEG_CTRL_RLEG_IP4_VRID_ENA       BIT(0)
+#define ANA_L3_RLEG_CTRL_RLEG_IP4_VRID_ENA_SET(x)\
+	FIELD_PREP(ANA_L3_RLEG_CTRL_RLEG_IP4_VRID_ENA, x)
+#define ANA_L3_RLEG_CTRL_RLEG_IP4_VRID_ENA_GET(x)\
+	FIELD_GET(ANA_L3_RLEG_CTRL_RLEG_IP4_VRID_ENA, x)
+
+/* ANA_L3:ARP:ARP_CFG_0 */
+#define ANA_L3_ARP_CFG_0(g)                                                    \
+	__REG(TARGET_ANA_L3, 0, 1, regs->gaddr[GA_ANA_L3_ARP], g,              \
+	      regs->gcnt[GC_ANA_L3_ARP], 32, 0, 0, 1, 4)
+
+#define ANA_L3_ARP_CFG_0_MAC_MSB                 GENMASK(31, 16)
+#define ANA_L3_ARP_CFG_0_MAC_MSB_SET(x)\
+	FIELD_PREP(ANA_L3_ARP_CFG_0_MAC_MSB, x)
+#define ANA_L3_ARP_CFG_0_MAC_MSB_GET(x)\
+	FIELD_GET(ANA_L3_ARP_CFG_0_MAC_MSB, x)
+
+#define ANA_L3_ARP_CFG_0_ARP_VMID\
+	GENMASK(regs->fsize[FW_ANA_L3_ARP_CFG_0_ARP_VMID] + 7 - 1, 7)
+#define ANA_L3_ARP_CFG_0_ARP_VMID_SET(x)\
+	spx5_field_prep(ANA_L3_ARP_CFG_0_ARP_VMID, x)
+#define ANA_L3_ARP_CFG_0_ARP_VMID_GET(x)\
+	spx5_field_get(ANA_L3_ARP_CFG_0_ARP_VMID, x)
+
+#define ANA_L3_ARP_CFG_0_ZERO_DMAC_CPU_QU        GENMASK(6, 4)
+#define ANA_L3_ARP_CFG_0_ZERO_DMAC_CPU_QU_SET(x)\
+	FIELD_PREP(ANA_L3_ARP_CFG_0_ZERO_DMAC_CPU_QU, x)
+#define ANA_L3_ARP_CFG_0_ZERO_DMAC_CPU_QU_GET(x)\
+	FIELD_GET(ANA_L3_ARP_CFG_0_ZERO_DMAC_CPU_QU, x)
+
+#define ANA_L3_ARP_CFG_0_SIP_RPF_ENA             BIT(3)
+#define ANA_L3_ARP_CFG_0_SIP_RPF_ENA_SET(x)\
+	FIELD_PREP(ANA_L3_ARP_CFG_0_SIP_RPF_ENA, x)
+#define ANA_L3_ARP_CFG_0_SIP_RPF_ENA_GET(x)\
+	FIELD_GET(ANA_L3_ARP_CFG_0_SIP_RPF_ENA, x)
+
+#define ANA_L3_ARP_CFG_0_SECUR_MATCH_VMID_ENA    BIT(2)
+#define ANA_L3_ARP_CFG_0_SECUR_MATCH_VMID_ENA_SET(x)\
+	FIELD_PREP(ANA_L3_ARP_CFG_0_SECUR_MATCH_VMID_ENA, x)
+#define ANA_L3_ARP_CFG_0_SECUR_MATCH_VMID_ENA_GET(x)\
+	FIELD_GET(ANA_L3_ARP_CFG_0_SECUR_MATCH_VMID_ENA, x)
+
+#define ANA_L3_ARP_CFG_0_SECUR_MATCH_MAC_ENA     BIT(1)
+#define ANA_L3_ARP_CFG_0_SECUR_MATCH_MAC_ENA_SET(x)\
+	FIELD_PREP(ANA_L3_ARP_CFG_0_SECUR_MATCH_MAC_ENA, x)
+#define ANA_L3_ARP_CFG_0_SECUR_MATCH_MAC_ENA_GET(x)\
+	FIELD_GET(ANA_L3_ARP_CFG_0_SECUR_MATCH_MAC_ENA, x)
+
+#define ANA_L3_ARP_CFG_0_ARP_ENA                 BIT(0)
+#define ANA_L3_ARP_CFG_0_ARP_ENA_SET(x)\
+	FIELD_PREP(ANA_L3_ARP_CFG_0_ARP_ENA, x)
+#define ANA_L3_ARP_CFG_0_ARP_ENA_GET(x)\
+	FIELD_GET(ANA_L3_ARP_CFG_0_ARP_ENA, x)
+
+/* ANA_L3:ARP:ARP_CFG_1 */
+#define ANA_L3_ARP_CFG_1(g)                                                    \
+	__REG(TARGET_ANA_L3, 0, 1, regs->gaddr[GA_ANA_L3_ARP], g,              \
+	      regs->gcnt[GC_ANA_L3_ARP], 32, 4, 0, 1, 4)
+
 /* ASM:DEV_STATISTICS:RX_IN_BYTES_CNT */
 #define ASM_RX_IN_BYTES_CNT(g)                                                 \
 	__REG(TARGET_ASM, 0, 1, 0, g, regs->gcnt[GC_ASM_DEV_STATISTICS], 512,  \
@@ -4515,6 +5120,34 @@ extern const struct sparx5_regs *regs;
 #define DSM_TAXI_CAL_CFG_CAL_PGM_SEL_GET(x)\
 	FIELD_GET(DSM_TAXI_CAL_CFG_CAL_PGM_SEL, x)
 
+/* EACL:COMMON:RLEG_CFG_0 */
+#define EACL_RLEG_CFG_0                                                        \
+	__REG(TARGET_EACL, 0, 1, regs->gaddr[GA_EACL_COMMON], 0, 1, 216, 64, 0,\
+	      1, 4)
+
+#define EACL_RLEG_CFG_0_RLEG_MAC_LSB             GENMASK(23, 0)
+#define EACL_RLEG_CFG_0_RLEG_MAC_LSB_SET(x)\
+	FIELD_PREP(EACL_RLEG_CFG_0_RLEG_MAC_LSB, x)
+#define EACL_RLEG_CFG_0_RLEG_MAC_LSB_GET(x)\
+	FIELD_GET(EACL_RLEG_CFG_0_RLEG_MAC_LSB, x)
+
+/* EACL:COMMON:RLEG_CFG_1 */
+#define EACL_RLEG_CFG_1                                                        \
+	__REG(TARGET_EACL, 0, 1, regs->gaddr[GA_EACL_COMMON], 0, 1, 216, 68, 0,\
+	      1, 4)
+
+#define EACL_RLEG_CFG_1_RLEG_MAC_TYPE_SEL        GENMASK(25, 24)
+#define EACL_RLEG_CFG_1_RLEG_MAC_TYPE_SEL_SET(x)\
+	FIELD_PREP(EACL_RLEG_CFG_1_RLEG_MAC_TYPE_SEL, x)
+#define EACL_RLEG_CFG_1_RLEG_MAC_TYPE_SEL_GET(x)\
+	FIELD_GET(EACL_RLEG_CFG_1_RLEG_MAC_TYPE_SEL, x)
+
+#define EACL_RLEG_CFG_1_RLEG_MAC_MSB             GENMASK(23, 0)
+#define EACL_RLEG_CFG_1_RLEG_MAC_MSB_SET(x)\
+	FIELD_PREP(EACL_RLEG_CFG_1_RLEG_MAC_MSB, x)
+#define EACL_RLEG_CFG_1_RLEG_MAC_MSB_GET(x)\
+	FIELD_GET(EACL_RLEG_CFG_1_RLEG_MAC_MSB, x)
+
 /* EACL:ES2_KEY_SELECT_PROFILE:VCAP_ES2_KEY_SEL */
 #define EACL_VCAP_ES2_KEY_SEL(g, r)                                            \
 	__REG(TARGET_EACL, 0, 1, regs->gaddr[GA_EACL_ES2_KEY_SELECT_PROFILE],  \
@@ -7341,6 +7974,34 @@ extern const struct sparx5_regs *regs;
 #define REW_ES0_CTRL_ES0_LU_ENA_GET(x)\
 	FIELD_GET(REW_ES0_CTRL_ES0_LU_ENA, x)
 
+/* REW:COMMON:RLEG_CFG_0 */
+#define REW_RLEG_CFG_0                                                         \
+	__REG(TARGET_REW, 0, 1, regs->gaddr[GA_REW_COMMON], 0, 1, 1232, 1156,  \
+	      0, 1, 4)
+
+#define REW_RLEG_CFG_0_RLEG_MAC_LSB              GENMASK(23, 0)
+#define REW_RLEG_CFG_0_RLEG_MAC_LSB_SET(x)\
+	FIELD_PREP(REW_RLEG_CFG_0_RLEG_MAC_LSB, x)
+#define REW_RLEG_CFG_0_RLEG_MAC_LSB_GET(x)\
+	FIELD_GET(REW_RLEG_CFG_0_RLEG_MAC_LSB, x)
+
+/* REW:COMMON:RLEG_CFG_1 */
+#define REW_RLEG_CFG_1                                                         \
+	__REG(TARGET_REW, 0, 1, regs->gaddr[GA_REW_COMMON], 0, 1, 1232, 1160,  \
+	      0, 1, 4)
+
+#define REW_RLEG_CFG_1_RLEG_MAC_TYPE_SEL         GENMASK(25, 24)
+#define REW_RLEG_CFG_1_RLEG_MAC_TYPE_SEL_SET(x)\
+	FIELD_PREP(REW_RLEG_CFG_1_RLEG_MAC_TYPE_SEL, x)
+#define REW_RLEG_CFG_1_RLEG_MAC_TYPE_SEL_GET(x)\
+	FIELD_GET(REW_RLEG_CFG_1_RLEG_MAC_TYPE_SEL, x)
+
+#define REW_RLEG_CFG_1_RLEG_MAC_MSB              GENMASK(23, 0)
+#define REW_RLEG_CFG_1_RLEG_MAC_MSB_SET(x)\
+	FIELD_PREP(REW_RLEG_CFG_1_RLEG_MAC_MSB, x)
+#define REW_RLEG_CFG_1_RLEG_MAC_MSB_GET(x)\
+	FIELD_GET(REW_RLEG_CFG_1_RLEG_MAC_MSB, x)
+
 /* REW:PORT:PORT_VLAN_CFG */
 #define REW_PORT_VLAN_CFG(g)                                                   \
 	__REG(TARGET_REW, 0, 1, regs->gaddr[GA_REW_PORT], g,                   \
@@ -7567,6 +8228,30 @@ extern const struct sparx5_regs *regs;
 #define REW_PTP_GEN_STAMP_FMT_RT_FMT_GET(x)\
 	FIELD_GET(REW_PTP_GEN_STAMP_FMT_RT_FMT, x)
 
+/* REW:VMID:RLEG_CTRL */
+#define REW_RLEG_CTRL(g)                                                       \
+	__REG(TARGET_REW, 0, 1, regs->gaddr[GA_REW_VMID], g,                   \
+	      regs->gcnt[GC_REW_VMID], 4, 0, 0, 1, 4)
+
+#define REW_RLEG_CTRL_DECAP_IRLEG\
+	GENMASK(regs->fsize[FW_REW_RLEG_CTRL_DECAP_IRLEG] + 13 - 1, 13)
+#define REW_RLEG_CTRL_DECAP_IRLEG_SET(x)\
+	spx5_field_prep(REW_RLEG_CTRL_DECAP_IRLEG, x)
+#define REW_RLEG_CTRL_DECAP_IRLEG_GET(x)\
+	spx5_field_get(REW_RLEG_CTRL_DECAP_IRLEG, x)
+
+#define REW_RLEG_CTRL_RLEG_VSTAX2_WAS_TAGGED     BIT(12)
+#define REW_RLEG_CTRL_RLEG_VSTAX2_WAS_TAGGED_SET(x)\
+	FIELD_PREP(REW_RLEG_CTRL_RLEG_VSTAX2_WAS_TAGGED, x)
+#define REW_RLEG_CTRL_RLEG_VSTAX2_WAS_TAGGED_GET(x)\
+	FIELD_GET(REW_RLEG_CTRL_RLEG_VSTAX2_WAS_TAGGED, x)
+
+#define REW_RLEG_CTRL_RLEG_EVID                  GENMASK(11, 0)
+#define REW_RLEG_CTRL_RLEG_EVID_SET(x)\
+	FIELD_PREP(REW_RLEG_CTRL_RLEG_EVID, x)
+#define REW_RLEG_CTRL_RLEG_EVID_GET(x)\
+	FIELD_GET(REW_RLEG_CTRL_RLEG_EVID, x)
+
 /* REW:RAM_CTRL:RAM_INIT */
 #define REW_RAM_INIT                                                           \
 	__REG(TARGET_REW, 0, 1, regs->gaddr[GA_REW_RAM_CTRL], 0, 1, 4, 0, 0, 1,\
diff --git a/drivers/net/ethernet/microchip/sparx5/sparx5_regs.c b/drivers/net/ethernet/microchip/sparx5/sparx5_regs.c
index 220e81b714d4..5fc11403ddf6 100644
--- a/drivers/net/ethernet/microchip/sparx5/sparx5_regs.c
+++ b/drivers/net/ethernet/microchip/sparx5/sparx5_regs.c
@@ -1,11 +1,11 @@
 // SPDX-License-Identifier: GPL-2.0+
 /* Microchip Sparx5 Switch driver
  *
- * Copyright (c) 2024 Microchip Technology Inc.
+ * Copyright (c) 2026 Microchip Technology Inc.
  */
 
-/* This file is autogenerated by cml-utils 2024-09-30 11:48:29 +0200.
- * Commit ID: 9d07b8d19363f3cd3590ddb3f7a2e2768e16524b
+/* This file is autogenerated by cml-utils 2026-02-16 21:50:29 +0100.
+ * Commit ID: 09d52195beeeb9682063ef0bd6eec50eebc137e2
  */
 
 #include "sparx5_regs.h"
@@ -77,12 +77,16 @@ const unsigned int sparx5_gaddr[GADDR_LAST] = {
 	[GA_ANA_CL_COMMON] = 166912,
 	[GA_ANA_L2_COMMON] = 566024,
 	[GA_ANA_L3_COMMON] = 493632,
+	[GA_ANA_L3_VMID] = 458752,
+	[GA_ANA_L3_ARP_PTR_REMAP] = 493824,
+	[GA_ANA_L3_ARP] = 327680,
 	[GA_ANA_L3_VLAN_ARP_L3MC_STICKY] = 491460,
 	[GA_ASM_CFG] = 33280,
 	[GA_ASM_PFC_TIMER_CFG] = 34716,
 	[GA_ASM_LBK_WM_CFG] = 34744,
 	[GA_ASM_LBK_MISC_CFG] = 34756,
 	[GA_ASM_RAM_CTRL] = 34832,
+	[GA_EACL_COMMON] = 118480,
 	[GA_EACL_ES2_KEY_SELECT_PROFILE] = 149504,
 	[GA_EACL_CNT_TBL] = 122880,
 	[GA_EACL_POL_CFG] = 150608,
@@ -102,6 +106,7 @@ const unsigned int sparx5_gaddr[GADDR_LAST] = {
 	[GA_QSYS_RAM_CTRL] = 2344,
 	[GA_REW_COMMON] = 387264,
 	[GA_REW_PORT] = 360448,
+	[GA_REW_VMID] = 389120,
 	[GA_REW_VOE_PORT_LM_CNT] = 393216,
 	[GA_REW_RAM_CTRL] = 378696,
 	[GA_VOP_RAM_CTRL] = 279176,
@@ -123,6 +128,8 @@ const unsigned int sparx5_gcnt[GCNT_LAST] = {
 	[GC_ANA_L2_ISDX_LIMIT] = 1536,
 	[GC_ANA_L2_ISDX] = 4096,
 	[GC_ANA_L3_VLAN] = 5120,
+	[GC_ANA_L3_VMID] = 511,
+	[GC_ANA_L3_ARP] = 2048,
 	[GC_ASM_DEV_STATISTICS] = 65,
 	[GC_EACL_ES2_KEY_SELECT_PROFILE] = 138,
 	[GC_EACL_CNT_TBL] = 2048,
@@ -132,6 +139,7 @@ const unsigned int sparx5_gcnt[GCNT_LAST] = {
 	[GC_PTP_PTP_PINS] = 5,
 	[GC_PTP_PHASE_DETECTOR_CTRL] = 5,
 	[GC_REW_PORT] = 70,
+	[GC_REW_VMID] = 511,
 	[GC_REW_VOE_PORT_LM_CNT] = 520,
 };
 
@@ -188,7 +196,12 @@ const unsigned int sparx5_fsize[FSIZE_LAST] = {
 	[FW_ANA_L2_AUTO_LRN_CFG_AUTO_LRN_ENA] = 32,
 	[FW_ANA_L2_DLB_CFG_DLB_IDX] = 13,
 	[FW_ANA_L2_TSN_CFG_TSN_SFID] = 10,
+	[FW_ANA_L3_L3_UC_ENA_L3_UC_ENA] = 32,
+	[FW_ANA_L3_SIP_SECURE_ENA1_SIP_CMP_ENA1] = 32,
+	[FW_ANA_L3_DIP_SECURE_ENA_DIP_CMP_ENA] = 32,
+	[FW_ANA_L3_VMID_CFG_VMID] = 9,
 	[FW_ANA_L3_VLAN_MASK_CFG_VLAN_PORT_MASK] = 32,
+	[FW_ANA_L3_ARP_CFG_0_ARP_VMID] = 9,
 	[FW_FDMA_CH_CFG_CH_DCB_DB_CNT] = 4,
 	[FW_GCB_HW_SGPIO_TO_SD_MAP_CFG_SGPIO_TO_SD_SEL] = 9,
 	[FW_HSCH_SE_CFG_SE_DWRR_CNT] = 7,
@@ -214,6 +227,7 @@ const unsigned int sparx5_fsize[FSIZE_LAST] = {
 	[FW_QSYS_ATOP_ATOP] = 12,
 	[FW_QSYS_ATOP_TOT_CFG_ATOP_TOT] = 12,
 	[FW_REW_RTAG_ETAG_CTRL_IPE_TBL] = 7,
+	[FW_REW_RLEG_CTRL_DECAP_IRLEG] = 9,
 	[FW_XQS_STAT_CFG_STAT_VIEW] = 13,
 	[FW_XQS_QLIMIT_SHR_TOP_CFG_QLIMIT_SHR_TOP] = 15,
 	[FW_XQS_QLIMIT_SHR_ATOP_CFG_QLIMIT_SHR_ATOP] = 15,
diff --git a/drivers/net/ethernet/microchip/sparx5/sparx5_regs.h b/drivers/net/ethernet/microchip/sparx5/sparx5_regs.h
index ea28130c2341..28dca1ed4ea7 100644
--- a/drivers/net/ethernet/microchip/sparx5/sparx5_regs.h
+++ b/drivers/net/ethernet/microchip/sparx5/sparx5_regs.h
@@ -1,11 +1,11 @@
 /* SPDX-License-Identifier: GPL-2.0+ */
 /* Microchip Sparx5 Switch driver
  *
- * Copyright (c) 2024 Microchip Technology Inc.
+ * Copyright (c) 2026 Microchip Technology Inc.
  */
 
-/* This file is autogenerated by cml-utils 2024-09-30 11:48:29 +0200.
- * Commit ID: 9d07b8d19363f3cd3590ddb3f7a2e2768e16524b
+/* This file is autogenerated by cml-utils 2026-02-16 21:50:30 +0100.
+ * Commit ID: 09d52195beeeb9682063ef0bd6eec50eebc137e2
  */
 
 #ifndef _SPARX5_REGS_H_
@@ -86,12 +86,16 @@ enum sparx5_gaddr_enum {
 	GA_ANA_CL_COMMON,
 	GA_ANA_L2_COMMON,
 	GA_ANA_L3_COMMON,
+	GA_ANA_L3_VMID,
+	GA_ANA_L3_ARP_PTR_REMAP,
+	GA_ANA_L3_ARP,
 	GA_ANA_L3_VLAN_ARP_L3MC_STICKY,
 	GA_ASM_CFG,
 	GA_ASM_PFC_TIMER_CFG,
 	GA_ASM_LBK_WM_CFG,
 	GA_ASM_LBK_MISC_CFG,
 	GA_ASM_RAM_CTRL,
+	GA_EACL_COMMON,
 	GA_EACL_ES2_KEY_SELECT_PROFILE,
 	GA_EACL_CNT_TBL,
 	GA_EACL_POL_CFG,
@@ -111,6 +115,7 @@ enum sparx5_gaddr_enum {
 	GA_QSYS_RAM_CTRL,
 	GA_REW_COMMON,
 	GA_REW_PORT,
+	GA_REW_VMID,
 	GA_REW_VOE_PORT_LM_CNT,
 	GA_REW_RAM_CTRL,
 	GA_VOP_RAM_CTRL,
@@ -133,6 +138,8 @@ enum sparx5_gcnt_enum {
 	GC_ANA_L2_ISDX_LIMIT,
 	GC_ANA_L2_ISDX,
 	GC_ANA_L3_VLAN,
+	GC_ANA_L3_VMID,
+	GC_ANA_L3_ARP,
 	GC_ASM_DEV_STATISTICS,
 	GC_EACL_ES2_KEY_SELECT_PROFILE,
 	GC_EACL_CNT_TBL,
@@ -142,6 +149,7 @@ enum sparx5_gcnt_enum {
 	GC_PTP_PTP_PINS,
 	GC_PTP_PHASE_DETECTOR_CTRL,
 	GC_REW_PORT,
+	GC_REW_VMID,
 	GC_REW_VOE_PORT_LM_CNT,
 	GCNT_LAST,
 };
@@ -201,7 +209,12 @@ enum sparx5_fsize_enum {
 	FW_ANA_L2_AUTO_LRN_CFG_AUTO_LRN_ENA,
 	FW_ANA_L2_DLB_CFG_DLB_IDX,
 	FW_ANA_L2_TSN_CFG_TSN_SFID,
+	FW_ANA_L3_L3_UC_ENA_L3_UC_ENA,
+	FW_ANA_L3_SIP_SECURE_ENA1_SIP_CMP_ENA1,
+	FW_ANA_L3_DIP_SECURE_ENA_DIP_CMP_ENA,
+	FW_ANA_L3_VMID_CFG_VMID,
 	FW_ANA_L3_VLAN_MASK_CFG_VLAN_PORT_MASK,
+	FW_ANA_L3_ARP_CFG_0_ARP_VMID,
 	FW_FDMA_CH_CFG_CH_DCB_DB_CNT,
 	FW_GCB_HW_SGPIO_TO_SD_MAP_CFG_SGPIO_TO_SD_SEL,
 	FW_HSCH_SE_CFG_SE_DWRR_CNT,
@@ -227,6 +240,7 @@ enum sparx5_fsize_enum {
 	FW_QSYS_ATOP_ATOP,
 	FW_QSYS_ATOP_TOT_CFG_ATOP_TOT,
 	FW_REW_RTAG_ETAG_CTRL_IPE_TBL,
+	FW_REW_RLEG_CTRL_DECAP_IRLEG,
 	FW_XQS_STAT_CFG_STAT_VIEW,
 	FW_XQS_QLIMIT_SHR_TOP_CFG_QLIMIT_SHR_TOP,
 	FW_XQS_QLIMIT_SHR_ATOP_CFG_QLIMIT_SHR_ATOP,

-- 
2.52.0



^ permalink raw reply related

* [PATCH net-next 4/9] net: microchip: vcap: expose helpers in vcap api and update debugfs
From: Jens Emil Schulz Østergaard @ 2026-06-12 12:37 UTC (permalink / raw)
  To: Horatiu Vultur, UNGLinuxDriver, Andrew Lunn, David S. Miller,
	Eric Dumazet, Jakub Kicinski, Paolo Abeni, Daniel Machon,
	Steen Hegelund, Kees Cook, Gustavo A. R. Silva
  Cc: netdev, linux-kernel, linux-arm-kernel, linux-hardening,
	Jens Emil Schulz Østergaard
In-Reply-To: <20260612-sparx5_l3_routing-v1-0-fc3c10160f49@microchip.com>

Add new helpers to the vcap client api, in preparation for L3 routing
functionality:

 - vcap_val_add_rule(): wraps vcap_val_rule() + vcap_add_rule().
 - vcap_rule_mod_action_bit(): modify a bit-typed action on an existing
   rule.

Rename VCAP_CID_PREROUTING to VCAP_CID_PREROUTING_L0 and add
VCAP_USER_L3, both needed by the upcoming LPM VCAP user.

Extend the debugfs display to handle the new IP4_XIP and IP6_XIP key
fields.

Fix a latent undefined-behaviour bug in the debugfs action-field
printer. The old mask expression (1 << width) - 1 is UB when width is
32. The bug is unreachable before this series, since no existing field
in any client hits this, but the LPM VCAP introduces VCAP_AF_MAC_LSB
which is 32 bit wide.

Reviewed-by: Daniel Machon <daniel.machon@microchip.com>
Reviewed-by: Steen Hegelund <Steen.Hegelund@microchip.com>
Signed-off-by: Jens Emil Schulz Østergaard <jensemil.schulzostergaard@microchip.com>
---
 drivers/net/ethernet/microchip/vcap/vcap_api.c     | 25 ++++++++++++++++++++++
 drivers/net/ethernet/microchip/vcap/vcap_api.h     |  4 +++-
 .../net/ethernet/microchip/vcap/vcap_api_client.h  |  6 ++++++
 .../net/ethernet/microchip/vcap/vcap_api_debugfs.c | 13 ++++++++---
 4 files changed, 44 insertions(+), 4 deletions(-)

diff --git a/drivers/net/ethernet/microchip/vcap/vcap_api.c b/drivers/net/ethernet/microchip/vcap/vcap_api.c
index 30700648672f..0905e4f192a0 100644
--- a/drivers/net/ethernet/microchip/vcap/vcap_api.c
+++ b/drivers/net/ethernet/microchip/vcap/vcap_api.c
@@ -2379,6 +2379,19 @@ int vcap_add_rule(struct vcap_rule *rule)
 }
 EXPORT_SYMBOL_GPL(vcap_add_rule);
 
+/* Validate and add rule to a VCAP instance */
+int vcap_val_add_rule(struct vcap_rule *rule, u16 l3_proto)
+{
+	int err;
+
+	err = vcap_val_rule(rule, l3_proto);
+	if (err)
+		return err;
+
+	return vcap_add_rule(rule);
+}
+EXPORT_SYMBOL_GPL(vcap_val_add_rule);
+
 /* Allocate a new rule with the provided arguments */
 struct vcap_rule *vcap_alloc_rule(struct vcap_control *vctrl,
 				  struct net_device *ndev, int vcap_chain_id,
@@ -3547,6 +3560,18 @@ int vcap_rule_mod_action_u32(struct vcap_rule *rule,
 }
 EXPORT_SYMBOL_GPL(vcap_rule_mod_action_u32);
 
+/* Modify a bit action with value in the rule */
+int vcap_rule_mod_action_bit(struct vcap_rule *rule,
+			     enum vcap_action_field action,
+			     enum vcap_bit val)
+{
+	struct vcap_client_actionfield_data data;
+
+	vcap_rule_set_action_bitsize(&data.u1, val);
+	return vcap_rule_mod_action(rule, action, VCAP_FIELD_BIT, &data);
+}
+EXPORT_SYMBOL_GPL(vcap_rule_mod_action_bit);
+
 /* Drop keys in a keylist and any keys that are not supported by the keyset */
 int vcap_filter_rule_keys(struct vcap_rule *rule,
 			  enum vcap_key_field keylist[], int length,
diff --git a/drivers/net/ethernet/microchip/vcap/vcap_api.h b/drivers/net/ethernet/microchip/vcap/vcap_api.h
index 6069ad95c27e..e197e7257560 100644
--- a/drivers/net/ethernet/microchip/vcap/vcap_api.h
+++ b/drivers/net/ethernet/microchip/vcap/vcap_api.h
@@ -22,7 +22,7 @@
 #define VCAP_CID_INGRESS_L5          1500000 /* Ingress Stage 1 Lookup 5 */
 
 #define VCAP_CID_PREROUTING_IPV6     3000000 /* Prerouting Stage */
-#define VCAP_CID_PREROUTING          6000000 /* Prerouting Stage */
+#define VCAP_CID_PREROUTING_L0       6000000 /* Prerouting Stage Lookup 0 */
 
 #define VCAP_CID_INGRESS_STAGE2_L0   8000000 /* Ingress Stage 2 Lookup 0 */
 #define VCAP_CID_INGRESS_STAGE2_L1   8100000 /* Ingress Stage 2 Lookup 1 */
@@ -41,7 +41,9 @@ enum vcap_user {
 	VCAP_USER_MRP,
 	VCAP_USER_CFM,
 	VCAP_USER_VLAN,
+	VCAP_USER_L3,
 	VCAP_USER_QOS,
+	/* permanent enabled users above here */
 	VCAP_USER_VCAP_UTIL,
 	VCAP_USER_TC,
 	VCAP_USER_TC_EXTRA,
diff --git a/drivers/net/ethernet/microchip/vcap/vcap_api_client.h b/drivers/net/ethernet/microchip/vcap/vcap_api_client.h
index cdf79e17ca54..3f17e1e76b7d 100644
--- a/drivers/net/ethernet/microchip/vcap/vcap_api_client.h
+++ b/drivers/net/ethernet/microchip/vcap/vcap_api_client.h
@@ -167,6 +167,8 @@ void vcap_free_rule(struct vcap_rule *rule);
 int vcap_val_rule(struct vcap_rule *rule, u16 l3_proto);
 /* Add rule to a VCAP instance */
 int vcap_add_rule(struct vcap_rule *rule);
+/* Validate and add rule to a VCAP instance */
+int vcap_val_add_rule(struct vcap_rule *rule, u16 l3_proto);
 /* Delete rule in a VCAP instance */
 int vcap_del_rule(struct vcap_control *vctrl, struct net_device *ndev, u32 id);
 /* Make a full copy of an existing rule with a new rule id */
@@ -266,6 +268,10 @@ int vcap_rule_mod_key_u32(struct vcap_rule *rule, enum vcap_key_field key,
 int vcap_rule_mod_action_u32(struct vcap_rule *rule,
 			     enum vcap_action_field action,
 			     u32 value);
+/* Modify a bit action with value in the rule */
+int vcap_rule_mod_action_bit(struct vcap_rule *rule,
+			     enum vcap_action_field action,
+			     enum vcap_bit val);
 
 /* Get a 32 bit key field value and mask from the rule */
 int vcap_rule_get_key_u32(struct vcap_rule *rule, enum vcap_key_field key,
diff --git a/drivers/net/ethernet/microchip/vcap/vcap_api_debugfs.c b/drivers/net/ethernet/microchip/vcap/vcap_api_debugfs.c
index 59bfbda29bb3..56464ece8e6b 100644
--- a/drivers/net/ethernet/microchip/vcap/vcap_api_debugfs.c
+++ b/drivers/net/ethernet/microchip/vcap/vcap_api_debugfs.c
@@ -40,7 +40,8 @@ static void vcap_debugfs_show_rule_keyfield(struct vcap_control *vctrl,
 		value = (u8 *)(&data->u32.value);
 		mask = (u8 *)(&data->u32.mask);
 
-		if (key == VCAP_KF_L3_IP4_SIP || key == VCAP_KF_L3_IP4_DIP) {
+		if (key == VCAP_KF_L3_IP4_SIP || key == VCAP_KF_L3_IP4_DIP ||
+		    key == VCAP_KF_IP4_XIP) {
 			out->prf(out->dst, "%pI4h/%pI4h", &data->u32.value,
 				 &data->u32.mask);
 		} else if (key == VCAP_KF_ETYPE ||
@@ -88,7 +89,8 @@ static void vcap_debugfs_show_rule_keyfield(struct vcap_control *vctrl,
 	case VCAP_FIELD_U128:
 		value = data->u128.value;
 		mask = data->u128.mask;
-		if (key == VCAP_KF_L3_IP6_SIP || key == VCAP_KF_L3_IP6_DIP) {
+		if (key == VCAP_KF_L3_IP6_SIP || key == VCAP_KF_L3_IP6_DIP ||
+		    key == VCAP_KF_IP6_XIP) {
 			u8 nvalue[16], nmask[16];
 
 			vcap_netbytes_copy(nvalue, data->u128.value,
@@ -133,7 +135,12 @@ vcap_debugfs_show_rule_actionfield(struct vcap_control *vctrl,
 		out->prf(out->dst, "%d", value[0]);
 		break;
 	case VCAP_FIELD_U32:
-		fmsk = (1 << actionfield[action].width) - 1;
+		if (action == VCAP_AF_MAC_LSB || action == VCAP_AF_MAC_MSB) {
+			hex = true;
+			break;
+		}
+		fmsk = actionfield[action].width ?
+		       GENMASK(actionfield[action].width - 1, 0) : 0;
 		val = *(u32 *)value;
 		out->prf(out->dst, "%u", val & fmsk);
 		break;

-- 
2.52.0



^ permalink raw reply related

* [PATCH net-next 3/9] net: microchip: vcap: make vcap actionset decoding type_id aware
From: Jens Emil Schulz Østergaard @ 2026-06-12 12:37 UTC (permalink / raw)
  To: Horatiu Vultur, UNGLinuxDriver, Andrew Lunn, David S. Miller,
	Eric Dumazet, Jakub Kicinski, Paolo Abeni, Daniel Machon,
	Steen Hegelund, Kees Cook, Gustavo A. R. Silva
  Cc: netdev, linux-kernel, linux-arm-kernel, linux-hardening,
	Jens Emil Schulz Østergaard
In-Reply-To: <20260612-sparx5_l3_routing-v1-0-fc3c10160f49@microchip.com>

When reading a rule back from hardware, decoding has to identify which
actionset the rule was written as. The existing logic was only aware of
the actionset subword length, which cannot distinguish actionsets that
share a subword length but differ in their type_id field. The LPM VCAP
added in a following patch introduces this case: ARP_PTR, L3MC_PTR and
ARP_ENTRY all occupy one subword and differ only by type_id.

A helper is introduced to extract the type_id bits directly from
stream[0]. This is valid by construction: the VCAP model places the
type_id field (when present) immediately after the typegroup bits in
the first subword.

Reviewed-by: Daniel Machon <daniel.machon@microchip.com>
Reviewed-by: Steen Hegelund <Steen.Hegelund@microchip.com>
Signed-off-by: Jens Emil Schulz Østergaard <jensemil.schulzostergaard@microchip.com>
---
 drivers/net/ethernet/microchip/vcap/vcap_api.c | 16 ++++++++++++++--
 1 file changed, 14 insertions(+), 2 deletions(-)

diff --git a/drivers/net/ethernet/microchip/vcap/vcap_api.c b/drivers/net/ethernet/microchip/vcap/vcap_api.c
index 6946fd738458..30700648672f 100644
--- a/drivers/net/ethernet/microchip/vcap/vcap_api.c
+++ b/drivers/net/ethernet/microchip/vcap/vcap_api.c
@@ -216,6 +216,13 @@ static void vcap_decode_field(u32 *stream, struct vcap_stream_iter *itr,
 	}
 }
 
+/* The type_id field is always right after the typegroup bits, if it exists */
+static u8 vcap_find_stream_type_id(u32 *stream, u16 tg_width,
+				   u16 typefld_width)
+{
+	return (stream[0] >> tg_width) & GENMASK(typefld_width - 1, 0);
+}
+
 /* Verify that the type id in the stream matches the type id of the keyset */
 static bool vcap_verify_keystream_keyset(struct vcap_control *vctrl,
 					 enum vcap_type vt,
@@ -1331,8 +1338,10 @@ vcap_verify_actionstream_actionset(struct vcap_control *vctrl,
 				   enum vcap_actionfield_set actionset)
 {
 	const struct vcap_typegroup *tgt;
+	const struct vcap_field *typefld;
 	const struct vcap_field *fields;
 	const struct vcap_set *info;
+	u8 value = 0;
 
 	if (vcap_actionfield_count(vctrl, vt, actionset) == 0)
 		return false;
@@ -1355,8 +1364,11 @@ vcap_verify_actionstream_actionset(struct vcap_control *vctrl,
 	if (!fields)
 		return false;
 
-	/* Later this will be expanded with a check of the type id */
-	return true;
+	typefld = &fields[VCAP_AF_TYPE];
+	value = vcap_find_stream_type_id(actionstream,
+					 tgt->width, typefld->width);
+
+	return value == info->type_id;
 }
 
 /* Find the subword width of the action typegroup that matches the stream data

-- 
2.52.0



^ permalink raw reply related

* [PATCH net-next 2/9] net: microchip: vcap: add lpm vcap to autogen vcap api
From: Jens Emil Schulz Østergaard @ 2026-06-12 12:37 UTC (permalink / raw)
  To: Horatiu Vultur, UNGLinuxDriver, Andrew Lunn, David S. Miller,
	Eric Dumazet, Jakub Kicinski, Paolo Abeni, Daniel Machon,
	Steen Hegelund, Kees Cook, Gustavo A. R. Silva
  Cc: netdev, linux-kernel, linux-arm-kernel, linux-hardening,
	Jens Emil Schulz Østergaard
In-Reply-To: <20260612-sparx5_l3_routing-v1-0-fc3c10160f49@microchip.com>

Add autogenerated VCAP rule encoding metadata, and VCAP parameters for
the LPM VCAP used for L3 routing.

Reviewed-by: Daniel Machon <daniel.machon@microchip.com>
Reviewed-by: Steen Hegelund <Steen.Hegelund@microchip.com>
Signed-off-by: Jens Emil Schulz Østergaard <jensemil.schulzostergaard@microchip.com>
---
 .../microchip/sparx5/lan969x/lan969x_vcap_ag_api.c | 373 ++++++++++++++++++++-
 .../ethernet/microchip/sparx5/sparx5_vcap_ag_api.c | 373 ++++++++++++++++++++-
 .../ethernet/microchip/sparx5/sparx5_vcap_ag_api.h |   6 +-
 drivers/net/ethernet/microchip/vcap/vcap_ag_api.h  |  94 +++++-
 .../net/ethernet/microchip/vcap/vcap_model_kunit.c |   6 +-
 5 files changed, 832 insertions(+), 20 deletions(-)

diff --git a/drivers/net/ethernet/microchip/sparx5/lan969x/lan969x_vcap_ag_api.c b/drivers/net/ethernet/microchip/sparx5/lan969x/lan969x_vcap_ag_api.c
index 7acc5bcf337a..e623b1dbe9cb 100644
--- a/drivers/net/ethernet/microchip/sparx5/lan969x/lan969x_vcap_ag_api.c
+++ b/drivers/net/ethernet/microchip/sparx5/lan969x/lan969x_vcap_ag_api.c
@@ -1,10 +1,10 @@
 // SPDX-License-Identifier: BSD-3-Clause
-/* Copyright (C) 2024 Microchip Technology Inc. and its subsidiaries.
+/* Copyright (C) 2026 Microchip Technology Inc. and its subsidiaries.
  * Microchip VCAP API
  */
 
-/* This file is autogenerated by cml-utils 2024-10-07 11:10:56 +0200.
- * Commit ID: b5ddc8e244eb2481a9524f1ddc630a8b41e7c391
+/* This file is autogenerated by cml-utils 2026-04-28 13:55:50 +0200.
+ * Commit ID: cd2494df16a1d3b14f650f720f6a8126b88b553a
  */
 
 #include <linux/types.h>
@@ -1380,6 +1380,83 @@ static const struct vcap_field es0_isdx_keyfield[] = {
 	},
 };
 
+static const struct vcap_field lpm_sgl_ip4_keyfield[] = {
+	[VCAP_KF_DST_FLAG] = {
+		.type = VCAP_FIELD_BIT,
+		.offset = 0,
+		.width = 1,
+	},
+	[VCAP_KF_AFFIX] = {
+		.type = VCAP_FIELD_U32,
+		.offset = 1,
+		.width = 10,
+	},
+	[VCAP_KF_IP4_XIP] = {
+		.type = VCAP_FIELD_U32,
+		.offset = 11,
+		.width = 32,
+	},
+};
+
+static const struct vcap_field lpm_dbl_ip4_keyfield[] = {
+	[VCAP_KF_TYPE] = {
+		.type = VCAP_FIELD_BIT,
+		.offset = 0,
+		.width = 1,
+	},
+	[VCAP_KF_AFFIX] = {
+		.type = VCAP_FIELD_U32,
+		.offset = 1,
+		.width = 10,
+	},
+	[VCAP_KF_IP4_SIP] = {
+		.type = VCAP_FIELD_U32,
+		.offset = 11,
+		.width = 32,
+	},
+	[VCAP_KF_IP4_DIP] = {
+		.type = VCAP_FIELD_U32,
+		.offset = 43,
+		.width = 32,
+	},
+};
+
+static const struct vcap_field lpm_sgl_ip6_keyfield[] = {
+	[VCAP_KF_DST_FLAG] = {
+		.type = VCAP_FIELD_BIT,
+		.offset = 0,
+		.width = 1,
+	},
+	[VCAP_KF_AFFIX] = {
+		.type = VCAP_FIELD_U32,
+		.offset = 1,
+		.width = 10,
+	},
+	[VCAP_KF_IP6_XIP] = {
+		.type = VCAP_FIELD_U128,
+		.offset = 11,
+		.width = 128,
+	},
+};
+
+static const struct vcap_field lpm_dbl_ip6_keyfield[] = {
+	[VCAP_KF_AFFIX] = {
+		.type = VCAP_FIELD_U32,
+		.offset = 0,
+		.width = 10,
+	},
+	[VCAP_KF_IP6_SIP] = {
+		.type = VCAP_FIELD_U128,
+		.offset = 10,
+		.width = 128,
+	},
+	[VCAP_KF_IP6_DIP] = {
+		.type = VCAP_FIELD_U128,
+		.offset = 138,
+		.width = 128,
+	},
+};
+
 static const struct vcap_field es2_mac_etype_keyfield[] = {
 	[VCAP_KF_TYPE] = {
 		.type = VCAP_FIELD_U32,
@@ -2338,6 +2415,29 @@ static const struct vcap_set es0_keyfield_set[] = {
 	},
 };
 
+static const struct vcap_set lpm_keyfield_set[] = {
+	[VCAP_KFS_SGL_IP4] = {
+		.type_id = -1,
+		.sw_per_item = 1,
+		.sw_cnt = 12,
+	},
+	[VCAP_KFS_DBL_IP4] = {
+		.type_id = 0,
+		.sw_per_item = 2,
+		.sw_cnt = 6,
+	},
+	[VCAP_KFS_SGL_IP6] = {
+		.type_id = -1,
+		.sw_per_item = 3,
+		.sw_cnt = 4,
+	},
+	[VCAP_KFS_DBL_IP6] = {
+		.type_id = -1,
+		.sw_per_item = 6,
+		.sw_cnt = 2,
+	},
+};
+
 static const struct vcap_set es2_keyfield_set[] = {
 	[VCAP_KFS_MAC_ETYPE] = {
 		.type_id = 0,
@@ -2390,6 +2490,13 @@ static const struct vcap_field *es0_keyfield_set_map[] = {
 	[VCAP_KFS_ISDX] = es0_isdx_keyfield,
 };
 
+static const struct vcap_field *lpm_keyfield_set_map[] = {
+	[VCAP_KFS_SGL_IP4] = lpm_sgl_ip4_keyfield,
+	[VCAP_KFS_DBL_IP4] = lpm_dbl_ip4_keyfield,
+	[VCAP_KFS_SGL_IP6] = lpm_sgl_ip6_keyfield,
+	[VCAP_KFS_DBL_IP6] = lpm_dbl_ip6_keyfield,
+};
+
 static const struct vcap_field *es2_keyfield_set_map[] = {
 	[VCAP_KFS_MAC_ETYPE] = es2_mac_etype_keyfield,
 	[VCAP_KFS_ARP] = es2_arp_keyfield,
@@ -2418,6 +2525,13 @@ static int es0_keyfield_set_map_size[] = {
 	[VCAP_KFS_ISDX] = ARRAY_SIZE(es0_isdx_keyfield),
 };
 
+static int lpm_keyfield_set_map_size[] = {
+	[VCAP_KFS_SGL_IP4] = ARRAY_SIZE(lpm_sgl_ip4_keyfield),
+	[VCAP_KFS_DBL_IP4] = ARRAY_SIZE(lpm_dbl_ip4_keyfield),
+	[VCAP_KFS_SGL_IP6] = ARRAY_SIZE(lpm_sgl_ip6_keyfield),
+	[VCAP_KFS_DBL_IP6] = ARRAY_SIZE(lpm_dbl_ip6_keyfield),
+};
+
 static int es2_keyfield_set_map_size[] = {
 	[VCAP_KFS_MAC_ETYPE] = ARRAY_SIZE(es2_mac_etype_keyfield),
 	[VCAP_KFS_ARP] = ARRAY_SIZE(es2_arp_keyfield),
@@ -2993,6 +3107,92 @@ static const struct vcap_field es0_es0_actionfield[] = {
 	},
 };
 
+static const struct vcap_field lpm_arp_ptr_actionfield[] = {
+	[VCAP_AF_TYPE] = {
+		.type = VCAP_FIELD_U32,
+		.offset = 0,
+		.width = 2,
+	},
+	[VCAP_AF_ARP_PTR] = {
+		.type = VCAP_FIELD_U32,
+		.offset = 2,
+		.width = 10,
+	},
+	[VCAP_AF_ARP_PTR_REMAP_ENA] = {
+		.type = VCAP_FIELD_BIT,
+		.offset = 12,
+		.width = 1,
+	},
+	[VCAP_AF_ECMP_CNT] = {
+		.type = VCAP_FIELD_U32,
+		.offset = 13,
+		.width = 4,
+	},
+	[VCAP_AF_RGID] = {
+		.type = VCAP_FIELD_U32,
+		.offset = 17,
+		.width = 3,
+	},
+};
+
+static const struct vcap_field lpm_arp_entry_actionfield[] = {
+	[VCAP_AF_TYPE] = {
+		.type = VCAP_FIELD_U32,
+		.offset = 0,
+		.width = 2,
+	},
+	[VCAP_AF_MAC_MSB] = {
+		.type = VCAP_FIELD_U32,
+		.offset = 2,
+		.width = 16,
+	},
+	[VCAP_AF_MAC_LSB] = {
+		.type = VCAP_FIELD_U32,
+		.offset = 18,
+		.width = 32,
+	},
+	[VCAP_AF_ARP_VMID] = {
+		.type = VCAP_FIELD_U32,
+		.offset = 50,
+		.width = 7,
+	},
+	[VCAP_AF_ZERO_DMAC_CPU_QU] = {
+		.type = VCAP_FIELD_U32,
+		.offset = 57,
+		.width = 3,
+	},
+	[VCAP_AF_SIP_RPF_ENA] = {
+		.type = VCAP_FIELD_BIT,
+		.offset = 60,
+		.width = 1,
+	},
+	[VCAP_AF_SECUR_MATCH_VMID_ENA] = {
+		.type = VCAP_FIELD_BIT,
+		.offset = 61,
+		.width = 1,
+	},
+	[VCAP_AF_SECUR_MATCH_MAC_ENA] = {
+		.type = VCAP_FIELD_BIT,
+		.offset = 62,
+		.width = 1,
+	},
+	[VCAP_AF_ARP_ENA] = {
+		.type = VCAP_FIELD_BIT,
+		.offset = 63,
+		.width = 1,
+	},
+	[VCAP_AF_ENCAP_ID] = {
+		.type = VCAP_FIELD_U32,
+		.offset = 64,
+		.width = 8,
+	},
+	[VCAP_AF_RSDX] = {
+		.type = VCAP_FIELD_U32,
+		.offset = 72,
+		.width = 10,
+	},
+};
+
 static const struct vcap_field es2_base_type_actionfield[] = {
 	[VCAP_AF_HIT_ME_ONCE] = {
 		.type = VCAP_FIELD_BIT,
@@ -3101,6 +3301,19 @@ static const struct vcap_set es0_actionfield_set[] = {
 	},
 };
 
+static const struct vcap_set lpm_actionfield_set[] = {
+	[VCAP_AFS_ARP_PTR] = {
+		.type_id = 0,
+		.sw_per_item = 1,
+		.sw_cnt = 12,
+	},
+	[VCAP_AFS_ARP_ENTRY] = {
+		.type_id = 2,
+		.sw_per_item = 1,
+		.sw_cnt = 12,
+	},
+};
+
 static const struct vcap_set es2_actionfield_set[] = {
 	[VCAP_AFS_BASE_TYPE] = {
 		.type_id = -1,
@@ -3124,6 +3337,11 @@ static const struct vcap_field *es0_actionfield_set_map[] = {
 	[VCAP_AFS_ES0] = es0_es0_actionfield,
 };
 
+static const struct vcap_field *lpm_actionfield_set_map[] = {
+	[VCAP_AFS_ARP_PTR] = lpm_arp_ptr_actionfield,
+	[VCAP_AFS_ARP_ENTRY] = lpm_arp_entry_actionfield,
+};
+
 static const struct vcap_field *es2_actionfield_set_map[] = {
 	[VCAP_AFS_BASE_TYPE] = es2_base_type_actionfield,
 };
@@ -3143,6 +3361,11 @@ static int es0_actionfield_set_map_size[] = {
 	[VCAP_AFS_ES0] = ARRAY_SIZE(es0_es0_actionfield),
 };
 
+static int lpm_actionfield_set_map_size[] = {
+	[VCAP_AFS_ARP_PTR] = ARRAY_SIZE(lpm_arp_ptr_actionfield),
+	[VCAP_AFS_ARP_ENTRY] = ARRAY_SIZE(lpm_arp_entry_actionfield),
+};
+
 static int es2_actionfield_set_map_size[] = {
 	[VCAP_AFS_BASE_TYPE] = ARRAY_SIZE(es2_base_type_actionfield),
 };
@@ -3308,6 +3531,82 @@ static const struct vcap_typegroup es0_x1_keyfield_set_typegroups[] = {
 	{}
 };
 
+static const struct vcap_typegroup lpm_x6_keyfield_set_typegroups[] = {
+	{
+		.offset = 0,
+		.width = 4,
+		.value = 8,
+	},
+	{
+		.offset = 52,
+		.width = 1,
+		.value = 0,
+	},
+	{
+		.offset = 104,
+		.width = 2,
+		.value = 0,
+	},
+	{
+		.offset = 156,
+		.width = 3,
+		.value = 0,
+	},
+	{
+		.offset = 208,
+		.width = 2,
+		.value = 0,
+	},
+	{
+		.offset = 260,
+		.width = 1,
+		.value = 0,
+	},
+	{}
+};
+
+static const struct vcap_typegroup lpm_x3_keyfield_set_typegroups[] = {
+	{
+		.offset = 0,
+		.width = 3,
+		.value = 4,
+	},
+	{
+		.offset = 52,
+		.width = 2,
+		.value = 0,
+	},
+	{
+		.offset = 104,
+		.width = 2,
+		.value = 0,
+	},
+	{}
+};
+
+static const struct vcap_typegroup lpm_x2_keyfield_set_typegroups[] = {
+	{
+		.offset = 0,
+		.width = 2,
+		.value = 2,
+	},
+	{
+		.offset = 52,
+		.width = 1,
+		.value = 0,
+	},
+	{}
+};
+
+static const struct vcap_typegroup lpm_x1_keyfield_set_typegroups[] = {
+	{
+		.offset = 0,
+		.width = 1,
+		.value = 1,
+	},
+	{}
+};
+
 static const struct vcap_typegroup es2_x12_keyfield_set_typegroups[] = {
 	{
 		.offset = 0,
@@ -3376,6 +3675,14 @@ static const struct vcap_typegroup *es0_keyfield_set_typegroups[] = {
 	[2] = NULL,
 };
 
+static const struct vcap_typegroup *lpm_keyfield_set_typegroups[] = {
+	[6] = lpm_x6_keyfield_set_typegroups,
+	[3] = lpm_x3_keyfield_set_typegroups,
+	[2] = lpm_x2_keyfield_set_typegroups,
+	[1] = lpm_x1_keyfield_set_typegroups,
+	[13] = NULL,
+};
+
 static const struct vcap_typegroup *es2_keyfield_set_typegroups[] = {
 	[12] = es2_x12_keyfield_set_typegroups,
 	[6] = es2_x6_keyfield_set_typegroups,
@@ -3453,6 +3760,10 @@ static const struct vcap_typegroup es0_x1_actionfield_set_typegroups[] = {
 	{}
 };
 
+static const struct vcap_typegroup lpm_x1_actionfield_set_typegroups[] = {
+	{}
+};
+
 static const struct vcap_typegroup es2_x3_actionfield_set_typegroups[] = {
 	{
 		.offset = 0,
@@ -3494,6 +3805,11 @@ static const struct vcap_typegroup *es0_actionfield_set_typegroups[] = {
 	[2] = NULL,
 };
 
+static const struct vcap_typegroup *lpm_actionfield_set_typegroups[] = {
+	[1] = lpm_x1_actionfield_set_typegroups,
+	[13] = NULL,
+};
+
 static const struct vcap_typegroup *es2_actionfield_set_typegroups[] = {
 	[3] = es2_x3_actionfield_set_typegroups,
 	[1] = es2_x1_actionfield_set_typegroups,
@@ -3504,6 +3820,8 @@ static const struct vcap_typegroup *es2_actionfield_set_typegroups[] = {
 static const char * const vcap_keyfield_set_names[] = {
 	[VCAP_KFS_NO_VALUE]                      =  "(None)",
 	[VCAP_KFS_ARP]                           =  "VCAP_KFS_ARP",
+	[VCAP_KFS_DBL_IP4]                       =  "VCAP_KFS_DBL_IP4",
+	[VCAP_KFS_DBL_IP6]                       =  "VCAP_KFS_DBL_IP6",
 	[VCAP_KFS_ETAG]                          =  "VCAP_KFS_ETAG",
 	[VCAP_KFS_IP4_OTHER]                     =  "VCAP_KFS_IP4_OTHER",
 	[VCAP_KFS_IP4_TCP_UDP]                   =  "VCAP_KFS_IP4_TCP_UDP",
@@ -3522,6 +3840,8 @@ static const char * const vcap_keyfield_set_names[] = {
 	[VCAP_KFS_NORMAL_7TUPLE]                 =  "VCAP_KFS_NORMAL_7TUPLE",
 	[VCAP_KFS_OAM]                           =  "VCAP_KFS_OAM",
 	[VCAP_KFS_PURE_5TUPLE_IP4]               =  "VCAP_KFS_PURE_5TUPLE_IP4",
+	[VCAP_KFS_SGL_IP4]                       =  "VCAP_KFS_SGL_IP4",
+	[VCAP_KFS_SGL_IP6]                       =  "VCAP_KFS_SGL_IP6",
 	[VCAP_KFS_SMAC_SIP4]                     =  "VCAP_KFS_SMAC_SIP4",
 	[VCAP_KFS_SMAC_SIP6]                     =  "VCAP_KFS_SMAC_SIP6",
 };
@@ -3529,6 +3849,8 @@ static const char * const vcap_keyfield_set_names[] = {
 /* Actionfieldset names */
 static const char * const vcap_actionfield_set_names[] = {
 	[VCAP_AFS_NO_VALUE]                      =  "(None)",
+	[VCAP_AFS_ARP_ENTRY]                     =  "VCAP_AFS_ARP_ENTRY",
+	[VCAP_AFS_ARP_PTR]                       =  "VCAP_AFS_ARP_PTR",
 	[VCAP_AFS_BASE_TYPE]                     =  "VCAP_AFS_BASE_TYPE",
 	[VCAP_AFS_CLASSIFICATION]                =  "VCAP_AFS_CLASSIFICATION",
 	[VCAP_AFS_CLASS_REDUCED]                 =  "VCAP_AFS_CLASS_REDUCED",
@@ -3565,6 +3887,7 @@ static const char * const vcap_keyfield_names[] = {
 	[VCAP_KF_8021Q_VLAN_TAGGED_IS]           =  "8021Q_VLAN_TAGGED_IS",
 	[VCAP_KF_8021Q_VLAN_TAGS]                =  "8021Q_VLAN_TAGS",
 	[VCAP_KF_ACL_GRP_ID]                     =  "ACL_GRP_ID",
+	[VCAP_KF_AFFIX]                          =  "AFFIX",
 	[VCAP_KF_ARP_ADDR_SPACE_OK_IS]           =  "ARP_ADDR_SPACE_OK_IS",
 	[VCAP_KF_ARP_LEN_OK_IS]                  =  "ARP_LEN_OK_IS",
 	[VCAP_KF_ARP_OPCODE]                     =  "ARP_OPCODE",
@@ -3573,6 +3896,7 @@ static const char * const vcap_keyfield_names[] = {
 	[VCAP_KF_ARP_SENDER_MATCH_IS]            =  "ARP_SENDER_MATCH_IS",
 	[VCAP_KF_ARP_TGT_MATCH_IS]               =  "ARP_TGT_MATCH_IS",
 	[VCAP_KF_COSID_CLS]                      =  "COSID_CLS",
+	[VCAP_KF_DST_FLAG]                       =  "DST_FLAG",
 	[VCAP_KF_ES0_ISDX_KEY_ENA]               =  "ES0_ISDX_KEY_ENA",
 	[VCAP_KF_ETYPE]                          =  "ETYPE",
 	[VCAP_KF_ETYPE_LEN_IS]                   =  "ETYPE_LEN_IS",
@@ -3586,7 +3910,13 @@ static const char * const vcap_keyfield_names[] = {
 	[VCAP_KF_IF_IGR_PORT_MASK_RNG]           =  "IF_IGR_PORT_MASK_RNG",
 	[VCAP_KF_IF_IGR_PORT_MASK_SEL]           =  "IF_IGR_PORT_MASK_SEL",
 	[VCAP_KF_IF_IGR_PORT_SEL]                =  "IF_IGR_PORT_SEL",
+	[VCAP_KF_IP4_DIP]                        =  "IP4_DIP",
 	[VCAP_KF_IP4_IS]                         =  "IP4_IS",
+	[VCAP_KF_IP4_SIP]                        =  "IP4_SIP",
+	[VCAP_KF_IP4_XIP]                        =  "IP4_XIP",
+	[VCAP_KF_IP6_DIP]                        =  "IP6_DIP",
+	[VCAP_KF_IP6_SIP]                        =  "IP6_SIP",
+	[VCAP_KF_IP6_XIP]                        =  "IP6_XIP",
 	[VCAP_KF_IP_MC_IS]                       =  "IP_MC_IS",
 	[VCAP_KF_IP_PAYLOAD_5TUPLE]              =  "IP_PAYLOAD_5TUPLE",
 	[VCAP_KF_IP_SNAP_IS]                     =  "IP_SNAP_IS",
@@ -3659,6 +3989,10 @@ static const char * const vcap_keyfield_names[] = {
 static const char * const vcap_actionfield_names[] = {
 	[VCAP_AF_NO_VALUE]                       =  "(None)",
 	[VCAP_AF_ACL_ID]                         =  "ACL_ID",
+	[VCAP_AF_ARP_ENA]                        =  "ARP_ENA",
+	[VCAP_AF_ARP_PTR]                        =  "ARP_PTR",
+	[VCAP_AF_ARP_PTR_REMAP_ENA]              =  "ARP_PTR_REMAP_ENA",
+	[VCAP_AF_ARP_VMID]                       =  "ARP_VMID",
 	[VCAP_AF_CLS_VID_SEL]                    =  "CLS_VID_SEL",
 	[VCAP_AF_CNT_ID]                         =  "CNT_ID",
 	[VCAP_AF_COPY_PORT_NUM]                  =  "COPY_PORT_NUM",
@@ -3676,6 +4010,8 @@ static const char * const vcap_actionfield_names[] = {
 	[VCAP_AF_DSCP_ENA]                       =  "DSCP_ENA",
 	[VCAP_AF_DSCP_SEL]                       =  "DSCP_SEL",
 	[VCAP_AF_DSCP_VAL]                       =  "DSCP_VAL",
+	[VCAP_AF_ECMP_CNT]                       =  "ECMP_CNT",
+	[VCAP_AF_ENCAP_ID]                       =  "ENCAP_ID",
 	[VCAP_AF_ES2_REW_CMD]                    =  "ES2_REW_CMD",
 	[VCAP_AF_ESDX]                           =  "ESDX",
 	[VCAP_AF_FWD_KILL_ENA]                   =  "FWD_KILL_ENA",
@@ -3690,6 +4026,8 @@ static const char * const vcap_actionfield_names[] = {
 	[VCAP_AF_ISDX_VAL]                       =  "ISDX_VAL",
 	[VCAP_AF_LOOP_ENA]                       =  "LOOP_ENA",
 	[VCAP_AF_LRN_DIS]                        =  "LRN_DIS",
+	[VCAP_AF_MAC_LSB]                        =  "MAC_LSB",
+	[VCAP_AF_MAC_MSB]                        =  "MAC_MSB",
 	[VCAP_AF_MAP_IDX]                        =  "MAP_IDX",
 	[VCAP_AF_MAP_KEY]                        =  "MAP_KEY",
 	[VCAP_AF_MAP_LOOKUP_SEL]                 =  "MAP_LOOKUP_SEL",
@@ -3723,7 +4061,12 @@ static const char * const vcap_actionfield_names[] = {
 	[VCAP_AF_QOS_ENA]                        =  "QOS_ENA",
 	[VCAP_AF_QOS_VAL]                        =  "QOS_VAL",
 	[VCAP_AF_REW_OP]                         =  "REW_OP",
+	[VCAP_AF_RGID]                           =  "RGID",
+	[VCAP_AF_RSDX]                           =  "RSDX",
 	[VCAP_AF_RT_DIS]                         =  "RT_DIS",
+	[VCAP_AF_SECUR_MATCH_MAC_ENA]            =  "SECUR_MATCH_MAC_ENA",
+	[VCAP_AF_SECUR_MATCH_VMID_ENA]           =  "SECUR_MATCH_VMID_ENA",
+	[VCAP_AF_SIP_RPF_ENA]                    =  "SIP_RPF_ENA",
 	[VCAP_AF_SWAP_MACS_ENA]                  =  "SWAP_MACS_ENA",
 	[VCAP_AF_TAG_A_DEI_SEL]                  =  "TAG_A_DEI_SEL",
 	[VCAP_AF_TAG_A_PCP_SEL]                  =  "TAG_A_PCP_SEL",
@@ -3743,6 +4086,7 @@ static const char * const vcap_actionfield_names[] = {
 	[VCAP_AF_VID_B_VAL]                      =  "VID_B_VAL",
 	[VCAP_AF_VID_C_VAL]                      =  "VID_C_VAL",
 	[VCAP_AF_VID_VAL]                        =  "VID_VAL",
+	[VCAP_AF_ZERO_DMAC_CPU_QU]               =  "ZERO_DMAC_CPU_QU",
 };
 
 /* VCAPs */
@@ -3810,6 +4154,27 @@ const struct vcap_info lan969x_vcaps[] = {
 		.keyfield_set_typegroups = es0_keyfield_set_typegroups,
 		.actionfield_set_typegroups = es0_actionfield_set_typegroups,
 	},
+	[VCAP_TYPE_LPM] = {
+		.name = "lpm",
+		.rows = 512,
+		.sw_count = 12,
+		.sw_width = 52,
+		.sticky_width = 1,
+		.act_width = 83,
+		.default_cnt = 0,
+		.require_cnt_dis = 0,
+		.version = 1,
+		.keyfield_set = lpm_keyfield_set,
+		.keyfield_set_size = ARRAY_SIZE(lpm_keyfield_set),
+		.actionfield_set = lpm_actionfield_set,
+		.actionfield_set_size = ARRAY_SIZE(lpm_actionfield_set),
+		.keyfield_set_map = lpm_keyfield_set_map,
+		.keyfield_set_map_size = lpm_keyfield_set_map_size,
+		.actionfield_set_map = lpm_actionfield_set_map,
+		.actionfield_set_map_size = lpm_actionfield_set_map_size,
+		.keyfield_set_typegroups = lpm_keyfield_set_typegroups,
+		.actionfield_set_typegroups = lpm_actionfield_set_typegroups,
+	},
 	[VCAP_TYPE_ES2] = {
 		.name = "es2",
 		.rows = 256,
@@ -3835,7 +4200,7 @@ const struct vcap_info lan969x_vcaps[] = {
 
 const struct vcap_statistics lan969x_vcap_stats = {
 	.name = "lan969x",
-	.count = 4,
+	.count = 5,
 	.keyfield_set_names = vcap_keyfield_set_names,
 	.actionfield_set_names = vcap_actionfield_set_names,
 	.keyfield_names = vcap_keyfield_names,
diff --git a/drivers/net/ethernet/microchip/sparx5/sparx5_vcap_ag_api.c b/drivers/net/ethernet/microchip/sparx5/sparx5_vcap_ag_api.c
index 556d6ea0acd1..7bc8b29757b1 100644
--- a/drivers/net/ethernet/microchip/sparx5/sparx5_vcap_ag_api.c
+++ b/drivers/net/ethernet/microchip/sparx5/sparx5_vcap_ag_api.c
@@ -1,10 +1,10 @@
 // SPDX-License-Identifier: BSD-3-Clause
-/* Copyright (C) 2023 Microchip Technology Inc. and its subsidiaries.
+/* Copyright (C) 2026 Microchip Technology Inc. and its subsidiaries.
  * Microchip VCAP API
  */
 
-/* This file is autogenerated by cml-utils 2023-02-10 11:15:56 +0100.
- * Commit ID: c30fb4bf0281cd4a7133bdab6682f9e43c872ada
+/* This file is autogenerated by cml-utils 2026-04-28 13:55:50 +0200.
+ * Commit ID: cd2494df16a1d3b14f650f720f6a8126b88b553a
  */
 
 #include <linux/types.h>
@@ -1381,6 +1381,83 @@ static const struct vcap_field es0_isdx_keyfield[] = {
 	},
 };
 
+static const struct vcap_field lpm_sgl_ip4_keyfield[] = {
+	[VCAP_KF_DST_FLAG] = {
+		.type = VCAP_FIELD_BIT,
+		.offset = 0,
+		.width = 1,
+	},
+	[VCAP_KF_AFFIX] = {
+		.type = VCAP_FIELD_U32,
+		.offset = 1,
+		.width = 10,
+	},
+	[VCAP_KF_IP4_XIP] = {
+		.type = VCAP_FIELD_U32,
+		.offset = 11,
+		.width = 32,
+	},
+};
+
+static const struct vcap_field lpm_dbl_ip4_keyfield[] = {
+	[VCAP_KF_TYPE] = {
+		.type = VCAP_FIELD_BIT,
+		.offset = 0,
+		.width = 1,
+	},
+	[VCAP_KF_AFFIX] = {
+		.type = VCAP_FIELD_U32,
+		.offset = 1,
+		.width = 10,
+	},
+	[VCAP_KF_IP4_SIP] = {
+		.type = VCAP_FIELD_U32,
+		.offset = 11,
+		.width = 32,
+	},
+	[VCAP_KF_IP4_DIP] = {
+		.type = VCAP_FIELD_U32,
+		.offset = 43,
+		.width = 32,
+	},
+};
+
+static const struct vcap_field lpm_sgl_ip6_keyfield[] = {
+	[VCAP_KF_DST_FLAG] = {
+		.type = VCAP_FIELD_BIT,
+		.offset = 0,
+		.width = 1,
+	},
+	[VCAP_KF_AFFIX] = {
+		.type = VCAP_FIELD_U32,
+		.offset = 1,
+		.width = 10,
+	},
+	[VCAP_KF_IP6_XIP] = {
+		.type = VCAP_FIELD_U128,
+		.offset = 11,
+		.width = 128,
+	},
+};
+
+static const struct vcap_field lpm_dbl_ip6_keyfield[] = {
+	[VCAP_KF_AFFIX] = {
+		.type = VCAP_FIELD_U32,
+		.offset = 0,
+		.width = 10,
+	},
+	[VCAP_KF_IP6_SIP] = {
+		.type = VCAP_FIELD_U128,
+		.offset = 10,
+		.width = 128,
+	},
+	[VCAP_KF_IP6_DIP] = {
+		.type = VCAP_FIELD_U128,
+		.offset = 138,
+		.width = 128,
+	},
+};
+
 static const struct vcap_field es2_mac_etype_keyfield[] = {
 	[VCAP_KF_TYPE] = {
 		.type = VCAP_FIELD_U32,
@@ -2339,6 +2416,29 @@ static const struct vcap_set es0_keyfield_set[] = {
 	},
 };
 
+static const struct vcap_set lpm_keyfield_set[] = {
+	[VCAP_KFS_SGL_IP4] = {
+		.type_id = -1,
+		.sw_per_item = 1,
+		.sw_cnt = 12,
+	},
+	[VCAP_KFS_DBL_IP4] = {
+		.type_id = 0,
+		.sw_per_item = 2,
+		.sw_cnt = 6,
+	},
+	[VCAP_KFS_SGL_IP6] = {
+		.type_id = -1,
+		.sw_per_item = 3,
+		.sw_cnt = 4,
+	},
+	[VCAP_KFS_DBL_IP6] = {
+		.type_id = -1,
+		.sw_per_item = 6,
+		.sw_cnt = 2,
+	},
+};
+
 static const struct vcap_set es2_keyfield_set[] = {
 	[VCAP_KFS_MAC_ETYPE] = {
 		.type_id = 0,
@@ -2391,6 +2491,13 @@ static const struct vcap_field *es0_keyfield_set_map[] = {
 	[VCAP_KFS_ISDX] = es0_isdx_keyfield,
 };
 
+static const struct vcap_field *lpm_keyfield_set_map[] = {
+	[VCAP_KFS_SGL_IP4] = lpm_sgl_ip4_keyfield,
+	[VCAP_KFS_DBL_IP4] = lpm_dbl_ip4_keyfield,
+	[VCAP_KFS_SGL_IP6] = lpm_sgl_ip6_keyfield,
+	[VCAP_KFS_DBL_IP6] = lpm_dbl_ip6_keyfield,
+};
+
 static const struct vcap_field *es2_keyfield_set_map[] = {
 	[VCAP_KFS_MAC_ETYPE] = es2_mac_etype_keyfield,
 	[VCAP_KFS_ARP] = es2_arp_keyfield,
@@ -2419,6 +2526,13 @@ static int es0_keyfield_set_map_size[] = {
 	[VCAP_KFS_ISDX] = ARRAY_SIZE(es0_isdx_keyfield),
 };
 
+static int lpm_keyfield_set_map_size[] = {
+	[VCAP_KFS_SGL_IP4] = ARRAY_SIZE(lpm_sgl_ip4_keyfield),
+	[VCAP_KFS_DBL_IP4] = ARRAY_SIZE(lpm_dbl_ip4_keyfield),
+	[VCAP_KFS_SGL_IP6] = ARRAY_SIZE(lpm_sgl_ip6_keyfield),
+	[VCAP_KFS_DBL_IP6] = ARRAY_SIZE(lpm_dbl_ip6_keyfield),
+};
+
 static int es2_keyfield_set_map_size[] = {
 	[VCAP_KFS_MAC_ETYPE] = ARRAY_SIZE(es2_mac_etype_keyfield),
 	[VCAP_KFS_ARP] = ARRAY_SIZE(es2_arp_keyfield),
@@ -2994,6 +3108,92 @@ static const struct vcap_field es0_es0_actionfield[] = {
 	},
 };
 
+static const struct vcap_field lpm_arp_ptr_actionfield[] = {
+	[VCAP_AF_TYPE] = {
+		.type = VCAP_FIELD_U32,
+		.offset = 0,
+		.width = 2,
+	},
+	[VCAP_AF_ARP_PTR] = {
+		.type = VCAP_FIELD_U32,
+		.offset = 2,
+		.width = 11,
+	},
+	[VCAP_AF_ARP_PTR_REMAP_ENA] = {
+		.type = VCAP_FIELD_BIT,
+		.offset = 13,
+		.width = 1,
+	},
+	[VCAP_AF_ECMP_CNT] = {
+		.type = VCAP_FIELD_U32,
+		.offset = 14,
+		.width = 4,
+	},
+	[VCAP_AF_RGID] = {
+		.type = VCAP_FIELD_U32,
+		.offset = 18,
+		.width = 3,
+	},
+};
+
+static const struct vcap_field lpm_arp_entry_actionfield[] = {
+	[VCAP_AF_TYPE] = {
+		.type = VCAP_FIELD_U32,
+		.offset = 0,
+		.width = 2,
+	},
+	[VCAP_AF_MAC_MSB] = {
+		.type = VCAP_FIELD_U32,
+		.offset = 2,
+		.width = 16,
+	},
+	[VCAP_AF_MAC_LSB] = {
+		.type = VCAP_FIELD_U32,
+		.offset = 18,
+		.width = 32,
+	},
+	[VCAP_AF_ARP_VMID] = {
+		.type = VCAP_FIELD_U32,
+		.offset = 50,
+		.width = 9,
+	},
+	[VCAP_AF_ZERO_DMAC_CPU_QU] = {
+		.type = VCAP_FIELD_U32,
+		.offset = 59,
+		.width = 3,
+	},
+	[VCAP_AF_SIP_RPF_ENA] = {
+		.type = VCAP_FIELD_BIT,
+		.offset = 62,
+		.width = 1,
+	},
+	[VCAP_AF_SECUR_MATCH_VMID_ENA] = {
+		.type = VCAP_FIELD_BIT,
+		.offset = 63,
+		.width = 1,
+	},
+	[VCAP_AF_SECUR_MATCH_MAC_ENA] = {
+		.type = VCAP_FIELD_BIT,
+		.offset = 64,
+		.width = 1,
+	},
+	[VCAP_AF_ARP_ENA] = {
+		.type = VCAP_FIELD_BIT,
+		.offset = 65,
+		.width = 1,
+	},
+	[VCAP_AF_ENCAP_ID] = {
+		.type = VCAP_FIELD_U32,
+		.offset = 66,
+		.width = 10,
+	},
+	[VCAP_AF_RSDX] = {
+		.type = VCAP_FIELD_U32,
+		.offset = 76,
+		.width = 12,
+	},
+};
+
 static const struct vcap_field es2_base_type_actionfield[] = {
 	[VCAP_AF_HIT_ME_ONCE] = {
 		.type = VCAP_FIELD_BIT,
@@ -3102,6 +3302,19 @@ static const struct vcap_set es0_actionfield_set[] = {
 	},
 };
 
+static const struct vcap_set lpm_actionfield_set[] = {
+	[VCAP_AFS_ARP_PTR] = {
+		.type_id = 0,
+		.sw_per_item = 1,
+		.sw_cnt = 12,
+	},
+	[VCAP_AFS_ARP_ENTRY] = {
+		.type_id = 2,
+		.sw_per_item = 1,
+		.sw_cnt = 12,
+	},
+};
+
 static const struct vcap_set es2_actionfield_set[] = {
 	[VCAP_AFS_BASE_TYPE] = {
 		.type_id = -1,
@@ -3125,6 +3338,11 @@ static const struct vcap_field *es0_actionfield_set_map[] = {
 	[VCAP_AFS_ES0] = es0_es0_actionfield,
 };
 
+static const struct vcap_field *lpm_actionfield_set_map[] = {
+	[VCAP_AFS_ARP_PTR] = lpm_arp_ptr_actionfield,
+	[VCAP_AFS_ARP_ENTRY] = lpm_arp_entry_actionfield,
+};
+
 static const struct vcap_field *es2_actionfield_set_map[] = {
 	[VCAP_AFS_BASE_TYPE] = es2_base_type_actionfield,
 };
@@ -3144,6 +3362,11 @@ static int es0_actionfield_set_map_size[] = {
 	[VCAP_AFS_ES0] = ARRAY_SIZE(es0_es0_actionfield),
 };
 
+static int lpm_actionfield_set_map_size[] = {
+	[VCAP_AFS_ARP_PTR] = ARRAY_SIZE(lpm_arp_ptr_actionfield),
+	[VCAP_AFS_ARP_ENTRY] = ARRAY_SIZE(lpm_arp_entry_actionfield),
+};
+
 static int es2_actionfield_set_map_size[] = {
 	[VCAP_AFS_BASE_TYPE] = ARRAY_SIZE(es2_base_type_actionfield),
 };
@@ -3334,6 +3557,82 @@ static const struct vcap_typegroup es0_x1_keyfield_set_typegroups[] = {
 	{}
 };
 
+static const struct vcap_typegroup lpm_x6_keyfield_set_typegroups[] = {
+	{
+		.offset = 0,
+		.width = 4,
+		.value = 8,
+	},
+	{
+		.offset = 52,
+		.width = 1,
+		.value = 0,
+	},
+	{
+		.offset = 104,
+		.width = 2,
+		.value = 0,
+	},
+	{
+		.offset = 156,
+		.width = 3,
+		.value = 0,
+	},
+	{
+		.offset = 208,
+		.width = 2,
+		.value = 0,
+	},
+	{
+		.offset = 260,
+		.width = 1,
+		.value = 0,
+	},
+	{}
+};
+
+static const struct vcap_typegroup lpm_x3_keyfield_set_typegroups[] = {
+	{
+		.offset = 0,
+		.width = 3,
+		.value = 4,
+	},
+	{
+		.offset = 52,
+		.width = 2,
+		.value = 0,
+	},
+	{
+		.offset = 104,
+		.width = 2,
+		.value = 0,
+	},
+	{}
+};
+
+static const struct vcap_typegroup lpm_x2_keyfield_set_typegroups[] = {
+	{
+		.offset = 0,
+		.width = 2,
+		.value = 2,
+	},
+	{
+		.offset = 52,
+		.width = 1,
+		.value = 0,
+	},
+	{}
+};
+
+static const struct vcap_typegroup lpm_x1_keyfield_set_typegroups[] = {
+	{
+		.offset = 0,
+		.width = 1,
+		.value = 1,
+	},
+	{}
+};
+
 static const struct vcap_typegroup es2_x12_keyfield_set_typegroups[] = {
 	{
 		.offset = 0,
@@ -3407,6 +3706,14 @@ static const struct vcap_typegroup *es0_keyfield_set_typegroups[] = {
 	[2] = NULL,
 };
 
+static const struct vcap_typegroup *lpm_keyfield_set_typegroups[] = {
+	[6] = lpm_x6_keyfield_set_typegroups,
+	[3] = lpm_x3_keyfield_set_typegroups,
+	[2] = lpm_x2_keyfield_set_typegroups,
+	[1] = lpm_x1_keyfield_set_typegroups,
+	[13] = NULL,
+};
+
 static const struct vcap_typegroup *es2_keyfield_set_typegroups[] = {
 	[12] = es2_x12_keyfield_set_typegroups,
 	[6] = es2_x6_keyfield_set_typegroups,
@@ -3484,6 +3791,10 @@ static const struct vcap_typegroup es0_x1_actionfield_set_typegroups[] = {
 	{}
 };
 
+static const struct vcap_typegroup lpm_x1_actionfield_set_typegroups[] = {
+	{}
+};
+
 static const struct vcap_typegroup es2_x3_actionfield_set_typegroups[] = {
 	{
 		.offset = 0,
@@ -3525,6 +3836,11 @@ static const struct vcap_typegroup *es0_actionfield_set_typegroups[] = {
 	[2] = NULL,
 };
 
+static const struct vcap_typegroup *lpm_actionfield_set_typegroups[] = {
+	[1] = lpm_x1_actionfield_set_typegroups,
+	[13] = NULL,
+};
+
 static const struct vcap_typegroup *es2_actionfield_set_typegroups[] = {
 	[3] = es2_x3_actionfield_set_typegroups,
 	[1] = es2_x1_actionfield_set_typegroups,
@@ -3535,6 +3851,8 @@ static const struct vcap_typegroup *es2_actionfield_set_typegroups[] = {
 static const char * const vcap_keyfield_set_names[] = {
 	[VCAP_KFS_NO_VALUE]                      =  "(None)",
 	[VCAP_KFS_ARP]                           =  "VCAP_KFS_ARP",
+	[VCAP_KFS_DBL_IP4]                       =  "VCAP_KFS_DBL_IP4",
+	[VCAP_KFS_DBL_IP6]                       =  "VCAP_KFS_DBL_IP6",
 	[VCAP_KFS_ETAG]                          =  "VCAP_KFS_ETAG",
 	[VCAP_KFS_IP4_OTHER]                     =  "VCAP_KFS_IP4_OTHER",
 	[VCAP_KFS_IP4_TCP_UDP]                   =  "VCAP_KFS_IP4_TCP_UDP",
@@ -3553,6 +3871,8 @@ static const char * const vcap_keyfield_set_names[] = {
 	[VCAP_KFS_NORMAL_7TUPLE]                 =  "VCAP_KFS_NORMAL_7TUPLE",
 	[VCAP_KFS_OAM]                           =  "VCAP_KFS_OAM",
 	[VCAP_KFS_PURE_5TUPLE_IP4]               =  "VCAP_KFS_PURE_5TUPLE_IP4",
+	[VCAP_KFS_SGL_IP4]                       =  "VCAP_KFS_SGL_IP4",
+	[VCAP_KFS_SGL_IP6]                       =  "VCAP_KFS_SGL_IP6",
 	[VCAP_KFS_SMAC_SIP4]                     =  "VCAP_KFS_SMAC_SIP4",
 	[VCAP_KFS_SMAC_SIP6]                     =  "VCAP_KFS_SMAC_SIP6",
 };
@@ -3560,6 +3880,8 @@ static const char * const vcap_keyfield_set_names[] = {
 /* Actionfieldset names */
 static const char * const vcap_actionfield_set_names[] = {
 	[VCAP_AFS_NO_VALUE]                      =  "(None)",
+	[VCAP_AFS_ARP_ENTRY]                     =  "VCAP_AFS_ARP_ENTRY",
+	[VCAP_AFS_ARP_PTR]                       =  "VCAP_AFS_ARP_PTR",
 	[VCAP_AFS_BASE_TYPE]                     =  "VCAP_AFS_BASE_TYPE",
 	[VCAP_AFS_CLASSIFICATION]                =  "VCAP_AFS_CLASSIFICATION",
 	[VCAP_AFS_CLASS_REDUCED]                 =  "VCAP_AFS_CLASS_REDUCED",
@@ -3596,6 +3918,7 @@ static const char * const vcap_keyfield_names[] = {
 	[VCAP_KF_8021Q_VLAN_TAGGED_IS]           =  "8021Q_VLAN_TAGGED_IS",
 	[VCAP_KF_8021Q_VLAN_TAGS]                =  "8021Q_VLAN_TAGS",
 	[VCAP_KF_ACL_GRP_ID]                     =  "ACL_GRP_ID",
+	[VCAP_KF_AFFIX]                          =  "AFFIX",
 	[VCAP_KF_ARP_ADDR_SPACE_OK_IS]           =  "ARP_ADDR_SPACE_OK_IS",
 	[VCAP_KF_ARP_LEN_OK_IS]                  =  "ARP_LEN_OK_IS",
 	[VCAP_KF_ARP_OPCODE]                     =  "ARP_OPCODE",
@@ -3604,6 +3927,7 @@ static const char * const vcap_keyfield_names[] = {
 	[VCAP_KF_ARP_SENDER_MATCH_IS]            =  "ARP_SENDER_MATCH_IS",
 	[VCAP_KF_ARP_TGT_MATCH_IS]               =  "ARP_TGT_MATCH_IS",
 	[VCAP_KF_COSID_CLS]                      =  "COSID_CLS",
+	[VCAP_KF_DST_FLAG]                       =  "DST_FLAG",
 	[VCAP_KF_ES0_ISDX_KEY_ENA]               =  "ES0_ISDX_KEY_ENA",
 	[VCAP_KF_ETYPE]                          =  "ETYPE",
 	[VCAP_KF_ETYPE_LEN_IS]                   =  "ETYPE_LEN_IS",
@@ -3617,7 +3941,13 @@ static const char * const vcap_keyfield_names[] = {
 	[VCAP_KF_IF_IGR_PORT_MASK_RNG]           =  "IF_IGR_PORT_MASK_RNG",
 	[VCAP_KF_IF_IGR_PORT_MASK_SEL]           =  "IF_IGR_PORT_MASK_SEL",
 	[VCAP_KF_IF_IGR_PORT_SEL]                =  "IF_IGR_PORT_SEL",
+	[VCAP_KF_IP4_DIP]                        =  "IP4_DIP",
 	[VCAP_KF_IP4_IS]                         =  "IP4_IS",
+	[VCAP_KF_IP4_SIP]                        =  "IP4_SIP",
+	[VCAP_KF_IP4_XIP]                        =  "IP4_XIP",
+	[VCAP_KF_IP6_DIP]                        =  "IP6_DIP",
+	[VCAP_KF_IP6_SIP]                        =  "IP6_SIP",
+	[VCAP_KF_IP6_XIP]                        =  "IP6_XIP",
 	[VCAP_KF_IP_MC_IS]                       =  "IP_MC_IS",
 	[VCAP_KF_IP_PAYLOAD_5TUPLE]              =  "IP_PAYLOAD_5TUPLE",
 	[VCAP_KF_IP_SNAP_IS]                     =  "IP_SNAP_IS",
@@ -3690,6 +4020,10 @@ static const char * const vcap_keyfield_names[] = {
 static const char * const vcap_actionfield_names[] = {
 	[VCAP_AF_NO_VALUE]                       =  "(None)",
 	[VCAP_AF_ACL_ID]                         =  "ACL_ID",
+	[VCAP_AF_ARP_ENA]                        =  "ARP_ENA",
+	[VCAP_AF_ARP_PTR]                        =  "ARP_PTR",
+	[VCAP_AF_ARP_PTR_REMAP_ENA]              =  "ARP_PTR_REMAP_ENA",
+	[VCAP_AF_ARP_VMID]                       =  "ARP_VMID",
 	[VCAP_AF_CLS_VID_SEL]                    =  "CLS_VID_SEL",
 	[VCAP_AF_CNT_ID]                         =  "CNT_ID",
 	[VCAP_AF_COPY_PORT_NUM]                  =  "COPY_PORT_NUM",
@@ -3707,6 +4041,8 @@ static const char * const vcap_actionfield_names[] = {
 	[VCAP_AF_DSCP_ENA]                       =  "DSCP_ENA",
 	[VCAP_AF_DSCP_SEL]                       =  "DSCP_SEL",
 	[VCAP_AF_DSCP_VAL]                       =  "DSCP_VAL",
+	[VCAP_AF_ECMP_CNT]                       =  "ECMP_CNT",
+	[VCAP_AF_ENCAP_ID]                       =  "ENCAP_ID",
 	[VCAP_AF_ES2_REW_CMD]                    =  "ES2_REW_CMD",
 	[VCAP_AF_ESDX]                           =  "ESDX",
 	[VCAP_AF_FWD_KILL_ENA]                   =  "FWD_KILL_ENA",
@@ -3721,6 +4057,8 @@ static const char * const vcap_actionfield_names[] = {
 	[VCAP_AF_ISDX_VAL]                       =  "ISDX_VAL",
 	[VCAP_AF_LOOP_ENA]                       =  "LOOP_ENA",
 	[VCAP_AF_LRN_DIS]                        =  "LRN_DIS",
+	[VCAP_AF_MAC_LSB]                        =  "MAC_LSB",
+	[VCAP_AF_MAC_MSB]                        =  "MAC_MSB",
 	[VCAP_AF_MAP_IDX]                        =  "MAP_IDX",
 	[VCAP_AF_MAP_KEY]                        =  "MAP_KEY",
 	[VCAP_AF_MAP_LOOKUP_SEL]                 =  "MAP_LOOKUP_SEL",
@@ -3754,7 +4092,12 @@ static const char * const vcap_actionfield_names[] = {
 	[VCAP_AF_QOS_ENA]                        =  "QOS_ENA",
 	[VCAP_AF_QOS_VAL]                        =  "QOS_VAL",
 	[VCAP_AF_REW_OP]                         =  "REW_OP",
+	[VCAP_AF_RGID]                           =  "RGID",
+	[VCAP_AF_RSDX]                           =  "RSDX",
 	[VCAP_AF_RT_DIS]                         =  "RT_DIS",
+	[VCAP_AF_SECUR_MATCH_MAC_ENA]            =  "SECUR_MATCH_MAC_ENA",
+	[VCAP_AF_SECUR_MATCH_VMID_ENA]           =  "SECUR_MATCH_VMID_ENA",
+	[VCAP_AF_SIP_RPF_ENA]                    =  "SIP_RPF_ENA",
 	[VCAP_AF_SWAP_MACS_ENA]                  =  "SWAP_MACS_ENA",
 	[VCAP_AF_TAG_A_DEI_SEL]                  =  "TAG_A_DEI_SEL",
 	[VCAP_AF_TAG_A_PCP_SEL]                  =  "TAG_A_PCP_SEL",
@@ -3774,6 +4117,7 @@ static const char * const vcap_actionfield_names[] = {
 	[VCAP_AF_VID_B_VAL]                      =  "VID_B_VAL",
 	[VCAP_AF_VID_C_VAL]                      =  "VID_C_VAL",
 	[VCAP_AF_VID_VAL]                        =  "VID_VAL",
+	[VCAP_AF_ZERO_DMAC_CPU_QU]               =  "ZERO_DMAC_CPU_QU",
 };
 
 /* VCAPs */
@@ -3841,6 +4185,27 @@ const struct vcap_info sparx5_vcaps[] = {
 		.keyfield_set_typegroups = es0_keyfield_set_typegroups,
 		.actionfield_set_typegroups = es0_actionfield_set_typegroups,
 	},
+	[VCAP_TYPE_LPM] = {
+		.name = "lpm",
+		.rows = 512,
+		.sw_count = 12,
+		.sw_width = 52,
+		.sticky_width = 1,
+		.act_width = 89,
+		.default_cnt = 0,
+		.require_cnt_dis = 0,
+		.version = 1,
+		.keyfield_set = lpm_keyfield_set,
+		.keyfield_set_size = ARRAY_SIZE(lpm_keyfield_set),
+		.actionfield_set = lpm_actionfield_set,
+		.actionfield_set_size = ARRAY_SIZE(lpm_actionfield_set),
+		.keyfield_set_map = lpm_keyfield_set_map,
+		.keyfield_set_map_size = lpm_keyfield_set_map_size,
+		.actionfield_set_map = lpm_actionfield_set_map,
+		.actionfield_set_map_size = lpm_actionfield_set_map_size,
+		.keyfield_set_typegroups = lpm_keyfield_set_typegroups,
+		.actionfield_set_typegroups = lpm_actionfield_set_typegroups,
+	},
 	[VCAP_TYPE_ES2] = {
 		.name = "es2",
 		.rows = 1024,
@@ -3866,7 +4231,7 @@ const struct vcap_info sparx5_vcaps[] = {
 
 const struct vcap_statistics sparx5_vcap_stats = {
 	.name = "sparx5",
-	.count = 4,
+	.count = 5,
 	.keyfield_set_names = vcap_keyfield_set_names,
 	.actionfield_set_names = vcap_actionfield_set_names,
 	.keyfield_names = vcap_keyfield_names,
diff --git a/drivers/net/ethernet/microchip/sparx5/sparx5_vcap_ag_api.h b/drivers/net/ethernet/microchip/sparx5/sparx5_vcap_ag_api.h
index e68f5639a40a..22bb416f433d 100644
--- a/drivers/net/ethernet/microchip/sparx5/sparx5_vcap_ag_api.h
+++ b/drivers/net/ethernet/microchip/sparx5/sparx5_vcap_ag_api.h
@@ -1,10 +1,10 @@
 /* SPDX-License-Identifier: BSD-3-Clause */
-/* Copyright (C) 2022 Microchip Technology Inc. and its subsidiaries.
+/* Copyright (C) 2026 Microchip Technology Inc. and its subsidiaries.
  * Microchip VCAP API
  */
 
-/* This file is autogenerated by cml-utils 2022-10-13 10:04:41 +0200.
- * Commit ID: fd7cafd175899f0672c73afb3a30fc872500ae86
+/* This file is autogenerated by cml-utils 2026-04-28 13:55:50 +0200.
+ * Commit ID: cd2494df16a1d3b14f650f720f6a8126b88b553a
  */
 
 #ifndef __SPARX5_VCAP_AG_API_H__
diff --git a/drivers/net/ethernet/microchip/vcap/vcap_ag_api.h b/drivers/net/ethernet/microchip/vcap/vcap_ag_api.h
index 4735fad05708..487d56440936 100644
--- a/drivers/net/ethernet/microchip/vcap/vcap_ag_api.h
+++ b/drivers/net/ethernet/microchip/vcap/vcap_ag_api.h
@@ -1,10 +1,10 @@
 /* SPDX-License-Identifier: BSD-3-Clause */
-/* Copyright (C) 2023 Microchip Technology Inc. and its subsidiaries.
+/* Copyright (C) 2026 Microchip Technology Inc. and its subsidiaries.
  * Microchip VCAP API
  */
 
-/* This file is autogenerated by cml-utils 2023-03-13 10:16:42 +0100.
- * Commit ID: 259f0efd6d6d91bfbf62858de153cc757b6bffa3 (dirty)
+/* This file is autogenerated by cml-utils 2026-04-28 13:55:50 +0200.
+ * Commit ID: cd2494df16a1d3b14f650f720f6a8126b88b553a
  */
 
 #ifndef __VCAP_AG_API__
@@ -16,6 +16,7 @@ enum vcap_type {
 	VCAP_TYPE_IS0,
 	VCAP_TYPE_IS1,
 	VCAP_TYPE_IS2,
+	VCAP_TYPE_LPM,
 	VCAP_TYPE_MAX
 };
 
@@ -26,6 +27,8 @@ enum vcap_keyfield_set {
 	VCAP_KFS_5TUPLE_IP6,        /* lan966x is1 X4 */
 	VCAP_KFS_7TUPLE,            /* lan966x is1 X4 */
 	VCAP_KFS_ARP,               /* sparx5 is2 X6, sparx5 es2 X6, lan966x is2 X2 */
+	VCAP_KFS_DBL_IP4,           /* sparx5 lpm X2 */
+	VCAP_KFS_DBL_IP6,           /* sparx5 lpm X6 */
 	VCAP_KFS_DBL_VID,           /* lan966x is1 X1 */
 	VCAP_KFS_DMAC_VID,          /* lan966x is1 X1 */
 	VCAP_KFS_ETAG,              /* sparx5 is0 X2 */
@@ -49,6 +52,8 @@ enum vcap_keyfield_set {
 	VCAP_KFS_OAM,               /* lan966x is2 X2 */
 	VCAP_KFS_PURE_5TUPLE_IP4,   /* sparx5 is0 X3 */
 	VCAP_KFS_RT,                /* lan966x is1 X1 */
+	VCAP_KFS_SGL_IP4,           /* sparx5 lpm X1 */
+	VCAP_KFS_SGL_IP6,           /* sparx5 lpm X3 */
 	VCAP_KFS_SMAC_SIP4,         /* lan966x is2 X1 */
 	VCAP_KFS_SMAC_SIP6,         /* lan966x is2 X2 */
 	VCAP_KFS_VID,               /* lan966x es0 X1 */
@@ -117,6 +122,10 @@ enum vcap_keyfield_set {
  *   tagged, 7: Triple tagged
  * VCAP_KF_ACL_GRP_ID: W8, sparx5: es2
  *   Used in interface map table
+ * VCAP_KF_AFFIX: W10, sparx5: lpm
+ *   LPM affix. Defaults to 0. Using the LPM affix, the VCAP LPM can be split into
+ *   multiple logically separate routing tables, e.g. to support features such as
+ *   VRF-lite.
  * VCAP_KF_ARP_ADDR_SPACE_OK_IS: W1, sparx5: is2/es2, lan966x: is2
  *   Set if hardware address is Ethernet
  * VCAP_KF_ARP_LEN_OK_IS: W1, sparx5: is2/es2, lan966x: is2
@@ -133,6 +142,9 @@ enum vcap_keyfield_set {
  *   Target Hardware Address = SMAC (RARP)
  * VCAP_KF_COSID_CLS: W3, sparx5: es0/es2
  *   Class of service
+ * VCAP_KF_DST_FLAG: W1, sparx5: lpm
+ *   0: IP4_XIP/IP6_XIP is only to be used for SIP matching, 1: IP4_XIP/IP6_XIP is
+ *   only to be used for DIP matching
  * VCAP_KF_ES0_ISDX_KEY_ENA: W1, sparx5: es2
  *   The value taken from the IFH .FWD.ES0_ISDX_KEY_ENA
  * VCAP_KF_ETYPE: W16, sparx5: is0/is2/es2, lan966x: is1/is2
@@ -167,8 +179,22 @@ enum vcap_keyfield_set {
  *   Mapping: 0: DEFAULT 1: LOOPBACK 2: MASQUERADE 3: CPU_VD
  * VCAP_KF_IF_IGR_PORT_SEL: W1, sparx5: es2
  *   Selector for IF_IGR_PORT: physical port number or ERLEG
+ * VCAP_KF_IP4_DIP: W32, sparx5: lpm
+ *   IPv4 destination address. Used for IPv4 MC routing.
  * VCAP_KF_IP4_IS: W1, sparx5: is0/is2/es2, lan966x: is1/is2
  *   Set if frame has EtherType = 0x800 and IP version = 4
+ * VCAP_KF_IP4_SIP: W32, sparx5: lpm
+ *   IPv4 source address. Used for IPv4 MC routing.
+ * VCAP_KF_IP4_XIP: W32, sparx5: lpm
+ *   IPv4 address. Used for IPv4 UC routing as well as IPv4 Source/Destination
+ *   Guard.
+ * VCAP_KF_IP6_DIP: W128, sparx5: lpm
+ *   IPv6 destination address. Used for IPv6 MC routing.
+ * VCAP_KF_IP6_SIP: W128, sparx5: lpm
+ *   IPv6 source address. Used for IPv6 MC routing.
+ * VCAP_KF_IP6_XIP: W128, sparx5: lpm
+ *   IPv6 address. Used for IPv6 UC routing as well as IPv6 Source/Destination
+ *   Guard.
  * VCAP_KF_IP_MC_IS: W1, sparx5: is0, lan966x: is1
  *   Set if frame is IPv4 frame and frame's destination MAC address is an IPv4
  *   multicast address (0x01005E0 /25). Set if frame is IPv6 frame and frame's
@@ -352,8 +378,8 @@ enum vcap_keyfield_set {
  *   Set if frame is IPv4/IPv6 TCP or UDP frame (IP protocol/next header equals 6
  *   or 17)
  * VCAP_KF_TYPE: sparx5 is0 W2, sparx5 is0 W1, sparx5 is2 W4, sparx5 is2 W2,
- *   sparx5 es0 W1, sparx5 es2 W3, lan966x is1 W1, lan966x is1 W2, lan966x is2 W4,
- *   lan966x is2 W2
+ *   sparx5 es0 W1, sparx5 lpm W1, sparx5 es2 W3, lan966x is1 W1, lan966x is1 W2,
+ *   lan966x is2 W4, lan966x is2 W2
  *   Keyset type id - set by the API
  */
 
@@ -387,6 +413,7 @@ enum vcap_key_field {
 	VCAP_KF_8021Q_VLAN_TAGGED_IS,
 	VCAP_KF_8021Q_VLAN_TAGS,
 	VCAP_KF_ACL_GRP_ID,
+	VCAP_KF_AFFIX,
 	VCAP_KF_ARP_ADDR_SPACE_OK_IS,
 	VCAP_KF_ARP_LEN_OK_IS,
 	VCAP_KF_ARP_OPCODE,
@@ -395,6 +422,7 @@ enum vcap_key_field {
 	VCAP_KF_ARP_SENDER_MATCH_IS,
 	VCAP_KF_ARP_TGT_MATCH_IS,
 	VCAP_KF_COSID_CLS,
+	VCAP_KF_DST_FLAG,
 	VCAP_KF_ES0_ISDX_KEY_ENA,
 	VCAP_KF_ETYPE,
 	VCAP_KF_ETYPE_LEN_IS,
@@ -408,7 +436,13 @@ enum vcap_key_field {
 	VCAP_KF_IF_IGR_PORT_MASK_RNG,
 	VCAP_KF_IF_IGR_PORT_MASK_SEL,
 	VCAP_KF_IF_IGR_PORT_SEL,
+	VCAP_KF_IP4_DIP,
 	VCAP_KF_IP4_IS,
+	VCAP_KF_IP4_SIP,
+	VCAP_KF_IP4_XIP,
+	VCAP_KF_IP6_DIP,
+	VCAP_KF_IP6_SIP,
+	VCAP_KF_IP6_XIP,
 	VCAP_KF_IP_MC_IS,
 	VCAP_KF_IP_PAYLOAD_5TUPLE,
 	VCAP_KF_IP_PAYLOAD_S1_IP6,
@@ -490,6 +524,8 @@ enum vcap_key_field {
 /* Actionset names with origin information */
 enum vcap_actionfield_set {
 	VCAP_AFS_NO_VALUE,          /* initial value */
+	VCAP_AFS_ARP_ENTRY,         /* sparx5 lpm X1 */
+	VCAP_AFS_ARP_PTR,           /* sparx5 lpm X1 */
 	VCAP_AFS_BASE_TYPE,         /* sparx5 is2 X3, sparx5 es2 X3, lan966x is2 X2 */
 	VCAP_AFS_CLASSIFICATION,    /* sparx5 is0 X2 */
 	VCAP_AFS_CLASS_REDUCED,     /* sparx5 is0 X1 */
@@ -506,6 +542,15 @@ enum vcap_actionfield_set {
  *   Logical ID for the entry. This ID is extracted together with the frame in the
  *   CPU extraction header. Only applicable to actions with CPU_COPY_ENA or
  *   HIT_ME_ONCE set.
+ * VCAP_AF_ARP_ENA: W1, sparx5: lpm
+ *   Enable entry for address resolution usage.
+ * VCAP_AF_ARP_PTR: W11, sparx5: lpm
+ *   Pointer to entry in ARP Table.
+ * VCAP_AF_ARP_PTR_REMAP_ENA: W1, sparx5: lpm
+ *   If this bit is set, ARP_PTR is used to point to an entry in the ARP pointer
+ *   remap table.
+ * VCAP_AF_ARP_VMID: W9, sparx5: lpm
+ *   Routing lookup. Egress router leg (EVMID).
  * VCAP_AF_CLS_VID_SEL: W3, sparx5: is0
  *   Controls the classified VID: 0: VID_NONE: No action. 1: VID_ADD: New VID =
  *   old VID + VID_VAL. 2: VID_REPLACE: New VID = VID_VAL. 3: VID_FIRST_TAG: New
@@ -562,6 +607,11 @@ enum vcap_actionfield_set {
  *   7: Mapped using mapping table 3, otherwise use mapping table 2
  * VCAP_AF_DSCP_VAL: W6, sparx5: is0/es0, lan966x: is1
  *   See DSCP_ENA.
+ * VCAP_AF_ECMP_CNT: W4, sparx5: lpm
+ *   Number of equal cost, multiple paths routes to DIP.
+ * VCAP_AF_ENCAP_ID: W10, sparx5: lpm
+ *   Index into REW:ENCAP_IP4 when encapsulating IP packet in IPv4. Disabled when
+ *   set to 0.
  * VCAP_AF_ES2_REW_CMD: W3, sparx5: es2
  *   Command forwarded to REW: 0: No action. 1: SWAP MAC addresses. 2: Do L2CP
  *   DMAC translation when entering or leaving a tunnel.
@@ -610,6 +660,12 @@ enum vcap_actionfield_set {
  *   0: Forward based on PIPELINE_PT and FWD_SEL
  * VCAP_AF_LRN_DIS: W1, sparx5: is2, lan966x: is2
  *   Setting this bit to 1 disables learning of frames hitting this action.
+ * VCAP_AF_MAC_LSB: W32, sparx5: lpm
+ *   32 least significant bits of MAC address. Used for ARP entry and/or
+ *   (SMAC,SIP)/(DMAC,DIP) check.
+ * VCAP_AF_MAC_MSB: W16, sparx5: lpm
+ *   16 most significant bits of MAC address. Used for ARP entry and/or
+ *   (SMAC,SIP)/(DMAC,DIP) check.
  * VCAP_AF_MAP_IDX: W9, sparx5: is0
  *   Index for QoS mapping table lookup
  * VCAP_AF_MAP_KEY: W3, sparx5: is0
@@ -715,9 +771,17 @@ enum vcap_actionfield_set {
  *   See QOS_ENA.
  * VCAP_AF_REW_OP: W16, lan966x: is2
  *   Rewriter operation command.
+ * VCAP_AF_RGID: W3, sparx5: lpm
+ *   Route Group ID. Used for SIP RPF check.
+ * VCAP_AF_RSDX: W12, sparx5: lpm
+ *   Router Leg Service Index for ARP table entry.
  * VCAP_AF_RT_DIS: W1, sparx5: is2
  *   If set, routing is disallowed. Only applies when IS_INNER_ACL is 0. See also
  *   IGR_ACL_ENA, EGR_ACL_ENA, and RLEG_STAT_IDX.
+ * VCAP_AF_SECUR_MATCH_MAC_ENA: W1, sparx5: lpm
+ *   Enable Security MAC check.
+ * VCAP_AF_SECUR_MATCH_VMID_ENA: W1, sparx5: lpm
+ *   Enable Security VMID check.
  * VCAP_AF_SFID_ENA: W1, lan966x: is1
  *   If set, SFID_VAL is used to lookup ANA::SFID.
  * VCAP_AF_SFID_VAL: W8, lan966x: is1
@@ -726,6 +790,8 @@ enum vcap_actionfield_set {
  *   If set, SGID_VAL is used to lookup ANA::SGID.
  * VCAP_AF_SGID_VAL: W8, lan966x: is1
  *   Stream gate identifier.
+ * VCAP_AF_SIP_RPF_ENA: W1, sparx5: lpm
+ *   Enable use for SIP RPF check.
  * VCAP_AF_SWAP_MACS_ENA: W1, sparx5: es0
  *   This setting is only active when FWD_SEL = 1 or FWD_SEL = 2 and PIPELINE_ACT
  *   = LBK_ASM. 0: No action. 1: Swap MACs and clear bit 40 in new SMAC.
@@ -774,7 +840,7 @@ enum vcap_actionfield_set {
  * VCAP_AF_TAG_C_VID_SEL: W2, sparx5: es0
  *   Selects VID for ES0 tag C. The resulting VID is termed C-TAG.VID. 0:
  *   Classified VID. 1: VID_C_VAL. 2: IFH.ENCAP.GVID. 3: Reserved.
- * VCAP_AF_TYPE: W1, sparx5: is0, lan966x: is1
+ * VCAP_AF_TYPE: sparx5 is0 W1, sparx5 lpm W2, lan966x is1 W1
  *   Actionset type id - Set by the API
  * VCAP_AF_UNTAG_VID_ENA: W1, sparx5: es0
  *   Controls insertion of tag C. Untag or insert mode can be selected. See
@@ -798,12 +864,18 @@ enum vcap_actionfield_set {
  *   If set, use VLAN_POP_CNT as the number of VLAN tags to pop from the incoming
  *   frame. This number is used by the Rewriter. Otherwise, VLAN_POP_CNT from
  *   ANA:PORT:VLAN_CFG.VLAN_POP_CNT is used
+ * VCAP_AF_ZERO_DMAC_CPU_QU: W3, sparx5: lpm
+ *   CPU queue used for CPU redirect if MAC address in ARP entry is all-zeros.
  */
 
 /* Actionfield names */
 enum vcap_action_field {
 	VCAP_AF_NO_VALUE,  /* initial value */
 	VCAP_AF_ACL_ID,
+	VCAP_AF_ARP_ENA,
+	VCAP_AF_ARP_PTR,
+	VCAP_AF_ARP_PTR_REMAP_ENA,
+	VCAP_AF_ARP_VMID,
 	VCAP_AF_CLS_VID_SEL,
 	VCAP_AF_CNT_ID,
 	VCAP_AF_COPY_PORT_NUM,
@@ -823,6 +895,8 @@ enum vcap_action_field {
 	VCAP_AF_DSCP_ENA,
 	VCAP_AF_DSCP_SEL,
 	VCAP_AF_DSCP_VAL,
+	VCAP_AF_ECMP_CNT,
+	VCAP_AF_ENCAP_ID,
 	VCAP_AF_ES2_REW_CMD,
 	VCAP_AF_ESDX,
 	VCAP_AF_FWD_KILL_ENA,
@@ -839,6 +913,8 @@ enum vcap_action_field {
 	VCAP_AF_ISDX_VAL,
 	VCAP_AF_LOOP_ENA,
 	VCAP_AF_LRN_DIS,
+	VCAP_AF_MAC_LSB,
+	VCAP_AF_MAC_MSB,
 	VCAP_AF_MAP_IDX,
 	VCAP_AF_MAP_KEY,
 	VCAP_AF_MAP_LOOKUP_SEL,
@@ -874,11 +950,16 @@ enum vcap_action_field {
 	VCAP_AF_QOS_ENA,
 	VCAP_AF_QOS_VAL,
 	VCAP_AF_REW_OP,
+	VCAP_AF_RGID,
+	VCAP_AF_RSDX,
 	VCAP_AF_RT_DIS,
+	VCAP_AF_SECUR_MATCH_MAC_ENA,
+	VCAP_AF_SECUR_MATCH_VMID_ENA,
 	VCAP_AF_SFID_ENA,
 	VCAP_AF_SFID_VAL,
 	VCAP_AF_SGID_ENA,
 	VCAP_AF_SGID_VAL,
+	VCAP_AF_SIP_RPF_ENA,
 	VCAP_AF_SWAP_MACS_ENA,
 	VCAP_AF_TAG_A_DEI_SEL,
 	VCAP_AF_TAG_A_PCP_SEL,
@@ -901,6 +982,7 @@ enum vcap_action_field {
 	VCAP_AF_VID_VAL,
 	VCAP_AF_VLAN_POP_CNT,
 	VCAP_AF_VLAN_POP_CNT_ENA,
+	VCAP_AF_ZERO_DMAC_CPU_QU,
 };
 
 #endif /* __VCAP_AG_API__ */
diff --git a/drivers/net/ethernet/microchip/vcap/vcap_model_kunit.c b/drivers/net/ethernet/microchip/vcap/vcap_model_kunit.c
index 5dbfc0d0c369..75819b49e110 100644
--- a/drivers/net/ethernet/microchip/vcap/vcap_model_kunit.c
+++ b/drivers/net/ethernet/microchip/vcap/vcap_model_kunit.c
@@ -1,10 +1,10 @@
 // SPDX-License-Identifier: BSD-3-Clause
-/* Copyright (C) 2023 Microchip Technology Inc. and its subsidiaries.
+/* Copyright (C) 2026 Microchip Technology Inc. and its subsidiaries.
  * Microchip VCAP test model interface for kunit testing
  */
 
-/* This file is autogenerated by cml-utils 2023-02-10 11:16:00 +0100.
- * Commit ID: c30fb4bf0281cd4a7133bdab6682f9e43c872ada
+/* This file is autogenerated by cml-utils 2026-04-28 13:55:50 +0200.
+ * Commit ID: cd2494df16a1d3b14f650f720f6a8126b88b553a
  */
 
 #include <linux/types.h>

-- 
2.52.0



^ permalink raw reply related

* [PATCH net-next 1/9] net: microchip: vcap: fix rule move for rules of coprime size
From: Jens Emil Schulz Østergaard @ 2026-06-12 12:37 UTC (permalink / raw)
  To: Horatiu Vultur, UNGLinuxDriver, Andrew Lunn, David S. Miller,
	Eric Dumazet, Jakub Kicinski, Paolo Abeni, Daniel Machon,
	Steen Hegelund, Kees Cook, Gustavo A. R. Silva
  Cc: netdev, linux-kernel, linux-arm-kernel, linux-hardening,
	Jens Emil Schulz Østergaard
In-Reply-To: <20260612-sparx5_l3_routing-v1-0-fc3c10160f49@microchip.com>

Fix a discrepancy between how software and hardware moves rule
addresses on insert and delete. Hardware moves a block of addresses by a
uniform offset. The software representation stores the addresses of
rules. These are updated individually by computing a space-optimal
address relative to its neighbor.

This worked when all rule sizes in a VCAP formed a divisibility
chain (e.g. 1, 2, 6, 12), because any valid offset for the largest
rule was also valid for all smaller rules. At the moment all clients of
the VCAP library only uses rules for which this is true.

But the LPM VCAP, necessary for l3 routing, will introduce rule types of
coprime size, so the algorithm must be fixed. This is in preparation for
adding support for L3 routing to sparx5 and lan969x, which requires the
LPM VCAP with rules of size 2 and 3. This patch fixes the issue by
making sure the software representation is updated in the same way as
the hardware.

The VCAP rule model has the following properties:

- A rule has keyset size K and actionset size M; rule size N =
  max(K, M).
- Rules are naturally aligned to their size (start_addr % N == 0).
- Rules are sorted by size with larger rules at higher addresses.
- HW moves a contiguous block of addresses by a uniform offset.

Introducing rules of coprime size 2 and 3 exposes these four bugs:

1) Move count on insert includes addresses beyond the moved block.
   When padding exists between rules of different sizes, the count
   overshoots, corrupting the rule above.

2) Move offset does not preserve alignment for the entire block.
   Per-rule optimal offsets differ from the uniform HW offset,
   causing HW/SW desync and misaligned rules.

3) On deletion, HW memory at the removed rule's addresses is not
   initialized. The moved block may not fully cover them due to
   padding, leaving stale data.

4) On deletion, padding between rules is not reclaimed.

Example for issue 1, insert corrupts rule above:

Starting state (X6 rule followed by X2):

  Addr  Rule
    35  X6
    34  X6
    33  X6
    32  X6
    31  X6
    30  X6
    29  X2
    28  X2
    27

Insert X3 between X6 and X2. SW places rules optimally, X2 needs
padding:

  Addr  Rule (SW)
    35  X6
    34  X6
    33  X6
    32  X6
    31  X6
    30  X6
    29  X3
    28  X3
    27  X3
    26       (pad)
    25  X2
    24  X2

Move count = X3.addr - X2.addr = 27 - 24 = 3. HW moves 3 addresses
starting at 28: addrs {28, 29, 30}. But addr 30 is the base of X6,
so the move corrupts it:

  Addr  Rule (HW)
    35  X6
    34  X6
    33  X6
    32  X6
    31  X6
    30  ??   <- corrupted, was base of X6
    ...
    26  X6   <- stale copy of addr 30
    25  X2
    24  X2

Example for issue 2, delete misaligns rule:

Starting state:

  Addr  Rule
    38       <- LAST_VALID_ADDR
    35  X6
    34  X6
    33  X6
    32  X6
    31  X6
    30  X6
    29  X3
    28  X3
    27  X3
    26       (pad)
    25  X2
    24  X2

Delete X6. Old code computes: offset = 39 - 30 - 6 = 3, gap = 0,
each rule moves by 6 + 0 + 3 = 9:

  X3: 27 + 9 = 36   (36 % 3 == 0, OK)
  X2: 24 + 9 = 33   (33 % 2 == 1, MISALIGNED)

Offset 9 is not a multiple of LCM(3, 2) = 6, breaking X2 alignment.

Example for issues 1 + 2, insert desyncs HW/SW and misaligns:

Starting state:

  Addr  Rule
    26  X3   <- LAST_VALID_ADDR
    25  X3
    24  X3
    23  X2
    22  X2

Insert X6. SW calculates different per-rule offsets:

  X3 needs offset  9 (to addr 15)
  X2 needs offset 10 (to addr 12)

Old code computes: count = 18 - 12 = 6 (should be 5), offset = 10.
HW applies uniform offset 10 to 6 addresses:

  Addr  Rule (SW)     Addr  Rule (HW)
    17  X3              17
    16  X3              16  X3
    15  X3              15  X3
    14                  14  X3  <- misaligned (14 % 3 != 0)
    13  X2              13  X2
    12  X2              12  X2

HW and SW are out of sync, and X3 is misaligned in HW.

The new move algorithm computes a single uniform offset for the
entire block, aligned to the LCM of all rule sizes in that block.
This guarantees that every rule lands on a naturally aligned address
after the HW move, and that HW and SW stay in sync.

On insert:

1. Determine the move block: all rules below the insertion point.
2. Compute the LCM of all rule sizes in the block
   (vcap_find_move_block_lcm).
3. Compute the move offset as the inserted rule's size rounded up
   to the next LCM multiple (vcap_move_from_block). This is the
   uniform offset applied to the entire block.
4. Move count is exactly the number of addresses occupied by the
   block (no overshoot into the rule above).
5. Issue a single HW move, then update all SW rule addresses by
   the same uniform offset (vcap_move_rules_sw).
6. Place the new rule in the gap created by the move.

On delete:

1. Initialize HW memory at the deleted rule's addresses before
   moving, since the moved block may not fully cover them due to
   padding.
2. Reclaim any padding between rules in the block.
3. Compute the LCM-aligned uniform offset and move count as above.
4. Issue a single HW move and update SW addresses uniformly.

No VCAP api client uses rules of coprime size today, so the bug does not
fire in any existing configuration. The LPM VCAP for sparx5 and lan969x,
added later in this series, is the first instance with rules of coprime
sizes (2 and 3), and therefore the first to reach this bug.

Reviewed-by: Steen Hegelund <Steen.Hegelund@microchip.com>
Signed-off-by: Jens Emil Schulz Østergaard <jensemil.schulzostergaard@microchip.com>
---
 drivers/net/ethernet/microchip/vcap/vcap_api.c     | 178 +++++++++++++--------
 .../net/ethernet/microchip/vcap/vcap_api_kunit.c   |  81 ++++++----
 2 files changed, 164 insertions(+), 95 deletions(-)

diff --git a/drivers/net/ethernet/microchip/vcap/vcap_api.c b/drivers/net/ethernet/microchip/vcap/vcap_api.c
index 0fdb5e363bad..6946fd738458 100644
--- a/drivers/net/ethernet/microchip/vcap/vcap_api.c
+++ b/drivers/net/ethernet/microchip/vcap/vcap_api.c
@@ -4,7 +4,9 @@
  * Copyright (c) 2022 Microchip Technology Inc. and its subsidiaries.
  */
 
+#include <linux/lcm.h>
 #include <linux/types.h>
+#include <linux/minmax.h>
 
 #include "vcap_api_private.h"
 
@@ -2096,12 +2098,63 @@ static u32 vcap_set_rule_id(struct vcap_rule_internal *ri)
 	return ri->data.id;
 }
 
+static void vcap_move_from_block(u32 base_addr,
+				 struct vcap_rule_internal *block_first,
+				 struct vcap_rule_internal *block_last,
+				 int block_lcm, struct vcap_rule_move *move)
+{
+	int unaligned_offset;
+
+	if (!block_first || !block_last)
+		return;
+
+	move->addr = block_last->addr;
+	move->count = block_first->addr + block_first->size - block_last->addr;
+	/* Integer division rounds toward zero. We want to round toward +inf
+	 * for the positive case (insertion) to ensure enough room.
+	 */
+	unaligned_offset = (block_first->addr + block_first->size) - base_addr;
+	if (unaligned_offset > 0)
+		move->offset =
+			((unaligned_offset + (block_lcm - 1)) / block_lcm) *
+			block_lcm;
+	else
+		move->offset = (unaligned_offset / block_lcm) * block_lcm;
+}
+
+static void vcap_move_rules_sw(struct vcap_admin *admin,
+			       struct vcap_rule_internal *pos,
+			       struct vcap_rule_move const *move)
+{
+	if (move->count == 0 || move->offset == 0)
+		return;
+
+	list_for_each_entry_continue(pos, &admin->rules, list)
+		pos->addr -= move->offset;
+}
+
+static int vcap_find_move_block_lcm(struct vcap_admin *admin,
+				    struct vcap_rule_internal *pos)
+{
+	int block_lcm = 1;
+
+	list_for_each_entry_continue(pos, &admin->rules, list) {
+		block_lcm = lcm(block_lcm, pos->size);
+		if (pos->size <= 2)
+			break;
+	}
+
+	return block_lcm;
+}
+
 static int vcap_insert_rule(struct vcap_rule_internal *ri,
 			    struct vcap_rule_move *move)
 {
+	struct vcap_rule_internal *duprule, *iter, *block_first = NULL,
+						   *block_last;
 	int sw_count = ri->vctrl->vcaps[ri->admin->vtype].sw_count;
-	struct vcap_rule_internal *duprule, *iter, *elem = NULL;
 	struct vcap_admin *admin = ri->admin;
+	int block_lcm;
 	u32 addr;
 
 	ri->sort_key = vcap_sort_key(sw_count, ri->size, ri->data.user,
@@ -2114,12 +2167,12 @@ static int vcap_insert_rule(struct vcap_rule_internal *ri,
 	 */
 	list_for_each_entry(iter, &admin->rules, list) {
 		if (ri->sort_key < iter->sort_key) {
-			elem = iter;
+			block_first = iter;
 			break;
 		}
 	}
 
-	if (!elem) {
+	if (!block_first) {
 		ri->addr = vcap_next_rule_addr(admin->last_used_addr, ri);
 		admin->last_used_addr = ri->addr;
 
@@ -2132,10 +2185,13 @@ static int vcap_insert_rule(struct vcap_rule_internal *ri,
 		return 0;
 	}
 
-	/* Reuse the space of the current rule */
-	addr = elem->addr + elem->size;
+	block_last = list_last_entry(&admin->rules, typeof(*ri), list);
+
+	/* Reuse the space and padding of the current rule */
+	addr = admin->last_valid_addr + 1;
+	if (!list_is_first(&block_first->list, &admin->rules))
+		addr = list_prev_entry(block_first, list)->addr;
 	ri->addr = vcap_next_rule_addr(addr, ri);
-	addr = ri->addr;
 
 	/* Add a copy of the rule to the VCAP list */
 	duprule = vcap_dup_rule(ri, ri->state == VCAP_RS_DISABLED);
@@ -2143,29 +2199,34 @@ static int vcap_insert_rule(struct vcap_rule_internal *ri,
 		return PTR_ERR(duprule);
 
 	/* Add before the current entry */
-	list_add_tail(&duprule->list, &elem->list);
+	list_add_tail(&duprule->list, &block_first->list);
 
-	/* Update the current rule */
-	elem->addr = vcap_next_rule_addr(addr, elem);
-	addr = elem->addr;
+	/* Collect block move info in struct move, to update VCAP HW later */
+	block_lcm = vcap_find_move_block_lcm(admin, duprule);
+	vcap_move_from_block(ri->addr, block_first, block_last, block_lcm,
+			     move);
 
-	/* Update the address in the remaining rules in the list */
-	list_for_each_entry_continue(elem, &admin->rules, list) {
-		elem->addr = vcap_next_rule_addr(addr, elem);
-		addr = elem->addr;
+	if ((int)block_last->addr - move->offset < admin->first_valid_addr) {
+		pr_err("%s:%d: No room for rule size: %u, %u\n", __func__,
+		       __LINE__, duprule->size, admin->first_valid_addr);
+		list_del(&duprule->list);
+		vcap_free_rule(&duprule->data);
+		return -ENOSPC;
 	}
 
-	/* Update the move info */
-	move->addr = admin->last_used_addr;
-	move->count = ri->addr - addr;
-	move->offset = admin->last_used_addr - addr;
-	admin->last_used_addr = addr;
+	/* Apply move to SW */
+	vcap_move_rules_sw(admin, duprule, move);
+	admin->last_used_addr =
+		list_last_entry(&admin->rules, typeof(*ri), list)->addr;
 	return 0;
 }
 
 static void vcap_move_rules(struct vcap_rule_internal *ri,
 			    struct vcap_rule_move *move)
 {
+	if (move->count == 0 || move->offset == 0)
+		return;
+
 	ri->vctrl->ops->move(ri->ndev, ri->admin, move->addr,
 			 move->offset, move->count);
 }
@@ -2275,8 +2336,7 @@ int vcap_add_rule(struct vcap_rule *rule)
 		       __func__, __LINE__, ret);
 		goto out;
 	}
-	if (move.count > 0)
-		vcap_move_rules(ri, &move);
+	vcap_move_rules(ri, &move);
 
 	/* Set the counter to zero */
 	ret = vcap_write_counter(ri, &ctr);
@@ -2488,59 +2548,52 @@ int vcap_mod_rule(struct vcap_rule *rule)
 }
 EXPORT_SYMBOL_GPL(vcap_mod_rule);
 
-/* Return the alignment offset for a new rule address */
-static int vcap_valid_rule_move(struct vcap_rule_internal *el, int offset)
-{
-	return (el->addr + offset) % el->size;
-}
-
-/* Update the rule address with an offset */
-static void vcap_adjust_rule_addr(struct vcap_rule_internal *el, int offset)
-{
-	el->addr += offset;
-}
-
 /* Rules needs to be moved to fill the gap of the deleted rule */
 static int vcap_fill_rule_gap(struct vcap_rule_internal *ri)
 {
+	struct vcap_rule_internal *block_first = NULL, *block_last = NULL;
 	struct vcap_admin *admin = ri->admin;
-	struct vcap_rule_internal *elem;
-	struct vcap_rule_move move;
-	int gap = 0, offset = 0;
+	struct net_device *ndev = ri->ndev;
+	struct vcap_rule_move move = {};
+	int block_lcm, addr;
 
-	/* If the first rule is deleted: Move other rules to the top */
-	if (list_is_first(&ri->list, &admin->rules))
-		offset = admin->last_valid_addr + 1 - ri->addr - ri->size;
+	if (!list_is_last(&ri->list, &admin->rules)) {
+		block_first = list_next_entry(ri, list);
+		block_last = list_last_entry(&admin->rules, typeof(*ri), list);
+	}
 
-	/* Locate gaps between odd size rules and adjust the move */
-	elem = ri;
-	list_for_each_entry_continue(elem, &admin->rules, list)
-		gap += vcap_valid_rule_move(elem, ri->size);
+	addr = admin->last_valid_addr + 1;
+	if (!list_is_first(&ri->list, &admin->rules))
+		addr = list_prev_entry(ri, list)->addr;
 
-	/* Update the address in the remaining rules in the list */
-	elem = ri;
-	list_for_each_entry_continue(elem, &admin->rules, list)
-		vcap_adjust_rule_addr(elem, ri->size + gap + offset);
+	block_lcm = vcap_find_move_block_lcm(admin, ri);
+	vcap_move_from_block(addr, block_first, block_last, block_lcm, &move);
 
-	/* Update the move info */
-	move.addr = admin->last_used_addr;
-	move.count = ri->addr - admin->last_used_addr - gap;
-	move.offset = -(ri->size + gap + offset);
+	/* Apply move to SW representation */
+	vcap_move_rules_sw(admin, ri, &move);
 
-	/* Do the actual move operation */
+	/* Initialize space used by ri, as it may not be overwritten by move */
+	ri->vctrl->ops->init(ndev, admin, ri->addr, ri->size);
+
+	/* Apply move to HW. The VCAP move command automatically disables
+	 * (initializes) the source addresses, so no separate init is needed
+	 * for the vacated positions.
+	 */
 	vcap_move_rules(ri, &move);
 
-	return gap + offset;
+	if (block_last)
+		return block_last->addr;
+
+	return addr;
 }
 
 /* Delete rule in a VCAP instance */
 int vcap_del_rule(struct vcap_control *vctrl, struct net_device *ndev, u32 id)
 {
-	struct vcap_rule_internal *ri, *elem;
+	struct vcap_rule_internal *ri;
 	struct vcap_admin *admin;
-	int gap = 0, err;
+	int err;
 
-	/* This will later also handle rule moving */
 	if (!ndev)
 		return -ENODEV;
 	err = vcap_api_check(vctrl);
@@ -2553,23 +2606,12 @@ int vcap_del_rule(struct vcap_control *vctrl, struct net_device *ndev, u32 id)
 
 	admin = ri->admin;
 
-	if (ri->addr > admin->last_used_addr)
-		gap = vcap_fill_rule_gap(ri);
+	admin->last_used_addr = vcap_fill_rule_gap(ri);
 
 	/* Delete the rule from the list of rules and the cache */
 	list_del(&ri->list);
-	vctrl->ops->init(ndev, admin, admin->last_used_addr, ri->size + gap);
 	vcap_free_rule(&ri->data);
 
-	/* Update the last used address, set to default when no rules */
-	if (list_empty(&admin->rules)) {
-		admin->last_used_addr = admin->last_valid_addr + 1;
-	} else {
-		elem = list_last_entry(&admin->rules, struct vcap_rule_internal,
-				       list);
-		admin->last_used_addr = elem->addr;
-	}
-
 	mutex_unlock(&admin->lock);
 	return err;
 }
diff --git a/drivers/net/ethernet/microchip/vcap/vcap_api_kunit.c b/drivers/net/ethernet/microchip/vcap/vcap_api_kunit.c
index ce26ccbdccdf..cec818938ec9 100644
--- a/drivers/net/ethernet/microchip/vcap/vcap_api_kunit.c
+++ b/drivers/net/ethernet/microchip/vcap/vcap_api_kunit.c
@@ -1575,7 +1575,7 @@ static void vcap_api_rule_insert_in_order_test(struct kunit *test)
 		.lookups = 4,
 		.last_valid_addr = 3071,
 		.first_valid_addr = 0,
-		.last_used_addr = 800,
+		.last_used_addr = 804,
 		.cache = {
 			.keystream = keydata,
 			.maskstream = mskdata,
@@ -1583,15 +1583,28 @@ static void vcap_api_rule_insert_in_order_test(struct kunit *test)
 		},
 	};
 
+	struct vcap_rule_internal ri0 = {
+		.data = {
+			.id = 2001,
+		},
+		.addr = 804,
+		.size = 12,
+		.sort_key = vcap_sort_key(12, 12, 0, 0),
+		.admin = &admin,
+		.counter_id = 2002,
+		.vctrl = &test_vctrl,
+	};
+
 	vcap_test_api_init(&admin);
+	list_add_tail(&ri0.list, &admin.rules);
 
 	/* Create rules with different sizes and check that they are placed
 	 * at the correct address in the VCAP according to size
 	 */
-	test_vcap_xn_rule_creator(test, 10000, VCAP_USER_QOS, 10, 500, 12, 780);
-	test_vcap_xn_rule_creator(test, 10000, VCAP_USER_QOS, 20, 400, 6, 774);
-	test_vcap_xn_rule_creator(test, 10000, VCAP_USER_QOS, 30, 300, 3, 771);
-	test_vcap_xn_rule_creator(test, 10000, VCAP_USER_QOS, 40, 200, 2, 768);
+	test_vcap_xn_rule_creator(test, 10000, VCAP_USER_QOS, 10, 500, 12, 792);
+	test_vcap_xn_rule_creator(test, 10000, VCAP_USER_QOS, 20, 400, 6, 786);
+	test_vcap_xn_rule_creator(test, 10000, VCAP_USER_QOS, 30, 300, 3, 783);
+	test_vcap_xn_rule_creator(test, 10000, VCAP_USER_QOS, 40, 200, 2, 780);
 
 	vcap_del_rule(&test_vctrl, &test_netdev, 200);
 	vcap_del_rule(&test_vctrl, &test_netdev, 300);
@@ -1613,7 +1626,7 @@ static void vcap_api_rule_insert_reverse_order_test(struct kunit *test)
 		.lookups = 4,
 		.last_valid_addr = 3071,
 		.first_valid_addr = 0,
-		.last_used_addr = 800,
+		.last_used_addr = 804,
 		.cache = {
 			.keystream = keydata,
 			.maskstream = mskdata,
@@ -1621,40 +1634,54 @@ static void vcap_api_rule_insert_reverse_order_test(struct kunit *test)
 		},
 	};
 	struct vcap_rule_internal *elem;
-	u32 exp_addr[] = {780, 774, 771, 768, 767};
+	u32 exp_addr[] = {804, 792, 786, 783, 780};
 	int idx;
 
+	/* Existing X12 rule at last_used_addr */
+	struct vcap_rule_internal ri0 = {
+		.data = {
+			.id = 2001,
+		},
+		.addr = 804,
+		.size = 12,
+		.sort_key = vcap_sort_key(12, 12, 0, 0),
+		.admin = &admin,
+		.counter_id = 2002,
+		.vctrl = &test_vctrl,
+	};
+
 	vcap_test_api_init(&admin);
+	list_add_tail(&ri0.list, &admin.rules);
 
 	/* Create rules with different sizes and check that they are placed
 	 * at the correct address in the VCAP according to size
 	 */
-	test_vcap_xn_rule_creator(test, 10000, VCAP_USER_QOS, 20, 200, 2, 798);
+	test_vcap_xn_rule_creator(test, 10000, VCAP_USER_QOS, 20, 200, 2, 802);
 	KUNIT_EXPECT_EQ(test, 0, test_move_offset);
 	KUNIT_EXPECT_EQ(test, 0, test_move_count);
 	KUNIT_EXPECT_EQ(test, 0, test_move_addr);
 
-	test_vcap_xn_rule_creator(test, 10000, VCAP_USER_QOS, 30, 300, 3, 795);
-	KUNIT_EXPECT_EQ(test, 6, test_move_offset);
-	KUNIT_EXPECT_EQ(test, 3, test_move_count);
-	KUNIT_EXPECT_EQ(test, 798, test_move_addr);
+	test_vcap_xn_rule_creator(test, 10000, VCAP_USER_QOS, 30, 300, 3, 801);
+	KUNIT_EXPECT_EQ(test, 4, test_move_offset);
+	KUNIT_EXPECT_EQ(test, 2, test_move_count);
+	KUNIT_EXPECT_EQ(test, 802, test_move_addr);
 
-	test_vcap_xn_rule_creator(test, 10000, VCAP_USER_QOS, 40, 400, 6, 792);
+	test_vcap_xn_rule_creator(test, 10000, VCAP_USER_QOS, 40, 400, 6, 798);
 	KUNIT_EXPECT_EQ(test, 6, test_move_offset);
 	KUNIT_EXPECT_EQ(test, 6, test_move_count);
-	KUNIT_EXPECT_EQ(test, 792, test_move_addr);
+	KUNIT_EXPECT_EQ(test, 798, test_move_addr);
 
-	test_vcap_xn_rule_creator(test, 10000, VCAP_USER_QOS, 50, 500, 12, 780);
-	KUNIT_EXPECT_EQ(test, 18, test_move_offset);
+	test_vcap_xn_rule_creator(test, 10000, VCAP_USER_QOS, 50, 500, 12, 792);
+	KUNIT_EXPECT_EQ(test, 12, test_move_offset);
 	KUNIT_EXPECT_EQ(test, 12, test_move_count);
-	KUNIT_EXPECT_EQ(test, 786, test_move_addr);
+	KUNIT_EXPECT_EQ(test, 792, test_move_addr);
 
 	idx = 0;
 	list_for_each_entry(elem, &admin.rules, list) {
 		KUNIT_EXPECT_EQ(test, exp_addr[idx], elem->addr);
 		++idx;
 	}
-	KUNIT_EXPECT_EQ(test, 768, admin.last_used_addr);
+	KUNIT_EXPECT_EQ(test, 780, admin.last_used_addr);
 
 	vcap_del_rule(&test_vctrl, &test_netdev, 500);
 	vcap_del_rule(&test_vctrl, &test_netdev, 400);
@@ -1774,7 +1801,7 @@ static void vcap_api_rule_remove_in_middle_test(struct kunit *test)
 	KUNIT_EXPECT_EQ(test, 768, test_move_addr);
 	KUNIT_EXPECT_EQ(test, -6, test_move_offset);
 	KUNIT_EXPECT_EQ(test, 6, test_move_count);
-	KUNIT_EXPECT_EQ(test, 768, test_init_start);
+	KUNIT_EXPECT_EQ(test, 774, test_init_start);
 	KUNIT_EXPECT_EQ(test, 6, test_init_count);
 	KUNIT_EXPECT_EQ(test, 774, admin.last_used_addr);
 
@@ -1784,8 +1811,8 @@ static void vcap_api_rule_remove_in_middle_test(struct kunit *test)
 	KUNIT_EXPECT_EQ(test, 774, test_move_addr);
 	KUNIT_EXPECT_EQ(test, -4, test_move_offset);
 	KUNIT_EXPECT_EQ(test, 2, test_move_count);
-	KUNIT_EXPECT_EQ(test, 774, test_init_start);
-	KUNIT_EXPECT_EQ(test, 4, test_init_count);
+	KUNIT_EXPECT_EQ(test, 777, test_init_start);
+	KUNIT_EXPECT_EQ(test, 3, test_init_count);
 	KUNIT_EXPECT_EQ(test, 778, admin.last_used_addr);
 
 	test_init_rule_deletion();
@@ -1794,8 +1821,8 @@ static void vcap_api_rule_remove_in_middle_test(struct kunit *test)
 	KUNIT_EXPECT_EQ(test, 778, test_move_addr);
 	KUNIT_EXPECT_EQ(test, -20, test_move_offset);
 	KUNIT_EXPECT_EQ(test, 2, test_move_count);
-	KUNIT_EXPECT_EQ(test, 778, test_init_start);
-	KUNIT_EXPECT_EQ(test, 20, test_init_count);
+	KUNIT_EXPECT_EQ(test, 780, test_init_start);
+	KUNIT_EXPECT_EQ(test, 12, test_init_count);
 	KUNIT_EXPECT_EQ(test, 798, admin.last_used_addr);
 
 	test_init_rule_deletion();
@@ -1855,11 +1882,11 @@ static void vcap_api_rule_remove_in_front_test(struct kunit *test)
 	ret = vcap_del_rule(&test_vctrl, &test_netdev, 400);
 	KUNIT_EXPECT_EQ(test, 0, ret);
 	KUNIT_EXPECT_EQ(test, 786, test_move_addr);
-	KUNIT_EXPECT_EQ(test, -8, test_move_offset);
+	KUNIT_EXPECT_EQ(test, -6, test_move_offset);
 	KUNIT_EXPECT_EQ(test, 6, test_move_count);
-	KUNIT_EXPECT_EQ(test, 786, test_init_start);
-	KUNIT_EXPECT_EQ(test, 8, test_init_count);
-	KUNIT_EXPECT_EQ(test, 794, admin.last_used_addr);
+	KUNIT_EXPECT_EQ(test, 792, test_init_start);
+	KUNIT_EXPECT_EQ(test, 6, test_init_count);
+	KUNIT_EXPECT_EQ(test, 792, admin.last_used_addr);
 
 	vcap_del_rule(&test_vctrl, &test_netdev, 200);
 	vcap_del_rule(&test_vctrl, &test_netdev, 300);

-- 
2.52.0



^ permalink raw reply related

* [PATCH net-next 0/9] net: sparx5: add L3 unicast routing offload
From: Jens Emil Schulz Østergaard @ 2026-06-12 12:37 UTC (permalink / raw)
  To: Horatiu Vultur, UNGLinuxDriver, Andrew Lunn, David S. Miller,
	Eric Dumazet, Jakub Kicinski, Paolo Abeni, Daniel Machon,
	Steen Hegelund, Kees Cook, Gustavo A. R. Silva
  Cc: netdev, linux-kernel, linux-arm-kernel, linux-hardening,
	Jens Emil Schulz Østergaard

Add L3 unicast route offloading for IPv4 and IPv6 on Sparx5 and
LAN969x. This enables hardware-accelerated l3 routing between VLAN
upper interfaces on a bridge, with ECMP support.

This series covers the core unicast data path: FIB offload, nexthop
groups, ARP/NDP neighbour resolution, and router leg management.

Routes are programmed into the hardware LPM (Longest Prefix Match)
VCAP, with nexthops resolved through the hardware ARP table for ECMP
or inline VCAP actions for single nexthops.

The LPM VCAP also requires type_id-aware rule decoding, since it has
actionsets of identical length that differ only in their type_id
field.

Patch 1 fixes a latent bug in the VCAP rule-move algorithm that
becomes reachable once the LPM VCAP is added, since LPM is the first
VCAP to use rules of coprime size (2 and 3).

Patches 2-4 are the remaining preparatory commits for the shared VCAP
API. They add the LPM VCAP keysets and actionsets to the autogenerated
model for lan969x and sparx5, update the action-side rule decoder to
differentiate actionsets by type_id, and expose two helpers
(vcap_val_add_rule() and vcap_rule_mod_action_bit()) used by the LPM
driver.

Patch 5 adds autogenerated register macros for the L3 routing blocks
on sparx5 and lan969x.

Patch 6 adds the LPM VCAP implementation for sparx5 and lan969x.

Patches 7-9 add L3 unicast routing to sparx5 and lan969x. The router
code is split into three patches:

Patch 7 (infrastructure + legs): Router leg lifecycle tied to VLAN
uppers on a bridge, address event handling, base MAC tracking,
blackhole leg for blackhole routes, and the hardware enable sequence.

Patch 8 (FIB + nexthop + neighbour CRUD): This is a large patch, and
splitting it further is difficult due to the dependencies in FIB and
neighbour event paths. The destroy path for a FIB entry must update
neighbour state and notify nexthops, so neighbour CRUD functions are
called from both the FIB event path and the neighbour event path.

Patch 9 (neighbour events): Registers the netevent notifier for
NETEVENT_NEIGH_UPDATE used in ARP/NDP resolution, wiring up the final
piece.

Signed-off-by: Jens Emil Schulz Østergaard <jensemil.schulzostergaard@microchip.com>
---
Jens Emil Schulz Østergaard (9):
      net: microchip: vcap: fix rule move for rules of coprime size
      net: microchip: vcap: add lpm vcap to autogen vcap api
      net: microchip: vcap: make vcap actionset decoding type_id aware
      net: microchip: vcap: expose helpers in vcap api and update debugfs
      net: sparx5: add l3 routing registers
      net: sparx5: vcap: add lpm vcap implementation
      net: sparx5: add L3 router infrastructure and leg management
      net: sparx5: add L3 FIB, nexthop and neighbour entry management
      net: sparx5: add neighbour event handling for L3 routing

 drivers/net/ethernet/microchip/sparx5/Makefile     |    2 +-
 .../ethernet/microchip/sparx5/lan969x/lan969x.c    |    2 +
 .../microchip/sparx5/lan969x/lan969x_regs.c        |   20 +-
 .../microchip/sparx5/lan969x/lan969x_vcap_ag_api.c |  373 ++-
 .../microchip/sparx5/lan969x/lan969x_vcap_impl.c   |   12 +
 .../net/ethernet/microchip/sparx5/sparx5_main.c    |   13 +-
 .../net/ethernet/microchip/sparx5/sparx5_main.h    |   46 +
 .../ethernet/microchip/sparx5/sparx5_main_regs.h   |  691 ++++-
 .../net/ethernet/microchip/sparx5/sparx5_regs.c    |   20 +-
 .../net/ethernet/microchip/sparx5/sparx5_regs.h    |   20 +-
 .../net/ethernet/microchip/sparx5/sparx5_router.c  | 3095 ++++++++++++++++++++
 .../ethernet/microchip/sparx5/sparx5_vcap_ag_api.c |  373 ++-
 .../ethernet/microchip/sparx5/sparx5_vcap_ag_api.h |    6 +-
 .../ethernet/microchip/sparx5/sparx5_vcap_impl.c   |  159 +-
 .../ethernet/microchip/sparx5/sparx5_vcap_impl.h   |    5 +
 drivers/net/ethernet/microchip/vcap/vcap_ag_api.h  |   94 +-
 drivers/net/ethernet/microchip/vcap/vcap_api.c     |  219 +-
 drivers/net/ethernet/microchip/vcap/vcap_api.h     |    4 +-
 .../net/ethernet/microchip/vcap/vcap_api_client.h  |    6 +
 .../net/ethernet/microchip/vcap/vcap_api_debugfs.c |   13 +-
 .../net/ethernet/microchip/vcap/vcap_api_kunit.c   |   81 +-
 .../net/ethernet/microchip/vcap/vcap_model_kunit.c |    6 +-
 22 files changed, 5123 insertions(+), 137 deletions(-)
---
base-commit: f6033078a9e671e3c8b83d387b91591a6f6a54e7
change-id: 20260210-sparx5_l3_routing-962458e4d3da

Best regards,
-- 
Jens Emil Schulz Østergaard <jensemil.schulzostergaard@microchip.com>



^ permalink raw reply

* Re: [PATCH] spi: uniphier: Fix completion initialization order before devm_request_irq()
From: Mark Brown @ 2026-06-12 12:37 UTC (permalink / raw)
  To: Kunihiko Hayashi
  Cc: linux-spi, linux-arm-kernel, linux-kernel, Sangyun Kim,
	Kyungwook Boo, stable, Masami Hiramatsu
In-Reply-To: <cd454dac-868d-43b9-9b50-9ba9f3f370a9@socionext.com>

[-- Attachment #1: Type: text/plain, Size: 371 bytes --]

On Fri, Jun 12, 2026 at 05:17:49PM +0900, Kunihiko Hayashi wrote:
> On 2026/06/11 23:09, Mark Brown wrote:

> > This doesn't apply against current code, please check and resend.

> That seems a bit strange. I applied this patch to v7.0 and linux-next successfully.
> Which tree did you apply to and fail?

It applies to none of spi/for-7.1, spi/for-7.2 nor spi/for-next.

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 488 bytes --]

^ permalink raw reply

* Re: [PATCH] net: macb: add TX stall timeout callback to recover from lost TSTART write
From: Nicolai Buchwitz @ 2026-06-12 12:23 UTC (permalink / raw)
  To: Andrea della Porta
  Cc: netdev, Theo Lebrun, Nicolas Ferre, Claudiu Beznea, Andrew Lunn,
	David S . Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
	linux-kernel, linux-arm-kernel, linux-rpi-kernel, Lukasz Raczylo,
	Steffen Jaeckel
In-Reply-To: <771b8faeaee1fce4a84a5ba2661d60b35a65a6d5.1781253818.git.andrea.porta@suse.com>

Hi Andrea

On 12.6.2026 11:01, Andrea della Porta wrote:
> From: Lukasz Raczylo <lukasz@raczylo.com>
> 
> The MACB found in the Raspberry Pi RP1 suffers from sporadic stalls on
> the TX queue.
> While the exact root cause is not yet fully understood, it is likely
> related to a hardware issue where a TSTART write to the NCR register
> is missed, preventing the transmission from being kicked off.
> 
> Implement a timeout callback to handle TX queue stalls, triggering the
> existing restart mechanism to recover.
> 
> Link: 
> https://lore.kernel.org/all/20260514215459.36109-1-lukasz@raczylo.com/
> Fixes: dc110d1b23564 ("net: cadence: macb: Add support for Raspberry Pi 
> RP1 ethernet controller")
> Signed-off-by: Lukasz Raczylo <lukasz@raczylo.com>
> Co-developed-by: Steffen Jaeckel <sjaeckel@suse.de>
> Signed-off-by: Steffen Jaeckel <sjaeckel@suse.de>
> Co-developed-by: Andrea della Porta <andrea.porta@suse.com>
> Signed-off-by: Andrea della Porta <andrea.porta@suse.com>
> ---
>  drivers/net/ethernet/cadence/macb_main.c | 11 +++++++++++
>  1 file changed, 11 insertions(+)
> 
> diff --git a/drivers/net/ethernet/cadence/macb_main.c 
> b/drivers/net/ethernet/cadence/macb_main.c
> index a12aa21244e83..615da65d5d68d 100644
> --- a/drivers/net/ethernet/cadence/macb_main.c
> +++ b/drivers/net/ethernet/cadence/macb_main.c
> @@ -4522,6 +4522,16 @@ static int macb_setup_tc(struct net_device *dev, 
> enum tc_setup_type type,
>  	}
>  }
> 
> +static void macb_tx_timeout(struct net_device *dev, unsigned int q)
> +{
> +	struct macb *bp = netdev_priv(dev);
> +
> +	if (net_ratelimit())

Do we need the net_ratelimit() check (and message) here? AFAIU the 
watchdog core already prints a message for every timeout.

> +		netdev_err(dev, "TX stall detected, re-kicking TSTART\n");
> +	dev->stats.tx_errors++;
> +	macb_tx_restart(&bp->queues[q]);
> +}
> +
>  static const struct net_device_ops macb_netdev_ops = {
>  	.ndo_open		= macb_open,
>  	.ndo_stop		= macb_close,
> @@ -4540,6 +4550,7 @@ static const struct net_device_ops 
> macb_netdev_ops = {
>  	.ndo_hwtstamp_set	= macb_hwtstamp_set,
>  	.ndo_hwtstamp_get	= macb_hwtstamp_get,
>  	.ndo_setup_tc		= macb_setup_tc,
> +	.ndo_tx_timeout		= macb_tx_timeout,

The commit message describes it as RP1 specific, but it gets applied to 
all other variants?

>  };
> 
>  /* Configure peripheral capabilities according to device tree

Thanks
Nicolai


^ permalink raw reply

* Re: [PATCH v7 02/30] drm/connector: Add HDMI 2.0 scrambler infrastructure
From: Maxime Ripard @ 2026-06-12 12:06 UTC (permalink / raw)
  To: Cristian Ciocaltea
  Cc: Maarten Lankhorst, Thomas Zimmermann, David Airlie, Simona Vetter,
	Andrzej Hajda, Neil Armstrong, Robert Foss, Laurent Pinchart,
	Jonas Karlman, Jernej Skrabec, Luca Ceresoli, Sandy Huang,
	Heiko Stübner, Andy Yan, Daniel Stone, Dave Stevenson,
	Maíra Canal, Raspberry Pi Kernel Maintenance, kernel,
	dri-devel, linux-kernel, linux-arm-kernel, linux-rockchip
In-Reply-To: <20260602-dw-hdmi-qp-scramb-v7-2-445eb54ee1ed@collabora.com>

[-- Attachment #1: Type: text/plain, Size: 6184 bytes --]

On Tue, Jun 02, 2026 at 01:44:02AM +0300, Cristian Ciocaltea wrote:
> Add the connector-level infrastructure to support HDMI 2.0 scrambling:
> 
> - A scrambler_supported flag to indicate whether the source supports the
>   scrambling capability, in which case the newly introduced
>   .scrambler_{enable|disable}() callbacks in drm_connector_hdmi_funcs
>   are mandatory
> - A scrambler_needed flag to be managed by the hdmi state helpers based
>   on the negotiated TMDS character rate and the source/sink scrambling
>   capabilities
> - A scrambler_enabled flag to track whether scrambling is currently
>   active
> - A delayed work item (scdc_work) with an associated callback (scdc_cb)
>   to monitor sink-side scrambling status and retry the setup if the sink
>   resets it
> 
> These are intended to be used by SCDC scrambling helpers to coordinate
> scrambling setup and teardown between the source driver and the DRM
> core.
> 
> Signed-off-by: Cristian Ciocaltea <cristian.ciocaltea@collabora.com>
> ---
>  drivers/gpu/drm/drm_connector.c | 18 +++++++++
>  include/drm/drm_connector.h     | 81 +++++++++++++++++++++++++++++++++++++++++
>  2 files changed, 99 insertions(+)
> 
> diff --git a/drivers/gpu/drm/drm_connector.c b/drivers/gpu/drm/drm_connector.c
> index a5d13b92b665..526dc2931b8a 100644
> --- a/drivers/gpu/drm/drm_connector.c
> +++ b/drivers/gpu/drm/drm_connector.c
> @@ -220,6 +220,19 @@ void drm_connector_free_work_fn(struct work_struct *work)
>  	}
>  }
>  
> +static void drm_connector_hdmi_scdc_work(struct work_struct *work)
> +{
> +	struct drm_connector *connector;
> +	struct drm_connector_hdmi *hdmi;
> +
> +	hdmi = container_of(to_delayed_work(work), struct drm_connector_hdmi,
> +			    scdc_work);
> +	connector = container_of(hdmi, struct drm_connector, hdmi);
> +
> +	if (hdmi->scdc_cb)
> +		hdmi->scdc_cb(connector);
> +}
> +
>  static int drm_connector_init_only(struct drm_device *dev,
>  				   struct drm_connector *connector,
>  				   const struct drm_connector_funcs *funcs,
> @@ -285,6 +298,7 @@ static int drm_connector_init_only(struct drm_device *dev,
>  	mutex_init(&connector->edid_override_mutex);
>  	mutex_init(&connector->hdmi.infoframes.lock);
>  	mutex_init(&connector->hdmi_audio.lock);
> +	INIT_DELAYED_WORK(&connector->hdmi.scdc_work, drm_connector_hdmi_scdc_work);
>  	connector->edid_blob_ptr = NULL;
>  	connector->epoch_counter = 0;
>  	connector->tile_blob_ptr = NULL;
> @@ -606,6 +620,10 @@ int drmm_connector_hdmi_init(struct drm_device *dev,
>  	    !hdmi_funcs->hdmi.write_infoframe)
>  		return -EINVAL;
>  
> +	if (connector->hdmi.scrambler_supported &&
> +	    (!hdmi_funcs->scrambler_enable || !hdmi_funcs->scrambler_disable))
> +		return -EINVAL;
> +
>  	ret = drmm_connector_init(dev, connector, funcs, connector_type, ddc);
>  	if (ret)
>  		return ret;
> diff --git a/include/drm/drm_connector.h b/include/drm/drm_connector.h
> index 529755c2e862..f1c5c15a6cce 100644
> --- a/include/drm/drm_connector.h
> +++ b/include/drm/drm_connector.h
> @@ -28,6 +28,7 @@
>  #include <linux/ctype.h>
>  #include <linux/hdmi.h>
>  #include <linux/notifier.h>
> +#include <linux/workqueue.h>
>  #include <drm/drm_mode_object.h>
>  #include <drm/drm_util.h>
>  #include <drm/drm_property.h>
> @@ -1057,6 +1058,19 @@ struct drm_connector_hdmi_state {
>  	 * @tmds_char_rate: TMDS Character Rate, in Hz.
>  	 */
>  	unsigned long long tmds_char_rate;
> +
> +	/**
> +	 * @scrambler_needed: Whether HDMI 2.0 SCDC scrambling is required
> +	 * for the negotiated mode/bpc/format.
> +	 *
> +	 * Computed by drm_atomic_helper_connector_hdmi_check() from
> +	 * @tmds_char_rate and the source/sink scrambling capabilities.
> +	 *
> +	 * Per HDMI 2.0, scrambling is mandatory above 340 MHz TMDS
> +	 * character rate. Optional scrambling at lower rates is
> +	 * deliberately not requested by the helper.
> +	 */
> +	bool scrambler_needed;
>  };
>  
>  /**
> @@ -1358,6 +1372,36 @@ struct drm_connector_hdmi_funcs {
>  	 */
>  	const struct drm_edid *(*read_edid)(struct drm_connector *connector);
>  
> +	/**
> +	 * @scrambler_enable:
> +	 *
> +	 * This callback is invoked through @drm_scdc_start_scrambling during
> +	 * a commit to setup SCDC scrambling and high TMDS clock ratio on
> +	 * source side.
> +	 *
> +	 * The @scrambler_enable callback is mandatory if HDMI 2.0 is to be
> +	 * supported.
> +	 *
> +	 * Returns:
> +	 * 0 on success, a negative error code otherwise
> +	 */
> +	int (*scrambler_enable)(struct drm_connector *connector);
> +
> +	/**
> +	 * @scrambler_disable:
> +	 *
> +	 * This callback is invoked through @drm_scdc_stop_scrambling during
> +	 * a commit to disable SCDC scrambling and high TMDS clock ratio on
> +	 * source side.
> +	 *
> +	 * The @scrambler_disable callback is mandatory if HDMI 2.0 is to be
> +	 * supported.
> +	 *
> +	 * Returns:
> +	 * 0 on success, a negative error code otherwise
> +	 */
> +	int (*scrambler_disable)(struct drm_connector *connector);
> +
>  	/**
>  	 * @avi:
>  	 *
> @@ -1960,6 +2004,43 @@ struct drm_connector_hdmi {
>  	 */
>  	unsigned long supported_formats;
>  
> +	/**
> +	 * @scrambler_supported: Indicates whether the HDMI controller
> +	 * (source) supports HDMI 2.0 SCDC scrambling.
> +	 *
> +	 * When true, @drm_connector_hdmi_funcs.scrambler_enable and
> +	 * @drm_connector_hdmi_funcs.scrambler_disable are mandatory.
> +	 * This is enforced by drmm_connector_hdmi_init().
> +	 *
> +	 * For HDMI bridge based drivers using drm_bridge_connector_init(),
> +	 * this is propagated automatically from bridges that set the
> +	 * DRM_BRIDGE_OP_HDMI_SCRAMBLER flag in their &drm_bridge->ops.
> +	 * Other drivers must set this field on @connector->hdmi before calling
> +	 * drmm_connector_hdmi_init().
> +	 */
> +	bool scrambler_supported;
> +
> +	/**
> +	 * @scrambler_enabled: Tracks whether HDMI 2.0 scrambler is currently enabled.
> +	 */
> +	bool scrambler_enabled;

This is already in the state. Why do you need to copy it over here, and
if you do, you'll need a lock.

Maxime

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 273 bytes --]

^ permalink raw reply

* Re: [PATCH v8 8/9] arm64: dts: imx943-evk: Add display support using IT6263
From: Laurentiu Palcu @ 2026-06-12 12:06 UTC (permalink / raw)
  To: Liu Ying
  Cc: imx, Rob Herring, Krzysztof Kozlowski, Conor Dooley, Frank Li,
	Sascha Hauer, Pengutronix Kernel Team, Fabio Estevam, dri-devel,
	devicetree, linux-arm-kernel, linux-kernel
In-Reply-To: <1a3d466c-c1b5-49f1-a9ab-1c827e906e91@nxp.com>

Hi Ying,

On Fri, Mar 06, 2026 at 04:45:25PM +0800, Liu Ying wrote:
> On Wed, Mar 04, 2026 at 11:34:17AM +0000, Laurentiu Palcu wrote:
> > The ITE IT6263 based NXP LVDS to HDMI converter can be attached to the
> > i.MX943 EVK board LVDS port using the mini-SAS connector. Since this is
> 
> Since the LVDS to HDMI converter can be attached or detached to the EVK
> board, it would be appropriate to use a DT overlay instead?

AFAIK, imx943 EVK ships with the IT6263 LVDS to HDMI adapter. So, I
believe it's preferable to have support for the adapter in the default
DTB instead of a DT overlay. 

-- 
Thanks,
Laurentiu


^ permalink raw reply

* Re: [PATCH v7 25/30] drm/vc4: hdmi: Convert to common HDMI 2.0 SCDC scrambling helpers
From: Maxime Ripard @ 2026-06-12 12:04 UTC (permalink / raw)
  To: Cristian Ciocaltea
  Cc: Maarten Lankhorst, Thomas Zimmermann, David Airlie, Simona Vetter,
	Andrzej Hajda, Neil Armstrong, Robert Foss, Laurent Pinchart,
	Jonas Karlman, Jernej Skrabec, Luca Ceresoli, Sandy Huang,
	Heiko Stübner, Andy Yan, Daniel Stone, Dave Stevenson,
	Maíra Canal, Raspberry Pi Kernel Maintenance, kernel,
	dri-devel, linux-kernel, linux-arm-kernel, linux-rockchip
In-Reply-To: <20260602-dw-hdmi-qp-scramb-v7-25-445eb54ee1ed@collabora.com>

[-- Attachment #1: Type: text/plain, Size: 15614 bytes --]

Hi,

On Tue, Jun 02, 2026 at 01:44:25AM +0300, Cristian Ciocaltea wrote:
> Replace the vc4-local scrambling implementation with the newly
> introduced DRM common SCDC scrambling infrastructure:
> 
> - Advertise source-side scrambling support by setting
>   connector->hdmi.scrambling_supported based on the variant's
>   max_pixel_clock before drmm_connector_hdmi_init().
> 
> - Provide minimal .scrambler_{enable|disable} connector callbacks that
>   only toggle the VC5 HDMI_SCRAMBLER_CTL register.  Sink-side SCDC
>   programming and periodic status monitoring are now delegated to
>   drm_scdc_{start|stop}_scrambling().
> 
> - Replace vc4_hdmi_enable_scrambling() with a conditional call to
>   drm_scdc_start_scrambling() in post_crtc_enable, gated on
>   conn_state->hdmi.scrambler_needed (computed by the HDMI state helper).
> 
> - Replace vc4_hdmi_disable_scrambling() with drm_scdc_stop_scrambling()
>   in post_crtc_disable.
> 
> - Drop vc4_hdmi_reset_link() and vc4_hdmi_handle_hotplug(), switching
>   the .detect_ctx() path to
>   drm_atomic_helper_connector_hdmi_hotplug_ctx() which internally calls
>   drm_scdc_sync_status() to trigger a CRTC reset on reconnection.
> 
> - Drop the local scrambling_work delayed workqueue and scdc_enabled
>   flag, now tracked by the common drm_connector_hdmi layer.
> 
> - Drop vc4_hdmi_supports_scrambling() and
>   vc4_hdmi_mode_needs_scrambling() helpers, inlining the remaining 4KP60
>   warning with an explicit drm_hdmi_compute_mode_clock() check.
> 
> - Seed connector->hdmi.scrambler_enabled = true in connector_init() to
>   ensure drm_scdc_stop_scrambling() runs at boot and disables any stale
>   scrambling state left by the bootloader.
> 
> No functional change is expected for the supported modes.
> 
> Signed-off-by: Cristian Ciocaltea <cristian.ciocaltea@collabora.com>

I'd really like it to be broken down into several patches:

> ---
>  drivers/gpu/drm/vc4/vc4_hdmi.c | 265 ++++++-----------------------------------
>  drivers/gpu/drm/vc4/vc4_hdmi.h |   8 --
>  2 files changed, 35 insertions(+), 238 deletions(-)
> 
> diff --git a/drivers/gpu/drm/vc4/vc4_hdmi.c b/drivers/gpu/drm/vc4/vc4_hdmi.c
> index 046ac4f43ba8..02f6ca6ab52b 100644
> --- a/drivers/gpu/drm/vc4/vc4_hdmi.c
> +++ b/drivers/gpu/drm/vc4/vc4_hdmi.c
> @@ -114,31 +114,6 @@
>  #define HSM_MIN_CLOCK_FREQ	120000000
>  #define CEC_CLOCK_FREQ 40000
>  
> -static bool vc4_hdmi_supports_scrambling(struct vc4_hdmi *vc4_hdmi)
> -{
> -	struct drm_display_info *display = &vc4_hdmi->connector.display_info;
> -
> -	lockdep_assert_held(&vc4_hdmi->mutex);
> -
> -	if (!display->is_hdmi)
> -		return false;
> -
> -	if (!display->hdmi.scdc.supported ||
> -	    !display->hdmi.scdc.scrambling.supported)
> -		return false;
> -
> -	return true;
> -}
> -
> -static bool vc4_hdmi_mode_needs_scrambling(const struct drm_display_mode *mode,
> -					   unsigned int bpc,
> -					   enum drm_output_color_format fmt)
> -{
> -	unsigned long long clock = drm_hdmi_compute_mode_clock(mode, bpc, fmt);
> -
> -	return clock > HDMI_1_3_TMDS_CHAR_RATE_MAX_HZ;
> -}
> -
>  static int vc4_hdmi_debugfs_regs(struct seq_file *m, void *unused)
>  {
>  	struct drm_debugfs_entry *entry = m->private;
> @@ -272,124 +247,6 @@ static void vc4_hdmi_cec_update_clk_div(struct vc4_hdmi *vc4_hdmi)
>  static void vc4_hdmi_cec_update_clk_div(struct vc4_hdmi *vc4_hdmi) {}
>  #endif
>  
> -static int vc4_hdmi_reset_link(struct drm_connector *connector,
> -			       struct drm_modeset_acquire_ctx *ctx)
> -{
> -	struct drm_device *drm;
> -	struct vc4_hdmi *vc4_hdmi;
> -	struct drm_connector_state *conn_state;
> -	struct drm_crtc_state *crtc_state;
> -	struct drm_crtc *crtc;
> -	bool scrambling_needed;
> -	u8 config;
> -	int ret;
> -
> -	if (!connector)
> -		return 0;
> -
> -	drm = connector->dev;
> -	ret = drm_modeset_lock(&drm->mode_config.connection_mutex, ctx);
> -	if (ret)
> -		return ret;
> -
> -	conn_state = connector->state;
> -	crtc = conn_state->crtc;
> -	if (!crtc)
> -		return 0;
> -
> -	ret = drm_modeset_lock(&crtc->mutex, ctx);
> -	if (ret)
> -		return ret;
> -
> -	crtc_state = crtc->state;
> -	if (!crtc_state->active)
> -		return 0;
> -
> -	vc4_hdmi = connector_to_vc4_hdmi(connector);
> -	mutex_lock(&vc4_hdmi->mutex);
> -
> -	if (!vc4_hdmi_supports_scrambling(vc4_hdmi)) {
> -		mutex_unlock(&vc4_hdmi->mutex);
> -		return 0;
> -	}
> -
> -	scrambling_needed = vc4_hdmi_mode_needs_scrambling(&vc4_hdmi->saved_adjusted_mode,
> -							   vc4_hdmi->output_bpc,
> -							   vc4_hdmi->output_format);
> -	if (!scrambling_needed) {
> -		mutex_unlock(&vc4_hdmi->mutex);
> -		return 0;
> -	}
> -
> -	if (conn_state->commit &&
> -	    !try_wait_for_completion(&conn_state->commit->hw_done)) {
> -		mutex_unlock(&vc4_hdmi->mutex);
> -		return 0;
> -	}
> -
> -	ret = drm_scdc_readb(connector->ddc, SCDC_TMDS_CONFIG, &config);
> -	if (ret < 0) {
> -		drm_err(drm, "Failed to read TMDS config: %d\n", ret);
> -		mutex_unlock(&vc4_hdmi->mutex);
> -		return 0;
> -	}
> -
> -	if (!!(config & SCDC_SCRAMBLING_ENABLE) == scrambling_needed) {
> -		mutex_unlock(&vc4_hdmi->mutex);
> -		return 0;
> -	}
> -
> -	mutex_unlock(&vc4_hdmi->mutex);
> -
> -	/*
> -	 * HDMI 2.0 says that one should not send scrambled data
> -	 * prior to configuring the sink scrambling, and that
> -	 * TMDS clock/data transmission should be suspended when
> -	 * changing the TMDS clock rate in the sink. So let's
> -	 * just do a full modeset here, even though some sinks
> -	 * would be perfectly happy if were to just reconfigure
> -	 * the SCDC settings on the fly.
> -	 */
> -	return drm_atomic_helper_reset_crtc(crtc, ctx);
> -}

This one doesn't look functionally equivalent to me to
drm_scdc_reset_crtc: this part was, in part, making sure we would only
reset the scrambler if it was enabled in the first place.
drm_scdc_reset_crtc() doesn't and will always trigger a modeset on
hotplug. That's unnecessary and a significant functional different.

I'd argue that it's drm_scdc_reset_crtc() that needs to align to what
vc4 was doing, not the opposite.

> -static void vc4_hdmi_handle_hotplug(struct vc4_hdmi *vc4_hdmi,
> -				    struct drm_modeset_acquire_ctx *ctx,
> -				    enum drm_connector_status status)
> -{
> -	struct drm_connector *connector = &vc4_hdmi->connector;
> -	int ret;
> -
> -	/*
> -	 * NOTE: This function should really be called with vc4_hdmi->mutex
> -	 * held, but doing so results in reentrancy issues since
> -	 * cec_s_phys_addr() might call .adap_enable, which leads to that
> -	 * funtion being called with our mutex held.
> -	 *
> -	 * A similar situation occurs with vc4_hdmi_reset_link() that
> -	 * will call into our KMS hooks if the scrambling was enabled.
> -	 *
> -	 * Concurrency isn't an issue at the moment since we don't share
> -	 * any state with any of the other frameworks so we can ignore
> -	 * the lock for now.
> -	 */
> -
> -	drm_atomic_helper_connector_hdmi_hotplug(connector, status);
> -
> -	if (status != connector_status_connected)
> -		return;
> -
> -	for (;;) {
> -		ret = vc4_hdmi_reset_link(connector, ctx);
> -		if (ret == -EDEADLK) {
> -			drm_modeset_backoff(ctx);
> -			continue;
> -		}
> -
> -		break;
> -	}
> -}
> -
>  static int vc4_hdmi_connector_detect_ctx(struct drm_connector *connector,
>  					 struct drm_modeset_acquire_ctx *ctx,
>  					 bool force)
> @@ -401,8 +258,8 @@ static int vc4_hdmi_connector_detect_ctx(struct drm_connector *connector,
>  	/*
>  	 * NOTE: This function should really take vc4_hdmi->mutex, but
>  	 * doing so results in reentrancy issues since
> -	 * vc4_hdmi_handle_hotplug() can call into other functions that
> -	 * would take the mutex while it's held here.
> +	 * drm_atomic_helper_connector_hdmi_hotplug_ctx() can call into other
> +	 * functions that would take the mutex while it's held here.
>  	 *
>  	 * Concurrency isn't an issue at the moment since we don't share
>  	 * any state with any of the other frameworks so we can ignore
> @@ -425,10 +282,11 @@ static int vc4_hdmi_connector_detect_ctx(struct drm_connector *connector,
>  			status = connector_status_connected;
>  	}
>  
> -	vc4_hdmi_handle_hotplug(vc4_hdmi, ctx, status);
> +	ret = drm_atomic_helper_connector_hdmi_hotplug_ctx(connector, status, ctx);
> +
>  	pm_runtime_put(&vc4_hdmi->pdev->dev);
>  
> -	return status;
> +	return ret == -EDEADLK ? ret : status;
>  }
>  
>  static int vc4_hdmi_connector_get_modes(struct drm_connector *connector)
> @@ -441,9 +299,12 @@ static int vc4_hdmi_connector_get_modes(struct drm_connector *connector)
>  	if (!vc4->hvs->vc5_hdmi_enable_hdmi_20) {
>  		struct drm_device *drm = connector->dev;
>  		const struct drm_display_mode *mode;
> +		unsigned long long clock;
>  
>  		list_for_each_entry(mode, &connector->probed_modes, head) {
> -			if (vc4_hdmi_mode_needs_scrambling(mode, 8, DRM_OUTPUT_COLOR_FORMAT_RGB444)) {
> +			clock = drm_hdmi_compute_mode_clock(mode, 8,
> +							    DRM_OUTPUT_COLOR_FORMAT_RGB444);
> +			if (clock > HDMI_1_3_TMDS_CHAR_RATE_MAX_HZ) {

This should be a patch of its own, but I think we should turn
vc4_hdmi_mode_needs_scrambling() into a helper, instead of checking the
clock rate in every driver that might need it. From a logical standpoint
it's equivalent, but not from a semantic one.

>  				drm_warn_once(drm, "The core clock cannot reach frequencies high enough to support 4k @ 60Hz.");
>  				drm_warn_once(drm, "Please change your config.txt file to add hdmi_enable_4kp60.");
>  			}
> @@ -540,6 +401,9 @@ static int vc4_hdmi_connector_init(struct drm_device *dev,
>  	if (vc4_hdmi->variant->supports_hdr)
>  		max_bpc = 12;
>  
> +	connector->hdmi.scrambler_supported =
> +		vc4_hdmi->variant->max_pixel_clock > HDMI_1_3_TMDS_CHAR_RATE_MAX_HZ;
> +
>  	ret = drmm_connector_hdmi_init(dev, connector,
>  				       "Broadcom", "Videocore",
>  				       &vc4_hdmi_connector_funcs,
> @@ -561,6 +425,14 @@ static int vc4_hdmi_connector_init(struct drm_device *dev,
>  
>  	drm_connector_helper_add(connector, &vc4_hdmi_connector_helper_funcs);
>  
> +	/*
> +	 * Since we don't know the state of the controller and its
> +	 * display (if any), let's assume it's always enabled.
> +	 * drm_scdc_stop_scrambling() will thus run at boot, make
> +	 * sure it's disabled, and avoid any inconsistency.
> +	 */
> +	connector->hdmi.scrambler_enabled = connector->hdmi.scrambler_supported;
> +
>  	/*
>  	 * Some of the properties below require access to state, like bpc.
>  	 * Allocate some default initial connector state with our reset helper.
> @@ -786,93 +658,30 @@ static int vc4_hdmi_write_spd_infoframe(struct drm_connector *connector,
>  					buffer, len);
>  }
>  
> -#define SCRAMBLING_POLLING_DELAY_MS	1000
> -
> -static void vc4_hdmi_enable_scrambling(struct drm_encoder *encoder)
> +static int vc4_hdmi_scrambler_enable(struct drm_connector *connector)
>  {
> -	struct vc4_hdmi *vc4_hdmi = encoder_to_vc4_hdmi(encoder);
> -	struct drm_connector *connector = &vc4_hdmi->connector;
> -	struct drm_device *drm = connector->dev;
> -	const struct drm_display_mode *mode = &vc4_hdmi->saved_adjusted_mode;
> +	struct vc4_hdmi *vc4_hdmi = connector_to_vc4_hdmi(connector);
>  	unsigned long flags;
> -	int idx;
> -
> -	lockdep_assert_held(&vc4_hdmi->mutex);
> -
> -	if (!vc4_hdmi_supports_scrambling(vc4_hdmi))
> -		return;
> -
> -	if (!vc4_hdmi_mode_needs_scrambling(mode,
> -					    vc4_hdmi->output_bpc,
> -					    vc4_hdmi->output_format))
> -		return;
> -
> -	if (!drm_dev_enter(drm, &idx))
> -		return;

drm_dev_enter should be kept here

> -	drm_scdc_set_high_tmds_clock_ratio(connector, true);
> -	drm_scdc_set_scrambling(connector, true);
>  
>  	spin_lock_irqsave(&vc4_hdmi->hw_lock, flags);
>  	HDMI_WRITE(HDMI_SCRAMBLER_CTL, HDMI_READ(HDMI_SCRAMBLER_CTL) |
>  		   VC5_HDMI_SCRAMBLER_CTL_ENABLE);
>  	spin_unlock_irqrestore(&vc4_hdmi->hw_lock, flags);
>  
> -	drm_dev_exit(idx);

And exit here.

> -static void vc4_hdmi_disable_scrambling(struct drm_encoder *encoder)
> +static int vc4_hdmi_scrambler_disable(struct drm_connector *connector)
>  {
> -	struct vc4_hdmi *vc4_hdmi = encoder_to_vc4_hdmi(encoder);
> -	struct drm_connector *connector = &vc4_hdmi->connector;
> -	struct drm_device *drm = connector->dev;
> +	struct vc4_hdmi *vc4_hdmi = connector_to_vc4_hdmi(connector);
>  	unsigned long flags;
> -	int idx;
> -
> -	lockdep_assert_held(&vc4_hdmi->mutex);
> -
> -	if (!vc4_hdmi->scdc_enabled)
> -		return;
> -
> -	vc4_hdmi->scdc_enabled = false;
> -
> -	if (delayed_work_pending(&vc4_hdmi->scrambling_work))
> -		cancel_delayed_work_sync(&vc4_hdmi->scrambling_work);
> -
> -	if (!drm_dev_enter(drm, &idx))
> -		return;

Ditto

>  	spin_lock_irqsave(&vc4_hdmi->hw_lock, flags);
>  	HDMI_WRITE(HDMI_SCRAMBLER_CTL, HDMI_READ(HDMI_SCRAMBLER_CTL) &
>  		   ~VC5_HDMI_SCRAMBLER_CTL_ENABLE);
>  	spin_unlock_irqrestore(&vc4_hdmi->hw_lock, flags);
>  
> -	drm_scdc_set_scrambling(connector, false);
> -	drm_scdc_set_high_tmds_clock_ratio(connector, false);
> -
> -	drm_dev_exit(idx);
> -}
> -
> -static void vc4_hdmi_scrambling_wq(struct work_struct *work)
> -{
> -	struct vc4_hdmi *vc4_hdmi = container_of(to_delayed_work(work),
> -						 struct vc4_hdmi,
> -						 scrambling_work);
> -	struct drm_connector *connector = &vc4_hdmi->connector;
> -
> -	if (drm_scdc_get_scrambling_status(connector))
> -		return;
> -
> -	drm_scdc_set_high_tmds_clock_ratio(connector, true);
> -	drm_scdc_set_scrambling(connector, true);
> -
> -	queue_delayed_work(system_percpu_wq, &vc4_hdmi->scrambling_work,
> -			   msecs_to_jiffies(SCRAMBLING_POLLING_DELAY_MS));
> +	return 0;
>  }
>  
>  static void vc4_hdmi_encoder_post_crtc_disable(struct drm_encoder *encoder,
> @@ -917,7 +726,7 @@ static void vc4_hdmi_encoder_post_crtc_disable(struct drm_encoder *encoder,
>  		spin_unlock_irqrestore(&vc4_hdmi->hw_lock, flags);
>  	}
>  
> -	vc4_hdmi_disable_scrambling(encoder);
> +	drm_scdc_stop_scrambling(&vc4_hdmi->connector);

I don't think the names here are right. It's not *only* related to scdc
but also to the HDMI controller. I'm fine with using a scdc prefix but
only for the things related to scdc. This is related (in part) to the
HDMI controller, so it should use a drm_connector_hdmi prefix.

>  	drm_dev_exit(idx);
>  
> @@ -1625,6 +1434,7 @@ static void vc4_hdmi_encoder_post_crtc_enable(struct drm_encoder *encoder,
>  	struct drm_display_info *display = &vc4_hdmi->connector.display_info;
>  	bool hsync_pos = mode->flags & DRM_MODE_FLAG_PHSYNC;
>  	bool vsync_pos = mode->flags & DRM_MODE_FLAG_PVSYNC;
> +	struct drm_connector_state *conn_state;
>  	unsigned long flags;
>  	int ret;
>  	int idx;
> @@ -1693,7 +1503,10 @@ static void vc4_hdmi_encoder_post_crtc_enable(struct drm_encoder *encoder,
>  	}
>  
>  	vc4_hdmi_recenter_fifo(vc4_hdmi);
> -	vc4_hdmi_enable_scrambling(encoder);
> +
> +	conn_state = drm_atomic_get_new_connector_state(state, connector);
> +	if (conn_state && conn_state->hdmi.scrambler_needed)
> +		drm_scdc_start_scrambling(connector);

And the nice thing with a drm_connector_hdmi_* prefix is that you don't
have to shoehorn it into SCDC support anymore, so you can take a state
as an argument, and do the check in the helper instead of doing it in
every driver and hoping they get it right.

Maxime

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 273 bytes --]

^ permalink raw reply

* Re: [PATCH v8 7/9] arm64: dts: imx943: Add display pipeline nodes
From: Laurentiu Palcu @ 2026-06-12 12:02 UTC (permalink / raw)
  To: Liu Ying
  Cc: imx, Rob Herring, Krzysztof Kozlowski, Conor Dooley, Frank Li,
	Sascha Hauer, Pengutronix Kernel Team, Fabio Estevam, dri-devel,
	devicetree, linux-arm-kernel, linux-kernel
In-Reply-To: <43be372e-8177-457c-9d4e-a2ed69e79c8a@nxp.com>

Hi Ying,

On Fri, Mar 06, 2026 at 04:27:48PM +0800, Liu Ying wrote:
> On Wed, Mar 04, 2026 at 11:34:16AM +0000, Laurentiu Palcu wrote:
> > Add display controller and LDB support in imx943.
> > 
> > Signed-off-by: Laurentiu Palcu <laurentiu.palcu@oss.nxp.com>
> > ---
> >  arch/arm64/boot/dts/freescale/imx943.dtsi | 53 ++++++++++++++++++++++++++++++-
> >  1 file changed, 52 insertions(+), 1 deletion(-)
> > 
> > diff --git a/arch/arm64/boot/dts/freescale/imx943.dtsi b/arch/arm64/boot/dts/freescale/imx943.dtsi
> > index 657c81b6016f2..9a91beef54e86 100644
> > --- a/arch/arm64/boot/dts/freescale/imx943.dtsi
> > +++ b/arch/arm64/boot/dts/freescale/imx943.dtsi
> > @@ -148,7 +148,7 @@ l3_cache: l3-cache {
> >  		};
> >  	};
> >  
> > -	clock-ldb-pll-div7 {
> > +	clock_ldb_pll_div7: clock-ldb-pll-div7 {
> >  		compatible = "fixed-factor-clock";
> >  		#clock-cells = <0>;
> >  		clocks = <&scmi_clk IMX94_CLK_LDBPLL>;
> > @@ -174,9 +174,60 @@ dispmix_csr: syscon@4b010000 {
> >  		lvds_csr: syscon@4b0c0000 {
> >  			compatible = "nxp,imx94-lvds-csr", "syscon";
> >  			reg = <0x0 0x4b0c0000 0x0 0x10000>;
> > +			#address-cells = <1>;
> > +			#size-cells = <1>;
> >  			clocks = <&scmi_clk IMX94_CLK_DISPAPB>;
> >  			#clock-cells = <1>;
> >  			power-domains = <&scmi_devpd IMX94_PD_DISPLAY>;
> > +
> > +			ldb: ldb@4 {
> > +				compatible = "fsl,imx94-ldb";
> 
> Should this be moved to imx94.dtsi, since the compatible string doesn't
> seem to be i.MX943 specific?

Agreed, I moved them to imx94.dtsi in v9.

-- 
Thanks,
Laurentiu


^ permalink raw reply

* [PATCH v9 8/9] arm64: dts: imx943-evk: Add display support using IT6263
From: Laurentiu Palcu @ 2026-06-12 11:58 UTC (permalink / raw)
  To: Ying Liu, Luca Ceresoli, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Frank Li, Sascha Hauer, Pengutronix Kernel Team,
	Fabio Estevam
  Cc: Laurentiu Palcu, linux-clk, imx, devicetree, linux-arm-kernel,
	linux-kernel, dri-devel
In-Reply-To: <20260612-dcif-upstreaming-v9-0-8d0ff89aa3c5@oss.nxp.com>

The ITE IT6263 based NXP LVDS to HDMI converter can be attached to the
i.MX943 EVK board LVDS port using the mini-SAS connector. Since this is
the default configuration for the EVK, add support for it here.

Signed-off-by: Laurentiu Palcu <laurentiu.palcu@oss.nxp.com>
---
 arch/arm64/boot/dts/freescale/imx943-evk.dts | 86 ++++++++++++++++++++++++++++
 1 file changed, 86 insertions(+)

diff --git a/arch/arm64/boot/dts/freescale/imx943-evk.dts b/arch/arm64/boot/dts/freescale/imx943-evk.dts
index 7cfd424689507..41a2a700a86a5 100644
--- a/arch/arm64/boot/dts/freescale/imx943-evk.dts
+++ b/arch/arm64/boot/dts/freescale/imx943-evk.dts
@@ -77,6 +77,36 @@ dmic: dmic {
 		#sound-dai-cells = <0>;
 	};
 
+	hdmi-connector {
+		compatible = "hdmi-connector";
+		label = "hdmi";
+		type = "a";
+
+		port {
+			hdmi_connector_in: endpoint {
+				remote-endpoint = <&it6263_out>;
+			};
+		};
+	};
+
+	reg_1v8_ext: regulator-1v8-ext {
+		compatible = "regulator-fixed";
+		regulator-name = "1V8_EXT";
+		regulator-min-microvolt = <1800000>;
+		regulator-max-microvolt = <1800000>;
+		regulator-boot-on;
+		regulator-always-on;
+	};
+
+	reg_3v3_ext: regulator-3v3-ext {
+		compatible = "regulator-fixed";
+		regulator-name = "3V3_EXT";
+		regulator-min-microvolt = <3300000>;
+		regulator-max-microvolt = <3300000>;
+		regulator-boot-on;
+		regulator-always-on;
+	};
+
 	reg_m2_pwr: regulator-m2-pwr {
 		compatible = "regulator-fixed";
 		regulator-name = "M.2-power";
@@ -210,6 +240,10 @@ memory@80000000 {
 	};
 };
 
+&dcif {
+	status = "okay";
+};
+
 &enetc1 {
 	clocks = <&scmi_clk IMX94_CLK_MAC4>;
 	clock-names = "ref";
@@ -248,6 +282,21 @@ &flexcan4 {
 	status = "okay";
 };
 
+&ldb {
+	assigned-clocks = <&scmi_clk IMX94_CLK_LDBPLL_VCO>,
+			  <&scmi_clk IMX94_CLK_LDBPLL>;
+	assigned-clock-rates = <4158000000>, <1039500000>;
+	status = "okay";
+
+	ports {
+		port@1 {
+			lvds_out: endpoint {
+				remote-endpoint = <&it6263_in>;
+			};
+		};
+	};
+};
+
 &lpi2c3 {
 	clock-frequency = <400000>;
 	pinctrl-0 = <&pinctrl_lpi2c3>;
@@ -331,6 +380,43 @@ i2c@3 {
 			reg = <3>;
 			#address-cells = <1>;
 			#size-cells = <0>;
+
+			hdmi@4c {
+				compatible = "ite,it6263";
+				reg = <0x4c>;
+				data-mapping = "jeida-24";
+				reset-gpios = <&pcal6416_i2c3_u171 8 GPIO_ACTIVE_LOW>;
+				ivdd-supply = <&reg_1v8_ext>;
+				ovdd-supply = <&reg_3v3_ext>;
+				txavcc18-supply = <&reg_1v8_ext>;
+				txavcc33-supply = <&reg_3v3_ext>;
+				pvcc1-supply = <&reg_1v8_ext>;
+				pvcc2-supply = <&reg_1v8_ext>;
+				avcc-supply = <&reg_3v3_ext>;
+				anvdd-supply = <&reg_1v8_ext>;
+				apvdd-supply = <&reg_1v8_ext>;
+
+				ports {
+					#address-cells = <1>;
+					#size-cells = <0>;
+
+					port@0 {
+						reg = <0>;
+
+						it6263_in: endpoint {
+							remote-endpoint = <&lvds_out>;
+						};
+					};
+
+					port@2 {
+						reg = <2>;
+
+						it6263_out: endpoint {
+							remote-endpoint = <&hdmi_connector_in>;
+						};
+					};
+				};
+			};
 		};
 
 		i2c@4 {

-- 
2.51.0


^ permalink raw reply related

* [PATCH v9 9/9] MAINTAINERS: Add entry for i.MX94 DCIF driver
From: Laurentiu Palcu @ 2026-06-12 11:58 UTC (permalink / raw)
  To: Ying Liu, Luca Ceresoli
  Cc: Laurentiu Palcu, linux-clk, imx, devicetree, linux-arm-kernel,
	linux-kernel, dri-devel
In-Reply-To: <20260612-dcif-upstreaming-v9-0-8d0ff89aa3c5@oss.nxp.com>

The driver is part of DRM subsystem and is located in
drivers/gpu/drm/imx/dcif.

Signed-off-by: Laurentiu Palcu <laurentiu.palcu@oss.nxp.com>
Reviewed-by: Frank Li <Frank.Li@nxp.com>
---
 MAINTAINERS | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/MAINTAINERS b/MAINTAINERS
index f1caa6e5198b9..e8931231b5b7e 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -19492,6 +19492,15 @@ S:	Maintained
 F:	Documentation/devicetree/bindings/media/nxp,imx8-jpeg.yaml
 F:	drivers/media/platform/nxp/imx-jpeg
 
+NXP i.MX 94 DCIF DRIVER
+M:	Laurentiu Palcu <laurentiu.palcu@oss.nxp.com>
+L:	dri-devel@lists.freedesktop.org
+L:	imx@lists.linux.dev
+S:	Maintained
+T:	git https://gitlab.freedesktop.org/drm/misc/kernel.git
+F:	Documentation/devicetree/bindings/display/imx/nxp,imx94-dcif.yaml
+F:	drivers/gpu/drm/imx/dcif/
+
 NXP i.MX CLOCK DRIVERS
 M:	Abel Vesa <abelvesa@kernel.org>
 R:	Peng Fan <peng.fan@nxp.com>

-- 
2.51.0


^ permalink raw reply related

* [PATCH v9 7/9] arm64: dts: imx94: Add display pipeline nodes
From: Laurentiu Palcu @ 2026-06-12 11:58 UTC (permalink / raw)
  To: Ying Liu, Luca Ceresoli, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Frank Li, Sascha Hauer, Pengutronix Kernel Team,
	Fabio Estevam
  Cc: Laurentiu Palcu, linux-clk, imx, devicetree, linux-arm-kernel,
	linux-kernel, dri-devel
In-Reply-To: <20260612-dcif-upstreaming-v9-0-8d0ff89aa3c5@oss.nxp.com>

Add the nodes necessary for the display pipeline on i.MX94:
 * LVDS/DISPLAY CSR;
 * clock-ldb-pll-div7 needed by DCIF and LDB;
 * Display controller interface (DCIF);
 * LVDS display bridge (LDB);

Co-developed-by: Peng Fan <peng.fan@nxp.com>
Signed-off-by: Peng Fan <peng.fan@nxp.com>
Signed-off-by: Laurentiu Palcu <laurentiu.palcu@oss.nxp.com>
---
 arch/arm64/boot/dts/freescale/imx94.dtsi | 82 ++++++++++++++++++++++++++++++++
 1 file changed, 82 insertions(+)

diff --git a/arch/arm64/boot/dts/freescale/imx94.dtsi b/arch/arm64/boot/dts/freescale/imx94.dtsi
index a6cb5a6e848b3..95d862682703c 100644
--- a/arch/arm64/boot/dts/freescale/imx94.dtsi
+++ b/arch/arm64/boot/dts/freescale/imx94.dtsi
@@ -3,6 +3,7 @@
  * Copyright 2024-2025 NXP
  */
 
+#include <dt-bindings/clock/nxp,imx94-clock.h>
 #include <dt-bindings/dma/fsl-edma.h>
 #include <dt-bindings/gpio/gpio.h>
 #include <dt-bindings/input/input.h>
@@ -39,6 +40,15 @@ clk_ext1: clock-ext1 {
 		clock-output-names = "clk_ext1";
 	};
 
+	clk_ldb_pll_div7: clock-ldb-pll-div7 {
+		compatible = "fixed-factor-clock";
+		#clock-cells = <0>;
+		clocks = <&scmi_clk IMX94_CLK_LDBPLL>;
+		clock-div = <7>;
+		clock-mult = <1>;
+		clock-output-names = "ldb_pll_div7";
+	};
+
 	sai1_mclk: clock-sai1-mclk1 {
 		compatible = "fixed-clock";
 		#clock-cells = <0>;
@@ -1305,6 +1315,78 @@ wdog4: watchdog@49230000 {
 			};
 		};
 
+		dispmix_csr: syscon@4b010000 {
+			compatible = "nxp,imx94-display-csr", "syscon";
+			reg = <0x0 0x4b010000 0x0 0x10000>;
+			clocks = <&scmi_clk IMX94_CLK_DISPAPB>;
+			#clock-cells = <1>;
+			power-domains = <&scmi_devpd IMX94_PD_DISPLAY>;
+			assigned-clocks = <&scmi_clk IMX94_CLK_DISPAXI>,
+					  <&scmi_clk IMX94_CLK_DISPAPB>;
+			assigned-clock-parents = <&scmi_clk IMX94_CLK_SYSPLL1_PFD1>,
+						 <&scmi_clk IMX94_CLK_SYSPLL1_PFD1_DIV2>;
+			assigned-clock-rates = <400000000>,  <133333333>;
+		};
+
+		lvds_csr: syscon@4b0c0000 {
+			compatible = "nxp,imx94-lvds-csr", "syscon";
+			reg = <0x0 0x4b0c0000 0x0 0x10000>;
+			#address-cells = <1>;
+			#size-cells = <1>;
+			clocks = <&scmi_clk IMX94_CLK_DISPAPB>;
+			#clock-cells = <1>;
+			power-domains = <&scmi_devpd IMX94_PD_DISPLAY>;
+
+			ldb: ldb@4 {
+				compatible = "fsl,imx94-ldb";
+				reg = <0x4 0x4>, <0x8 0x4>;
+				reg-names = "ldb", "lvds";
+				clocks = <&lvds_csr IMX94_CLK_DISPMIX_LVDS_CLK_GATE>;
+				clock-names = "ldb";
+				status = "disabled";
+
+				ports {
+					#address-cells = <1>;
+					#size-cells = <0>;
+
+					port@0 {
+						reg = <0>;
+
+						lvds_in: endpoint {
+							remote-endpoint = <&dcif_out>;
+						};
+					};
+
+					port@1 {
+						reg = <1>;
+					};
+				};
+			};
+		};
+
+		dcif: display-controller@4b120000 {
+			compatible = "nxp,imx94-dcif";
+			reg = <0x0 0x4b120000 0x0 0x300000>;
+			interrupts = <GIC_SPI 377 IRQ_TYPE_LEVEL_HIGH>,
+				     <GIC_SPI 378 IRQ_TYPE_LEVEL_HIGH>,
+				     <GIC_SPI 379 IRQ_TYPE_LEVEL_HIGH>;
+			interrupt-names = "common", "bg_layer", "fg_layer";
+			clocks = <&scmi_clk IMX94_CLK_DISPAPB>,
+				 <&scmi_clk IMX94_CLK_DISPAXI>,
+				 <&dispmix_csr IMX94_CLK_DISPMIX_CLK_SEL>;
+			clock-names = "apb", "axi", "pix";
+			assigned-clocks = <&dispmix_csr IMX94_CLK_DISPMIX_CLK_SEL>;
+			assigned-clock-parents = <&clk_ldb_pll_div7>;
+			power-domains = <&scmi_devpd IMX94_PD_DISPLAY>;
+			status = "disabled";
+
+			port {
+				dcif_out: endpoint {
+					remote-endpoint = <&lvds_in>;
+				};
+			};
+		};
+
 		hsio_blk_ctl: syscon@4c0100c0 {
 			compatible = "nxp,imx95-hsio-blk-ctl", "syscon";
 			reg = <0x0 0x4c0100c0 0x0 0x1>;

-- 
2.51.0


^ permalink raw reply related

* [PATCH v9 6/9] dt-bindings: clock: nxp,imx95-blk-ctl: Add ldb child node
From: Laurentiu Palcu @ 2026-06-12 11:58 UTC (permalink / raw)
  To: Ying Liu, Luca Ceresoli, Abel Vesa, Peng Fan, Michael Turquette,
	Stephen Boyd, Brian Masney, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Frank Li, Sascha Hauer, Pengutronix Kernel Team,
	Fabio Estevam
  Cc: Laurentiu Palcu, linux-clk, imx, devicetree, linux-arm-kernel,
	linux-kernel, dri-devel
In-Reply-To: <20260612-dcif-upstreaming-v9-0-8d0ff89aa3c5@oss.nxp.com>

Since the BLK CTL registers, like the LVDS CSR, can be used to control the
LVDS Display Bridge controllers, add 'ldb' child node to handle
these use cases.

Reviewed-by: Krzysztof Kozlowski <krzysztof.kozlowski@linaro.org>
Signed-off-by: Laurentiu Palcu <laurentiu.palcu@oss.nxp.com>
---
 .../bindings/clock/nxp,imx95-blk-ctl.yaml          | 26 ++++++++++++++++++++++
 1 file changed, 26 insertions(+)

diff --git a/Documentation/devicetree/bindings/clock/nxp,imx95-blk-ctl.yaml b/Documentation/devicetree/bindings/clock/nxp,imx95-blk-ctl.yaml
index 27403b4c52d62..2b3c762aba1e0 100644
--- a/Documentation/devicetree/bindings/clock/nxp,imx95-blk-ctl.yaml
+++ b/Documentation/devicetree/bindings/clock/nxp,imx95-blk-ctl.yaml
@@ -26,6 +26,12 @@ properties:
   reg:
     maxItems: 1
 
+  '#address-cells':
+    const: 1
+
+  '#size-cells':
+    const: 1
+
   power-domains:
     maxItems: 1
 
@@ -39,6 +45,11 @@ properties:
       ID in its "clocks" phandle cell. See
       include/dt-bindings/clock/nxp,imx95-clock.h
 
+patternProperties:
+  '^ldb@[0-9a-f]+$':
+    type: object
+    $ref: /schemas/display/bridge/fsl,ldb.yaml#
+
 required:
   - compatible
   - reg
@@ -46,6 +57,21 @@ required:
   - power-domains
   - clocks
 
+allOf:
+  - if:
+      not:
+        properties:
+          compatible:
+            contains:
+              const: nxp,imx94-lvds-csr
+    then:
+      patternProperties:
+        "^ldb@[0-9a-f]+$": false
+    else:
+      required:
+        - '#address-cells'
+        - '#size-cells'
+
 additionalProperties: false
 
 examples:

-- 
2.51.0


^ permalink raw reply related

* [PATCH v9 5/9] drm/imx: Add support for i.MX94 DCIF
From: Laurentiu Palcu @ 2026-06-12 11:58 UTC (permalink / raw)
  To: Ying Liu, Luca Ceresoli, Maarten Lankhorst, Maxime Ripard,
	Thomas Zimmermann, David Airlie, Simona Vetter, Frank Li,
	Sascha Hauer, Pengutronix Kernel Team, Fabio Estevam
  Cc: Laurentiu Palcu, linux-clk, imx, devicetree, linux-arm-kernel,
	linux-kernel, dri-devel
In-Reply-To: <20260612-dcif-upstreaming-v9-0-8d0ff89aa3c5@oss.nxp.com>

From: Sandor Yu <sandor.yu@nxp.com>

The i.MX94 Display Control Interface features:
 * Up to maximum 3 layers of alpha blending:
    - 1 background layer(Layer 0);
    - 1 foreground layer(Layer 1);
    - A programmable constant color behind the background layer;
 * Each layer supports:
    - programmable plane size;
    - programmable background color;
    - embedded alpha and global alpha;
 * Data output with CRC checksum for 4 programmable regions;

Reviewed-by: Luca Ceresoli <luca.ceresoli@bootlin.com> # bridge refcounting
Signed-off-by: Sandor Yu <sandor.yu@nxp.com>
Co-developed-by: Laurentiu Palcu <laurentiu.palcu@oss.nxp.com>
Signed-off-by: Laurentiu Palcu <laurentiu.palcu@oss.nxp.com>
---
 drivers/gpu/drm/imx/Kconfig           |   1 +
 drivers/gpu/drm/imx/Makefile          |   1 +
 drivers/gpu/drm/imx/dcif/Kconfig      |  15 +
 drivers/gpu/drm/imx/dcif/Makefile     |   5 +
 drivers/gpu/drm/imx/dcif/dcif-crc.c   | 215 +++++++++++
 drivers/gpu/drm/imx/dcif/dcif-crc.h   |  43 +++
 drivers/gpu/drm/imx/dcif/dcif-crtc.c  | 705 ++++++++++++++++++++++++++++++++++
 drivers/gpu/drm/imx/dcif/dcif-drv.c   | 233 +++++++++++
 drivers/gpu/drm/imx/dcif/dcif-drv.h   |  89 +++++
 drivers/gpu/drm/imx/dcif/dcif-kms.c   |  96 +++++
 drivers/gpu/drm/imx/dcif/dcif-plane.c | 308 +++++++++++++++
 drivers/gpu/drm/imx/dcif/dcif-reg.h   | 267 +++++++++++++
 12 files changed, 1978 insertions(+)

diff --git a/drivers/gpu/drm/imx/Kconfig b/drivers/gpu/drm/imx/Kconfig
index 3e8c6edbc17c2..1b6ced5c60b51 100644
--- a/drivers/gpu/drm/imx/Kconfig
+++ b/drivers/gpu/drm/imx/Kconfig
@@ -1,6 +1,7 @@
 # SPDX-License-Identifier: GPL-2.0-only
 
 source "drivers/gpu/drm/imx/dc/Kconfig"
+source "drivers/gpu/drm/imx/dcif/Kconfig"
 source "drivers/gpu/drm/imx/dcss/Kconfig"
 source "drivers/gpu/drm/imx/ipuv3/Kconfig"
 source "drivers/gpu/drm/imx/lcdc/Kconfig"
diff --git a/drivers/gpu/drm/imx/Makefile b/drivers/gpu/drm/imx/Makefile
index c7b317640d71d..2b9fd85eefaa3 100644
--- a/drivers/gpu/drm/imx/Makefile
+++ b/drivers/gpu/drm/imx/Makefile
@@ -1,6 +1,7 @@
 # SPDX-License-Identifier: GPL-2.0
 
 obj-$(CONFIG_DRM_IMX8_DC) += dc/
+obj-$(CONFIG_DRM_IMX_DCIF) += dcif/
 obj-$(CONFIG_DRM_IMX_DCSS) += dcss/
 obj-$(CONFIG_DRM_IMX) += ipuv3/
 obj-$(CONFIG_DRM_IMX_LCDC) += lcdc/
diff --git a/drivers/gpu/drm/imx/dcif/Kconfig b/drivers/gpu/drm/imx/dcif/Kconfig
new file mode 100644
index 0000000000000..c33c662721d36
--- /dev/null
+++ b/drivers/gpu/drm/imx/dcif/Kconfig
@@ -0,0 +1,15 @@
+config DRM_IMX_DCIF
+	tristate "DRM support for NXP i.MX94 DCIF"
+	select DRM_KMS_HELPER
+	select VIDEOMODE_HELPERS
+	select DRM_GEM_DMA_HELPER
+	select DRM_DISPLAY_HELPER
+	select DRM_BRIDGE_CONNECTOR
+	select DRM_CLIENT_SELECTION
+	depends on DRM && OF && ARCH_MXC
+	depends on COMMON_CLK
+	help
+	  Enable NXP i.MX94 Display Control Interface(DCIF) support. The DCIF is
+	  a system master that fetches graphics stored in memory and displays
+	  them on a TFT LCD panel or connects to a display interface depending
+	  on the chip configuration.
diff --git a/drivers/gpu/drm/imx/dcif/Makefile b/drivers/gpu/drm/imx/dcif/Makefile
new file mode 100644
index 0000000000000..b429572040f0e
--- /dev/null
+++ b/drivers/gpu/drm/imx/dcif/Makefile
@@ -0,0 +1,5 @@
+# SPDX-License-Identifier: GPL-2.0
+
+imx-dcif-drm-objs := dcif-crc.o dcif-crtc.o dcif-drv.o dcif-kms.o dcif-plane.o
+
+obj-$(CONFIG_DRM_IMX_DCIF) += imx-dcif-drm.o
diff --git a/drivers/gpu/drm/imx/dcif/dcif-crc.c b/drivers/gpu/drm/imx/dcif/dcif-crc.c
new file mode 100644
index 0000000000000..dee36e5ca6793
--- /dev/null
+++ b/drivers/gpu/drm/imx/dcif/dcif-crc.c
@@ -0,0 +1,215 @@
+// SPDX-License-Identifier: GPL-2.0
+
+/*
+ * Copyright 2025 NXP
+ */
+
+#include <linux/regmap.h>
+
+#include <drm/drm_atomic.h>
+#include <drm/drm_rect.h>
+
+#include "dcif-crc.h"
+#include "dcif-reg.h"
+
+#define MAX_DCIF_CRC_NUM       4
+
+static int dcif_crc_config(struct dcif_dev *dcif, struct drm_rect *roi, int ncrc)
+{
+	int pos, size;
+
+	if (ncrc >= MAX_DCIF_CRC_NUM)
+		return -EINVAL;
+
+	pos = DCIF_CRC_POS_CRC_HOR_POS(roi->x1) |
+	      DCIF_CRC_POS_CRC_VER_POS(roi->y1);
+	size = DCIF_CRC_SIZE_CRC_HOR_SIZE(roi->x2 - roi->x1) |
+	       DCIF_CRC_SIZE_CRC_VER_SIZE(roi->y2 - roi->y1);
+
+	regmap_write(dcif->regmap, DCIF_CRC_POS_R(ncrc), pos);
+	regmap_write(dcif->regmap, DCIF_CRC_SIZE_R(ncrc), size);
+
+	regmap_set_bits(dcif->regmap, DCIF_CRC_CTRL,
+			DCIF_CRC_CTRL_CRC_EN(ncrc) | DCIF_CRC_CTRL_CRC_ERR_CNT_RST);
+
+	return 0;
+}
+
+void dcif_crc_source_enable(struct dcif_dev *dcif, enum dcif_crc_source source,
+			    struct drm_rect *roi, int ncrc)
+{
+	if (ncrc >= MAX_DCIF_CRC_NUM)
+		return;
+
+	if (source == DCIF_CRC_SRC_NONE)
+		return;
+
+	if (dcif->crc_is_enabled)
+		return;
+
+	dcif_crc_config(dcif, roi, ncrc);
+
+	regmap_set_bits(dcif->regmap, DCIF_CRC_CTRL,
+			DCIF_CRC_CTRL_CRC_MODE | DCIF_CRC_CTRL_CRC_SHADOW_LOAD_EN |
+			DCIF_CRC_CTRL_CRC_TRIG);
+
+	dcif->crc_is_enabled = true;
+}
+
+void dcif_crc_source_disable(struct dcif_dev *dcif, int ncrc)
+{
+	if (!dcif->crc_is_enabled)
+		return;
+
+	if (ncrc >= MAX_DCIF_CRC_NUM)
+		return;
+
+	regmap_clear_bits(dcif->regmap, DCIF_CRC_CTRL, DCIF_CRC_CTRL_CRC_EN(ncrc));
+
+	dcif->crc_is_enabled = false;
+}
+
+/*
+ * Supported modes and source names:
+ * 1) auto mode:
+ *    "auto" should be selected as the source name.
+ *    The evaluation window is the same to the display region as
+ *    indicated by drm_crtc_state->adjusted_mode.
+ *
+ * 2) region of interest(ROI) mode:
+ *    "roi:x1,y1,x2,y2" should be selected as the source name.
+ *    The region of interest is defined by the inclusive upper left
+ *    position at (x1, y1) and the exclusive lower right position
+ *    at (x2, y2), see struct drm_rect for the same idea.
+ *    The evaluation window is the region of interest.
+ */
+static int dcif_crc_source_parse(struct drm_crtc *crtc, const char *source_name,
+				 enum dcif_crc_source *s, struct drm_rect *roi)
+{
+	static const char roi_prefix[] = "roi:";
+
+	if (!source_name) {
+		*s = DCIF_CRC_SRC_NONE;
+	} else if (!strcmp(source_name, "auto")) {
+		struct drm_display_mode *mode = &crtc->state->adjusted_mode;
+
+		roi->x1 = 0;
+		roi->y1 = 0;
+		roi->x2 = mode->hdisplay;
+		roi->y2 = mode->vdisplay;
+
+		*s = DCIF_CRC_SRC_FRAME;
+	} else if (strstarts(source_name, roi_prefix)) {
+		int len = strlen(roi_prefix);
+		unsigned int params[4];
+		char *options, *opt;
+		int i = 0, ret;
+
+		char *buf __free(kfree) = kstrdup(source_name + len, GFP_KERNEL);
+		if (!buf)
+			return -ENOMEM;
+
+		options = buf;
+
+		while ((opt = strsep(&options, ",")) != NULL) {
+			if (i > 3)
+				return -EINVAL;
+
+			ret = kstrtouint(opt, 10, &params[i]);
+			if (ret < 0)
+				return ret;
+
+			i++;
+		}
+
+		if (i != 4)
+			return -EINVAL;
+
+		roi->x1 = params[0];
+		roi->y1 = params[1];
+		roi->x2 = params[2];
+		roi->y2 = params[3];
+
+		if (!drm_rect_visible(roi))
+			return -EINVAL;
+
+		*s = DCIF_CRC_SRC_FRAME_ROI;
+	} else {
+		return -EINVAL;
+	}
+
+	return 0;
+}
+
+int dcif_crc_source_verify(struct drm_crtc *crtc, const char *source_name,
+			   size_t *values_cnt)
+{
+	struct dcif_dev *dcif = crtc_to_dcif_dev(crtc);
+	enum dcif_crc_source source;
+	struct drm_rect roi;
+
+	if (dcif_crc_source_parse(crtc, source_name, &source, &roi) < 0) {
+		dev_dbg(dcif->drm.dev, "unknown source %s\n", source_name);
+		return -EINVAL;
+	}
+
+	*values_cnt = 1;
+
+	return 0;
+}
+
+int dcif_crc_source_set(struct drm_crtc *crtc, const char *source_name)
+{
+	struct dcif_dev *dcif = crtc_to_dcif_dev(crtc);
+	struct drm_modeset_acquire_ctx ctx;
+	struct drm_crtc_state *crtc_state;
+	struct drm_atomic_commit *state;
+	struct drm_rect roi = {0, 0, 0, 0};
+	enum dcif_crc_source source;
+	int ret;
+
+	if (dcif_crc_source_parse(crtc, source_name, &source, &roi) < 0) {
+		dev_dbg(dcif->drm.dev, "unknown source %s\n", source_name);
+		return -EINVAL;
+	}
+
+	/* Perform an atomic commit to set the CRC source. */
+	drm_modeset_acquire_init(&ctx, 0);
+
+	state = drm_atomic_commit_alloc(crtc->dev);
+	if (!state) {
+		ret = -ENOMEM;
+		goto unlock;
+	}
+
+	state->acquire_ctx = &ctx;
+
+retry:
+	crtc_state = drm_atomic_get_crtc_state(state, crtc);
+	if (!IS_ERR(crtc_state)) {
+		struct dcif_crtc_state *dcif_crtc_state;
+
+		dcif_crtc_state = to_dcif_crtc_state(crtc_state);
+
+		dcif_crtc_state->crc.source = source;
+		dcif_copy_roi(&roi, &dcif_crtc_state->crc.roi);
+
+		ret = drm_atomic_commit(state);
+	} else {
+		ret = PTR_ERR(crtc_state);
+	}
+
+	if (ret == -EDEADLK) {
+		drm_atomic_commit_clear(state);
+		drm_modeset_backoff(&ctx);
+		goto retry;
+	}
+
+	drm_atomic_commit_put(state);
+
+unlock:
+	drm_modeset_drop_locks(&ctx);
+	drm_modeset_acquire_fini(&ctx);
+
+	return ret;
+}
diff --git a/drivers/gpu/drm/imx/dcif/dcif-crc.h b/drivers/gpu/drm/imx/dcif/dcif-crc.h
new file mode 100644
index 0000000000000..6ccb1b7186732
--- /dev/null
+++ b/drivers/gpu/drm/imx/dcif/dcif-crc.h
@@ -0,0 +1,43 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+
+/*
+ * Copyright 2025 NXP
+ */
+
+#ifndef __DCIF_CRC_H__
+#define __DCIF_CRC_H__
+
+#include <linux/types.h>
+
+#include "dcif-drv.h"
+
+static inline bool enable_dcif_crc_needed(struct dcif_crtc_state *new_dcstate,
+					  struct dcif_crtc_state *old_dcstate)
+{
+	return old_dcstate->crc.source == DCIF_CRC_SRC_NONE &&
+	       new_dcstate->crc.source != DCIF_CRC_SRC_NONE;
+}
+
+static inline bool disable_dcif_crc_needed(struct dcif_crtc_state *new_dcstate,
+					   struct dcif_crtc_state *old_dcstate)
+{
+	return old_dcstate->crc.source != DCIF_CRC_SRC_NONE &&
+	       new_dcstate->crc.source == DCIF_CRC_SRC_NONE;
+}
+
+static inline void dcif_copy_roi(struct drm_rect *from, struct drm_rect *to)
+{
+	to->x1 = from->x1;
+	to->y1 = from->y1;
+	to->x2 = from->x2;
+	to->y2 = from->y2;
+}
+
+int dcif_crc_source_verify(struct drm_crtc *crtc, const char *source_name,
+			   size_t *values_cnt);
+int dcif_crc_source_set(struct drm_crtc *crtc, const char *source_name);
+void dcif_crc_source_enable(struct dcif_dev *dcif, enum dcif_crc_source source,
+			    struct drm_rect *roi, int ncrc);
+void dcif_crc_source_disable(struct dcif_dev *dcif, int ncrc);
+
+#endif /* __DCIF_CRC_H__ */
diff --git a/drivers/gpu/drm/imx/dcif/dcif-crtc.c b/drivers/gpu/drm/imx/dcif/dcif-crtc.c
new file mode 100644
index 0000000000000..1fa2dfb2e0b98
--- /dev/null
+++ b/drivers/gpu/drm/imx/dcif/dcif-crtc.c
@@ -0,0 +1,705 @@
+// SPDX-License-Identifier: GPL-2.0
+
+/*
+ * Copyright 2025 NXP
+ */
+
+#include <linux/irqreturn.h>
+#include <linux/media-bus-format.h>
+#include <linux/pm_runtime.h>
+#include <linux/regmap.h>
+
+#include <drm/drm_atomic.h>
+#include <drm/drm_atomic_helper.h>
+#include <drm/drm_atomic_state_helper.h>
+#include <drm/drm_bridge.h>
+#include <drm/drm_fb_dma_helper.h>
+#include <drm/drm_fourcc.h>
+#include <drm/drm_framebuffer.h>
+#include <drm/drm_plane.h>
+#include <drm/drm_print.h>
+#include <drm/drm_vblank.h>
+
+#include "dcif-crc.h"
+#include "dcif-drv.h"
+#include "dcif-reg.h"
+
+#define DCIF_MAX_PIXEL_CLOCK		148500000
+
+/* -----------------------------------------------------------------------------
+ * CRTC
+ */
+
+/*
+ * For conversion from YCbCr to RGB, the CSC operates as follows:
+ *
+ * |R|   |A1 A2 A3|   |Y  + D1|
+ * |G| = |B1 B2 B3| * |Cb + D2|
+ * |B|   |C1 C2 C3|   |Cr + D3|
+ *
+ * The A, B and C coefficients are expressed as signed Q3.8 fixed point values and
+ * the D coefficients as signed Q9.0.
+ */
+static const u32 dcif_yuv2rgb_coeffs[3][2][6] = {
+	[DRM_COLOR_YCBCR_BT601] = {
+		[DRM_COLOR_YCBCR_LIMITED_RANGE] = {
+			/*
+			 * BT.601 limited range:
+			 *
+			 * |R|   |1.1644  0.0000  1.5960|   |Y  - 16 |
+			 * |G| = |1.1644 -0.3917 -0.8129| * |Cb - 128|
+			 * |B|   |1.1644  2.0172  0.0000|   |Cr - 128|
+			 */
+			DCIF_CSC_COEF0_L0_A1(0x12a) | DCIF_CSC_COEF0_L0_A2(0x000),
+			DCIF_CSC_COEF1_L0_A3(0x199) | DCIF_CSC_COEF1_L0_B1(0x12a),
+			DCIF_CSC_COEF2_L0_B2(0x79c) | DCIF_CSC_COEF2_L0_B3(0x730),
+			DCIF_CSC_COEF3_L0_C1(0x12a) | DCIF_CSC_COEF3_L0_C2(0x204),
+			DCIF_CSC_COEF4_L0_C3(0x000) | DCIF_CSC_COEF4_L0_D1(0x1f0),
+			DCIF_CSC_COEF5_L0_D2(0x180) | DCIF_CSC_COEF5_L0_D3(0x180),
+		},
+		[DRM_COLOR_YCBCR_FULL_RANGE] = {
+			/*
+			 * BT.601 full range:
+			 *
+			 * |R|   |1.0000  0.0000  1.4020|   |Y  - 0  |
+			 * |G| = |1.0000 -0.3441 -0.7141| * |Cb - 128|
+			 * |B|   |1.0000  1.7720  0.0000|   |Cr - 128|
+			 */
+			DCIF_CSC_COEF0_L0_A1(0x100) | DCIF_CSC_COEF0_L0_A2(0x000),
+			DCIF_CSC_COEF1_L0_A3(0x167) | DCIF_CSC_COEF1_L0_B1(0x100),
+			DCIF_CSC_COEF2_L0_B2(0x7a8) | DCIF_CSC_COEF2_L0_B3(0x749),
+			DCIF_CSC_COEF3_L0_C1(0x100) | DCIF_CSC_COEF3_L0_C2(0x1c6),
+			DCIF_CSC_COEF4_L0_C3(0x000) | DCIF_CSC_COEF4_L0_D1(0x000),
+			DCIF_CSC_COEF5_L0_D2(0x180) | DCIF_CSC_COEF5_L0_D3(0x180),
+		},
+	},
+	[DRM_COLOR_YCBCR_BT709] = {
+		[DRM_COLOR_YCBCR_LIMITED_RANGE] = {
+			/*
+			 * Rec.709 limited range:
+			 *
+			 * |R|   |1.1644  0.0000  1.7927|   |Y  - 16 |
+			 * |G| = |1.1644 -0.2132 -0.5329| * |Cb - 128|
+			 * |B|   |1.1644  2.1124  0.0000|   |Cr - 128|
+			 */
+			DCIF_CSC_COEF0_L0_A1(0x12a) | DCIF_CSC_COEF0_L0_A2(0x000),
+			DCIF_CSC_COEF1_L0_A3(0x1cb) | DCIF_CSC_COEF1_L0_B1(0x12a),
+			DCIF_CSC_COEF2_L0_B2(0x7c9) | DCIF_CSC_COEF2_L0_B3(0x778),
+			DCIF_CSC_COEF3_L0_C1(0x12a) | DCIF_CSC_COEF3_L0_C2(0x21d),
+			DCIF_CSC_COEF4_L0_C3(0x000) | DCIF_CSC_COEF4_L0_D1(0x1f0),
+			DCIF_CSC_COEF5_L0_D2(0x180) | DCIF_CSC_COEF5_L0_D3(0x180),
+		},
+		[DRM_COLOR_YCBCR_FULL_RANGE] = {
+			/*
+			 * Rec.709 full range:
+			 *
+			 * |R|   |1.0000  0.0000  1.5748|   |Y  - 0  |
+			 * |G| = |1.0000 -0.1873 -0.4681| * |Cb - 128|
+			 * |B|   |1.0000  1.8556  0.0000|   |Cr - 128|
+			 */
+			DCIF_CSC_COEF0_L0_A1(0x100) | DCIF_CSC_COEF0_L0_A2(0x000),
+			DCIF_CSC_COEF1_L0_A3(0x193) | DCIF_CSC_COEF1_L0_B1(0x100),
+			DCIF_CSC_COEF2_L0_B2(0x7d0) | DCIF_CSC_COEF2_L0_B3(0x788),
+			DCIF_CSC_COEF3_L0_C1(0x100) | DCIF_CSC_COEF3_L0_C2(0x1db),
+			DCIF_CSC_COEF4_L0_C3(0x000) | DCIF_CSC_COEF4_L0_D1(0x000),
+			DCIF_CSC_COEF5_L0_D2(0x180) | DCIF_CSC_COEF5_L0_D3(0x180),
+		},
+	},
+	[DRM_COLOR_YCBCR_BT2020] = {
+		[DRM_COLOR_YCBCR_LIMITED_RANGE] = {
+			/*
+			 * BT.2020 limited range:
+			 *
+			 * |R|   |1.1644  0.0000  1.6787|   |Y  - 16 |
+			 * |G| = |1.1644 -0.1874 -0.6505| * |Cb - 128|
+			 * |B|   |1.1644  2.1418  0.0000|   |Cr - 128|
+			 */
+			DCIF_CSC_COEF0_L0_A1(0x12a) | DCIF_CSC_COEF0_L0_A2(0x000),
+			DCIF_CSC_COEF1_L0_A3(0x1ae) | DCIF_CSC_COEF1_L0_B1(0x12a),
+			DCIF_CSC_COEF2_L0_B2(0x7d0) | DCIF_CSC_COEF2_L0_B3(0x759),
+			DCIF_CSC_COEF3_L0_C1(0x12a) | DCIF_CSC_COEF3_L0_C2(0x224),
+			DCIF_CSC_COEF4_L0_C3(0x000) | DCIF_CSC_COEF4_L0_D1(0x1f0),
+			DCIF_CSC_COEF5_L0_D2(0x180) | DCIF_CSC_COEF5_L0_D3(0x180),
+		},
+		[DRM_COLOR_YCBCR_FULL_RANGE] = {
+			/*
+			 * BT.2020 full range:
+			 *
+			 * |R|   |1.0000  0.0000  1.4746|   |Y  - 0  |
+			 * |G| = |1.0000 -0.1646 -0.5714| * |Cb - 128|
+			 * |B|   |1.0000  1.8814  0.0000|   |Cr - 128|
+			 */
+			DCIF_CSC_COEF0_L0_A1(0x100) | DCIF_CSC_COEF0_L0_A2(0x000),
+			DCIF_CSC_COEF1_L0_A3(0x179) | DCIF_CSC_COEF1_L0_B1(0x100),
+			DCIF_CSC_COEF2_L0_B2(0x7d6) | DCIF_CSC_COEF2_L0_B3(0x76e),
+			DCIF_CSC_COEF3_L0_C1(0x100) | DCIF_CSC_COEF3_L0_C2(0x1e2),
+			DCIF_CSC_COEF4_L0_C3(0x000) | DCIF_CSC_COEF4_L0_D1(0x000),
+			DCIF_CSC_COEF5_L0_D2(0x180) | DCIF_CSC_COEF5_L0_D3(0x180),
+		},
+	},
+};
+
+static enum drm_mode_status dcif_crtc_mode_valid(struct drm_crtc *crtc,
+						 const struct drm_display_mode *mode)
+{
+	if (mode->crtc_clock > DCIF_MAX_PIXEL_CLOCK)
+		return MODE_CLOCK_HIGH;
+
+	return MODE_OK;
+}
+
+static void dcif_set_formats(struct dcif_dev *dcif, struct drm_plane_state *plane_state,
+			     const u32 bus_format)
+{
+	const u32 format = plane_state->fb->format->format;
+	struct drm_device *drm = &dcif->drm;
+	bool in_yuv = false;
+	u32 reg = 0;
+
+	switch (bus_format) {
+	case MEDIA_BUS_FMT_RGB565_1X16:
+		reg |= DCIF_DPI_CTRL_DATA_PATTERN(PATTERN_RGB565);
+		break;
+	case MEDIA_BUS_FMT_RGB888_1X24:
+		reg |= DCIF_DPI_CTRL_DATA_PATTERN(PATTERN_RGB888);
+		break;
+	case MEDIA_BUS_FMT_RBG888_1X24:
+		reg |= DCIF_DPI_CTRL_DATA_PATTERN(PATTERN_RBG888);
+		break;
+	case MEDIA_BUS_FMT_BGR888_1X24:
+		reg |= DCIF_DPI_CTRL_DATA_PATTERN(PATTERN_BGR888);
+		break;
+	case MEDIA_BUS_FMT_GBR888_1X24:
+		reg |= DCIF_DPI_CTRL_DATA_PATTERN(PATTERN_GBR888);
+		break;
+	default:
+		dev_err(drm->dev, "Unknown media bus format 0x%x\n", bus_format);
+		break;
+	}
+
+	regmap_update_bits(dcif->regmap, DCIF_DPI_CTRL, DCIF_DPI_CTRL_DATA_PATTERN_MASK, reg);
+
+	reg = 0;
+	switch (format) {
+	/* RGB Formats */
+	case DRM_FORMAT_RGB565:
+		reg |= DCIF_CTRLDESC0_FORMAT(CTRLDESCL0_FORMAT_RGB565);
+		break;
+	case DRM_FORMAT_RGB888:
+		reg |= DCIF_CTRLDESC0_FORMAT(CTRLDESCL0_FORMAT_RGB888);
+		break;
+	case DRM_FORMAT_XRGB1555:
+	case DRM_FORMAT_ARGB1555:
+		reg |= DCIF_CTRLDESC0_FORMAT(CTRLDESCL0_FORMAT_ARGB1555);
+		break;
+	case DRM_FORMAT_XRGB4444:
+	case DRM_FORMAT_ARGB4444:
+		reg |= DCIF_CTRLDESC0_FORMAT(CTRLDESCL0_FORMAT_ARGB4444);
+		break;
+	case DRM_FORMAT_XBGR8888:
+	case DRM_FORMAT_ABGR8888:
+		reg |= DCIF_CTRLDESC0_FORMAT(CTRLDESCL0_FORMAT_ABGR8888);
+		break;
+	case DRM_FORMAT_XRGB8888:
+	case DRM_FORMAT_ARGB8888:
+		reg |= DCIF_CTRLDESC0_FORMAT(CTRLDESCL0_FORMAT_ARGB8888);
+		break;
+
+	/* YUV Formats */
+	case DRM_FORMAT_YUYV:
+		reg |= DCIF_CTRLDESC0_FORMAT(CTRLDESCL0_FORMAT_YCBCR422) |
+		       DCIF_CTRLDESC0_YUV_FORMAT(CTRLDESCL0_YUV_FORMAT_VY2UY1);
+		in_yuv = true;
+		break;
+	case DRM_FORMAT_YVYU:
+		reg |= DCIF_CTRLDESC0_FORMAT(CTRLDESCL0_FORMAT_YCBCR422) |
+		       DCIF_CTRLDESC0_YUV_FORMAT(CTRLDESCL0_YUV_FORMAT_UY2VY1);
+		in_yuv = true;
+		break;
+	case DRM_FORMAT_UYVY:
+		reg |= DCIF_CTRLDESC0_FORMAT(CTRLDESCL0_FORMAT_YCBCR422) |
+		       DCIF_CTRLDESC0_YUV_FORMAT(CTRLDESCL0_YUV_FORMAT_Y2VY1U);
+		in_yuv = true;
+		break;
+	case DRM_FORMAT_VYUY:
+		reg |= DCIF_CTRLDESC0_FORMAT(CTRLDESCL0_FORMAT_YCBCR422) |
+		       DCIF_CTRLDESC0_YUV_FORMAT(CTRLDESCL0_YUV_FORMAT_Y2UY1V);
+		in_yuv = true;
+		break;
+
+	default:
+		dev_err(drm->dev, "Unknown pixel format 0x%x\n", format);
+		break;
+	}
+
+	regmap_update_bits(dcif->regmap, DCIF_CTRLDESC0(0),
+			   DCIF_CTRLDESC0_FORMAT_MASK | DCIF_CTRLDESC0_YUV_FORMAT_MASK,
+			   reg);
+
+	if (in_yuv) {
+		/* Enable CSC YCbCr -> RGB */
+		const u32 *coeffs =
+			dcif_yuv2rgb_coeffs[plane_state->color_encoding][plane_state->color_range];
+
+		regmap_bulk_write(dcif->regmap, DCIF_CSC_COEF0_L0, coeffs, 6);
+
+		regmap_write(dcif->regmap, DCIF_CSC_CTRL_L0,
+			     DCIF_CSC_CTRL_L0_CSC_EN |
+			     DCIF_CSC_CTRL_L0_CSC_MODE_YCBCR2RGB);
+	} else {
+		regmap_write(dcif->regmap, DCIF_CSC_CTRL_L0, 0);
+	}
+}
+
+static void dcif_set_mode(struct dcif_dev *dcif, u32 bus_flags)
+{
+	struct drm_display_mode *m = &dcif->crtc.state->adjusted_mode;
+	u32 reg = 0;
+
+	if (m->flags & DRM_MODE_FLAG_NHSYNC)
+		reg |= DCIF_DPI_CTRL_HSYNC_POL_LOW;
+	if (m->flags & DRM_MODE_FLAG_NVSYNC)
+		reg |= DCIF_DPI_CTRL_VSYNC_POL_LOW;
+	if (bus_flags & DRM_BUS_FLAG_DE_LOW)
+		reg |= DCIF_DPI_CTRL_DE_POL_LOW;
+	if (bus_flags & DRM_BUS_FLAG_PIXDATA_DRIVE_NEGEDGE)
+		reg |= DCIF_DPI_CTRL_PCLK_EDGE_FALLING;
+
+	regmap_update_bits(dcif->regmap, DCIF_DPI_CTRL, DCIF_DPI_CTRL_POL_MASK, reg);
+
+	/* config display timings */
+	reg = DCIF_DISP_SIZE_DISP_WIDTH(m->hdisplay) |
+	      DCIF_DISP_SIZE_DISP_HEIGHT(m->vdisplay);
+	regmap_write(dcif->regmap, DCIF_DISP_SIZE, reg);
+
+	reg = DCIF_DPI_HSYN_PAR_BP_H(m->htotal - m->hsync_end) |
+	      DCIF_DPI_HSYN_PAR_FP_H(m->hsync_start - m->hdisplay);
+	regmap_write(dcif->regmap, DCIF_DPI_HSYN_PAR, reg);
+
+	reg = DCIF_DPI_VSYN_PAR_BP_V(m->vtotal - m->vsync_end) |
+	      DCIF_DPI_VSYN_PAR_FP_V(m->vsync_start - m->vdisplay);
+	regmap_write(dcif->regmap, DCIF_DPI_VSYN_PAR, reg);
+
+	reg = DCIF_DPI_VSYN_HSYN_WIDTH_PW_V(m->vsync_end - m->vsync_start) |
+	      DCIF_DPI_VSYN_HSYN_WIDTH_PW_H(m->hsync_end - m->hsync_start);
+	regmap_write(dcif->regmap, DCIF_DPI_VSYN_HSYN_WIDTH, reg);
+
+	/* Layer 0 frame size */
+	reg = DCIF_CTRLDESC2_HEIGHT(m->vdisplay) |
+	      DCIF_CTRLDESC2_WIDTH(m->hdisplay);
+	regmap_write(dcif->regmap, DCIF_CTRLDESC2(0), reg);
+
+	/*
+	 * Configure P_SIZE, T_SIZE and pitch
+	 * 1. P_SIZE and T_SIZE should never be less than AXI bus width.
+	 * 2. P_SIZE should never be less than T_SIZE.
+	 */
+	reg = DCIF_CTRLDESC3_P_SIZE(2) | DCIF_CTRLDESC3_T_SIZE(2) |
+	      DCIF_CTRLDESC3_PITCH(dcif->crtc.primary->state->fb->pitches[0]);
+	regmap_write(dcif->regmap, DCIF_CTRLDESC3(0), reg);
+}
+
+static void dcif_enable_plane_panic(struct dcif_dev *dcif)
+{
+	u32 reg;
+
+	/* Set FIFO Panic watermarks, low 1/3, high 2/3. */
+	reg = DCIF_PANIC_THRES_LOW(1 * PANIC0_THRES_MAX / 3) |
+	      DCIF_PANIC_THRES_HIGH(2 * PANIC0_THRES_MAX / 3) |
+	      DCIF_PANIC_THRES_REQ_EN;
+	regmap_write(dcif->regmap, DCIF_PANIC_THRES(0), reg);
+	regmap_write(dcif->regmap, DCIF_PANIC_THRES(1), reg);
+
+	regmap_set_bits(dcif->regmap, DCIF_IE1(dcif->cpu_domain),
+			DCIF_INT1_FIFO_PANIC0 | DCIF_INT1_FIFO_PANIC1);
+}
+
+static void dcif_disable_plane_panic(struct dcif_dev *dcif)
+{
+	regmap_clear_bits(dcif->regmap, DCIF_IE1(dcif->cpu_domain),
+			  DCIF_INT1_FIFO_PANIC0 | DCIF_INT1_FIFO_PANIC1);
+	regmap_clear_bits(dcif->regmap, DCIF_PANIC_THRES(0), DCIF_PANIC_THRES_REQ_EN);
+	regmap_clear_bits(dcif->regmap, DCIF_PANIC_THRES(1), DCIF_PANIC_THRES_REQ_EN);
+}
+
+static void dcif_enable_controller(struct dcif_dev *dcif)
+{
+	/* Enable Display */
+	regmap_set_bits(dcif->regmap, DCIF_DISP_CTRL, DCIF_DISP_CTRL_DISP_ON);
+
+	/* Enable layer 0 */
+	regmap_set_bits(dcif->regmap, DCIF_CTRLDESC0(0), DCIF_CTRLDESC0_EN);
+}
+
+static void dcif_disable_controller(struct dcif_dev *dcif)
+{
+	u32 reg;
+	int ret;
+
+	/* Disable layer 0 */
+	regmap_clear_bits(dcif->regmap, DCIF_CTRLDESC0(0), DCIF_CTRLDESC0_EN);
+
+	ret = regmap_read_poll_timeout(dcif->regmap, DCIF_CTRLDESC0(0), reg,
+				       !(reg & DCIF_CTRLDESC0_EN), 0,
+				       36000); /* Wait ~2 frame times max */
+	if (ret)
+		drm_err(&dcif->drm, "Failed to disable controller!\n");
+
+	/* Disable Display */
+	regmap_clear_bits(dcif->regmap, DCIF_DISP_CTRL, DCIF_DISP_CTRL_DISP_ON);
+}
+
+static void dcif_shadow_load_enable(struct dcif_dev *dcif)
+{
+	regmap_write_bits(dcif->regmap, DCIF_CTRLDESC0(0), DCIF_CTRLDESC0_SHADOW_LOAD_EN,
+			  DCIF_CTRLDESC0_SHADOW_LOAD_EN);
+}
+
+static void dcif_reset_block(struct dcif_dev *dcif)
+{
+	regmap_set_bits(dcif->regmap, DCIF_DISP_CTRL, DCIF_DISP_CTRL_SW_RST);
+
+	regmap_clear_bits(dcif->regmap, DCIF_DISP_CTRL, DCIF_DISP_CTRL_SW_RST);
+}
+
+static void dcif_crtc_atomic_destroy_state(struct drm_crtc *crtc,
+					   struct drm_crtc_state *state)
+{
+	__drm_atomic_helper_crtc_destroy_state(state);
+	kfree(to_dcif_crtc_state(state));
+}
+
+static void dcif_crtc_reset(struct drm_crtc *crtc)
+{
+	struct dcif_crtc_state *state;
+
+	if (crtc->state)
+		dcif_crtc_atomic_destroy_state(crtc, crtc->state);
+
+	crtc->state = NULL;
+
+	state = kzalloc_obj(*state, GFP_KERNEL);
+	if (state)
+		__drm_atomic_helper_crtc_reset(crtc, &state->base);
+}
+
+static struct drm_crtc_state *dcif_crtc_atomic_duplicate_state(struct drm_crtc *crtc)
+{
+	struct dcif_crtc_state *old = to_dcif_crtc_state(crtc->state);
+	struct dcif_crtc_state *new;
+
+	if (WARN_ON(!crtc->state))
+		return NULL;
+
+	new = kzalloc_obj(*new, GFP_KERNEL);
+	if (!new)
+		return NULL;
+
+	__drm_atomic_helper_crtc_duplicate_state(crtc, &new->base);
+
+	new->bus_format = old->bus_format;
+	new->bus_flags = old->bus_flags;
+	new->crc.source = old->crc.source;
+	dcif_copy_roi(&old->crc.roi, &new->crc.roi);
+
+	return &new->base;
+}
+
+static void dcif_crtc_mode_set_nofb(struct drm_crtc_state *crtc_state,
+				    struct drm_plane_state *plane_state)
+{
+	struct dcif_crtc_state *dcif_crtc_state = to_dcif_crtc_state(crtc_state);
+	struct drm_device *drm = crtc_state->crtc->dev;
+	struct dcif_dev *dcif = crtc_to_dcif_dev(crtc_state->crtc);
+	struct drm_display_mode *m = &crtc_state->adjusted_mode;
+
+	dev_dbg(drm->dev, "Pixel clock: %dkHz\n", m->crtc_clock);
+	dev_dbg(drm->dev, "Bridge bus_flags: 0x%08X\n", dcif_crtc_state->bus_flags);
+	dev_dbg(drm->dev, "Mode flags: 0x%08X\n", m->flags);
+
+	dcif_reset_block(dcif);
+
+	dcif_set_formats(dcif, plane_state, dcif_crtc_state->bus_format);
+
+	dcif_set_mode(dcif, dcif_crtc_state->bus_flags);
+}
+
+static void dcif_crtc_queue_state_event(struct drm_crtc *crtc)
+{
+	struct dcif_dev *dcif = crtc_to_dcif_dev(crtc);
+
+	scoped_guard(spinlock_irq, &crtc->dev->event_lock) {
+		if (crtc->state->event) {
+			WARN_ON(drm_crtc_vblank_get(crtc));
+			WARN_ON(dcif->event);
+			dcif->event = crtc->state->event;
+			crtc->state->event = NULL;
+		}
+	}
+}
+
+static struct drm_bridge *dcif_crtc_get_bridge(struct drm_crtc *crtc,
+					       struct drm_crtc_state *crtc_state)
+{
+	struct drm_connector_state *conn_state;
+	struct drm_encoder *encoder;
+	struct drm_connector *conn;
+	struct drm_bridge *bridge;
+	int i;
+
+	for_each_new_connector_in_state(crtc_state->state, conn, conn_state, i) {
+		if (crtc != conn_state->crtc)
+			continue;
+
+		encoder = conn_state->best_encoder;
+
+		bridge = drm_bridge_chain_get_first_bridge(encoder);
+		if (bridge)
+			return bridge;
+	}
+
+	return NULL;
+}
+
+static void dcif_crtc_query_output_bus_format(struct drm_crtc *crtc,
+					      struct drm_crtc_state *crtc_state)
+{
+	struct dcif_crtc_state *dcif_state = to_dcif_crtc_state(crtc_state);
+	struct drm_bridge *bridge __free(drm_bridge_put) = NULL;
+	struct drm_bridge_state *bridge_state;
+
+	dcif_state->bus_format = MEDIA_BUS_FMT_RGB888_1X24;
+	dcif_state->bus_flags = 0;
+
+	bridge = dcif_crtc_get_bridge(crtc, crtc_state);
+	if (!bridge)
+		return;
+
+	bridge_state = drm_atomic_get_new_bridge_state(crtc_state->state, bridge);
+	if (!bridge_state)
+		return;
+
+	dcif_state->bus_format = bridge_state->input_bus_cfg.format;
+	dcif_state->bus_flags = bridge_state->input_bus_cfg.flags;
+}
+
+static int dcif_crtc_atomic_check(struct drm_crtc *crtc, struct drm_atomic_commit *state)
+{
+	struct drm_crtc_state *crtc_state = drm_atomic_get_new_crtc_state(state, crtc);
+	bool enable_primary = crtc_state->plane_mask & drm_plane_mask(crtc->primary);
+	int ret;
+
+	if (crtc_state->active && !enable_primary)
+		return -EINVAL;
+
+	dcif_crtc_query_output_bus_format(crtc, crtc_state);
+
+	if (crtc_state->active_changed && crtc_state->active) {
+		if (!crtc_state->mode_changed) {
+			crtc_state->mode_changed = true;
+			ret = drm_atomic_helper_check_modeset(crtc->dev, state);
+			if (ret)
+				return ret;
+		}
+	}
+
+	return 0;
+}
+
+static void dcif_crtc_atomic_flush(struct drm_crtc *crtc,
+				   struct drm_atomic_commit *state)
+{
+	struct drm_crtc_state *old_crtc_state = drm_atomic_get_old_crtc_state(state, crtc);
+	struct dcif_crtc_state *old_dcif_crtc_state = to_dcif_crtc_state(old_crtc_state);
+	struct drm_crtc_state *crtc_state = drm_atomic_get_new_crtc_state(state, crtc);
+	struct dcif_crtc_state *dcif_crtc_state = to_dcif_crtc_state(crtc_state);
+	struct dcif_dev *dcif = crtc_to_dcif_dev(crtc);
+
+	dcif_shadow_load_enable(dcif);
+
+	if (drm_atomic_crtc_needs_modeset(crtc->state))
+		return;
+
+	if (dcif->has_crc && disable_dcif_crc_needed(dcif_crtc_state,
+						     old_dcif_crtc_state))
+		dcif_crc_source_disable(dcif, 0);
+
+	dcif_crtc_queue_state_event(crtc);
+
+	if (dcif->has_crc && enable_dcif_crc_needed(dcif_crtc_state,
+						    old_dcif_crtc_state))
+		dcif_crc_source_enable(dcif, dcif_crtc_state->crc.source,
+				       &dcif_crtc_state->crc.roi, 0);
+}
+
+static void dcif_crtc_atomic_enable(struct drm_crtc *crtc,
+				    struct drm_atomic_commit *state)
+{
+	struct drm_crtc_state *crtc_state = drm_atomic_get_new_crtc_state(state, crtc);
+	struct drm_plane_state *plane_state = drm_atomic_get_new_plane_state(state, crtc->primary);
+	struct dcif_crtc_state *dcif_crtc_state = to_dcif_crtc_state(crtc_state);
+	struct drm_display_mode *adj = &crtc_state->adjusted_mode;
+	struct dcif_dev *dcif = crtc_to_dcif_dev(crtc);
+	struct drm_device *drm = crtc->dev;
+	dma_addr_t baseaddr;
+	int ret;
+
+	dev_dbg(drm->dev, "mode " DRM_MODE_FMT "\n", DRM_MODE_ARG(adj));
+
+	/* enable power when we start to set mode for CRTC */
+	ret = pm_runtime_resume_and_get(drm->dev);
+	if (ret < 0) {
+		drm_err(drm, "failed to resume DCIF, ret = %d\n", ret);
+		return;
+	}
+	dcif->crtc_pm_enabled = true;
+
+	drm_crtc_vblank_on(crtc);
+
+	dcif_crtc_mode_set_nofb(crtc_state, plane_state);
+
+	baseaddr = drm_fb_dma_get_gem_addr(plane_state->fb, plane_state, 0);
+	if (baseaddr)
+		regmap_write(dcif->regmap, DCIF_CTRLDESC4(0), baseaddr);
+
+	dcif_enable_plane_panic(dcif);
+	dcif_enable_controller(dcif);
+
+	dcif_crtc_queue_state_event(crtc);
+
+	if (dcif->has_crc && dcif_crtc_state->crc.source != DCIF_CRC_SRC_NONE)
+		dcif_crc_source_enable(dcif, dcif_crtc_state->crc.source,
+				       &dcif_crtc_state->crc.roi, 0);
+}
+
+static void dcif_crtc_atomic_disable(struct drm_crtc *crtc,
+				     struct drm_atomic_commit *state)
+{
+	struct drm_crtc_state *crtc_state = drm_atomic_get_new_crtc_state(state, crtc);
+	struct dcif_crtc_state *dcif_crtc_state = to_dcif_crtc_state(crtc_state);
+	struct dcif_dev *dcif = crtc_to_dcif_dev(crtc);
+	struct drm_device *drm = crtc->dev;
+
+	if (dcif->has_crc && dcif_crtc_state->crc.source != DCIF_CRC_SRC_NONE)
+		dcif_crc_source_disable(dcif, 0);
+
+	dcif_disable_controller(dcif);
+	dcif_disable_plane_panic(dcif);
+
+	drm_crtc_vblank_off(crtc);
+
+	if (dcif->crtc_pm_enabled) {
+		dcif->crtc_pm_enabled = false;
+		pm_runtime_put_sync(drm->dev);
+	}
+
+	scoped_guard(spinlock_irq, &crtc->dev->event_lock) {
+		if (crtc->state->event && !crtc->state->active) {
+			drm_crtc_send_vblank_event(crtc, crtc->state->event);
+			crtc->state->event = NULL;
+		}
+	}
+}
+
+static const struct drm_crtc_helper_funcs dcif_crtc_helper_funcs = {
+	.mode_valid	= dcif_crtc_mode_valid,
+	.atomic_check	= dcif_crtc_atomic_check,
+	.atomic_flush	= dcif_crtc_atomic_flush,
+	.atomic_enable	= dcif_crtc_atomic_enable,
+	.atomic_disable	= dcif_crtc_atomic_disable,
+};
+
+static int dcif_crtc_enable_vblank(struct drm_crtc *crtc)
+{
+	struct dcif_dev *dcif = crtc_to_dcif_dev(crtc);
+	int domain = dcif->cpu_domain;
+
+	/* Clear and enable VS_BLANK IRQ */
+	regmap_set_bits(dcif->regmap, DCIF_IS0(domain), DCIF_INT0_VS_BLANK);
+	regmap_set_bits(dcif->regmap, DCIF_IE0(domain), DCIF_INT0_VS_BLANK);
+
+	return 0;
+}
+
+static void dcif_crtc_disable_vblank(struct drm_crtc *crtc)
+{
+	struct dcif_dev *dcif = crtc_to_dcif_dev(crtc);
+	int domain = dcif->cpu_domain;
+
+	/* Disable and clear VS_BLANK IRQ */
+	regmap_clear_bits(dcif->regmap, DCIF_IE0(domain), DCIF_INT0_VS_BLANK);
+	regmap_clear_bits(dcif->regmap, DCIF_IS0(domain), DCIF_INT0_VS_BLANK);
+}
+
+static const struct drm_crtc_funcs dcif_crtc_funcs = {
+	.reset			= dcif_crtc_reset,
+	.destroy		= drm_crtc_cleanup,
+	.set_config		= drm_atomic_helper_set_config,
+	.page_flip		= drm_atomic_helper_page_flip,
+	.atomic_duplicate_state	= dcif_crtc_atomic_duplicate_state,
+	.atomic_destroy_state	= dcif_crtc_atomic_destroy_state,
+	.enable_vblank		= dcif_crtc_enable_vblank,
+	.disable_vblank		= dcif_crtc_disable_vblank,
+	.set_crc_source		= dcif_crc_source_set,
+	.verify_crc_source	= dcif_crc_source_verify,
+};
+
+irqreturn_t dcif_irq_handler(int irq, void *data)
+{
+	struct drm_device *drm = data;
+	struct dcif_dev *dcif = to_dcif_dev(drm);
+	int domain = dcif->cpu_domain;
+	u32 stat0, stat1, crc;
+
+	regmap_read(dcif->regmap, DCIF_IS0(domain), &stat0);
+	regmap_read(dcif->regmap, DCIF_IS1(domain), &stat1);
+	regmap_write(dcif->regmap, DCIF_IS0(domain), stat0);
+	regmap_write(dcif->regmap, DCIF_IS1(domain), stat1);
+
+	if (stat0 & DCIF_INT0_VS_BLANK) {
+		drm_crtc_handle_vblank(&dcif->crtc);
+
+		scoped_guard(spinlock_irqsave, &drm->event_lock) {
+			if (dcif->event) {
+				drm_crtc_send_vblank_event(&dcif->crtc, dcif->event);
+				dcif->event = NULL;
+				drm_crtc_vblank_put(&dcif->crtc);
+			}
+			if (dcif->crc_is_enabled) {
+				regmap_read(dcif->regmap, DCIF_CRC_VAL_R(0), &crc);
+				drm_crtc_add_crc_entry(&dcif->crtc, false, 0, &crc);
+				dev_dbg(drm->dev, "crc=0x%x\n",  crc);
+			}
+		}
+	}
+
+	if (stat1 & (DCIF_INT1_FIFO_PANIC0 | DCIF_INT1_FIFO_PANIC1)) {
+		u32 panic = stat1 & (DCIF_INT1_FIFO_PANIC0 | DCIF_INT1_FIFO_PANIC1);
+
+		dev_dbg_ratelimited(drm->dev, "FIFO panic on %s\n",
+				    panic == (DCIF_INT1_FIFO_PANIC0 | DCIF_INT1_FIFO_PANIC1) ?
+				    "layers 0 & 1" : panic == DCIF_INT1_FIFO_PANIC0 ? "layer 0" :
+				    "layer 1");
+	}
+
+	return IRQ_HANDLED;
+}
+
+int dcif_crtc_init(struct dcif_dev *dcif)
+{
+	int ret;
+
+	ret = dcif_plane_init(dcif);
+	if (ret)
+		return ret;
+
+	drm_crtc_helper_add(&dcif->crtc, &dcif_crtc_helper_funcs);
+	ret = drm_crtc_init_with_planes(&dcif->drm, &dcif->crtc, &dcif->planes.primary, NULL,
+					&dcif_crtc_funcs, NULL);
+	if (ret) {
+		drm_err(&dcif->drm, "failed to initialize CRTC: %d\n", ret);
+		return ret;
+	}
+
+	return 0;
+}
diff --git a/drivers/gpu/drm/imx/dcif/dcif-drv.c b/drivers/gpu/drm/imx/dcif/dcif-drv.c
new file mode 100644
index 0000000000000..50ca6461ffb30
--- /dev/null
+++ b/drivers/gpu/drm/imx/dcif/dcif-drv.c
@@ -0,0 +1,233 @@
+// SPDX-License-Identifier: GPL-2.0
+
+/*
+ * Copyright 2025 NXP
+ */
+
+#include <linux/dma-mapping.h>
+#include <linux/interrupt.h>
+#include <linux/mfd/syscon.h>
+#include <linux/mod_devicetable.h>
+#include <linux/platform_device.h>
+#include <linux/pm_runtime.h>
+#include <linux/regmap.h>
+
+#include <drm/clients/drm_client_setup.h>
+#include <drm/drm_atomic_helper.h>
+#include <drm/drm_drv.h>
+#include <drm/drm_fbdev_dma.h>
+#include <drm/drm_gem_dma_helper.h>
+#include <drm/drm_modeset_helper.h>
+#include <drm/drm_print.h>
+
+#include "dcif-drv.h"
+#include "dcif-reg.h"
+
+#define DCIF_CPU_DOMAIN			0
+
+DEFINE_DRM_GEM_DMA_FOPS(dcif_driver_fops);
+
+static struct drm_driver dcif_driver = {
+	.driver_features	= DRIVER_MODESET | DRIVER_GEM | DRIVER_ATOMIC,
+	DRM_GEM_DMA_DRIVER_OPS,
+	DRM_FBDEV_DMA_DRIVER_OPS,
+	.fops			= &dcif_driver_fops,
+	.name			= "imx-dcif",
+	.desc			= "i.MX DCIF DRM graphics",
+	.major			= 1,
+	.minor			= 0,
+	.patchlevel		= 0,
+};
+
+static void dcif_read_chip_info(struct dcif_dev *dcif)
+{
+	struct drm_device *drm = &dcif->drm;
+	u32 val, vmin, vmaj;
+	int ret;
+
+	ret = pm_runtime_resume_and_get(drm->dev);
+	if (ret < 0) {
+		drm_err(drm, "failed to resume DCIF: %d\n", ret);
+		return;
+	}
+
+	regmap_read(dcif->regmap, DCIF_VER, &val);
+
+	dcif->has_crc = val & DCIF_FEATURE_CRC;
+
+	vmin = DCIF_VER_GET_MINOR(val);
+	vmaj = DCIF_VER_GET_MAJOR(val);
+	DRM_DEV_DEBUG(drm->dev, "DCIF version is %d.%d\n", vmaj, vmin);
+
+	pm_runtime_put_sync(drm->dev);
+}
+
+static const struct regmap_config dcif_regmap_config = {
+	.reg_bits = 32,
+	.val_bits = 32,
+	.reg_stride = 4,
+	.fast_io = true,
+	.max_register = 0x20250,
+	.cache_type = REGCACHE_NONE,
+	.disable_locking = true,
+};
+
+static int dcif_probe(struct platform_device *pdev)
+{
+	struct dcif_dev *dcif;
+	struct drm_device *drm;
+	int ret;
+	int i;
+
+	dcif = devm_drm_dev_alloc(&pdev->dev, &dcif_driver, struct dcif_dev, drm);
+	if (IS_ERR(dcif))
+		return PTR_ERR(dcif);
+
+	/* CPU 0 domain for interrupt control */
+	dcif->cpu_domain = DCIF_CPU_DOMAIN;
+
+	drm = &dcif->drm;
+	dev_set_drvdata(&pdev->dev, dcif);
+
+	dcif->reg_base = devm_platform_ioremap_resource(pdev, 0);
+	if (IS_ERR(dcif->reg_base))
+		return dev_err_probe(drm->dev, PTR_ERR(dcif->reg_base),
+				     "failed to get reg base\n");
+
+	for (i = 0; i < 3; i++) {
+		dcif->irq[i] = platform_get_irq(pdev, i);
+		if (dcif->irq[i] < 0)
+			return dev_err_probe(drm->dev, dcif->irq[i],
+					     "failed to get domain%d irq\n", i);
+	}
+
+	dcif->regmap = devm_regmap_init_mmio(drm->dev, dcif->reg_base, &dcif_regmap_config);
+	if (IS_ERR(dcif->regmap))
+		return dev_err_probe(drm->dev, PTR_ERR(dcif->regmap),
+				     "failed to init DCIF regmap\n");
+
+	dcif->num_clks = devm_clk_bulk_get_all(drm->dev, &dcif->clks);
+	if (dcif->num_clks < 0)
+		return dev_err_probe(drm->dev, dcif->num_clks,
+				     "cannot get required clocks\n");
+
+	dma_set_mask_and_coherent(drm->dev, DMA_BIT_MASK(32));
+
+	devm_pm_runtime_enable(drm->dev);
+
+	ret = devm_request_irq(drm->dev, dcif->irq[dcif->cpu_domain],
+			       dcif_irq_handler, 0, drm->driver->name, drm);
+	if (ret < 0)
+		return dev_err_probe(drm->dev, ret, "failed to install IRQ handler\n");
+
+	dcif_read_chip_info(dcif);
+
+	ret = dcif_kms_prepare(dcif);
+	if (ret)
+		return ret;
+
+	ret = drm_dev_register(drm, 0);
+	if (ret)
+		return dev_err_probe(drm->dev, ret, "failed to register drm device\n");
+
+	drm_client_setup(drm, NULL);
+
+	return 0;
+}
+
+static void dcif_remove(struct platform_device *pdev)
+{
+	struct dcif_dev *dcif = dev_get_drvdata(&pdev->dev);
+	struct drm_device *drm = &dcif->drm;
+
+	drm_dev_unregister(drm);
+
+	drm_atomic_helper_shutdown(drm);
+}
+
+static void dcif_shutdown(struct platform_device *pdev)
+{
+	struct dcif_dev *dcif = dev_get_drvdata(&pdev->dev);
+	struct drm_device *drm = &dcif->drm;
+
+	drm_atomic_helper_shutdown(drm);
+}
+
+static int dcif_runtime_suspend(struct device *dev)
+{
+	struct dcif_dev *dcif = dev_get_drvdata(dev);
+
+	clk_bulk_disable_unprepare(dcif->num_clks, dcif->clks);
+
+	return 0;
+}
+
+static int dcif_runtime_resume(struct device *dev)
+{
+	struct dcif_dev *dcif = dev_get_drvdata(dev);
+	int ret;
+
+	ret = clk_bulk_prepare_enable(dcif->num_clks, dcif->clks);
+	if (ret) {
+		dev_err(dev, "failed to enable clocks: %d\n", ret);
+		return ret;
+	}
+
+	return 0;
+}
+
+static int dcif_suspend(struct device *dev)
+{
+	struct dcif_dev *dcif = dev_get_drvdata(dev);
+	int ret;
+
+	ret = drm_mode_config_helper_suspend(&dcif->drm);
+	if (ret < 0)
+		return ret;
+
+	if (pm_runtime_suspended(dev))
+		return 0;
+
+	return dcif_runtime_suspend(dev);
+}
+
+static int dcif_resume(struct device *dev)
+{
+	struct dcif_dev *dcif = dev_get_drvdata(dev);
+	int ret;
+
+	if (!pm_runtime_suspended(dev)) {
+		ret = dcif_runtime_resume(dev);
+		if (ret < 0)
+			return ret;
+	}
+
+	return drm_mode_config_helper_resume(&dcif->drm);
+}
+
+static const struct dev_pm_ops dcif_pm_ops = {
+	SET_SYSTEM_SLEEP_PM_OPS(dcif_suspend, dcif_resume)
+	SET_RUNTIME_PM_OPS(dcif_runtime_suspend, dcif_runtime_resume, NULL)
+};
+
+static const struct of_device_id dcif_dt_ids[] = {
+	{ .compatible = "nxp,imx94-dcif", },
+	{ /* sentinel */ }
+};
+MODULE_DEVICE_TABLE(of, dcif_dt_ids);
+
+static struct platform_driver dcif_platform_driver = {
+	.probe	= dcif_probe,
+	.remove	= dcif_remove,
+	.shutdown = dcif_shutdown,
+	.driver	= {
+		.name		= "imx-dcif-drm",
+		.of_match_table	= dcif_dt_ids,
+		.pm		= pm_ptr(&dcif_pm_ops),
+	},
+};
+module_platform_driver(dcif_platform_driver);
+
+MODULE_AUTHOR("NXP Semiconductor");
+MODULE_DESCRIPTION("i.MX94 DCIF DRM driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/gpu/drm/imx/dcif/dcif-drv.h b/drivers/gpu/drm/imx/dcif/dcif-drv.h
new file mode 100644
index 0000000000000..895b2efc51a26
--- /dev/null
+++ b/drivers/gpu/drm/imx/dcif/dcif-drv.h
@@ -0,0 +1,89 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+
+/*
+ * Copyright 2025 NXP
+ */
+
+#ifndef __DCIF_DRV_H__
+#define __DCIF_DRV_H__
+
+#include <linux/clk.h>
+#include <linux/irqreturn.h>
+
+#include <drm/drm_crtc.h>
+#include <drm/drm_device.h>
+#include <drm/drm_encoder.h>
+#include <drm/drm_plane.h>
+#include <drm/drm_vblank.h>
+
+#define DCIF_CPU_DOMAINS	3
+
+struct dcif_dev {
+	struct drm_device drm;
+	void __iomem *reg_base;
+
+	struct regmap *regmap;
+	int irq[DCIF_CPU_DOMAINS];
+
+	int num_clks;
+	struct clk_bulk_data *clks;
+
+	struct drm_crtc crtc;
+	struct {
+		struct drm_plane primary;
+		struct drm_plane overlay;
+	} planes;
+	struct drm_encoder encoder;
+
+	struct drm_pending_vblank_event *event;
+
+	/* Implement crc */
+	bool has_crc;
+	bool crc_is_enabled;
+
+	/* Tracks whether atomic_enable obtained a PM runtime reference */
+	bool crtc_pm_enabled;
+
+	/* CPU domain for interrupt control */
+	int cpu_domain;
+};
+
+enum dcif_crc_source {
+	DCIF_CRC_SRC_NONE,
+	DCIF_CRC_SRC_FRAME,
+	DCIF_CRC_SRC_FRAME_ROI,
+};
+
+struct dcif_crc {
+	enum dcif_crc_source	source;
+	struct drm_rect		roi;
+};
+
+struct dcif_crtc_state {
+	struct drm_crtc_state	base;
+	struct dcif_crc		crc;
+	u32			bus_format;
+	u32			bus_flags;
+};
+
+static inline struct dcif_dev *to_dcif_dev(struct drm_device *drm_dev)
+{
+	return container_of(drm_dev, struct dcif_dev, drm);
+}
+
+static inline struct dcif_dev *crtc_to_dcif_dev(struct drm_crtc *crtc)
+{
+	return to_dcif_dev(crtc->dev);
+}
+
+static inline struct dcif_crtc_state *to_dcif_crtc_state(struct drm_crtc_state *s)
+{
+	return container_of(s, struct dcif_crtc_state, base);
+}
+
+irqreturn_t dcif_irq_handler(int irq, void *data);
+int dcif_crtc_init(struct dcif_dev *dcif);
+int dcif_plane_init(struct dcif_dev *dcif);
+int dcif_kms_prepare(struct dcif_dev *dcif);
+
+#endif
diff --git a/drivers/gpu/drm/imx/dcif/dcif-kms.c b/drivers/gpu/drm/imx/dcif/dcif-kms.c
new file mode 100644
index 0000000000000..8981434b0a803
--- /dev/null
+++ b/drivers/gpu/drm/imx/dcif/dcif-kms.c
@@ -0,0 +1,96 @@
+// SPDX-License-Identifier: GPL-2.0
+
+/*
+ * Copyright 2025 NXP
+ */
+
+#include <drm/drm_atomic_helper.h>
+#include <drm/drm_bridge.h>
+#include <drm/drm_bridge_connector.h>
+#include <drm/drm_gem_framebuffer_helper.h>
+#include <drm/drm_print.h>
+#include <drm/drm_probe_helper.h>
+#include <drm/drm_simple_kms_helper.h>
+
+#include "dcif-drv.h"
+
+static int dcif_kms_init(struct dcif_dev *dcif)
+{
+	struct drm_device *drm = &dcif->drm;
+	struct device_node *np = drm->dev->of_node;
+	struct drm_connector *connector;
+	struct drm_bridge *bridge;
+	int ret;
+
+	ret = dcif_crtc_init(dcif);
+	if (ret)
+		return ret;
+
+	bridge = devm_drm_of_get_bridge(drm->dev, np, 0, 0);
+	if (IS_ERR(bridge))
+		return dev_err_probe(drm->dev, PTR_ERR(bridge), "Failed to find bridge\n");
+
+	dcif->encoder.possible_crtcs = drm_crtc_mask(&dcif->crtc);
+	ret = drm_simple_encoder_init(drm, &dcif->encoder, DRM_MODE_ENCODER_NONE);
+	if (ret) {
+		drm_err(drm, "failed to initialize encoder: %d\n", ret);
+		return ret;
+	}
+
+	ret = drm_bridge_attach(&dcif->encoder, bridge, NULL, DRM_BRIDGE_ATTACH_NO_CONNECTOR);
+	if (ret) {
+		drm_err(drm, "failed to attach bridge to encoder: %d\n", ret);
+		return ret;
+	}
+
+	connector = drm_bridge_connector_init(drm, &dcif->encoder);
+	if (IS_ERR(connector)) {
+		drm_err(drm, "failed to initialize bridge connector: %ld\n", PTR_ERR(connector));
+		return PTR_ERR(connector);
+	}
+
+	return 0;
+}
+
+static const struct drm_mode_config_funcs dcif_mode_config_funcs = {
+	.fb_create     = drm_gem_fb_create,
+	.atomic_check  = drm_atomic_helper_check,
+	.atomic_commit = drm_atomic_helper_commit,
+};
+
+static const struct drm_mode_config_helper_funcs dcif_mode_config_helpers = {
+	.atomic_commit_tail = drm_atomic_helper_commit_tail_rpm,
+};
+
+int dcif_kms_prepare(struct dcif_dev *dcif)
+{
+	struct drm_device *drm = &dcif->drm;
+	int ret;
+
+	ret = drmm_mode_config_init(drm);
+	if (ret)
+		return ret;
+
+	ret = dcif_kms_init(dcif);
+	if (ret)
+		return ret;
+
+	drm->mode_config.min_width	= 1;
+	drm->mode_config.min_height	= 1;
+	drm->mode_config.max_width	= 1920;
+	drm->mode_config.max_height	= 1920;
+	drm->mode_config.funcs		= &dcif_mode_config_funcs;
+	drm->mode_config.helper_private	= &dcif_mode_config_helpers;
+
+	ret = drm_vblank_init(drm, 1);
+	if (ret < 0) {
+		drm_err(drm, "failed to initialize vblank: %d\n", ret);
+		return ret;
+	}
+
+	drm_mode_config_reset(drm);
+
+	drmm_kms_helper_poll_init(drm);
+
+	return 0;
+}
diff --git a/drivers/gpu/drm/imx/dcif/dcif-plane.c b/drivers/gpu/drm/imx/dcif/dcif-plane.c
new file mode 100644
index 0000000000000..7b5a68dab6587
--- /dev/null
+++ b/drivers/gpu/drm/imx/dcif/dcif-plane.c
@@ -0,0 +1,308 @@
+// SPDX-License-Identifier: GPL-2.0
+
+/*
+ * Copyright 2025 NXP
+ */
+
+#include <linux/regmap.h>
+
+#include <drm/drm_atomic.h>
+#include <drm/drm_atomic_helper.h>
+#include <drm/drm_blend.h>
+#include <drm/drm_fb_dma_helper.h>
+#include <drm/drm_fourcc.h>
+#include <drm/drm_framebuffer.h>
+#include <drm/drm_gem_atomic_helper.h>
+#include <drm/drm_gem_dma_helper.h>
+#include <drm/drm_print.h>
+#include <drm/drm_rect.h>
+
+#include "dcif-drv.h"
+#include "dcif-reg.h"
+
+static const u32 dcif_primary_plane_formats[] = {
+	/* RGB */
+	DRM_FORMAT_RGB565,
+	DRM_FORMAT_RGB888,
+	DRM_FORMAT_XBGR8888,
+	DRM_FORMAT_ABGR8888,
+	DRM_FORMAT_XRGB1555,
+	DRM_FORMAT_ARGB1555,
+	DRM_FORMAT_XRGB4444,
+	DRM_FORMAT_ARGB4444,
+	DRM_FORMAT_XRGB8888,
+	DRM_FORMAT_ARGB8888,
+
+	/* Packed YCbCr */
+	DRM_FORMAT_YUYV,
+	DRM_FORMAT_YVYU,
+	DRM_FORMAT_UYVY,
+	DRM_FORMAT_VYUY,
+};
+
+static const u32 dcif_overlay_plane_formats[] = {
+	/* RGB */
+	DRM_FORMAT_RGB565,
+	DRM_FORMAT_RGB888,
+	DRM_FORMAT_XBGR8888,
+	DRM_FORMAT_ABGR8888,
+	DRM_FORMAT_XRGB1555,
+	DRM_FORMAT_ARGB1555,
+	DRM_FORMAT_XRGB4444,
+	DRM_FORMAT_ARGB4444,
+	DRM_FORMAT_XRGB8888,
+	DRM_FORMAT_ARGB8888,
+};
+
+static inline struct dcif_dev *plane_to_dcif_dev(struct drm_plane *plane)
+{
+	return to_dcif_dev(plane->dev);
+}
+
+static inline dma_addr_t drm_plane_state_to_baseaddr(struct drm_plane_state *state)
+{
+	struct drm_framebuffer *fb = state->fb;
+	struct drm_gem_dma_object *dma_obj;
+	unsigned int x = state->src.x1 >> 16;
+	unsigned int y = state->src.y1 >> 16;
+
+	dma_obj = drm_fb_dma_get_gem_obj(fb, 0);
+	if (!dma_obj)
+		return 0;
+
+	return dma_obj->dma_addr + fb->offsets[0] + fb->pitches[0] * y + fb->format->cpp[0] * x;
+}
+
+static int dcif_plane_get_layer_id(struct drm_plane *plane)
+{
+	return (plane->type == DRM_PLANE_TYPE_PRIMARY) ? 0 : 1;
+}
+
+static int dcif_plane_atomic_check(struct drm_plane *plane, struct drm_atomic_commit *state)
+{
+	struct drm_plane_state *new_plane_state = drm_atomic_get_new_plane_state(state, plane);
+	struct drm_plane_state *old_plane_state = drm_atomic_get_old_plane_state(state, plane);
+	struct dcif_dev *dcif = plane_to_dcif_dev(plane);
+	struct drm_framebuffer *fb = new_plane_state->fb;
+	struct drm_framebuffer *old_fb = old_plane_state->fb;
+	struct drm_crtc_state *crtc_state;
+	int ret;
+
+	if (!fb)
+		return 0;
+
+	crtc_state = drm_atomic_get_new_crtc_state(state, &dcif->crtc);
+	if (WARN_ON(!crtc_state))
+		return -EINVAL;
+
+	/*
+	 * Force CRTC mode change if framebuffer stride or pixel format have changed.
+	 */
+	if (plane->type == DRM_PLANE_TYPE_PRIMARY && old_fb &&
+	    (fb->pitches[0] != old_fb->pitches[0] || fb->format->format != old_fb->format->format))
+		crtc_state->mode_changed = true;
+
+	ret = drm_atomic_helper_check_plane_state(new_plane_state, crtc_state,
+						  DRM_PLANE_NO_SCALING,
+						  DRM_PLANE_NO_SCALING,
+						  true,
+						  true);
+	if (ret)
+		return ret;
+
+	if (new_plane_state->fb->format->has_alpha &&
+	    new_plane_state->pixel_blend_mode != DRM_MODE_BLEND_PIXEL_NONE &&
+	    new_plane_state->alpha != DRM_BLEND_ALPHA_OPAQUE)
+		return -EINVAL;
+
+	return 0;
+}
+
+static void dcif_plane_atomic_update(struct drm_plane *plane, struct drm_atomic_commit *state)
+{
+	struct drm_plane_state *new_state = drm_atomic_get_new_plane_state(state, plane);
+	struct dcif_dev *dcif = plane_to_dcif_dev(plane);
+	int layer_id = dcif_plane_get_layer_id(plane);
+	struct drm_framebuffer *fb = new_state->fb;
+	u32 crtc_x, crtc_y, crtc_h, crtc_w;
+	u32 layer_fmt = 0, yuv_fmt = 0;
+	dma_addr_t baseaddr;
+	u32 reg;
+
+	if (!fb)
+		return;
+
+	crtc_x = new_state->dst.x1;
+	crtc_y = new_state->dst.y1;
+	crtc_w = drm_rect_width(&new_state->dst);
+	crtc_h = drm_rect_height(&new_state->dst);
+
+	/* visible portion of plane on crtc */
+	regmap_write(dcif->regmap, DCIF_CTRLDESC1(layer_id),
+		     DCIF_CTRLDESC1_POSX(crtc_x) | DCIF_CTRLDESC1_POSY(crtc_y));
+	regmap_write(dcif->regmap, DCIF_CTRLDESC2(layer_id),
+		     DCIF_CTRLDESC2_WIDTH(crtc_w) | DCIF_CTRLDESC2_HEIGHT(crtc_h));
+
+	/* pitch size */
+	reg = DCIF_CTRLDESC3_P_SIZE(2) | DCIF_CTRLDESC3_T_SIZE(2) |
+	      DCIF_CTRLDESC3_PITCH(fb->pitches[0]);
+	regmap_write(dcif->regmap, DCIF_CTRLDESC3(layer_id), reg);
+
+	/*  address */
+	baseaddr = drm_plane_state_to_baseaddr(new_state);
+
+	drm_dbg_kms(plane->dev, "[PLANE:%d:%s] fb address %pad, pitch 0x%08x\n",
+		    plane->base.id, plane->name, &baseaddr, fb->pitches[0]);
+
+	regmap_write(dcif->regmap, DCIF_CTRLDESC4(layer_id), baseaddr);
+
+	/* Format */
+	switch (fb->format->format) {
+	/* RGB Formats */
+	case DRM_FORMAT_RGB565:
+		layer_fmt = CTRLDESCL0_FORMAT_RGB565;
+		break;
+	case DRM_FORMAT_RGB888:
+		layer_fmt = CTRLDESCL0_FORMAT_RGB888;
+		break;
+	case DRM_FORMAT_XRGB1555:
+	case DRM_FORMAT_ARGB1555:
+		layer_fmt = CTRLDESCL0_FORMAT_ARGB1555;
+		break;
+	case DRM_FORMAT_XRGB4444:
+	case DRM_FORMAT_ARGB4444:
+		layer_fmt = CTRLDESCL0_FORMAT_ARGB4444;
+		break;
+	case DRM_FORMAT_XBGR8888:
+	case DRM_FORMAT_ABGR8888:
+		layer_fmt = CTRLDESCL0_FORMAT_ABGR8888;
+		break;
+	case DRM_FORMAT_XRGB8888:
+	case DRM_FORMAT_ARGB8888:
+		layer_fmt = CTRLDESCL0_FORMAT_ARGB8888;
+		break;
+
+	/* YUV Formats */
+	case DRM_FORMAT_YUYV:
+		layer_fmt = CTRLDESCL0_FORMAT_YCBCR422;
+		yuv_fmt = CTRLDESCL0_YUV_FORMAT_VY2UY1;
+		break;
+	case DRM_FORMAT_YVYU:
+		layer_fmt = CTRLDESCL0_FORMAT_YCBCR422;
+		yuv_fmt = CTRLDESCL0_YUV_FORMAT_UY2VY1;
+		break;
+	case DRM_FORMAT_UYVY:
+		layer_fmt = CTRLDESCL0_FORMAT_YCBCR422;
+		yuv_fmt = CTRLDESCL0_YUV_FORMAT_Y2VY1U;
+		break;
+	case DRM_FORMAT_VYUY:
+		layer_fmt = CTRLDESCL0_FORMAT_YCBCR422;
+		yuv_fmt = CTRLDESCL0_YUV_FORMAT_Y2UY1V;
+		break;
+
+	default:
+		dev_err(dcif->drm.dev, "Unknown pixel format 0x%x\n", fb->format->format);
+		break;
+	}
+
+	reg = DCIF_CTRLDESC0_EN | DCIF_CTRLDESC0_SHADOW_LOAD_EN |
+	      DCIF_CTRLDESC0_FORMAT(layer_fmt) | DCIF_CTRLDESC0_YUV_FORMAT(yuv_fmt);
+
+	/* Alpha */
+	if (new_state->pixel_blend_mode == DRM_MODE_BLEND_PIXEL_NONE ||
+	    !new_state->fb->format->has_alpha)
+		reg |= DCIF_CTRLDESC0_GLOBAL_ALPHA(new_state->alpha >> 8) | ALPHA_GLOBAL;
+	else if (new_state->pixel_blend_mode == DRM_MODE_BLEND_COVERAGE)
+		reg |= ALPHA_EMBEDDED;
+	else
+		/*
+		 * DCIF does not support premultiplied per-pixel blending but,
+		 * since PREMULTI's property presence is mandatory to not break
+		 * userspace, we just disable alpha blending for this one.
+		 */
+		reg |= DCIF_CTRLDESC0_GLOBAL_ALPHA(255) | ALPHA_GLOBAL;
+
+	regmap_write(dcif->regmap, DCIF_CTRLDESC0(layer_id), reg);
+}
+
+static void dcif_overlay_plane_atomic_disable(struct drm_plane *plane,
+					      struct drm_atomic_commit *state)
+{
+	struct dcif_dev *dcif = plane_to_dcif_dev(plane);
+
+	regmap_update_bits(dcif->regmap, DCIF_CTRLDESC0(1),
+			   DCIF_CTRLDESC0_EN | DCIF_CTRLDESC0_SHADOW_LOAD_EN,
+			   DCIF_CTRLDESC0_SHADOW_LOAD_EN);
+}
+
+static const struct drm_plane_helper_funcs dcif_primary_plane_helper_funcs = {
+	.prepare_fb	= drm_gem_plane_helper_prepare_fb,
+	.atomic_check	= dcif_plane_atomic_check,
+	.atomic_update	= dcif_plane_atomic_update,
+};
+
+static const struct drm_plane_helper_funcs dcif_overlay_plane_helper_funcs = {
+	.atomic_check	= dcif_plane_atomic_check,
+	.atomic_update	= dcif_plane_atomic_update,
+	.atomic_disable = dcif_overlay_plane_atomic_disable,
+};
+
+static const struct drm_plane_funcs dcif_plane_funcs = {
+	.update_plane		= drm_atomic_helper_update_plane,
+	.disable_plane		= drm_atomic_helper_disable_plane,
+	.destroy		= drm_plane_cleanup,
+	.reset			= drm_atomic_helper_plane_reset,
+	.atomic_duplicate_state	= drm_atomic_helper_plane_duplicate_state,
+	.atomic_destroy_state	= drm_atomic_helper_plane_destroy_state,
+};
+
+int dcif_plane_init(struct dcif_dev *dcif)
+{
+	const u32 supported_encodings = BIT(DRM_COLOR_YCBCR_BT601) |
+					BIT(DRM_COLOR_YCBCR_BT709) |
+					BIT(DRM_COLOR_YCBCR_BT2020);
+	const u32 supported_ranges = BIT(DRM_COLOR_YCBCR_LIMITED_RANGE) |
+				     BIT(DRM_COLOR_YCBCR_FULL_RANGE);
+	int ret;
+
+	/* primary plane */
+	drm_plane_helper_add(&dcif->planes.primary, &dcif_primary_plane_helper_funcs);
+	ret = drm_universal_plane_init(&dcif->drm, &dcif->planes.primary, 1, &dcif_plane_funcs,
+				       dcif_primary_plane_formats,
+				       ARRAY_SIZE(dcif_primary_plane_formats), NULL,
+				       DRM_PLANE_TYPE_PRIMARY, NULL);
+	if (ret) {
+		drm_err(&dcif->drm, "failed to initialize primary plane: %d\n", ret);
+		return ret;
+	}
+
+	ret = drm_plane_create_color_properties(&dcif->planes.primary, supported_encodings,
+						supported_ranges, DRM_COLOR_YCBCR_BT601,
+						DRM_COLOR_YCBCR_LIMITED_RANGE);
+	if (ret)
+		return ret;
+
+	ret = drm_plane_create_alpha_property(&dcif->planes.primary);
+	if (ret)
+		return ret;
+
+	/* overlay plane */
+	drm_plane_helper_add(&dcif->planes.overlay, &dcif_overlay_plane_helper_funcs);
+	ret = drm_universal_plane_init(&dcif->drm, &dcif->planes.overlay, 1, &dcif_plane_funcs,
+				       dcif_overlay_plane_formats,
+				       ARRAY_SIZE(dcif_overlay_plane_formats), NULL,
+				       DRM_PLANE_TYPE_OVERLAY, NULL);
+	if (ret) {
+		drm_err(&dcif->drm, "failed to initialize overlay plane: %d\n", ret);
+		return ret;
+	}
+
+	ret = drm_plane_create_alpha_property(&dcif->planes.overlay);
+	if (ret)
+		return ret;
+
+	return drm_plane_create_blend_mode_property(&dcif->planes.overlay,
+						    BIT(DRM_MODE_BLEND_PIXEL_NONE) |
+						    BIT(DRM_MODE_BLEND_PREMULTI)   |
+						    BIT(DRM_MODE_BLEND_COVERAGE));
+}
diff --git a/drivers/gpu/drm/imx/dcif/dcif-reg.h b/drivers/gpu/drm/imx/dcif/dcif-reg.h
new file mode 100644
index 0000000000000..acf9e3071aa52
--- /dev/null
+++ b/drivers/gpu/drm/imx/dcif/dcif-reg.h
@@ -0,0 +1,267 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+
+/*
+ * Copyright 2025 NXP
+ */
+#ifndef __DCIF_REG_H__
+#define __DCIF_REG_H__
+
+#include <linux/bits.h>
+
+/* Version ID Register */
+#define DCIF_VER				0x0
+#define   DCIF_VER_GET_FEATURE(x)		FIELD_GET(GENMASK(15, 0), x)
+#define   DCIF_VER_GET_MINOR(x)			FIELD_GET(GENMASK(23, 16), x)
+#define   DCIF_VER_GET_MAJOR(x)			FIELD_GET(GENMASK(31, 24), x)
+#define   DCIF_FEATURE_CRC			BIT(1)
+
+/* Parameter Registers */
+#define DCIF_PAR_0				0x4
+#define   DCIF_PAR_0_LAYER_NUM(x)		FIELD_PREP(GENMASK(3, 0), x)
+#define   DCIF_PAR_0_DOMAIN_NUM(x)		FIELD_PREP(GENMASK(5, 4), x)
+#define   DCIF_PAR_0_AXI_DATA_WIDTH(x)		FIELD_PREP(GENMASK(7, 6), x)
+#define   DCIF_PAR_0_CLUT_RAM_NUM(x)		FIELD_PREP(GENMASK(11, 8), x)
+#define   DCIF_PAR_0_CSC_NUM(x)			FIELD_PREP(GENMASK(13, 12), x)
+#define   DCIF_PAR_0_CRC_REGION_NUM(x)		FIELD_PREP(GENMASK(18, 16), x)
+#define   DCIF_PAR_0_BACKUP(x)			FIELD_PREP(GENMASK(31, 28), x)
+
+#define DCIF_PAR_1				0x8
+#define   DCIF_PAR_1_LAYER0_FIFO_SIZE(x)	FIELD_PREP(GENMASK(3, 0), x)
+#define   DCIF_PAR_1_LAYER1_FIFO_SIZE(x)	FIELD_PREP(GENMASK(7, 4), x)
+
+/* Display Control and Parameter Registers */
+#define DCIF_DISP_CTRL				0x10
+#define   DCIF_DISP_CTRL_DISP_ON		BIT(0)
+#define   DCIF_DISP_CTRL_AXI_RD_HOLD		BIT(30)
+#define   DCIF_DISP_CTRL_SW_RST			BIT(31)
+#define DCIF_DISP_PAR				0x14
+#define   DCIF_DISP_PAR_BGND_B(x)		FIELD_PREP(GENMASK(7, 0), x)
+#define   DCIF_DISP_PAR_BGND_G(x)		FIELD_PREP(GENMASK(15, 8), x)
+#define   DCIF_DISP_PAR_BGND_R(x)		FIELD_PREP(GENMASK(23, 16), x)
+#define DCIF_DISP_SIZE				0x18
+#define   DCIF_DISP_SIZE_DISP_WIDTH(x)		FIELD_PREP(GENMASK(11, 0), x)
+#define   DCIF_DISP_SIZE_DISP_HEIGHT(x)		FIELD_PREP(GENMASK(27, 16), x)
+
+/* Display Status Registers */
+#define DCIF_DISP_SR0				0x1C
+#define   DCIF_DISP_SR0_AXI_RD_PEND(x)		FIELD_PREP(GENMASK(4, 0), x)
+#define   DCIF_DISP_SR0_DPI_BUSY(x)		FIELD_PREP(GENMASK(14, 14), x)
+#define   DCIF_DISP_SR0_AXI_RD_BUSY(x)		FIELD_PREP(GENMASK(15, 15), x)
+#define DCIF_DISP_SR0_TXFIFO_CNT(x)		FIELD_PREP(GENMASK(23, 16), x)
+
+#define DCIF_DISP_SR1				0x20
+#define   DCIF_DISP_SR1_H_CNT(x)		FIELD_PREP(GENMASK(11, 0), x)
+#define   DCIF_DISP_SR1_V_CNT(x)		FIELD_PREP(GENMASK(27, 16), x)
+
+/* Interrupt Enable and Status Registers, n=0-2*/
+#define DCIF_IE0(n)				(0x24 + (n) * 0x10000)
+#define DCIF_IS0(n)				(0x28 + (n) * 0x10000)
+#define   DCIF_INT0_VSYNC			BIT(0)
+#define   DCIF_INT0_UNDERRUN			BIT(1)
+#define   DCIF_INT0_VS_BLANK			BIT(2)
+#define   DCIF_INT0_HIST_DONE			BIT(5)
+#define   DCIF_INT0_CRC_ERR			BIT(6)
+#define   DCIF_INT0_CRC_ERR_SAT			BIT(7)
+
+#define DCIF_IE1(n)				(0x2C + (n) * 0x10000)
+#define DCIF_IS1(n)				(0x30 + (n) * 0x10000)
+#define   DCIF_INT1_FIFO_PANIC0			BIT(0)
+#define   DCIF_INT1_FIFO_PANIC1			BIT(1)
+#define   DCIF_INT1_DMA_ERR0			BIT(8)
+#define   DCIF_INT1_DMA_ERR1			BIT(9)
+#define   DCIF_INT1_DMA_DONE0			BIT(16)
+#define   DCIF_INT1_DMA_DONE1			BIT(17)
+#define   DCIF_INT1_FIFO_EMPTY0			BIT(24)
+#define   DCIF_INT1_FIFO_EMPTY1			BIT(25)
+
+/* DPI Control and Sync Parameter Registers */
+#define DCIF_DPI_CTRL				0x40
+#define   DCIF_DPI_CTRL_HSYNC_POL_LOW		BIT(0)
+#define   DCIF_DPI_CTRL_VSYNC_POL_LOW		BIT(1)
+#define   DCIF_DPI_CTRL_DE_POL_LOW		BIT(2)
+#define   DCIF_DPI_CTRL_PCLK_EDGE_FALLING	BIT(3)
+#define   DCIF_DPI_CTRL_POL_MASK		GENMASK(3, 0)
+#define   DCIF_DPI_CTRL_DATA_INV(x)		FIELD_PREP(GENMASK(4, 4), x)
+#define   DCIF_DPI_CTRL_DEF_BGND_EN(x)		FIELD_PREP(GENMASK(5, 5), x)
+#define   DCIF_DPI_CTRL_FETCH_OPT(x)		FIELD_PREP(GENMASK(9, 8), x)
+#define   DCIF_DPI_CTRL_DISP_MODE(x)		FIELD_PREP(GENMASK(13, 12), x)
+#define   DCIF_DPI_CTRL_DATA_PATTERN_MASK	GENMASK(18, 16)
+#define   DCIF_DPI_CTRL_DATA_PATTERN(x)		FIELD_PREP(GENMASK(18, 16), x)
+#define   PATTERN_RGB888			0
+#define   PATTERN_RBG888			1
+#define   PATTERN_GBR888			2
+#define   PATTERN_GRB888			3
+#define   PATTERN_BRG888			4
+#define   PATTERN_BGR888			5
+#define   PATTERN_RGB555			6
+#define   PATTERN_RGB565			7
+
+#define DCIF_DPI_HSYN_PAR			0x44
+#define   DCIF_DPI_HSYN_PAR_FP_H(x)		FIELD_PREP(GENMASK(11, 0), x)
+#define   DCIF_DPI_HSYN_PAR_BP_H(x)		FIELD_PREP(GENMASK(27, 16), x)
+
+#define DCIF_DPI_VSYN_PAR			0x48
+#define   DCIF_DPI_VSYN_PAR_FP_V(x)		FIELD_PREP(GENMASK(11, 0), x)
+#define   DCIF_DPI_VSYN_PAR_BP_V(x)		FIELD_PREP(GENMASK(27, 16), x)
+
+#define DCIF_DPI_VSYN_HSYN_WIDTH		0x4C
+#define   DCIF_DPI_VSYN_HSYN_WIDTH_PW_H(x)	FIELD_PREP(GENMASK(11, 0), x)
+#define   DCIF_DPI_VSYN_HSYN_WIDTH_PW_V(x)	FIELD_PREP(GENMASK(27, 16), x)
+
+/* Control Descriptor Registers, n=0-1*/
+#define DCIF_CTRLDESC0(n)			(0x10000 + (n) * 0x10000)
+#define   DCIF_CTRLDESC0_AB_MODE(x)		FIELD_PREP(GENMASK(1, 0), x)
+#define   ALPHA_EMBEDDED			0
+#define   ALPHA_GLOBAL				1
+#define   DCIF_CTRLDESC0_YUV_FORMAT_MASK	GENMASK(15, 14)
+#define   DCIF_CTRLDESC0_YUV_FORMAT(x)		FIELD_PREP(GENMASK(15, 14), x)
+#define   CTRLDESCL0_YUV_FORMAT_Y2VY1U		0x0
+#define   CTRLDESCL0_YUV_FORMAT_Y2UY1V		0x1
+#define   CTRLDESCL0_YUV_FORMAT_VY2UY1		0x2
+#define   CTRLDESCL0_YUV_FORMAT_UY2VY1		0x3
+#define   DCIF_CTRLDESC0_GLOBAL_ALPHA(x)	FIELD_PREP(GENMASK(23, 16), x)
+#define   DCIF_CTRLDESC0_FORMAT_MASK		GENMASK(27, 24)
+#define   DCIF_CTRLDESC0_FORMAT(x)		FIELD_PREP(GENMASK(27, 24), x)
+#define   CTRLDESCL0_FORMAT_RGB565		0x4
+#define   CTRLDESCL0_FORMAT_ARGB1555		0x5
+#define   CTRLDESCL0_FORMAT_ARGB4444		0x6
+#define   CTRLDESCL0_FORMAT_YCBCR422		0x7
+#define   CTRLDESCL0_FORMAT_RGB888		0x8
+#define   CTRLDESCL0_FORMAT_ARGB8888		0x9
+#define   CTRLDESCL0_FORMAT_ABGR8888		0xa
+#define   DCIF_CTRLDESC0_SHADOW_LOAD_EN		BIT(30)
+#define   DCIF_CTRLDESC0_EN			BIT(31)
+
+#define DCIF_CTRLDESC1(n)			(0x10004 + (n) * 0x10000)
+#define   DCIF_CTRLDESC1_POSX(x)		FIELD_PREP(GENMASK(11, 0), x)
+#define   DCIF_CTRLDESC1_POSY(x)		FIELD_PREP(GENMASK(27, 16), x)
+
+#define DCIF_CTRLDESC2(n)			(0x10008 + (n) * 0x10000)
+#define   DCIF_CTRLDESC2_WIDTH(x)		FIELD_PREP(GENMASK(11, 0), x)
+#define   DCIF_CTRLDESC2_HEIGHT(x)		FIELD_PREP(GENMASK(27, 16), x)
+
+#define DCIF_CTRLDESC3(n)			(0x1000C + (n) * 0x10000)
+#define   DCIF_CTRLDESC3_PITCH(x)		FIELD_PREP(GENMASK(15, 0), x)
+#define   DCIF_CTRLDESC3_T_SIZE(x)		FIELD_PREP(GENMASK(17, 16), x)
+#define   DCIF_CTRLDESC3_P_SIZE(x)		FIELD_PREP(GENMASK(22, 20), x)
+
+#define DCIF_CTRLDESC4(n)			(0x10010 + (n) * 0x10000)
+#define   DCIF_CTRLDESC4_ADDR(x)		FIELD_PREP(GENMASK(31, 0), x)
+
+#define DCIF_CTRLDESC5(n)			(0x10014 + (n) * 0x10000)
+#define DCIF_CTRLDESC6(n)			(0x10018 + (n) * 0x10000)
+#define   DCIF_CTRLDESC6_BCLR_B(x)		FIELD_PREP(GENMASK(7, 0), x)
+#define   DCIF_CTRLDESC6_BCLR_G(x)		FIELD_PREP(GENMASK(15, 8), x)
+#define   DCIF_CTRLDESC6_BCLR_R(x)		FIELD_PREP(GENMASK(23, 16), x)
+#define   DCIF_CTRLDESC6_BCLR_A(x)		FIELD_PREP(GENMASK(31, 24), x)
+
+/* CLUT control Register */
+#define DCIF_CLUT_CTRL				0x1003C
+#define   DCIF_CLUT_CTRL_CLUT0_SEL(x)		FIELD_PREP(GENMASK(0, 0), x)
+#define   DCIF_CLUT_CTRL_CLUT1_SEL(x)		FIELD_PREP(GENMASK(3, 3), x)
+#define   DCIF_CLUT_CTRL_CLUT_MUX(x)		FIELD_PREP(GENMASK(29, 28), x)
+#define   DCIF_CLUT_CTRL_CLUT_SHADOW_LOAD_EN(x)	FIELD_PREP(GENMASK(31, 31), x)
+
+/* FIFO Panic Threshold Register, n=0-1 */
+#define DCIF_PANIC_THRES(n)			(0x10040 + (n) * 0x10000)
+#define   DCIF_PANIC_THRES_LOW_MASK		GENMASK(11, 0)
+#define   DCIF_PANIC_THRES_LOW(x)		FIELD_PREP(GENMASK(11, 00), x)
+#define   DCIF_PANIC_THRES_HIGH_MASK		GENMASK(27, 16)
+#define   DCIF_PANIC_THRES_HIGH(x)		FIELD_PREP(GENMASK(27, 16), x)
+#define   DCIF_PANIC_THRES_REQ_EN		BIT(31)
+#define   PANIC0_THRES_MAX			511
+
+/* Layer Status Register 0, n=0-1 */
+#define DCIF_LAYER_SR0(n)			(0x10044 + (n) * 0x10000)
+#define   DCIF_LAYER_SR0_L0_FIFO_CNT_MASK	GENMASK(9, 0)
+#define   DCIF_LAYER_SR0_L0_FIFO_CNT(x)		FIELD_PREP(GENMASK(9, 0), x)
+
+/* Color Space Conversion Control and Coefficient Registers for Layer 0 */
+#define DCIF_CSC_CTRL_L0			0x10050
+#define   DCIF_CSC_CTRL_L0_CSC_EN		BIT(0)
+#define   DCIF_CSC_CTRL_L0_CSC_MODE_YCBCR2RGB	BIT(1)
+
+#define DCIF_CSC_COEF0_L0			0x10054
+#define   DCIF_CSC_COEF0_L0_A1(x)		FIELD_PREP_CONST(GENMASK(10, 0), x)
+#define   DCIF_CSC_COEF0_L0_A2(x)		FIELD_PREP_CONST(GENMASK(26, 16), x)
+
+#define DCIF_CSC_COEF1_L0			0x10058
+#define   DCIF_CSC_COEF1_L0_A3(x)		FIELD_PREP_CONST(GENMASK(10, 0), x)
+#define   DCIF_CSC_COEF1_L0_B1(x)		FIELD_PREP_CONST(GENMASK(26, 16), x)
+
+#define DCIF_CSC_COEF2_L0			0x1005C
+#define   DCIF_CSC_COEF2_L0_B2(x)		FIELD_PREP_CONST(GENMASK(10, 0), x)
+#define   DCIF_CSC_COEF2_L0_B3(x)		FIELD_PREP_CONST(GENMASK(26, 16), x)
+
+#define DCIF_CSC_COEF3_L0			0x10060
+#define   DCIF_CSC_COEF3_L0_C1(x)		FIELD_PREP_CONST(GENMASK(10, 0), x)
+#define   DCIF_CSC_COEF3_L0_C2(x)		FIELD_PREP_CONST(GENMASK(26, 16), x)
+
+#define DCIF_CSC_COEF4_L0			0x10064
+#define   DCIF_CSC_COEF4_L0_C3(x)		FIELD_PREP_CONST(GENMASK(10, 0), x)
+#define   DCIF_CSC_COEF4_L0_D1(x)		FIELD_PREP_CONST(GENMASK(24, 16), x)
+
+#define DCIF_CSC_COEF5_L0			0x10068
+#define   DCIF_CSC_COEF5_L0_D2(x)		FIELD_PREP_CONST(GENMASK(8, 0), x)
+#define   DCIF_CSC_COEF5_L0_D3(x)		FIELD_PREP_CONST(GENMASK(24, 16), x)
+
+/* CRC Control, Threshold, and Histogram Coefficient Registers */
+#define DCIF_CRC_CTRL				0x20100
+#define   DCIF_CRC_CTRL_CRC_EN(x)		(1 << (x))
+#define   DCIF_CRC_CTRL_HIST_REGION_SEL(x)	FIELD_PREP(GENMASK(17, 16), x)
+#define   DCIF_CRC_CTRL_HIST_MODE		BIT(21)
+#define   DCIF_CRC_CTRL_HIST_TRIG		BIT(22)
+#define   DCIF_CRC_CTRL_HIST_EN			BIT(23)
+#define   DCIF_CRC_CTRL_CRC_MODE		BIT(28)
+#define   DCIF_CRC_CTRL_CRC_TRIG		BIT(29)
+#define   DCIF_CRC_CTRL_CRC_ERR_CNT_RST		BIT(30)
+#define   DCIF_CRC_CTRL_CRC_SHADOW_LOAD_EN	BIT(31)
+
+#define DCIF_CRC_THRES				0x20104
+#define   DCIF_CRC_THRES_CRC_THRESHOLD_MASK	GENMASK(31, 0)
+#define   DCIF_CRC_THRES_CRC_THRESHOLD(x)	FIELD_PREP(GENMASK(31, 0), x)
+
+#define DCIF_CRC_HIST_COEF			0x20108
+#define   DCIF_CRC_HIST_COEF_HIST_WB_MASK	GENMASK(7, 0)
+#define   DCIF_CRC_HIST_COEF_HIST_WB(x)		FIELD_PREP(GENMASK(7, 0), x)
+#define   DCIF_CRC_HIST_COEF_HIST_WG_MASK	GENMASK(15, 8)
+#define   DCIF_CRC_HIST_COEF_HIST_WG(x)		FIELD_PREP(GENMASK(15, 8), x)
+#define   DCIF_CRC_HIST_COEF_HIST_WR_MASK	GENMASK(23, 16)
+#define   DCIF_CRC_HIST_COEF_HIST_WR(x)		FIELD_PREP(GENMASK(23, 16), x)
+
+#define DCIF_CRC_ERR_CNT			0x2010C
+#define   DCIF_CRC_ERR_CNT_CRC_ERR_CNT_MASK	GENMASK(31, 0)
+#define   DCIF_CRC_ERR_CNT_CRC_ERR_CNT(x)	FIELD_PREP(GENMASK(31, 0), x)
+
+#define DCIF_CRC_SR				0x20110
+#define   DCIF_CRC_SR_HIST_CNT_SAT_MASK		BIT(13)
+#define   DCIF_CRC_SR_HIST_CNT_SAT(x)		FIELD_PREP(GENMASK(13, 13), x)
+#define   DCIF_CRC_SR_HIST_SAT_MASK		BIT(14)
+#define   DCIF_CRC_SR_HIST_SAT(x)		FIELD_PREP(GENMASK(14, 14), x)
+#define   DCIF_CRC_SR_HIST_BUSY_MASK		BIT(15)
+#define   DCIF_CRC_SR_HIST_BUSY(x)		FIELD_PREP(GENMASK(15, 15), x)
+#define   DCIF_CRC_SR_CRC_STATUS_MASK		BIT(31)
+#define   DCIF_CRC_SR_CRC_STATUS(x)		FIELD_PREP(GENMASK(31, 31), x)
+
+#define DCIF_CRC_HIST_CNT_B(n)			(0x20114 + (n) * 4)
+#define   DCIF_B_BIN_CNT_MASK			GENMASK(20, 0)
+#define   DCIF_B_BIN_CNT(x)			FIELD_PREP(GENMASK(20, 0), x)
+
+/* CRC Region Position, Size, Value, and Expected Value Registers, n=0-3 */
+#define DCIF_CRC_POS_R(n)			(0x20214 + (n) * 0x10)
+#define   DCIF_CRC_POS_CRC_HOR_POS(x)		FIELD_PREP(GENMASK(11, 0), x)
+#define   DCIF_CRC_POS_CRC_VER_POS(x)		FIELD_PREP(GENMASK(27, 16), x)
+
+#define DCIF_CRC_SIZE_R(n)			(0x20218 + (n) * 0x10)
+#define   DCIF_CRC_SIZE_CRC_HOR_SIZE(x)		FIELD_PREP(GENMASK(11, 0), x)
+#define   DCIF_CRC_SIZE_CRC_VER_SIZE(x)		FIELD_PREP(GENMASK(27, 16), x)
+
+#define DCIF_CRC_VAL_R(n)			(0x2021C + (n) * 0x10)
+#define   DCIF_CRC_VAL_CRC_VAL_MASK		GENMASK(31, 0)
+#define   DCIF_CRC_VAL_CRC_VAL(x)		FIELD_PREP(GENMASK(31, 0), x)
+
+#define DCIF_CRC_EXP_VAL_R(n)			(0x20220 + (n) * 0x10)
+#define   DCIF_CRC_EXP_VAL_CRC_EXP_VAL_MASK	GENMASK(31, 0)
+#define   DCIF_CRC_EXP_VAL_CRC_EXP_VAL(x)	FIELD_PREP(GENMASK(31, 0), x)
+
+#endif /* __DCIF_REG_H__ */

-- 
2.51.0


^ permalink raw reply related

* [PATCH v9 4/9] dt-bindings: display: imx: Add i.MX94 DCIF
From: Laurentiu Palcu @ 2026-06-12 11:58 UTC (permalink / raw)
  To: Ying Liu, Luca Ceresoli, Philipp Zabel, Maarten Lankhorst,
	Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
	Rob Herring, Krzysztof Kozlowski, Conor Dooley, Frank Li,
	Sascha Hauer, Pengutronix Kernel Team, Fabio Estevam
  Cc: Laurentiu Palcu, linux-clk, imx, devicetree, linux-arm-kernel,
	linux-kernel, dri-devel
In-Reply-To: <20260612-dcif-upstreaming-v9-0-8d0ff89aa3c5@oss.nxp.com>

DCIF is the i.MX94 Display Controller Interface which is used to
drive a TFT LCD panel or connects to a display interface depending
on the chip configuration.

Signed-off-by: Laurentiu Palcu <laurentiu.palcu@oss.nxp.com>
---
 .../bindings/display/imx/nxp,imx94-dcif.yaml       | 90 ++++++++++++++++++++++
 1 file changed, 90 insertions(+)

diff --git a/Documentation/devicetree/bindings/display/imx/nxp,imx94-dcif.yaml b/Documentation/devicetree/bindings/display/imx/nxp,imx94-dcif.yaml
new file mode 100644
index 0000000000000..8894e87666972
--- /dev/null
+++ b/Documentation/devicetree/bindings/display/imx/nxp,imx94-dcif.yaml
@@ -0,0 +1,90 @@
+# SPDX-License-Identifier: (GPL-2.0 OR BSD-2-Clause)
+# Copyright 2025 NXP
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/display/imx/nxp,imx94-dcif.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: i.MX94 Display Control Interface (DCIF)
+
+maintainers:
+  - Laurentiu Palcu <laurentiu.palcu@oss.nxp.com>
+
+description:
+  The Display Control Interface(DCIF) is a system master that fetches graphics
+  stored in memory and displays them on a TFT LCD panel or connects to a
+  display interface depending on the chip configuration.
+
+properties:
+  compatible:
+    const: nxp,imx94-dcif
+
+  reg:
+    maxItems: 1
+
+  interrupts:
+    items:
+      - description: CPU domain 0 (controlled by common registers group).
+      - description: CPU domain 1 (controlled by background layer registers group).
+      - description: CPU domain 2 (controlled by foreground layer registers group).
+
+  interrupt-names:
+    items:
+      - const: common
+      - const: bg_layer
+      - const: fg_layer
+
+  clocks:
+    maxItems: 3
+
+  clock-names:
+    items:
+      - const: apb
+      - const: axi
+      - const: pix
+
+  power-domains:
+    maxItems: 1
+
+  port:
+    $ref: /schemas/graph.yaml#/$defs/port-base
+    unevaluatedProperties: false
+    description: Display Pixel Interface(DPI) output port
+
+    properties:
+      endpoint:
+        $ref: /schemas/media/video-interfaces.yaml#
+        unevaluatedProperties: false
+
+required:
+  - compatible
+  - reg
+  - interrupts
+  - clocks
+  - power-domains
+  - port
+
+additionalProperties: false
+
+examples:
+  - |
+    #include <dt-bindings/interrupt-controller/arm-gic.h>
+
+    display-controller@4b120000 {
+        compatible = "nxp,imx94-dcif";
+        reg = <0x4b120000 0x300000>;
+        interrupts = <GIC_SPI 377 IRQ_TYPE_LEVEL_HIGH>,
+                     <GIC_SPI 378 IRQ_TYPE_LEVEL_HIGH>,
+                     <GIC_SPI 379 IRQ_TYPE_LEVEL_HIGH>;
+        interrupt-names = "common", "bg_layer", "fg_layer";
+        clocks = <&scmi_clk 69>, <&scmi_clk 70>, <&dispmix_csr 0>;
+        clock-names = "apb", "axi", "pix";
+        assigned-clocks = <&dispmix_csr 0>;
+        assigned-clock-parents = <&ldb_pll_pixel>;
+        power-domains = <&scmi_devpd 11>;
+        port {
+            dcif_out: endpoint {
+                remote-endpoint = <&ldb_in>;
+            };
+        };
+    };

-- 
2.51.0


^ permalink raw reply related

* [PATCH v9 3/9] drm/bridge: fsl-ldb: Add support for i.MX94
From: Laurentiu Palcu @ 2026-06-12 11:58 UTC (permalink / raw)
  To: Ying Liu, Luca Ceresoli, Andrzej Hajda, Neil Armstrong,
	Robert Foss, Laurent Pinchart, Jonas Karlman, Jernej Skrabec,
	Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
	Simona Vetter
  Cc: Laurentiu Palcu, linux-clk, imx, devicetree, linux-arm-kernel,
	linux-kernel, dri-devel
In-Reply-To: <20260612-dcif-upstreaming-v9-0-8d0ff89aa3c5@oss.nxp.com>

i.MX94 series LDB controller shares the same LDB and LVDS control
registers as i.MX8MP and i.MX93 but supports a higher maximum clock
frequency.

Add a 'max_clk_khz' member to the fsl_ldb_devdata structure in order to
be able to set different max frequencies for other platforms.

Signed-off-by: Laurentiu Palcu <laurentiu.palcu@oss.nxp.com>
---
 drivers/gpu/drm/bridge/fsl-ldb.c | 15 ++++++++++++++-
 1 file changed, 14 insertions(+), 1 deletion(-)

diff --git a/drivers/gpu/drm/bridge/fsl-ldb.c b/drivers/gpu/drm/bridge/fsl-ldb.c
index bd03c36ee696c..b4959f654f2ac 100644
--- a/drivers/gpu/drm/bridge/fsl-ldb.c
+++ b/drivers/gpu/drm/bridge/fsl-ldb.c
@@ -57,6 +57,7 @@ enum fsl_ldb_devtype {
 	IMX6SX_LDB,
 	IMX8MP_LDB,
 	IMX93_LDB,
+	IMX94_LDB,
 };
 
 struct fsl_ldb_devdata {
@@ -64,21 +65,31 @@ struct fsl_ldb_devdata {
 	u32 lvds_ctrl;
 	bool lvds_en_bit;
 	bool single_ctrl_reg;
+	u32 max_clk_khz;
 };
 
 static const struct fsl_ldb_devdata fsl_ldb_devdata[] = {
 	[IMX6SX_LDB] = {
 		.ldb_ctrl = 0x18,
 		.single_ctrl_reg = true,
+		.max_clk_khz = 80000,
 	},
 	[IMX8MP_LDB] = {
 		.ldb_ctrl = 0x5c,
 		.lvds_ctrl = 0x128,
+		.max_clk_khz = 80000,
 	},
 	[IMX93_LDB] = {
 		.ldb_ctrl = 0x20,
 		.lvds_ctrl = 0x24,
 		.lvds_en_bit = true,
+		.max_clk_khz = 80000,
+	},
+	[IMX94_LDB] = {
+		.ldb_ctrl = 0x04,
+		.lvds_ctrl = 0x08,
+		.lvds_en_bit = true,
+		.max_clk_khz = 148500,
 	},
 };
 
@@ -274,7 +285,7 @@ fsl_ldb_mode_valid(struct drm_bridge *bridge,
 {
 	struct fsl_ldb *fsl_ldb = to_fsl_ldb(bridge);
 
-	if (mode->clock > (fsl_ldb_is_dual(fsl_ldb) ? 160000 : 80000))
+	if (mode->clock > (fsl_ldb_is_dual(fsl_ldb) ? 2 : 1) * fsl_ldb->devdata->max_clk_khz)
 		return MODE_CLOCK_HIGH;
 
 	return MODE_OK;
@@ -386,6 +397,8 @@ static const struct of_device_id fsl_ldb_match[] = {
 	  .data = &fsl_ldb_devdata[IMX8MP_LDB], },
 	{ .compatible = "fsl,imx93-ldb",
 	  .data = &fsl_ldb_devdata[IMX93_LDB], },
+	{ .compatible = "fsl,imx94-ldb",
+	  .data = &fsl_ldb_devdata[IMX94_LDB], },
 	{ /* sentinel */ },
 };
 MODULE_DEVICE_TABLE(of, fsl_ldb_match);

-- 
2.51.0


^ permalink raw reply related


This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox