[PATCH net-next v12 0/9] net: dsa: add DSA support for the LAN9645x switch chip family

HOTtoday

12 messages, 2 authors, 22h ago · open the first message on its own page

[PATCH net-next v12 0/9] net: dsa: add DSA support for the LAN9645x switch chip family

From: Jens Emil Schulz Østergaard <hidden>
Date: 2026-09-08 07:41:17

This series provides the Microchip LAN9645X Switch driver.

The LAN9645x is a family of chips with ethernet switch functionality and
multiple peripheral functions. The switch delivers up to 9 ethernet
ports and 12 Gbps switching bandwidth.

The switch chip has 5 integrated copper PHYs, support for 2x RGMII
interfaces, 2x SGMII and one QSGMII interface.

The switch chip is from the same design architecture family as ocelot
and lan966x, and the driver reflects this similarity. However, LAN9645x
does not have an internal CPU in any package, and must be driven
externally. For register IO it supports interfaces such as SPI, I2C and
MDIO.

The chip supports a variety of network features such as

* Mactable for MDB/FDB functionality
* Bridge forwarding offload
* VLAN-aware bridging
* IGMP/MLD snooping
* Link aggregation
* PTP timestamping
* FRER (802.1CB)
* Media Redundancy Protocol
* Parallel Redundancy and High-Availability Seamless Redundancy
  (HSR/PRP) in DANH/DANP mode
* Per stream filtering and policing
* Shapers such as Credit Based Shaping and Time Aware Shaing
* Frame preemption
* A TCAM (VCAP) for line-rate frame processing

The LAN9645x family consists of the following SKUs:

LAN96455F
LAN96457F
LAN96459F
LAN96455S
LAN96457S
LAN96459S

The difference between the SKUs is the number of supported ports (5, 7
or 9) and features supported. The F subfamily supports HSR/PRP and TSN,
while the S subfamily does not.

The intended way to bind this driver is using a parent MFD driver,
responsible for the register IO protocol, and distributing regmaps to
child devices. The goal is to use the same approach as the MFD driver in
drivers/mfd/ocelot-spi.c, which also resets the chip before it
instantiates its children.

This driver expects to request named regmaps from a parent device. This
approach is similar to the DSA driver

drivers/net/dsa/ocelot/ocelot_ext.c

which supports being driven by an external CPU via SPI with parent
device drivers/mfd/ocelot-spi.c.

The MFD driver will come in a later series, because there are
requirements on the number of child devices before a driver qualifies as
a MFD device.

Development is done using the LAN966x as a host CPU, running the lan966x
swichdev driver, using the EVB-LAN9668 EDS2 board.

The datasheet is available here:
https://ww1.microchip.com/downloads/aemDocuments/documents/UNG/ProductDocuments/DataSheets/LAN9645xF-Data-Sheet-DS00006065.pdf

This series will deliver the following features:

* Standalone ports
* Bridge forwarding and FDB offloading
* Multicast forwarding and MDB offloading
* VLAN-aware bridge
* Stats integration

More support will be added at a later stage. Here is a tentative plan of
future patches for this DSA driver:

* Add LAG support.
* Add TC matchall mirror support.
* Add TC matchall police support.
* Add DCB/qos support.
* Add simple TC support: mqprio, cbs, tbf, ebf.
* Add TC flower filter support.
* Add HSR/PRP offloading support.
* Add PTP support.
* Add TC taprio support.

For completeness I include tentative plan of planned patches for
LAN9645x peripherals:

* Extend pinctrl-ocelot for LAN9645x:
  https://lore.kernel.org/linux-gpio/20260119-pinctrl_ocelot_extend_support_for_lan9645x-v1-0-1228155ed0ee@microchip.com/
* Add driver for internal PHY:
  https://lore.kernel.org/netdev/20260123-phy_micrel_add_support_for_lan9645x_internal_phy-v1-1-8484b1a5a7fd@microchip.com/
* MFD driver for managing register IO protocol and child device
  initialization.
* Extend pinctrl-microchip-sgpio for LAN9645x support.
* Extend i2c_designware for LAN9645x support.
* Add driver for outbound interrupt controller.
* Add serdes driver for lan9645x.

Signed-off-by: Jens Emil Schulz Østergaard <redacted>
---
Changes in v12:
- reorder the series so bridge support lands after vlan, mac table and
  mdb support. port_fast_age() now exists before the STP state handling
  that needs it, and host address filtering is in place before bridging.
- include the CPU port module in the flood masks from the start, and
  remove it again in the bridge patch where port_set_host_flood() takes
  over on demand host flooding. Previously the host could not receive
  unicast until the mac table patch.
- vlan: serialize the VLAN table with fwd_domain_lock, and describe the
  untagged VLAN, TPID and C-component conformance limitations.
- tag driver: move an out-of-band VLAN tag into the payload before
  reading it, instead of relying on the conduit never advertising a VLAN
  TX offload.
- tag driver: restore the classified VLAN as a C-tag rather than following
  the IFH tag type, so a terminated frame lands on the VID the hardware
  forwarded it on.
- basic: drop the tail drop watermark setup, which programmed a
  condition that could not trigger.
- mdb: handle PGID exhaustion on both the add and delete paths, rather
  than leaving a departed port in the group.
- mdb: classify IPv4 multicast on the 01:00:5E prefix alone. Hardware does
  not apply the 23-bit rule, so some groups were programmed as entries it
  never matches.
- stats: give the hardware counters and the software shadow a common
  baseline, fix some rtnl_link_stats64 fields, and stop reporting the
  three eth-mac fields the hardware cannot measure.
- stats: stop the counter poller in .shutdown, which kept reading
  registers after the parent had been shut down.
- Link to v11: https://lore.kernel.org/r/20260805-dsa_lan9645x_switch_driver_base-v11-0-007ebc983a0a@microchip.com

Changes in v11:
- Update comment about vlan tag hwaccel use in tag driver.
- Bump DSA_TAG_PROTO value in include/net/dsa.h.
- stats: add comment about calling contexts of sw_lock in
  lan9645x_stats_get_stats64
- Link to v10: https://lore.kernel.org/r/20260713-dsa_lan9645x_switch_driver_base-v10-0-a4886a08fb15@microchip.com

Changes in v10:
- Individual patches mention specific v10 changes
- Update tag driver after skb ownership model change in DSA taggers.
- Link to v9: https://lore.kernel.org/r/20260708-dsa_lan9645x_switch_driver_base-v9-0-0d1512a326d7@microchip.com

Changes in v9:
- Individual patches mention specific v9 changes
- Drop RGMII muxing to port 4.
- Link to v8: https://lore.kernel.org/r/20260702-dsa_lan9645x_switch_driver_base-v8-0-90228d8bba58@microchip.com

Changes in v8:
- Individual patches mention specific v8 changes
- Do not use the name CPU_PORT for the chips internal CPU port module,
  as it collides with the DSA CPU port (the NPI front port). Drop the
  CPU_PORT macro and reference the module as lan9645x->num_phys_ports,
  mirroring ocelot/felix.
- Reword the port map and related comments to distinguish the chip CPU
  port modules (indices 9-10) from the DSA CPU port.
- Add a port_mux_lock mutex to serialize port mux arbitration in
  the phylink mac_prepare path.
- Link to v7: https://lore.kernel.org/r/20260603-dsa_lan9645x_switch_driver_base-v7-0-b2f90e676707@microchip.com

Changes in v7:
- Individual patches mention specific v7 changes
- Do not mark the BPDU range 01:80:C2:00:00:0X as offloaded in the tag driver.
- Refactor cpu queue based frame classification to use categories default, trap
  and copy, which mirrors usage instead of being based on frame types.
- Add registers ANA:COMMON:CPUQ_8021_CFG for bpdu cpu queue configuration.
- Use cpu queue LAN9645X_CPUQ_TRAP for bpdu frames.
- Refactor IGMP/MLD/IPMC_CTRL to use new LAN9645X_CPUQ_DEF,
  LAN9645X_CPUQ_TRAP and LAN9645X_CPUQ_COPY queues.
- Add __aligned(2) to mac variable in lan9645x_mdb_update_dest.
- Link to v6: https://lore.kernel.org/r/20260527-dsa_lan9645x_switch_driver_base-v6-0-4d409ae64f3c@microchip.com

Changes in v6:
- Individual patches mention specific v6 changes
- Rebased on net-next, bumping DSA_TAG_PROTO_LAN9645X_VALUE to 34
- Link to v5: https://lore.kernel.org/r/20260518-dsa_lan9645x_switch_driver_base-v5-0-968fbf34ffa3@microchip.com

Changes in v5:
- Individual patches mention specific v5 changes
- Undo offset fix in postpull_rcsum. The original logic was correct for
  CHECKSUM_COMPLETE host NICs
- Use __always_inline in lan9645x_ifh_{get,set}
- remove double space after = in set_merge_mask
- remove unused fields dd_dis and tsn_dis, and add SKU supported port validation
  during setup
- phylink: remove MAC_2500FD
- phylink: add comment about empty supported_interfaces for port 5-6.
- phylink: fix 2:1 rgmii port muxing for port module 4 and 7 to be fully
  dynamic and validate requested mux settings.
- phylink: add comment about 2:1 rgmii port muxing for port module 4
  and 7.
- rx/tx-internal-delay-ps checked against supported 2ns value
- init lan9645x->npi = -1 at probe and check port < 0 in npi_deinit
- use ds->ageing_time_max
- use packed p->host_flood_req for atomic r/w
- fix typo in set_ageing_time comment
- include lan9645x->bridge deref under lock in brige_join
- switch -EBUSY to -EINVAL for vlan add/del in the reserved range.
- remove reserved HSR vlan
- lan9645x_mac_init returns error on table init timeout
- add comment about skipping LOCKED entries on fdb dump
- skip igmp/mld redir for npi port
- make lan9645x_stats_init void
- add SCNT_TX_BUFDROP to tx_dropped
- change rmon range {0,64} -> {64, 64}. Runt frames counted elsewhere.
- remove rx_crc, rx_symbol_err from rx_packets, as they are already
  counted in SZ_* buckets.
- add defensive cancel_delayed_work_sync in stats_free
- Link to v4: https://lore.kernel.org/r/20260430-dsa_lan9645x_switch_driver_base-v4-0-f1b6005fa8b7@microchip.com

Changes in v4:
- v3 was deferred, but I made some changes based on the Sashiko review
- Individual patches mention specific v4 changes
- Fix offset in postpull_rcsum so prefix eth header is cleared, not
  actual eth header, so tag driver works with CHECKSUM_COMPLETE host
  NICs
- Fix untagged rx on vlan aware port with pvid
- Add comment to QSYS_RES_CFG configuration
- Phylink_mac_prepare: fix to make sure we can dynamically change rgmii
  on port 4
- Move ports allocation to probe
- tag_npi_setup: reject cascaded setups
- Skip WARN_ON in lan9645x_to_port
- set_host_flood changed to per port work to coalesce values and skip
  atomic allocations
- Fix clear HOST_PVID vlan membership when a port joins a bridge.
- Explicit default value write to tag type register for untagged frames
- Use lan_rmw for ANA_DROP_CFG
- Add comment for error path in lan9645x_vlan_hw_wr
- Remove mac_entries list and just do direct IO to mac table from
  fdb_add/fdb_del.
- Clean up fresh mdb when hw mac table write fails.
- Remove rx_uc and tx_uc from ethtool stats list, as they are derivable
  from the eth-mac group
- Split stats_init into stats_alloc and stats_init, use alloc in probe
  and init in dsa_setup
- Link to v3: https://lore.kernel.org/r/20260410-dsa_lan9645x_switch_driver_base-v3-0-aadc8595306d@microchip.com

Changes in v3:
- Individual patches mention specific v3 changes.
- Add guard before vlan_remove_tag on xmit
- Add pskb_may_pull checks on rx
- Remove additionalProperties: true in bindings
- Remove unnecessary | from description in bindings
- Change top level $ref to dsa.yaml#/$defs/ethernet-ports
- Use ethernet-ports and ethernet-port
- Move ethernet-ports under properties instead of patternProperties
- Move unevaluatedProperties: false after $ref
- Update bindings example to use ethernet-ports and ethernet-port
- Move DEV_MAC_TAGS_CFG to port setup, instead of vlan config, so vlan
  overhead is always included in port frame maxlen calculation.
- Remove code disabling ipv6 on conduit
- Use of_property_read_u32 for {rx,tx}-internal-delay-ps
- Use dsa_user_ports(ds) instead of
  GENMASK(lan9645x->num_phys_ports - 1, 0) as base flood mask.
- Add comment explaining obey vlan
- Allow disabling aging with explicit zero parameters.
- Fix non-forwarding STP states in bridge fwd calculation.
- Restore host flood state on bridge leave.
- Avoid mac_entry dealloc when mac table writes fail.
- Avoid mdb_entry dealloc when mac table writes fail.
- Dealloc mac_entries on deinit.
- Dealloc mdb_entries on deinit.
- Link to v2: https://lore.kernel.org/r/20260324-dsa_lan9645x_switch_driver_base-v2-0-f7504e3b0681@microchip.com

Changes in v2:
- Individual patches have specific v2 changes.
- Ran DSA, and std counters, selftests, which prompted several changes.
  The following selftests pass, except for some expected failures:
    - bridge_vlan_aware.sh
    - bridge_vlan_unaware.sh
    - bridge_vlan_mcast.sh
    - no_forwarding.sh
    - bridge_mdb.sh
    - bridge_mld.sh
    - test_fdb_stress_test.sh
    - .../drivers/net/hw/ethtool_rmon.sh
    - .../drivers/net/hw/ethtool_std_stats.sh (from Ioana's series)
- Added new patch for MDB management, as this was required for selftests.
- Added port_set_host_flood to enable unknown traffic to standalone during
  promisc/ALL_MULTI (selftests).
- Remove the dubugfs.
- Link to v1: https://lore.kernel.org/r/20260303-dsa_lan9645x_switch_driver_base-v1-0-bff8ca1396f5@microchip.com

---
Jens Emil Schulz Østergaard (9):
      net: dsa: add tag driver for LAN9645X
      dt-bindings: net: lan9645x: add LAN9645X switch bindings
      net: dsa: lan9645x: add autogenerated register macros
      net: dsa: lan9645x: add basic dsa driver for LAN9645X
      net: dsa: lan9645x: add vlan support
      net: dsa: lan9645x: add mac table integration
      net: dsa: lan9645x: add mdb management
      net: dsa: lan9645x: add bridge support
      net: dsa: lan9645x: add port statistics

 .../net/dsa/microchip,lan96455s-switch.yaml        |  179 ++
 MAINTAINERS                                        |   10 +
 drivers/net/dsa/Kconfig                            |    2 +
 drivers/net/dsa/microchip/Makefile                 |    1 +
 drivers/net/dsa/microchip/lan9645x/Kconfig         |   12 +
 drivers/net/dsa/microchip/lan9645x/Makefile        |   12 +
 drivers/net/dsa/microchip/lan9645x/lan9645x_mac.c  |  268 +++
 drivers/net/dsa/microchip/lan9645x/lan9645x_main.c | 1031 +++++++++++
 drivers/net/dsa/microchip/lan9645x/lan9645x_main.h |  434 +++++
 drivers/net/dsa/microchip/lan9645x/lan9645x_mdb.c  |  569 ++++++
 drivers/net/dsa/microchip/lan9645x/lan9645x_npi.c  |   90 +
 .../net/dsa/microchip/lan9645x/lan9645x_phylink.c  |  361 ++++
 drivers/net/dsa/microchip/lan9645x/lan9645x_port.c |  175 ++
 drivers/net/dsa/microchip/lan9645x/lan9645x_regs.h | 1910 ++++++++++++++++++++
 .../net/dsa/microchip/lan9645x/lan9645x_stats.c    |  856 +++++++++
 .../net/dsa/microchip/lan9645x/lan9645x_stats.h    |  229 +++
 drivers/net/dsa/microchip/lan9645x/lan9645x_vlan.c |  411 +++++
 include/linux/dsa/lan9645x.h                       |   54 +
 include/net/dsa.h                                  |    2 +
 net/dsa/Kconfig                                    |   11 +
 net/dsa/Makefile                                   |    1 +
 net/dsa/tag_lan9645x.c                             |  474 +++++
 22 files changed, 7092 insertions(+)
---
base-commit: bc6fe9d301d59fe5cb236d646abe792bffae5fa6
change-id: 20260210-dsa_lan9645x_switch_driver_base-312bbfc37edb

Best regards,
-- 
Jens Emil Schulz Østergaard [off-list ref]

[PATCH net-next v12 1/9] net: dsa: add tag driver for LAN9645X

From: Jens Emil Schulz Østergaard <hidden>
Date: 2026-09-08 07:41:22

Add tag driver for LAN9645x using a front port as CPU port. This mode
is called an NPI port in the datasheet.
Use long prefix on extraction (RX) and no prefix on injection (TX). A
long prefix on extraction helps get through the conduit port on host
side, since it will see a broadcast MAC.

The LAN9645x chip is in the same design architecture family as ocelot
and lan966x. The tagging protocol has the same structure as these chips,
but the particular fields are different or have different sizes.
Therefore, this tag driver is similar to tag_ocelot.c, but the
differences in fields makes it hard to reuse.

LAN9645x supports 3 different tag formats for extraction/injection of
frames from a CPU port: long prefix, short prefix and no prefix.

The tag is prepended to the frame. The critical data for the chip is
contained in an internal frame header (IFH) which is 28 bytes. The
prefix formats look like this:

Long prefix (16 bytes) + IFH:
- DMAC    = 0xffffffffffff on extraction.
- SMAC    = 0xfeffffffffff on extraction.
- ETYPE   = 0x8880
- payload = 0x0011
- IFH

Short prefix (4 bytes) + IFH:
- 0x8880
- 0x0011
- IFH

No prefix:
- IFH

The format can be configured asymmetrically on RX and TX.

The IFH get/set functions are declared as inline. All the field
constants are compile-time known, so when these calls are inlined
efficient code is generated with branches pruned and loops unrolled.
During testing it was observed that without explicit inlining GCC would
have trouble inlining the functions, which hurt performance.

A frame extracted from a VLAN-aware port arrives without its VLAN tag.
REW_PORT_CFG.NO_REWRITE is clear on the NPI port, so the rewriter applies
the classified VLAN_POP_CNT, and the classified VID reaches the CPU only
through the IFH. The tagger restores it as a hwaccel tag, except when it
equals the port pvid, where a frame tagged with the pvid is
indistinguishable from an untagged one.

The restored tag is always a C-tag, not the TPID the IFH TAG_TYPE reports.
Both TPIDs are recognized as VLAN tags unconditionally, with no per port
control to treat one as untagged customer data, so an S-tag is already
consumed as the VLAN tag before extraction. Restoring 802.1AD would make
the bridge push the tag back into the payload and reclassify the frame to
the port pvid, placing it on a different VID than the one the hardware
forwarded it on. Normalizing to a C-tag keeps the terminated path
consistent with the forwarded path, at the cost of not preserving the
S-tag TPID towards the CPU. This assumes a bridge vlan_protocol of
802.1Q, which is the only protocol the driver offloads.

Reviewed-by: Steen Hegelund <Steen.Hegelund@microchip.com>
Signed-off-by: Jens Emil Schulz Østergaard <redacted>
---
Changes in v12:
- Move an out-of-band VLAN tag into the payload with
  __vlan_hwaccel_push_inside() before reading it, rather than relying on
  the conduit never advertising NETIF_F_HW_VLAN_CTAG_TX or
  NETIF_F_HW_VLAN_STAG_TX in vlan_features.
- Return the skb from lan9645x_xmit_get_vlan_info(), since
  __vlan_hwaccel_push_inside() may reallocate and frees the skb on
  failure.
- Annotate the IFH field defines with injection/extraction
  applicability
- Drop IFH_TIMESTAMP because it is 38 bits wide. Rely on NS/SUB_NS
  subfields.
- Document how HW derives the DMAC offset from pop_cnt and etype_ofs,
  and drop frames whose encoding only a tag push could produce.
- Drop unncessary IFH_SRCPORT on injection.
- Drop the WARN_ON_ONCE() when the source port does not resolve to a
  user port.
- Move the IFH field position and size defines from
  include/linux/dsa/lan9645x.h into net/dsa/tag_lan9645x.c.
- Rename LAN9645X_IFH_LEN to LAN9645X_IFH_LEN_BYTES, so it does not read
  as the position of the IFH LEN field.
- Use skb_vlan_eth_hdr() in lan9645x_xmit_get_vlan_info().
- Drop BTM_MSK() TOP_MSK() helpers for GENMASK().
- Rename set_merge_mask() to lan9645x_ifh_merge_byte().
- Name the vendor in the Kconfig prompt, spell the part LAN9645x, and
  move the entry to its sorted position after NET_DSA_TAG_KSZ.
- Clamp the classified QoS class to the width of IFH_QOS_CLASS. A
  skb->priority above 7 aliased down onto an unrelated class.
- Comment why needed_headroom covers the extraction prefix.
- Restore the classified VLAN as a C-tag rather than following the IFH
  TAG_TYPE, so a terminated S-tagged frame lands on the same VID the
  hardware forwards it on.

Changes in v11:
- Update comment about vlan tag hwaccel use in tag driver.
- Bump DSA_TAG_PROTO value in include/net/dsa.h.

Changes in v10:
- Update tag driver after skb ownership model change in DSA taggers.
  Explicitly freeing the skb at rcv/xmit return points.

Changes in v8:
- Drop the cpu_port local and use ds->num_ports directly as the IFH
  source port (the CPU port module index).
- Reword the reflection comment to refer to "the CPU".

Changes in v7:
- Introduce cpu queue based frame classification, and refactor to the
  categories default, trap and copy, which mirrors usage instead of
  being based on frame types.

Changes in v6:
- rebased on net-next, bumping DSA_TAG_PROTO_LAN9645X_VALUE to 34

Changes in v5:
- Undo offset fix in postpull_rcsum. The original logic was correct for
  CHECKSUM_COMPLETE host NICs
- Use __always_inline in lan9645x_ifh_{get,set}
- remove double space after = in set_merge_mask

Changes in v4:
- Fix offset in postpull_rcsum so prefix eth header is cleared, not
  actual eth header, so tag driver works with CHECKSUM_COMPLETE host
  NICs
- Fix untagged rx on vlan aware port with pvid

Changes in v3:
- guard vlan_remove_tag behind skb_headlen(skb) >= VLAN_ETH_HLEN on xmit
- add pskb_may_pull checks in rx path

Changes in v2:
- sorting in net/dsa/Kconfig
- sorting in net/dsa/Makefile
- remove default zero promisc_on_conduit
- move functions to to .c file
- add justification for inline usage to commit message
- add __skb_put_padto on xmit path
- fix hwaccel_put_tag
---
 MAINTAINERS                  |   8 +
 include/linux/dsa/lan9645x.h |  54 +++++
 include/net/dsa.h            |   2 +
 net/dsa/Kconfig              |  11 +
 net/dsa/Makefile             |   1 +
 net/dsa/tag_lan9645x.c       | 474 +++++++++++++++++++++++++++++++++++++++++++
 6 files changed, 550 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index b23fb6f2f4ef..967b098d32d9 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -17793,6 +17793,14 @@ L:	netdev@vger.kernel.org
 S:	Maintained
 F:	drivers/net/phy/microchip_t1.c
 
+MICROCHIP LAN9645X ETHERNET SWITCH DRIVER
+M:	Jens Emil Schulz Østergaard <jensemil.schulzostergaard@microchip.com>
+M:	UNGLinuxDriver@microchip.com
+L:	netdev@vger.kernel.org
+S:	Maintained
+F:	include/linux/dsa/lan9645x.h
+F:	net/dsa/tag_lan9645x.c
+
 MICROCHIP LAN966X ETHERNET DRIVER
 M:	Horatiu Vultur <horatiu.vultur@microchip.com>
 M:	UNGLinuxDriver@microchip.com
diff --git a/include/linux/dsa/lan9645x.h b/include/linux/dsa/lan9645x.h
new file mode 100644
index 000000000000..e774aa137092
--- /dev/null
+++ b/include/linux/dsa/lan9645x.h
@@ -0,0 +1,54 @@
+/* SPDX-License-Identifier: GPL-2.0
+ * Copyright (C) 2026 Microchip Technology Inc.
+ */
+
+#ifndef _NET_DSA_TAG_LAN9645X_H_
+#define _NET_DSA_TAG_LAN9645X_H_
+
+#include <linux/bits.h>
+#include <linux/types.h>
+
+/* LAN9645x supports 3 different formats on an NPI port, long prefix, short
+ * prefix and no prefix. The format can be configured asymmetrically on RX and
+ * TX. We use long prefix on extraction (RX), and no prefix on injection.
+ * The long prefix on extraction helps get through the conduit port on host
+ * side, since it will see a broadcast MAC.
+ *
+ * The internal frame header (IFH) is 28 bytes.
+ *
+ * Long prefix, 16 bytes + IFH:
+ * - DMAC    = 0xFFFFFFFFFFFF on extraction.
+ * - SMAC    = 0xFEFFFFFFFFFF on extraction.
+ * - ETYPE   = 0x8880
+ * - payload = 0x0011
+ * - IFH
+ *
+ * Short prefix, 4 bytes + IFH:
+ * - 0x8880
+ * - 0x0011
+ * - IFH
+ *
+ * No prefix:
+ * - IFH
+ *
+ */
+#define LAN9645X_IFH_TAG_TYPE_C		0
+#define LAN9645X_IFH_TAG_TYPE_S		1
+#define LAN9645X_IFH_LEN_U32		7
+#define LAN9645X_IFH_LEN_BYTES		(LAN9645X_IFH_LEN_U32 * sizeof(u32))
+#define LAN9645X_IFH_BITS		(LAN9645X_IFH_LEN_BYTES * BITS_PER_BYTE)
+#define LAN9645X_LONG_PREFIX_LEN	16
+#define LAN9645X_TOTAL_TAG_LEN \
+	(LAN9645X_LONG_PREFIX_LEN + LAN9645X_IFH_LEN_BYTES)
+
+/* Chip has 8 cpu queues. The cpu queues used by a frame are passed as a mask in
+ * the IFH on extraction. We use this to avoid classifying BPDU, IGMP and MLD
+ * frames in the tag driver.
+ */
+enum {
+	LAN9645X_CPUQ_DEF = 0,
+	LAN9645X_CPUQ_TRAP = 1,
+	LAN9645X_CPUQ_COPY = 2,
+};
+
+#endif /* _NET_DSA_TAG_LAN9645X_H_ */
diff --git a/include/net/dsa.h b/include/net/dsa.h
index 7507d632e7c6..8531302bef47 100644
--- a/include/net/dsa.h
+++ b/include/net/dsa.h
@@ -61,6 +61,7 @@ struct tc_action;
 #define DSA_TAG_PROTO_NETC_VALUE		33
 #define DSA_TAG_PROTO_KSZ8463_VALUE		34
 #define DSA_TAG_PROTO_MT7628_VALUE		35
+#define DSA_TAG_PROTO_LAN9645X_VALUE		36
 
 enum dsa_tag_protocol {
 	DSA_TAG_PROTO_NONE		= DSA_TAG_PROTO_NONE_VALUE,
@@ -99,6 +100,7 @@ enum dsa_tag_protocol {
 	DSA_TAG_PROTO_NETC		= DSA_TAG_PROTO_NETC_VALUE,
 	DSA_TAG_PROTO_KSZ8463		= DSA_TAG_PROTO_KSZ8463_VALUE,
 	DSA_TAG_PROTO_MT7628		= DSA_TAG_PROTO_MT7628_VALUE,
+	DSA_TAG_PROTO_LAN9645X		= DSA_TAG_PROTO_LAN9645X_VALUE,
 };
 
 struct dsa_switch;
diff --git a/net/dsa/Kconfig b/net/dsa/Kconfig
index 23b4b74004ed..e2df8923fded 100644
--- a/net/dsa/Kconfig
+++ b/net/dsa/Kconfig
@@ -131,6 +131,17 @@ config NET_DSA_TAG_KSZ
 	  Say Y if you want to enable support for tagging frames for the
 	  Microchip 8795/937x/9477/9893 families of switches.
 
+config NET_DSA_TAG_LAN9645X
+	tristate "Tag driver for Microchip LAN9645x switches"
+	help
+	  Say Y or M if you want to enable NPI tagging for the Microchip
+	  LAN9645x switches. In this mode, the frames over the Ethernet CPU
+	  port are prepended with a hardware-defined injection/extraction frame
+	  header. On injection a 28 byte internal frame header (IFH) is used.
+	  On extraction a 16 byte prefix is prepended before the internal frame
+	  header. This prefix starts with a broadcast MAC, to ease passage
+	  through the host side RX filter.
+
 config NET_DSA_TAG_NETC
 	tristate "Tag driver for NXP NETC switches"
 	help
diff --git a/net/dsa/Makefile b/net/dsa/Makefile
index d15bcf5c68f0..0a5f10ad60d8 100644
--- a/net/dsa/Makefile
+++ b/net/dsa/Makefile
@@ -27,6 +27,7 @@ obj-$(CONFIG_NET_DSA_TAG_GSWIP) += tag_gswip.o
 obj-$(CONFIG_NET_DSA_TAG_HELLCREEK) += tag_hellcreek.o
 obj-$(CONFIG_NET_DSA_TAG_KSZ) += tag_ksz.o
 obj-$(CONFIG_NET_DSA_TAG_LAN9303) += tag_lan9303.o
+obj-$(CONFIG_NET_DSA_TAG_LAN9645X) += tag_lan9645x.o
 obj-$(CONFIG_NET_DSA_TAG_MT7628) += tag_mt7628.o
 obj-$(CONFIG_NET_DSA_TAG_MTK) += tag_mtk.o
 obj-$(CONFIG_NET_DSA_TAG_MXL_862XX) += tag_mxl862xx.o
diff --git a/net/dsa/tag_lan9645x.c b/net/dsa/tag_lan9645x.c
new file mode 100644
index 000000000000..f54646d4b394
--- /dev/null
+++ b/net/dsa/tag_lan9645x.c
@@ -0,0 +1,474 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2026 Microchip Technology Inc.
+ */
+
+#include <linux/dsa/lan9645x.h>
+
+#include "tag.h"
+
+#define LAN9645X_NAME "lan9645x"
+
+/* The internal frame header (IFH) is 28 bytes, and the fields are documented
+ * below. Some fields are only used on either injection or extraction.
+ *
+ * Injection header
+ */
+#define IFH_INJ_TIMESTAMP		192
+#define IFH_BYPASS			191
+#define IFH_MASQ			190
+/* Extraction header */
+#define IFH_TIMESTAMP_NS		194
+#define IFH_TIMESTAMP_SUBNS		186
+/* Injection header */
+#define IFH_MASQ_PORT			186
+#define IFH_RCT_INJ			185
+/* Extraction header */
+#define IFH_LEN				171
+#define IFH_WRDMODE			169
+/* Extraction/Injection header */
+#define IFH_RTAGD			167
+/* Extraction header */
+#define IFH_CUTTHRU			166
+/* Extraction/Injection header */
+#define IFH_REW_CMD			156
+#define IFH_REW_OAM			155
+#define IFH_PDU_TYPE			151
+#define IFH_FCS_UPD			150
+#define IFH_DP				149
+/* Reserved */
+#define IFH_RTE_INB_UPDATE		148
+/* Extraction/Injection header */
+#define IFH_POP_CNT			146
+#define IFH_ETYPE_OFS			144
+/* Extraction header */
+#define IFH_SRCPORT			140
+/* Extraction/Injection header */
+#define IFH_SEQ_NUM			120
+#define IFH_TAG_TYPE			119
+#define IFH_TCI				103
+#define IFH_DSCP			97
+#define IFH_QOS_CLASS			94
+#define IFH_CPUQ			86
+/* Extraction header */
+#define IFH_LEARN_FLAGS			84
+/* Extraction/Injection header */
+#define IFH_SFLOW_ID			80
+#define IFH_ACL_HIT			79
+#define IFH_ACL_IDX			73
+#define IFH_ISDX			65
+#define IFH_DSTS			55
+/* Extraction header */
+#define IFH_FLOOD			53
+/* Extraction/Injection header */
+#define IFH_SEQ_OP			51
+#define IFH_IPV				48
+/* Injection header */
+#define IFH_AFI				47
+/* Reserved */
+#define IFH_RTP_ID			37
+#define IFH_RTP_SUBID			36
+#define IFH_PN_DATA_STATUS		28
+#define IFH_PN_TRANSF_STATUS_ZERO	27
+#define IFH_PN_CC			11
+/* Extraction/Injection header */
+#define IFH_DUPL_DISC_ENA		10
+/* Extraction header */
+#define IFH_RCT_AVAIL			9
+
+#define IFH_INJ_TIMESTAMP_SZ		32
+#define IFH_BYPASS_SZ			1
+#define IFH_MASQ_SZ			1
+#define IFH_TIMESTAMP_NS_SZ		30
+#define IFH_TIMESTAMP_SUBNS_SZ		8
+#define IFH_MASQ_PORT_SZ		4
+#define IFH_RCT_INJ_SZ			1
+#define IFH_LEN_SZ			14
+#define IFH_WRDMODE_SZ			2
+#define IFH_RTAGD_SZ			2
+#define IFH_CUTTHRU_SZ			1
+#define IFH_REW_CMD_SZ			10
+#define IFH_REW_OAM_SZ			1
+#define IFH_PDU_TYPE_SZ			4
+#define IFH_FCS_UPD_SZ			1
+#define IFH_DP_SZ			1
+#define IFH_RTE_INB_UPDATE_SZ		1
+#define IFH_POP_CNT_SZ			2
+#define IFH_ETYPE_OFS_SZ		2
+#define IFH_SRCPORT_SZ			4
+#define IFH_SEQ_NUM_SZ			16
+#define IFH_TAG_TYPE_SZ			1
+#define IFH_TCI_SZ			16
+#define IFH_DSCP_SZ			6
+#define IFH_QOS_CLASS_SZ		3
+#define IFH_CPUQ_SZ			8
+#define IFH_LEARN_FLAGS_SZ		2
+#define IFH_SFLOW_ID_SZ			4
+#define IFH_ACL_HIT_SZ			1
+#define IFH_ACL_IDX_SZ			6
+#define IFH_ISDX_SZ			8
+#define IFH_DSTS_SZ			10
+#define IFH_FLOOD_SZ			2
+#define IFH_SEQ_OP_SZ			2
+#define IFH_IPV_SZ			3
+#define IFH_AFI_SZ			1
+#define IFH_RTP_ID_SZ			10
+#define IFH_RTP_SUBID_SZ		1
+#define IFH_PN_DATA_STATUS_SZ		8
+#define IFH_PN_TRANSF_STATUS_ZERO_SZ	1
+#define IFH_PN_CC_SZ			16
+#define IFH_DUPL_DISC_ENA_SZ		1
+#define IFH_RCT_AVAIL_SZ		1
+
+static __always_inline void lan9645x_ifh_merge_byte(u8 *dst, u8 src, u8 mask)
+{
+	*dst = *dst ^ ((*dst ^ src) & mask);
+}
+
+/* The internal frame header (IFH) is a big-endian 28 byte unpadded bit array.
+ * Frames can be prepended with an IFH on injection and extraction. There
+ * are two field layouts, one for extraction and one for injection.
+ *
+ *    IFH bits go from high to low, for instance
+ *    ifh[0]  = [223:216]
+ *    ifh[27] = [7:0]
+ *
+ * Here is an example of setting a value starting at bit 13 of bit length 17.
+ *
+ * val    = 0x1ff
+ * pos    = 13
+ * length = 17
+ *
+ *
+ * IFH[]   0                         23       24       25        26      27
+ *
+ *                                           end_u8           start_u8
+ *      +--------+----------------+--------+--------+--------+--------+--------+
+ *      |        |                |        |        |        |        |        |
+ * IFH  |        | ....           |        |  vvvvvvvvvvvvvvvvvvv     |        |
+ *      |        |                |        |  |     |        |  |     |        |
+ *      +--------+----------------+--------+--+-----+--------+--+-----+--------+
+ * Bits  223                       39    32 31|   24 23    16 15|    8 7      0
+ *                                            |                 |
+ *                                            |                 |
+ *                                            |                 |
+ *                                            v                 v
+ *                                        end       = 29       pos        = 13
+ *                                        end_rem   = 5        pos_rem    = 5
+ *                                        end_u8    = 3        start_u8   = 1
+ *                                    GENMASK(5, 0) = 0x3f  GENMASK(7, 5) = 0xe0
+ *
+ *
+ * In end_u8 and start_u8 we must merge the existing IFH byte with the new
+ * value. In the 'middle' bytes of the value we can overwrite the corresponding
+ * IFH byte.
+ */
+static __always_inline void lan9645x_ifh_set(u8 *ifh, u32 val, size_t pos,
+					     size_t length)
+{
+	size_t end = (pos + length) - 1;
+	size_t end_rem = end & 0x7;
+	size_t pos_rem = pos & 0x7;
+	size_t start_u8 = pos >> 3;
+	size_t end_u8 = end >> 3;
+	u8 end_mask, start_mask;
+	size_t vshift;
+	u8 *ptr;
+
+	BUILD_BUG_ON_MSG(length > 32, "IFH field size wider than 32.");
+	BUILD_BUG_ON_MSG(length == 0, "IFH field size of 0.");
+	BUILD_BUG_ON_MSG(pos + length > LAN9645X_IFH_BITS,
+			 "IFH field overflows IFH");
+
+	end_mask = GENMASK(end_rem, 0);
+	start_mask = GENMASK(7, pos_rem);
+
+	ptr = &ifh[LAN9645X_IFH_LEN_BYTES - 1 - end_u8];
+
+	if (end_u8 == start_u8)
+		return lan9645x_ifh_merge_byte(ptr, val << pos_rem,
+					       end_mask & start_mask);
+
+	vshift = length - end_rem - 1;
+	lan9645x_ifh_merge_byte(ptr++, val >> vshift, end_mask);
+
+	for (size_t j = 1; j < end_u8 - start_u8; j++) {
+		vshift -= 8;
+		*ptr++ = val >> vshift;
+	}
+
+	lan9645x_ifh_merge_byte(ptr, val << pos_rem, start_mask);
+}
+
+static __always_inline u32 lan9645x_ifh_get(const u8 *ifh, size_t pos,
+					    size_t length)
+{
+	size_t end = (pos + length) - 1;
+	size_t end_rem = end & 0x7;
+	size_t pos_rem = pos & 0x7;
+	size_t start_u8 = pos >> 3;
+	size_t end_u8 = end >> 3;
+	u8 end_mask, start_mask;
+	const u8 *ptr;
+	u32 val;
+
+	BUILD_BUG_ON_MSG(length > 32, "IFH field size wider than 32.");
+	BUILD_BUG_ON_MSG(length == 0, "IFH field size of 0.");
+	BUILD_BUG_ON_MSG(pos + length > LAN9645X_IFH_BITS,
+			 "IFH field overflows IFH");
+
+	end_mask = GENMASK(end_rem, 0);
+	start_mask = GENMASK(7, pos_rem);
+
+	ptr = &ifh[LAN9645X_IFH_LEN_BYTES - 1 - end_u8];
+
+	if (end_u8 == start_u8)
+		return (*ptr & end_mask & start_mask) >> pos_rem;
+
+	val = *ptr++ & end_mask;
+
+	for (size_t j = 1; j < end_u8 - start_u8; j++)
+		val = val << 8 | *ptr++;
+
+	return val << (8 - pos_rem) | (*ptr & start_mask) >> pos_rem;
+}
+
+static struct sk_buff *lan9645x_xmit_get_vlan_info(struct sk_buff *skb,
+						   struct net_device *br,
+						   u32 *vlan_tci,
+						   u32 *tag_type)
+{
+	struct vlan_ethhdr *hdr;
+	u16 proto, tci;
+
+	/* If the VLAN tag is in the hwaccel area, move it to the payload so
+	 * that both cases are handled uniformly below, and so that the conduit
+	 * cannot insert it into the middle of the IFH we are about to prepend.
+	 */
+	if (unlikely(skb_vlan_tag_present(skb))) {
+		skb = __vlan_hwaccel_push_inside(skb);
+		if (!skb)
+			return NULL;
+	}
+
+	if (!br || !br_vlan_enabled(br)) {
+		*vlan_tci = 0;
+		*tag_type = LAN9645X_IFH_TAG_TYPE_C;
+		return skb;
+	}
+
+	hdr = skb_vlan_eth_hdr(skb);
+	br_vlan_get_proto(br, &proto);
+
+	if (skb_headlen(skb) >= VLAN_ETH_HLEN &&
+	    ntohs(hdr->h_vlan_proto) == proto) {
+		vlan_remove_tag(skb, &tci);
+		*vlan_tci = tci;
+	} else {
+		rcu_read_lock();
+		br_vlan_get_pvid_rcu(br, &tci);
+		rcu_read_unlock();
+		*vlan_tci = tci;
+	}
+
+	*tag_type = (proto != ETH_P_8021Q) ? LAN9645X_IFH_TAG_TYPE_S :
+					     LAN9645X_IFH_TAG_TYPE_C;
+
+	return skb;
+}
+
+static void lan9645x_offload_fwd_mark(struct sk_buff *skb, u32 cpuq)
+{
+	/* Trapped frames must be forwarded by the stack. */
+	if (cpuq & BIT(LAN9645X_CPUQ_TRAP)) {
+		skb->offload_fwd_mark = 0;
+		return;
+	}
+
+	dsa_default_offload_fwd_mark(skb);
+}
+
+static struct sk_buff *lan9645x_xmit(struct sk_buff *skb,
+				     struct net_device *ndev)
+{
+	struct dsa_port *dp = dsa_user_to_port(ndev);
+	u32 vlan_tci, tag_type;
+	u32 qos_class;
+	void *ifh;
+
+	skb = lan9645x_xmit_get_vlan_info(skb, dsa_port_bridge_dev_get(dp),
+					  &vlan_tci, &tag_type);
+	if (!skb)
+		return NULL;
+
+	/* We need to make sure frame has the proper size after IFH is stripped
+	 * by hw.
+	 */
+	if (skb_put_padto(skb, ETH_ZLEN))
+		return NULL;
+
+	qos_class = netdev_get_num_tc(ndev) ?
+		    netdev_get_prio_tc_map(ndev, skb->priority) :
+		    skb->priority;
+	qos_class = min_t(u32, qos_class, GENMASK(IFH_QOS_CLASS_SZ - 1, 0));
+
+	/* Make room for IFH */
+	ifh = skb_push(skb, LAN9645X_IFH_LEN_BYTES);
+	memset(ifh, 0, LAN9645X_IFH_LEN_BYTES);
+
+	lan9645x_ifh_set(ifh, 1, IFH_BYPASS, IFH_BYPASS_SZ);
+	lan9645x_ifh_set(ifh, tag_type, IFH_TAG_TYPE, IFH_TAG_TYPE_SZ);
+	lan9645x_ifh_set(ifh, vlan_tci, IFH_TCI, IFH_TCI_SZ);
+	lan9645x_ifh_set(ifh, qos_class, IFH_QOS_CLASS, IFH_QOS_CLASS_SZ);
+	lan9645x_ifh_set(ifh, BIT(dp->index), IFH_DSTS, IFH_DSTS_SZ);
+
+	return skb;
+}
+
+static struct sk_buff *lan9645x_rcv(struct sk_buff *skb,
+				    struct net_device *ndev)
+{
+	u32 src_port, qos_class, vlan_tci, popcnt, etype_ofs, cpuq;
+	struct dsa_port *dp;
+	u32 ifh_gap_len = 0;
+	u8 *ifh;
+
+	/* Conduit already consumed DMAC,SMAC,ETYPE from long prefix. Go back
+	 * to beginning of frame.
+	 */
+	skb_push(skb, ETH_HLEN);
+
+	if (unlikely(!pskb_may_pull(skb, LAN9645X_TOTAL_TAG_LEN))) {
+		kfree_skb(skb);
+		return NULL;
+	}
+
+	/* IFH starts after our long prefix */
+	ifh = skb_pull(skb, LAN9645X_LONG_PREFIX_LEN);
+
+	popcnt = lan9645x_ifh_get(ifh, IFH_POP_CNT, IFH_POP_CNT_SZ);
+	etype_ofs = lan9645x_ifh_get(ifh, IFH_ETYPE_OFS, IFH_ETYPE_OFS_SZ);
+	src_port = lan9645x_ifh_get(ifh, IFH_SRCPORT, IFH_SRCPORT_SZ);
+	vlan_tci = lan9645x_ifh_get(ifh, IFH_TCI, IFH_TCI_SZ);
+	qos_class = lan9645x_ifh_get(ifh, IFH_QOS_CLASS, IFH_QOS_CLASS_SZ);
+	cpuq = lan9645x_ifh_get(ifh, IFH_CPUQ, IFH_CPUQ_SZ);
+
+	/* Tag pushing is disabled on the NPI port via REW_TAG_CFG, so if this
+	 * fires REW_TAG_CFG is misconfigured.
+	 */
+	if (popcnt == 1 ||
+	    (popcnt == 0 && etype_ofs > 0)) {
+		kfree_skb(skb);
+		return NULL;
+	}
+
+	/* Since REW_PORT_CFG_NO_REWRITE=0 is required on the NPI port, we need
+	 * to account for any tags popped by the hardware, as that will leave a
+	 * gap between the IFH and DMAC. Tag pushing is disabled.
+	 *
+	 * The IFH fields do not have intuitive values. This is how HW does the
+	 * calculation:
+	 *
+	 * DMAC_DT = (ifh.pop_cnt == 0 && ifh.etype_ofs == 0) ? 4 : ifh.pop_cnt
+	 * DMAC_OFFSET = TAG_SIZE + 4*(DMAC_DT - 2)
+	 *
+	 * With tag pushing disabled we have either
+	 *
+	 * popcnt=0 and etype_ofs=0     => 2x pop
+	 * popcnt=3 and etype_ofs=*     => 1x pop
+	 * popcnt=2 and etype_ofs=*     => no pop
+	 *
+	 * The remaining combinations indicate a push and will not occur.
+	 */
+	if (popcnt == 0 && etype_ofs == 0)
+		ifh_gap_len = 2 * VLAN_HLEN;
+	else if (popcnt == 3)
+		ifh_gap_len = VLAN_HLEN;
+
+	/* Set skb->data at start of real header */
+	skb_pull(skb, LAN9645X_IFH_LEN_BYTES);
+
+	if (unlikely(!pskb_may_pull(skb, ifh_gap_len + ETH_HLEN))) {
+		kfree_skb(skb);
+		return NULL;
+	}
+
+	skb_pull(skb, ifh_gap_len);
+	skb_reset_mac_header(skb);
+	skb_set_network_header(skb, ETH_HLEN);
+	skb_reset_mac_len(skb);
+
+	/* Reset skb->data past the actual ethernet header. */
+	skb_pull(skb, ETH_HLEN);
+
+	/* We must deliver the skb so skb->csum only covers the data beyond the
+	 * real ethernet header. The fake ethernet header in the prefix is
+	 * not part of skb->csum already. We must subtract what remains of the
+	 * prefix, the ifh and the gap. The start is derived from the current
+	 * skb->data rather than saved on entry, because the pskb_may_pull()
+	 * calls above may have reallocated skb->head.
+	 */
+	skb_postpull_rcsum(skb,
+			   skb->data - LAN9645X_TOTAL_TAG_LEN - ifh_gap_len,
+			   LAN9645X_TOTAL_TAG_LEN + ifh_gap_len);
+
+	skb->dev = dsa_conduit_find_user(ndev, 0, src_port);
+	if (!skb->dev) {
+		/* Reflection is disabled for frames from the tag driver itself,
+		 * however it is possible that a frame sent directly on the
+		 * conduit gets reflected, so we drop it here.
+		 */
+		kfree_skb(skb);
+		return NULL;
+	}
+
+	lan9645x_offload_fwd_mark(skb, cpuq);
+
+	skb->priority = qos_class;
+
+	/* While we have REW_PORT_CFG_NO_REWRITE=0 on the NPI port, we still
+	 * disable port VLAN tag pushing with REW_TAG_CFG. A frame ingressing
+	 * on a vlan aware port, which is forwarded to the CPU, will not carry
+	 * vlan info in the frame data, because the tag is popped. The
+	 * classified VID is only communicated via the IFH, never in the
+	 * payload. We therefore restore it via hwaccel and must not pop an
+	 * in-band tag here.
+	 */
+	dp = dsa_user_to_port(skb->dev);
+
+	if (dsa_port_is_vlan_filtering(dp) && vlan_tci) {
+		u16 port_pvid = 0;
+
+		br_vlan_get_pvid_rcu(skb->dev, &port_pvid);
+
+		/* The tag is restored as a C-tag, not as the TAG_TYPE the IFH
+		 * reports. The classifier recognizes both TPIDs as VLAN tags,
+		 * so an S-tag has already been used for classification by the
+		 * time we get here. Restoring it as 802.1AD would make the
+		 * bridge push it back into the payload and reclassify the frame
+		 * to the port pvid, on a different VID than the one the
+		 * hardware forwarded it on.
+		 */
+		if ((vlan_tci & VLAN_VID_MASK) != port_pvid)
+			__vlan_hwaccel_put_tag(skb, htons(ETH_P_8021Q),
+					       vlan_tci);
+	}
+
+	return skb;
+}
+
+static const struct dsa_device_ops lan9645x_netdev_ops = {
+	.name			= LAN9645X_NAME,
+	.proto			= DSA_TAG_PROTO_LAN9645X,
+	.xmit			= lan9645x_xmit,
+	.rcv			= lan9645x_rcv,
+	/* Covers the extraction prefix too, since dsa_tag_protocol_overhead()
+	 * sizes the conduit MTU from this.
+	 */
+	.needed_headroom	= LAN9645X_TOTAL_TAG_LEN,
+};
+
+MODULE_DESCRIPTION("DSA tag driver for LAN9645x family of switches, using NPI port");
+MODULE_LICENSE("GPL");
+MODULE_ALIAS_DSA_TAG_DRIVER(DSA_TAG_PROTO_LAN9645X, LAN9645X_NAME);
+
+module_dsa_tag_driver(lan9645x_netdev_ops);
-- 
2.52.0

[PATCH net-next v12 2/9] dt-bindings: net: lan9645x: add LAN9645X switch bindings

From: Jens Emil Schulz Østergaard <hidden>
Date: 2026-09-08 07:41:25

Add bindings for LAN9645X switch. We use a fallback compatible for the
smallest SKU microchip,lan96455s-switch. The last digit of the part number
is the number of usable front ports and the letter is the feature set.

The switch is a child of an externally controlled parent that hands out
one regmap per register target, so reg and reg-names describe the target
layout the way mscc,vsc7514-switch.yaml does for the equivalent
mscc,vsc7512-switch node. That binding, and the mfd parent it hangs off
in mscc,ocelot.yaml, is the model this driver and its parent follow.

rx/tx-internal-delay-ps is used to model a direct MAC to MAC fixed link,
using RGMII, where exactly one of them must supply the delay.
Documentation/networking/phy.rst describes plain rgmii as the mode where
the MAC or the PCB adds the delay rather than the PHY.

Reviewed-by: Steen Hegelund <Steen.Hegelund@microchip.com>
Signed-off-by: Jens Emil Schulz Østergaard <redacted>
---
Changes in v12:
- Remove Rob's Reviewed-by
- Match both the port@ and ethernet-port@ spellings, so the per port
  constraints apply. dsa.yaml accepts either, and the local pattern only
  matched the long form.
- Allow 0 as well as 2000 for rx/tx-internal-delay-ps, matching what the
  driver accepts, and restrict them to those two values on RGMII ports.
- Constrain the ethernet-ports node the way renesas,rzn1-a5psw.yaml does,
  with additionalProperties instead of a local dsa-port.yaml $ref.
- Put the example properties in canonical order, so dt-check-style
  --mode=strict is clean.
- Describe the switch register targets in reg, with a matching reg-names,
  following mscc,vsc7514-switch.yaml.
- Document the default of rx/tx-internal-delay-ps, as
  microchip,lan937x.yaml does.
- Describe the SKU naming in the binding description.

Changes in v5:
- No changes.

Changes in v4:
- No changes.

Changes in v3:
- remove additionalProperties: true
- remove unnecessary | from description
- change top level $ref to dsa.yaml#/$defs/ethernet-ports
- use ethernet-ports and ethernet-port
- move ethernet-ports under properties instead of patternProperties
- move unevaluatedProperties: false after $ref
- update example to use ethernet-ports and ethernet-port

Changes in v2:
- rename file to microchip,lan96455s-switch.yaml
- remove led vendor property
- add {rx,tx}-internal-delay-ps for rgmii delay
- remove labels from example
- remove container node from example
---
 .../net/dsa/microchip,lan96455s-switch.yaml        | 179 +++++++++++++++++++++
 MAINTAINERS                                        |   1 +
 2 files changed, 180 insertions(+)
diff --git a/Documentation/devicetree/bindings/net/dsa/microchip,lan96455s-switch.yaml b/Documentation/devicetree/bindings/net/dsa/microchip,lan96455s-switch.yaml
new file mode 100644
index 000000000000..21affce218c2
--- /dev/null
+++ b/Documentation/devicetree/bindings/net/dsa/microchip,lan96455s-switch.yaml
@@ -0,0 +1,179 @@
+# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/net/dsa/microchip,lan96455s-switch.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Microchip LAN9645x Ethernet switch
+
+maintainers:
+  - Jens Emil Schulz Østergaard <jensemil.schulzostergaard@microchip.com>
+
+description: |
+  The LAN9645x switch is a multi-port Gigabit AVB/TSN Ethernet switch with
+  five integrated 10/100/1000Base-T PHYs. In addition to the integrated PHYs,
+  it supports up to 2 RGMII/RMII, up to 2 BASE-X/SERDES/2.5GBASE-X and one
+  Quad-SGMII interfaces.
+
+  The part number encodes the SKU. The last digit is the number of usable
+  front ports, 5, 7 or 9. The letter is the feature set, f for full and s for
+  standard, where full adds HSR, PRP, TAS, frame preemption and PSFP.
+
+properties:
+  compatible:
+    oneOf:
+      - enum:
+          - microchip,lan96455s-switch
+      - items:
+          - enum:
+              - microchip,lan96455f-switch
+              - microchip,lan96457f-switch
+              - microchip,lan96459f-switch
+              - microchip,lan96457s-switch
+              - microchip,lan96459s-switch
+          - const: microchip,lan96455s-switch
+
+  reg:
+    items:
+      - description: General configuration block target
+      - description:
+          CPU device queue system target, the register based frame injection
+          and extraction interface
+      - description: Chip top level target
+      - description: Rewriter target
+      - description: Switching engine target
+      - description: HSIO target
+      - description: Port 0 device target
+      - description: Port 1 device target
+      - description: Port 2 device target
+      - description: Port 3 device target
+      - description: Port 4 device target
+      - description: Port 5 device target
+      - description: Port 6 device target
+      - description: Port 7 device target
+      - description: Port 8 device target
+      - description: Queue system target
+      - description: Analyzer target
+
+  reg-names:
+    items:
+      - const: gcb
+      - const: qs
+      - const: chip_top
+      - const: rew
+      - const: sys
+      - const: hsio
+      - const: dev0
+      - const: dev1
+      - const: dev2
+      - const: dev3
+      - const: dev4
+      - const: dev5
+      - const: dev6
+      - const: dev7
+      - const: dev8
+      - const: qsys
+      - const: ana
+
+  ethernet-ports:
+    type: object
+    additionalProperties: true
+    patternProperties:
+      "^(ethernet-)?port@[0-8]$":
+        type: object
+        description: Ethernet switch ports
+        additionalProperties: true
+
+        allOf:
+          - if:
+              properties:
+                phy-mode:
+                  contains:
+                    enum:
+                      - rgmii
+                      - rgmii-rxid
+                      - rgmii-txid
+                      - rgmii-id
+            then:
+              properties:
+                rx-internal-delay-ps:
+                  $ref: "#/$defs/internal-delay-ps"
+                tx-internal-delay-ps:
+                  $ref: "#/$defs/internal-delay-ps"
+
+$ref: dsa.yaml#/$defs/ethernet-ports
+
+required:
+  - compatible
+  - reg
+  - reg-names
+  - ethernet-ports
+
+unevaluatedProperties: false
+
+$defs:
+  internal-delay-ps:
+    description:
+      Disable the delay line using 0 ps, or enable the 2000 ps delay. The
+      delay line is not tunable, so no other phase can be selected.
+    enum: [0, 2000]
+    default: 0
+
+examples:
+  - |
+    ethernet-switch@4000 {
+        compatible = "microchip,lan96459f-switch", "microchip,lan96455s-switch";
+        reg = <0x4000 0x244>, <0x8000 0x4c>, <0x10000 0x134>,
+              <0x18000 0x1910>, <0x2c000 0x12d4>, <0x30000 0xe4>,
+              <0x38000 0xb8>, <0x3c000 0xb8>, <0x40000 0xb8>,
+              <0x44000 0xb8>, <0x48000 0xb8>, <0x4c000 0xb8>,
+              <0x50000 0xb8>, <0x54000 0xb8>, <0x58000 0xb8>,
+              <0x100000 0xa000>, <0x140000 0x7468>;
+        reg-names = "gcb", "qs", "chip_top", "rew", "sys", "hsio", "dev0",
+                    "dev1", "dev2", "dev3", "dev4", "dev5", "dev6", "dev7",
+                    "dev8", "qsys", "ana";
+
+        ethernet-ports {
+            #address-cells = <1>;
+            #size-cells = <0>;
+
+            ethernet-port@0 {
+                reg = <0>;
+                phy-handle = <&cuphy0>;
+                phy-mode = "gmii";
+            };
+
+            ethernet-port@1 {
+                reg = <1>;
+                phy-handle = <&cuphy1>;
+                phy-mode = "gmii";
+            };
+
+            ethernet-port@2 {
+                reg = <2>;
+                phy-handle = <&cuphy2>;
+                phy-mode = "gmii";
+            };
+
+            ethernet-port@3 {
+                reg = <3>;
+                phy-handle = <&cuphy3>;
+                phy-mode = "gmii";
+            };
+
+            ethernet-port@7 {
+                reg = <7>;
+                ethernet = <&cpu_host_port>;
+                phy-mode = "rgmii";
+                rx-internal-delay-ps = <2000>;
+                tx-internal-delay-ps = <2000>;
+
+                fixed-link {
+                    full-duplex;
+                    pause;
+                    speed = <1000>;
+                };
+            };
+        };
+    };
+...
diff --git a/MAINTAINERS b/MAINTAINERS
index 967b098d32d9..042431460bb9 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -17798,6 +17798,7 @@ M:	Jens Emil Schulz Østergaard <jensemil.schulzostergaard@microchip.com>
 M:	UNGLinuxDriver@microchip.com
 L:	netdev@vger.kernel.org
 S:	Maintained
+F:	Documentation/devicetree/bindings/net/dsa/microchip,lan96455s-switch.yaml
 F:	include/linux/dsa/lan9645x.h
 F:	net/dsa/tag_lan9645x.c
 
-- 
2.52.0

[PATCH net-next v12 3/9] net: dsa: lan9645x: add autogenerated register macros

From: Jens Emil Schulz Østergaard <hidden>
Date: 2026-09-08 07:41:30

Add autogenerated register macros and update MAINTAINERS file. The
register macros are generated using the same tool we use for lan966x,
sparx5 and lan969x.

Reviewed-by: Steen Hegelund <Steen.Hegelund@microchip.com>
Signed-off-by: Jens Emil Schulz Østergaard <redacted>
---
Changes in v12:
- Regenerate without the unused AFI target.

Changes in v7:
- Add ANA:COMMON:CPUQ_8021_CFG for bpdu cpu queue configuration.

Changes in v3:
- No changes

Changes in v2:
- Add cpuq registers
---
 MAINTAINERS                                        |    1 +
 drivers/net/dsa/microchip/lan9645x/lan9645x_regs.h | 1910 ++++++++++++++++++++
 2 files changed, 1911 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index 042431460bb9..b07685633398 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -17799,6 +17799,7 @@ M:	UNGLinuxDriver@microchip.com
 L:	netdev@vger.kernel.org
 S:	Maintained
 F:	Documentation/devicetree/bindings/net/dsa/microchip,lan96455s-switch.yaml
+F:	drivers/net/dsa/microchip/lan9645x/*
 F:	include/linux/dsa/lan9645x.h
 F:	net/dsa/tag_lan9645x.c
 
diff --git a/drivers/net/dsa/microchip/lan9645x/lan9645x_regs.h b/drivers/net/dsa/microchip/lan9645x/lan9645x_regs.h
new file mode 100644
index 000000000000..9ea12e002bcb
--- /dev/null
+++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_regs.h
@@ -0,0 +1,1910 @@
+/* SPDX-License-Identifier: GPL-2.0+
+ *
+ * Copyright (c) 2026 Microchip Technology Inc.
+ */
+
+/* This file is autogenerated by cml-utils 2026-09-07 11:24:57 +0200.
+ * Commit ID: 3d638c6862e074e40811e9ebac05f56ebe77541e
+ */
+
+#ifndef _LAN9645X_REGS_H_
+#define _LAN9645X_REGS_H_
+
+#include <linux/bitfield.h>
+#include <linux/types.h>
+#include <linux/bug.h>
+
+enum lan9645x_target {
+	TARGET_ANA = 1,
+	TARGET_CHIP_TOP = 2,
+	TARGET_DEV = 5,
+	TARGET_GCB = 16,
+	TARGET_HSIO = 17,
+	TARGET_QS = 26,
+	TARGET_QSYS = 27,
+	TARGET_REW = 28,
+	TARGET_SYS = 29,
+	NUM_TARGETS = 38
+};
+
+#define __REG(...)    __VA_ARGS__
+
+/*      ANA:ANA:ADVLEARN */
+#define ANA_ADVLEARN              __REG(TARGET_ANA,\
+					0, 1, 27136, 0, 1, 284, 0, 0, 1, 4)
+
+#define ANA_ADVLEARN_VLAN_CHK                    BIT(0)
+#define ANA_ADVLEARN_VLAN_CHK_SET(x)\
+	FIELD_PREP(ANA_ADVLEARN_VLAN_CHK, x)
+#define ANA_ADVLEARN_VLAN_CHK_GET(x)\
+	FIELD_GET(ANA_ADVLEARN_VLAN_CHK, x)
+
+/*      ANA:ANA:VLANMASK */
+#define ANA_VLANMASK              __REG(TARGET_ANA,\
+					0, 1, 27136, 0, 1, 284, 8, 0, 1, 4)
+
+#define ANA_VLANMASK_VLANMASK                    GENMASK(9, 0)
+#define ANA_VLANMASK_VLANMASK_SET(x)\
+	FIELD_PREP(ANA_VLANMASK_VLANMASK, x)
+#define ANA_VLANMASK_VLANMASK_GET(x)\
+	FIELD_GET(ANA_VLANMASK_VLANMASK, x)
+
+/*      ANA:ANA:ANAGEFIL */
+#define ANA_ANAGEFIL              __REG(TARGET_ANA,\
+					0, 1, 27136, 0, 1, 284, 12, 0, 1, 4)
+
+#define ANA_ANAGEFIL_AGE_LOCKED                  BIT(20)
+#define ANA_ANAGEFIL_AGE_LOCKED_SET(x)\
+	FIELD_PREP(ANA_ANAGEFIL_AGE_LOCKED, x)
+#define ANA_ANAGEFIL_AGE_LOCKED_GET(x)\
+	FIELD_GET(ANA_ANAGEFIL_AGE_LOCKED, x)
+
+#define ANA_ANAGEFIL_PID_EN                      BIT(19)
+#define ANA_ANAGEFIL_PID_EN_SET(x)\
+	FIELD_PREP(ANA_ANAGEFIL_PID_EN, x)
+#define ANA_ANAGEFIL_PID_EN_GET(x)\
+	FIELD_GET(ANA_ANAGEFIL_PID_EN, x)
+
+#define ANA_ANAGEFIL_PID_VAL                     GENMASK(18, 14)
+#define ANA_ANAGEFIL_PID_VAL_SET(x)\
+	FIELD_PREP(ANA_ANAGEFIL_PID_VAL, x)
+#define ANA_ANAGEFIL_PID_VAL_GET(x)\
+	FIELD_GET(ANA_ANAGEFIL_PID_VAL, x)
+
+#define ANA_ANAGEFIL_VID_EN                      BIT(13)
+#define ANA_ANAGEFIL_VID_EN_SET(x)\
+	FIELD_PREP(ANA_ANAGEFIL_VID_EN, x)
+#define ANA_ANAGEFIL_VID_EN_GET(x)\
+	FIELD_GET(ANA_ANAGEFIL_VID_EN, x)
+
+#define ANA_ANAGEFIL_VID_VAL                     GENMASK(12, 0)
+#define ANA_ANAGEFIL_VID_VAL_SET(x)\
+	FIELD_PREP(ANA_ANAGEFIL_VID_VAL, x)
+#define ANA_ANAGEFIL_VID_VAL_GET(x)\
+	FIELD_GET(ANA_ANAGEFIL_VID_VAL, x)
+
+/*      ANA:ANA:AUTOAGE */
+#define ANA_AUTOAGE               __REG(TARGET_ANA,\
+					0, 1, 27136, 0, 1, 284, 44, 0, 1, 4)
+
+#define ANA_AUTOAGE_AGE_FAST                     BIT(21)
+#define ANA_AUTOAGE_AGE_FAST_SET(x)\
+	FIELD_PREP(ANA_AUTOAGE_AGE_FAST, x)
+#define ANA_AUTOAGE_AGE_FAST_GET(x)\
+	FIELD_GET(ANA_AUTOAGE_AGE_FAST, x)
+
+#define ANA_AUTOAGE_AGE_PERIOD                   GENMASK(20, 1)
+#define ANA_AUTOAGE_AGE_PERIOD_SET(x)\
+	FIELD_PREP(ANA_AUTOAGE_AGE_PERIOD, x)
+#define ANA_AUTOAGE_AGE_PERIOD_GET(x)\
+	FIELD_GET(ANA_AUTOAGE_AGE_PERIOD, x)
+
+#define ANA_AUTOAGE_AUTOAGE_LOCKED               BIT(0)
+#define ANA_AUTOAGE_AUTOAGE_LOCKED_SET(x)\
+	FIELD_PREP(ANA_AUTOAGE_AUTOAGE_LOCKED, x)
+#define ANA_AUTOAGE_AUTOAGE_LOCKED_GET(x)\
+	FIELD_GET(ANA_AUTOAGE_AUTOAGE_LOCKED, x)
+
+/*      ANA:ANA:FLOODING */
+#define ANA_FLOODING(r)           __REG(TARGET_ANA,\
+					0, 1, 27136, 0, 1, 284, 68, r, 8, 4)
+
+#define ANA_FLOODING_FLD_UNICAST                 GENMASK(17, 12)
+#define ANA_FLOODING_FLD_UNICAST_SET(x)\
+	FIELD_PREP(ANA_FLOODING_FLD_UNICAST, x)
+#define ANA_FLOODING_FLD_UNICAST_GET(x)\
+	FIELD_GET(ANA_FLOODING_FLD_UNICAST, x)
+
+#define ANA_FLOODING_FLD_BROADCAST               GENMASK(11, 6)
+#define ANA_FLOODING_FLD_BROADCAST_SET(x)\
+	FIELD_PREP(ANA_FLOODING_FLD_BROADCAST, x)
+#define ANA_FLOODING_FLD_BROADCAST_GET(x)\
+	FIELD_GET(ANA_FLOODING_FLD_BROADCAST, x)
+
+#define ANA_FLOODING_FLD_MULTICAST               GENMASK(5, 0)
+#define ANA_FLOODING_FLD_MULTICAST_SET(x)\
+	FIELD_PREP(ANA_FLOODING_FLD_MULTICAST, x)
+#define ANA_FLOODING_FLD_MULTICAST_GET(x)\
+	FIELD_GET(ANA_FLOODING_FLD_MULTICAST, x)
+
+/*      ANA:ANA:FLOODING_IPMC */
+#define ANA_FLOODING_IPMC         __REG(TARGET_ANA,\
+					0, 1, 27136, 0, 1, 284, 100, 0, 1, 4)
+
+#define ANA_FLOODING_IPMC_FLD_MC4_CTRL           GENMASK(23, 18)
+#define ANA_FLOODING_IPMC_FLD_MC4_CTRL_SET(x)\
+	FIELD_PREP(ANA_FLOODING_IPMC_FLD_MC4_CTRL, x)
+#define ANA_FLOODING_IPMC_FLD_MC4_CTRL_GET(x)\
+	FIELD_GET(ANA_FLOODING_IPMC_FLD_MC4_CTRL, x)
+
+#define ANA_FLOODING_IPMC_FLD_MC4_DATA           GENMASK(17, 12)
+#define ANA_FLOODING_IPMC_FLD_MC4_DATA_SET(x)\
+	FIELD_PREP(ANA_FLOODING_IPMC_FLD_MC4_DATA, x)
+#define ANA_FLOODING_IPMC_FLD_MC4_DATA_GET(x)\
+	FIELD_GET(ANA_FLOODING_IPMC_FLD_MC4_DATA, x)
+
+#define ANA_FLOODING_IPMC_FLD_MC6_CTRL           GENMASK(11, 6)
+#define ANA_FLOODING_IPMC_FLD_MC6_CTRL_SET(x)\
+	FIELD_PREP(ANA_FLOODING_IPMC_FLD_MC6_CTRL, x)
+#define ANA_FLOODING_IPMC_FLD_MC6_CTRL_GET(x)\
+	FIELD_GET(ANA_FLOODING_IPMC_FLD_MC6_CTRL, x)
+
+#define ANA_FLOODING_IPMC_FLD_MC6_DATA           GENMASK(5, 0)
+#define ANA_FLOODING_IPMC_FLD_MC6_DATA_SET(x)\
+	FIELD_PREP(ANA_FLOODING_IPMC_FLD_MC6_DATA, x)
+#define ANA_FLOODING_IPMC_FLD_MC6_DATA_GET(x)\
+	FIELD_GET(ANA_FLOODING_IPMC_FLD_MC6_DATA, x)
+
+/*      ANA:PGID:PGID */
+#define ANA_PGID(g)               __REG(TARGET_ANA,\
+					0, 1, 27648, g, 90, 8, 0, 0, 1, 4)
+
+#define ANA_PGID_PGID                            GENMASK(9, 0)
+#define ANA_PGID_PGID_SET(x)\
+	FIELD_PREP(ANA_PGID_PGID, x)
+#define ANA_PGID_PGID_GET(x)\
+	FIELD_GET(ANA_PGID_PGID, x)
+
+/*      ANA:PGID:PGID_CFG */
+#define ANA_PGID_CFG(g)           __REG(TARGET_ANA,\
+					0, 1, 27648, g, 90, 8, 4, 0, 1, 4)
+
+#define ANA_PGID_CFG_SAN_ENA                     BIT(4)
+#define ANA_PGID_CFG_SAN_ENA_SET(x)\
+	FIELD_PREP(ANA_PGID_CFG_SAN_ENA, x)
+#define ANA_PGID_CFG_SAN_ENA_GET(x)\
+	FIELD_GET(ANA_PGID_CFG_SAN_ENA, x)
+
+#define ANA_PGID_CFG_CPUQ_DST_PGID               GENMASK(3, 1)
+#define ANA_PGID_CFG_CPUQ_DST_PGID_SET(x)\
+	FIELD_PREP(ANA_PGID_CFG_CPUQ_DST_PGID, x)
+#define ANA_PGID_CFG_CPUQ_DST_PGID_GET(x)\
+	FIELD_GET(ANA_PGID_CFG_CPUQ_DST_PGID, x)
+
+#define ANA_PGID_CFG_OBEY_VLAN                   BIT(0)
+#define ANA_PGID_CFG_OBEY_VLAN_SET(x)\
+	FIELD_PREP(ANA_PGID_CFG_OBEY_VLAN, x)
+#define ANA_PGID_CFG_OBEY_VLAN_GET(x)\
+	FIELD_GET(ANA_PGID_CFG_OBEY_VLAN, x)
+
+/*      ANA:ANA_TABLES:MACHDATA */
+#define ANA_MACHDATA              __REG(TARGET_ANA,\
+					0, 1, 23680, 0, 1, 128, 44, 0, 1, 4)
+
+#define ANA_MACHDATA_VID                         GENMASK(28, 16)
+#define ANA_MACHDATA_VID_SET(x)\
+	FIELD_PREP(ANA_MACHDATA_VID, x)
+#define ANA_MACHDATA_VID_GET(x)\
+	FIELD_GET(ANA_MACHDATA_VID, x)
+
+#define ANA_MACHDATA_MACHDATA                    GENMASK(15, 0)
+#define ANA_MACHDATA_MACHDATA_SET(x)\
+	FIELD_PREP(ANA_MACHDATA_MACHDATA, x)
+#define ANA_MACHDATA_MACHDATA_GET(x)\
+	FIELD_GET(ANA_MACHDATA_MACHDATA, x)
+
+/*      ANA:ANA_TABLES:MACLDATA */
+#define ANA_MACLDATA              __REG(TARGET_ANA,\
+					0, 1, 23680, 0, 1, 128, 48, 0, 1, 4)
+
+/*      ANA:ANA_TABLES:MACACCESS */
+#define ANA_MACACCESS             __REG(TARGET_ANA,\
+					0, 1, 23680, 0, 1, 128, 52, 0, 1, 4)
+
+#define ANA_MACACCESS_CHANGE2SW                  BIT(17)
+#define ANA_MACACCESS_CHANGE2SW_SET(x)\
+	FIELD_PREP(ANA_MACACCESS_CHANGE2SW, x)
+#define ANA_MACACCESS_CHANGE2SW_GET(x)\
+	FIELD_GET(ANA_MACACCESS_CHANGE2SW, x)
+
+#define ANA_MACACCESS_MAC_CPU_COPY               BIT(16)
+#define ANA_MACACCESS_MAC_CPU_COPY_SET(x)\
+	FIELD_PREP(ANA_MACACCESS_MAC_CPU_COPY, x)
+#define ANA_MACACCESS_MAC_CPU_COPY_GET(x)\
+	FIELD_GET(ANA_MACACCESS_MAC_CPU_COPY, x)
+
+#define ANA_MACACCESS_SRC_KILL                   BIT(15)
+#define ANA_MACACCESS_SRC_KILL_SET(x)\
+	FIELD_PREP(ANA_MACACCESS_SRC_KILL, x)
+#define ANA_MACACCESS_SRC_KILL_GET(x)\
+	FIELD_GET(ANA_MACACCESS_SRC_KILL, x)
+
+#define ANA_MACACCESS_IGNORE_VLAN                BIT(14)
+#define ANA_MACACCESS_IGNORE_VLAN_SET(x)\
+	FIELD_PREP(ANA_MACACCESS_IGNORE_VLAN, x)
+#define ANA_MACACCESS_IGNORE_VLAN_GET(x)\
+	FIELD_GET(ANA_MACACCESS_IGNORE_VLAN, x)
+
+#define ANA_MACACCESS_AGED_FLAG                  BIT(13)
+#define ANA_MACACCESS_AGED_FLAG_SET(x)\
+	FIELD_PREP(ANA_MACACCESS_AGED_FLAG, x)
+#define ANA_MACACCESS_AGED_FLAG_GET(x)\
+	FIELD_GET(ANA_MACACCESS_AGED_FLAG, x)
+
+#define ANA_MACACCESS_VALID                      BIT(12)
+#define ANA_MACACCESS_VALID_SET(x)\
+	FIELD_PREP(ANA_MACACCESS_VALID, x)
+#define ANA_MACACCESS_VALID_GET(x)\
+	FIELD_GET(ANA_MACACCESS_VALID, x)
+
+#define ANA_MACACCESS_ENTRYTYPE                  GENMASK(11, 10)
+#define ANA_MACACCESS_ENTRYTYPE_SET(x)\
+	FIELD_PREP(ANA_MACACCESS_ENTRYTYPE, x)
+#define ANA_MACACCESS_ENTRYTYPE_GET(x)\
+	FIELD_GET(ANA_MACACCESS_ENTRYTYPE, x)
+
+#define ANA_MACACCESS_DEST_IDX                   GENMASK(9, 4)
+#define ANA_MACACCESS_DEST_IDX_SET(x)\
+	FIELD_PREP(ANA_MACACCESS_DEST_IDX, x)
+#define ANA_MACACCESS_DEST_IDX_GET(x)\
+	FIELD_GET(ANA_MACACCESS_DEST_IDX, x)
+
+#define ANA_MACACCESS_MAC_TABLE_CMD              GENMASK(3, 0)
+#define ANA_MACACCESS_MAC_TABLE_CMD_SET(x)\
+	FIELD_PREP(ANA_MACACCESS_MAC_TABLE_CMD, x)
+#define ANA_MACACCESS_MAC_TABLE_CMD_GET(x)\
+	FIELD_GET(ANA_MACACCESS_MAC_TABLE_CMD, x)
+
+/*      ANA:ANA_TABLES:MACTINDX */
+#define ANA_MACTINDX              __REG(TARGET_ANA,\
+					0, 1, 23680, 0, 1, 128, 56, 0, 1, 4)
+
+#define ANA_MACTINDX_BUCKET                      GENMASK(12, 11)
+#define ANA_MACTINDX_BUCKET_SET(x)\
+	FIELD_PREP(ANA_MACTINDX_BUCKET, x)
+#define ANA_MACTINDX_BUCKET_GET(x)\
+	FIELD_GET(ANA_MACTINDX_BUCKET, x)
+
+#define ANA_MACTINDX_M_INDEX                     GENMASK(10, 0)
+#define ANA_MACTINDX_M_INDEX_SET(x)\
+	FIELD_PREP(ANA_MACTINDX_M_INDEX, x)
+#define ANA_MACTINDX_M_INDEX_GET(x)\
+	FIELD_GET(ANA_MACTINDX_M_INDEX, x)
+
+/*      ANA:ANA_TABLES:VLAN_PORT_MASK */
+#define ANA_VLAN_PORT_MASK        __REG(TARGET_ANA,\
+					0, 1, 23680, 0, 1, 128, 60, 0, 1, 4)
+
+#define ANA_VLAN_PORT_MASK_VLAN_PORT_MASK        GENMASK(9, 0)
+#define ANA_VLAN_PORT_MASK_VLAN_PORT_MASK_SET(x)\
+	FIELD_PREP(ANA_VLAN_PORT_MASK_VLAN_PORT_MASK, x)
+#define ANA_VLAN_PORT_MASK_VLAN_PORT_MASK_GET(x)\
+	FIELD_GET(ANA_VLAN_PORT_MASK_VLAN_PORT_MASK, x)
+
+/*      ANA:ANA_TABLES:VLANACCESS */
+#define ANA_VLANACCESS            __REG(TARGET_ANA,\
+					0, 1, 23680, 0, 1, 128, 64, 0, 1, 4)
+
+#define ANA_VLANACCESS_VLAN_TBL_CMD              GENMASK(1, 0)
+#define ANA_VLANACCESS_VLAN_TBL_CMD_SET(x)\
+	FIELD_PREP(ANA_VLANACCESS_VLAN_TBL_CMD, x)
+#define ANA_VLANACCESS_VLAN_TBL_CMD_GET(x)\
+	FIELD_GET(ANA_VLANACCESS_VLAN_TBL_CMD, x)
+
+/*      ANA:ANA_TABLES:VLANTIDX */
+#define ANA_VLANTIDX              __REG(TARGET_ANA,\
+					0, 1, 23680, 0, 1, 128, 68, 0, 1, 4)
+
+#define ANA_VLANTIDX_VLAN_PGID_CPU_DIS           BIT(18)
+#define ANA_VLANTIDX_VLAN_PGID_CPU_DIS_SET(x)\
+	FIELD_PREP(ANA_VLANTIDX_VLAN_PGID_CPU_DIS, x)
+#define ANA_VLANTIDX_VLAN_PGID_CPU_DIS_GET(x)\
+	FIELD_GET(ANA_VLANTIDX_VLAN_PGID_CPU_DIS, x)
+
+#define ANA_VLANTIDX_VLAN_SEC_FWD_ENA            BIT(17)
+#define ANA_VLANTIDX_VLAN_SEC_FWD_ENA_SET(x)\
+	FIELD_PREP(ANA_VLANTIDX_VLAN_SEC_FWD_ENA, x)
+#define ANA_VLANTIDX_VLAN_SEC_FWD_ENA_GET(x)\
+	FIELD_GET(ANA_VLANTIDX_VLAN_SEC_FWD_ENA, x)
+
+#define ANA_VLANTIDX_VLAN_FLOOD_DIS              BIT(16)
+#define ANA_VLANTIDX_VLAN_FLOOD_DIS_SET(x)\
+	FIELD_PREP(ANA_VLANTIDX_VLAN_FLOOD_DIS, x)
+#define ANA_VLANTIDX_VLAN_FLOOD_DIS_GET(x)\
+	FIELD_GET(ANA_VLANTIDX_VLAN_FLOOD_DIS, x)
+
+#define ANA_VLANTIDX_VLAN_PRIV_VLAN              BIT(15)
+#define ANA_VLANTIDX_VLAN_PRIV_VLAN_SET(x)\
+	FIELD_PREP(ANA_VLANTIDX_VLAN_PRIV_VLAN, x)
+#define ANA_VLANTIDX_VLAN_PRIV_VLAN_GET(x)\
+	FIELD_GET(ANA_VLANTIDX_VLAN_PRIV_VLAN, x)
+
+#define ANA_VLANTIDX_VLAN_LEARN_DISABLED         BIT(14)
+#define ANA_VLANTIDX_VLAN_LEARN_DISABLED_SET(x)\
+	FIELD_PREP(ANA_VLANTIDX_VLAN_LEARN_DISABLED, x)
+#define ANA_VLANTIDX_VLAN_LEARN_DISABLED_GET(x)\
+	FIELD_GET(ANA_VLANTIDX_VLAN_LEARN_DISABLED, x)
+
+#define ANA_VLANTIDX_VLAN_MIRROR                 BIT(13)
+#define ANA_VLANTIDX_VLAN_MIRROR_SET(x)\
+	FIELD_PREP(ANA_VLANTIDX_VLAN_MIRROR, x)
+#define ANA_VLANTIDX_VLAN_MIRROR_GET(x)\
+	FIELD_GET(ANA_VLANTIDX_VLAN_MIRROR, x)
+
+#define ANA_VLANTIDX_VLAN_SRC_CHK                BIT(12)
+#define ANA_VLANTIDX_VLAN_SRC_CHK_SET(x)\
+	FIELD_PREP(ANA_VLANTIDX_VLAN_SRC_CHK, x)
+#define ANA_VLANTIDX_VLAN_SRC_CHK_GET(x)\
+	FIELD_GET(ANA_VLANTIDX_VLAN_SRC_CHK, x)
+
+#define ANA_VLANTIDX_V_INDEX                     GENMASK(11, 0)
+#define ANA_VLANTIDX_V_INDEX_SET(x)\
+	FIELD_PREP(ANA_VLANTIDX_V_INDEX, x)
+#define ANA_VLANTIDX_V_INDEX_GET(x)\
+	FIELD_GET(ANA_VLANTIDX_V_INDEX, x)
+
+/*      ANA:PORT:VLAN_CFG */
+#define ANA_VLAN_CFG(g)           __REG(TARGET_ANA,\
+					0, 1, 24576, g, 10, 256, 0, 0, 1, 4)
+
+#define ANA_VLAN_CFG_VLAN_PFC_ENA                BIT(21)
+#define ANA_VLAN_CFG_VLAN_PFC_ENA_SET(x)\
+	FIELD_PREP(ANA_VLAN_CFG_VLAN_PFC_ENA, x)
+#define ANA_VLAN_CFG_VLAN_PFC_ENA_GET(x)\
+	FIELD_GET(ANA_VLAN_CFG_VLAN_PFC_ENA, x)
+
+#define ANA_VLAN_CFG_VLAN_AWARE_ENA              BIT(20)
+#define ANA_VLAN_CFG_VLAN_AWARE_ENA_SET(x)\
+	FIELD_PREP(ANA_VLAN_CFG_VLAN_AWARE_ENA, x)
+#define ANA_VLAN_CFG_VLAN_AWARE_ENA_GET(x)\
+	FIELD_GET(ANA_VLAN_CFG_VLAN_AWARE_ENA, x)
+
+#define ANA_VLAN_CFG_VLAN_POP_CNT                GENMASK(19, 18)
+#define ANA_VLAN_CFG_VLAN_POP_CNT_SET(x)\
+	FIELD_PREP(ANA_VLAN_CFG_VLAN_POP_CNT, x)
+#define ANA_VLAN_CFG_VLAN_POP_CNT_GET(x)\
+	FIELD_GET(ANA_VLAN_CFG_VLAN_POP_CNT, x)
+
+#define ANA_VLAN_CFG_VLAN_INNER_TAG_ENA          BIT(17)
+#define ANA_VLAN_CFG_VLAN_INNER_TAG_ENA_SET(x)\
+	FIELD_PREP(ANA_VLAN_CFG_VLAN_INNER_TAG_ENA, x)
+#define ANA_VLAN_CFG_VLAN_INNER_TAG_ENA_GET(x)\
+	FIELD_GET(ANA_VLAN_CFG_VLAN_INNER_TAG_ENA, x)
+
+#define ANA_VLAN_CFG_VLAN_TAG_TYPE               BIT(16)
+#define ANA_VLAN_CFG_VLAN_TAG_TYPE_SET(x)\
+	FIELD_PREP(ANA_VLAN_CFG_VLAN_TAG_TYPE, x)
+#define ANA_VLAN_CFG_VLAN_TAG_TYPE_GET(x)\
+	FIELD_GET(ANA_VLAN_CFG_VLAN_TAG_TYPE, x)
+
+#define ANA_VLAN_CFG_VLAN_PCP                    GENMASK(15, 13)
+#define ANA_VLAN_CFG_VLAN_PCP_SET(x)\
+	FIELD_PREP(ANA_VLAN_CFG_VLAN_PCP, x)
+#define ANA_VLAN_CFG_VLAN_PCP_GET(x)\
+	FIELD_GET(ANA_VLAN_CFG_VLAN_PCP, x)
+
+#define ANA_VLAN_CFG_VLAN_DEI                    BIT(12)
+#define ANA_VLAN_CFG_VLAN_DEI_SET(x)\
+	FIELD_PREP(ANA_VLAN_CFG_VLAN_DEI, x)
+#define ANA_VLAN_CFG_VLAN_DEI_GET(x)\
+	FIELD_GET(ANA_VLAN_CFG_VLAN_DEI, x)
+
+#define ANA_VLAN_CFG_VLAN_VID                    GENMASK(11, 0)
+#define ANA_VLAN_CFG_VLAN_VID_SET(x)\
+	FIELD_PREP(ANA_VLAN_CFG_VLAN_VID, x)
+#define ANA_VLAN_CFG_VLAN_VID_GET(x)\
+	FIELD_GET(ANA_VLAN_CFG_VLAN_VID, x)
+
+/*      ANA:PORT:DROP_CFG */
+#define ANA_DROP_CFG(g)           __REG(TARGET_ANA,\
+					0, 1, 24576, g, 10, 256, 4, 0, 1, 4)
+
+#define ANA_DROP_CFG_DROP_UNTAGGED_ENA           BIT(6)
+#define ANA_DROP_CFG_DROP_UNTAGGED_ENA_SET(x)\
+	FIELD_PREP(ANA_DROP_CFG_DROP_UNTAGGED_ENA, x)
+#define ANA_DROP_CFG_DROP_UNTAGGED_ENA_GET(x)\
+	FIELD_GET(ANA_DROP_CFG_DROP_UNTAGGED_ENA, x)
+
+#define ANA_DROP_CFG_DROP_S_TAGGED_ENA           BIT(5)
+#define ANA_DROP_CFG_DROP_S_TAGGED_ENA_SET(x)\
+	FIELD_PREP(ANA_DROP_CFG_DROP_S_TAGGED_ENA, x)
+#define ANA_DROP_CFG_DROP_S_TAGGED_ENA_GET(x)\
+	FIELD_GET(ANA_DROP_CFG_DROP_S_TAGGED_ENA, x)
+
+#define ANA_DROP_CFG_DROP_C_TAGGED_ENA           BIT(4)
+#define ANA_DROP_CFG_DROP_C_TAGGED_ENA_SET(x)\
+	FIELD_PREP(ANA_DROP_CFG_DROP_C_TAGGED_ENA, x)
+#define ANA_DROP_CFG_DROP_C_TAGGED_ENA_GET(x)\
+	FIELD_GET(ANA_DROP_CFG_DROP_C_TAGGED_ENA, x)
+
+#define ANA_DROP_CFG_DROP_PRIO_S_TAGGED_ENA      BIT(3)
+#define ANA_DROP_CFG_DROP_PRIO_S_TAGGED_ENA_SET(x)\
+	FIELD_PREP(ANA_DROP_CFG_DROP_PRIO_S_TAGGED_ENA, x)
+#define ANA_DROP_CFG_DROP_PRIO_S_TAGGED_ENA_GET(x)\
+	FIELD_GET(ANA_DROP_CFG_DROP_PRIO_S_TAGGED_ENA, x)
+
+#define ANA_DROP_CFG_DROP_PRIO_C_TAGGED_ENA      BIT(2)
+#define ANA_DROP_CFG_DROP_PRIO_C_TAGGED_ENA_SET(x)\
+	FIELD_PREP(ANA_DROP_CFG_DROP_PRIO_C_TAGGED_ENA, x)
+#define ANA_DROP_CFG_DROP_PRIO_C_TAGGED_ENA_GET(x)\
+	FIELD_GET(ANA_DROP_CFG_DROP_PRIO_C_TAGGED_ENA, x)
+
+#define ANA_DROP_CFG_DROP_NULL_MAC_ENA           BIT(1)
+#define ANA_DROP_CFG_DROP_NULL_MAC_ENA_SET(x)\
+	FIELD_PREP(ANA_DROP_CFG_DROP_NULL_MAC_ENA, x)
+#define ANA_DROP_CFG_DROP_NULL_MAC_ENA_GET(x)\
+	FIELD_GET(ANA_DROP_CFG_DROP_NULL_MAC_ENA, x)
+
+#define ANA_DROP_CFG_DROP_MC_SMAC_ENA            BIT(0)
+#define ANA_DROP_CFG_DROP_MC_SMAC_ENA_SET(x)\
+	FIELD_PREP(ANA_DROP_CFG_DROP_MC_SMAC_ENA, x)
+#define ANA_DROP_CFG_DROP_MC_SMAC_ENA_GET(x)\
+	FIELD_GET(ANA_DROP_CFG_DROP_MC_SMAC_ENA, x)
+
+/*      ANA:PORT:CPU_FWD_CFG */
+#define ANA_CPU_FWD_CFG(g)        __REG(TARGET_ANA,\
+					0, 1, 24576, g, 10, 256, 96, 0, 1, 4)
+
+#define ANA_CPU_FWD_CFG_NO_HSR_REDIR_ENA         BIT(9)
+#define ANA_CPU_FWD_CFG_NO_HSR_REDIR_ENA_SET(x)\
+	FIELD_PREP(ANA_CPU_FWD_CFG_NO_HSR_REDIR_ENA, x)
+#define ANA_CPU_FWD_CFG_NO_HSR_REDIR_ENA_GET(x)\
+	FIELD_GET(ANA_CPU_FWD_CFG_NO_HSR_REDIR_ENA, x)
+
+#define ANA_CPU_FWD_CFG_SPV_COPY_ENA             BIT(8)
+#define ANA_CPU_FWD_CFG_SPV_COPY_ENA_SET(x)\
+	FIELD_PREP(ANA_CPU_FWD_CFG_SPV_COPY_ENA, x)
+#define ANA_CPU_FWD_CFG_SPV_COPY_ENA_GET(x)\
+	FIELD_GET(ANA_CPU_FWD_CFG_SPV_COPY_ENA, x)
+
+#define ANA_CPU_FWD_CFG_VRAP_REDIR_ENA           BIT(7)
+#define ANA_CPU_FWD_CFG_VRAP_REDIR_ENA_SET(x)\
+	FIELD_PREP(ANA_CPU_FWD_CFG_VRAP_REDIR_ENA, x)
+#define ANA_CPU_FWD_CFG_VRAP_REDIR_ENA_GET(x)\
+	FIELD_GET(ANA_CPU_FWD_CFG_VRAP_REDIR_ENA, x)
+
+#define ANA_CPU_FWD_CFG_MLD_REDIR_ENA            BIT(6)
+#define ANA_CPU_FWD_CFG_MLD_REDIR_ENA_SET(x)\
+	FIELD_PREP(ANA_CPU_FWD_CFG_MLD_REDIR_ENA, x)
+#define ANA_CPU_FWD_CFG_MLD_REDIR_ENA_GET(x)\
+	FIELD_GET(ANA_CPU_FWD_CFG_MLD_REDIR_ENA, x)
+
+#define ANA_CPU_FWD_CFG_IGMP_REDIR_ENA           BIT(5)
+#define ANA_CPU_FWD_CFG_IGMP_REDIR_ENA_SET(x)\
+	FIELD_PREP(ANA_CPU_FWD_CFG_IGMP_REDIR_ENA, x)
+#define ANA_CPU_FWD_CFG_IGMP_REDIR_ENA_GET(x)\
+	FIELD_GET(ANA_CPU_FWD_CFG_IGMP_REDIR_ENA, x)
+
+#define ANA_CPU_FWD_CFG_IPMC_CTRL_COPY_ENA       BIT(4)
+#define ANA_CPU_FWD_CFG_IPMC_CTRL_COPY_ENA_SET(x)\
+	FIELD_PREP(ANA_CPU_FWD_CFG_IPMC_CTRL_COPY_ENA, x)
+#define ANA_CPU_FWD_CFG_IPMC_CTRL_COPY_ENA_GET(x)\
+	FIELD_GET(ANA_CPU_FWD_CFG_IPMC_CTRL_COPY_ENA, x)
+
+#define ANA_CPU_FWD_CFG_SRC_COPY_ENA             BIT(3)
+#define ANA_CPU_FWD_CFG_SRC_COPY_ENA_SET(x)\
+	FIELD_PREP(ANA_CPU_FWD_CFG_SRC_COPY_ENA, x)
+#define ANA_CPU_FWD_CFG_SRC_COPY_ENA_GET(x)\
+	FIELD_GET(ANA_CPU_FWD_CFG_SRC_COPY_ENA, x)
+
+#define ANA_CPU_FWD_CFG_ALLBRIDGE_DROP_ENA       BIT(2)
+#define ANA_CPU_FWD_CFG_ALLBRIDGE_DROP_ENA_SET(x)\
+	FIELD_PREP(ANA_CPU_FWD_CFG_ALLBRIDGE_DROP_ENA, x)
+#define ANA_CPU_FWD_CFG_ALLBRIDGE_DROP_ENA_GET(x)\
+	FIELD_GET(ANA_CPU_FWD_CFG_ALLBRIDGE_DROP_ENA, x)
+
+#define ANA_CPU_FWD_CFG_ALLBRIDGE_REDIR_ENA      BIT(1)
+#define ANA_CPU_FWD_CFG_ALLBRIDGE_REDIR_ENA_SET(x)\
+	FIELD_PREP(ANA_CPU_FWD_CFG_ALLBRIDGE_REDIR_ENA, x)
+#define ANA_CPU_FWD_CFG_ALLBRIDGE_REDIR_ENA_GET(x)\
+	FIELD_GET(ANA_CPU_FWD_CFG_ALLBRIDGE_REDIR_ENA, x)
+
+#define ANA_CPU_FWD_CFG_OAM_ENA                  BIT(0)
+#define ANA_CPU_FWD_CFG_OAM_ENA_SET(x)\
+	FIELD_PREP(ANA_CPU_FWD_CFG_OAM_ENA, x)
+#define ANA_CPU_FWD_CFG_OAM_ENA_GET(x)\
+	FIELD_GET(ANA_CPU_FWD_CFG_OAM_ENA, x)
+
+/*      ANA:PORT:CPU_FWD_BPDU_CFG */
+#define ANA_CPU_FWD_BPDU_CFG(g)   __REG(TARGET_ANA,\
+					0, 1, 24576, g, 10, 256, 100, 0, 1, 4)
+
+#define ANA_CPU_FWD_BPDU_CFG_BPDU_DROP_ENA       GENMASK(31, 16)
+#define ANA_CPU_FWD_BPDU_CFG_BPDU_DROP_ENA_SET(x)\
+	FIELD_PREP(ANA_CPU_FWD_BPDU_CFG_BPDU_DROP_ENA, x)
+#define ANA_CPU_FWD_BPDU_CFG_BPDU_DROP_ENA_GET(x)\
+	FIELD_GET(ANA_CPU_FWD_BPDU_CFG_BPDU_DROP_ENA, x)
+
+#define ANA_CPU_FWD_BPDU_CFG_BPDU_REDIR_ENA      GENMASK(15, 0)
+#define ANA_CPU_FWD_BPDU_CFG_BPDU_REDIR_ENA_SET(x)\
+	FIELD_PREP(ANA_CPU_FWD_BPDU_CFG_BPDU_REDIR_ENA, x)
+#define ANA_CPU_FWD_BPDU_CFG_BPDU_REDIR_ENA_GET(x)\
+	FIELD_GET(ANA_CPU_FWD_BPDU_CFG_BPDU_REDIR_ENA, x)
+
+/*      ANA:PORT:PORT_CFG */
+#define ANA_PORT_CFG(g)           __REG(TARGET_ANA,\
+					0, 1, 24576, g, 10, 256, 112, 0, 1, 4)
+
+#define ANA_PORT_CFG_SRC_MIRROR_ENA              BIT(13)
+#define ANA_PORT_CFG_SRC_MIRROR_ENA_SET(x)\
+	FIELD_PREP(ANA_PORT_CFG_SRC_MIRROR_ENA, x)
+#define ANA_PORT_CFG_SRC_MIRROR_ENA_GET(x)\
+	FIELD_GET(ANA_PORT_CFG_SRC_MIRROR_ENA, x)
+
+#define ANA_PORT_CFG_LIMIT_DROP                  BIT(12)
+#define ANA_PORT_CFG_LIMIT_DROP_SET(x)\
+	FIELD_PREP(ANA_PORT_CFG_LIMIT_DROP, x)
+#define ANA_PORT_CFG_LIMIT_DROP_GET(x)\
+	FIELD_GET(ANA_PORT_CFG_LIMIT_DROP, x)
+
+#define ANA_PORT_CFG_LIMIT_CPU                   BIT(11)
+#define ANA_PORT_CFG_LIMIT_CPU_SET(x)\
+	FIELD_PREP(ANA_PORT_CFG_LIMIT_CPU, x)
+#define ANA_PORT_CFG_LIMIT_CPU_GET(x)\
+	FIELD_GET(ANA_PORT_CFG_LIMIT_CPU, x)
+
+#define ANA_PORT_CFG_LOCKED_PORTMOVE_DROP        BIT(10)
+#define ANA_PORT_CFG_LOCKED_PORTMOVE_DROP_SET(x)\
+	FIELD_PREP(ANA_PORT_CFG_LOCKED_PORTMOVE_DROP, x)
+#define ANA_PORT_CFG_LOCKED_PORTMOVE_DROP_GET(x)\
+	FIELD_GET(ANA_PORT_CFG_LOCKED_PORTMOVE_DROP, x)
+
+#define ANA_PORT_CFG_LOCKED_PORTMOVE_CPU         BIT(9)
+#define ANA_PORT_CFG_LOCKED_PORTMOVE_CPU_SET(x)\
+	FIELD_PREP(ANA_PORT_CFG_LOCKED_PORTMOVE_CPU, x)
+#define ANA_PORT_CFG_LOCKED_PORTMOVE_CPU_GET(x)\
+	FIELD_GET(ANA_PORT_CFG_LOCKED_PORTMOVE_CPU, x)
+
+#define ANA_PORT_CFG_LEARNDROP                   BIT(8)
+#define ANA_PORT_CFG_LEARNDROP_SET(x)\
+	FIELD_PREP(ANA_PORT_CFG_LEARNDROP, x)
+#define ANA_PORT_CFG_LEARNDROP_GET(x)\
+	FIELD_GET(ANA_PORT_CFG_LEARNDROP, x)
+
+#define ANA_PORT_CFG_LEARNCPU                    BIT(7)
+#define ANA_PORT_CFG_LEARNCPU_SET(x)\
+	FIELD_PREP(ANA_PORT_CFG_LEARNCPU, x)
+#define ANA_PORT_CFG_LEARNCPU_GET(x)\
+	FIELD_GET(ANA_PORT_CFG_LEARNCPU, x)
+
+#define ANA_PORT_CFG_LEARNAUTO                   BIT(6)
+#define ANA_PORT_CFG_LEARNAUTO_SET(x)\
+	FIELD_PREP(ANA_PORT_CFG_LEARNAUTO, x)
+#define ANA_PORT_CFG_LEARNAUTO_GET(x)\
+	FIELD_GET(ANA_PORT_CFG_LEARNAUTO, x)
+
+#define ANA_PORT_CFG_LEARN_ENA                   BIT(5)
+#define ANA_PORT_CFG_LEARN_ENA_SET(x)\
+	FIELD_PREP(ANA_PORT_CFG_LEARN_ENA, x)
+#define ANA_PORT_CFG_LEARN_ENA_GET(x)\
+	FIELD_GET(ANA_PORT_CFG_LEARN_ENA, x)
+
+#define ANA_PORT_CFG_RECV_ENA                    BIT(4)
+#define ANA_PORT_CFG_RECV_ENA_SET(x)\
+	FIELD_PREP(ANA_PORT_CFG_RECV_ENA, x)
+#define ANA_PORT_CFG_RECV_ENA_GET(x)\
+	FIELD_GET(ANA_PORT_CFG_RECV_ENA, x)
+
+#define ANA_PORT_CFG_PORTID_VAL                  GENMASK(3, 0)
+#define ANA_PORT_CFG_PORTID_VAL_SET(x)\
+	FIELD_PREP(ANA_PORT_CFG_PORTID_VAL, x)
+#define ANA_PORT_CFG_PORTID_VAL_GET(x)\
+	FIELD_GET(ANA_PORT_CFG_PORTID_VAL, x)
+
+/*      ANA:PFC:PFC_CFG */
+#define ANA_PFC_CFG(g)            __REG(TARGET_ANA,\
+					0, 1, 28672, g, 9, 64, 0, 0, 1, 4)
+
+#define ANA_PFC_CFG_RX_PFC_ENA                   GENMASK(9, 2)
+#define ANA_PFC_CFG_RX_PFC_ENA_SET(x)\
+	FIELD_PREP(ANA_PFC_CFG_RX_PFC_ENA, x)
+#define ANA_PFC_CFG_RX_PFC_ENA_GET(x)\
+	FIELD_GET(ANA_PFC_CFG_RX_PFC_ENA, x)
+
+#define ANA_PFC_CFG_FC_LINK_SPEED                GENMASK(1, 0)
+#define ANA_PFC_CFG_FC_LINK_SPEED_SET(x)\
+	FIELD_PREP(ANA_PFC_CFG_FC_LINK_SPEED, x)
+#define ANA_PFC_CFG_FC_LINK_SPEED_GET(x)\
+	FIELD_GET(ANA_PFC_CFG_FC_LINK_SPEED, x)
+
+/*      ANA:COMMON:AGGR_CFG */
+#define ANA_AGGR_CFG              __REG(TARGET_ANA,\
+					0, 1, 29248, 0, 1, 552, 0, 0, 1, 4)
+
+#define ANA_AGGR_CFG_AC_RND_ENA                  BIT(6)
+#define ANA_AGGR_CFG_AC_RND_ENA_SET(x)\
+	FIELD_PREP(ANA_AGGR_CFG_AC_RND_ENA, x)
+#define ANA_AGGR_CFG_AC_RND_ENA_GET(x)\
+	FIELD_GET(ANA_AGGR_CFG_AC_RND_ENA, x)
+
+#define ANA_AGGR_CFG_AC_DMAC_ENA                 BIT(5)
+#define ANA_AGGR_CFG_AC_DMAC_ENA_SET(x)\
+	FIELD_PREP(ANA_AGGR_CFG_AC_DMAC_ENA, x)
+#define ANA_AGGR_CFG_AC_DMAC_ENA_GET(x)\
+	FIELD_GET(ANA_AGGR_CFG_AC_DMAC_ENA, x)
+
+#define ANA_AGGR_CFG_AC_SMAC_ENA                 BIT(4)
+#define ANA_AGGR_CFG_AC_SMAC_ENA_SET(x)\
+	FIELD_PREP(ANA_AGGR_CFG_AC_SMAC_ENA, x)
+#define ANA_AGGR_CFG_AC_SMAC_ENA_GET(x)\
+	FIELD_GET(ANA_AGGR_CFG_AC_SMAC_ENA, x)
+
+#define ANA_AGGR_CFG_AC_IP6_FLOW_LBL_ENA         BIT(3)
+#define ANA_AGGR_CFG_AC_IP6_FLOW_LBL_ENA_SET(x)\
+	FIELD_PREP(ANA_AGGR_CFG_AC_IP6_FLOW_LBL_ENA, x)
+#define ANA_AGGR_CFG_AC_IP6_FLOW_LBL_ENA_GET(x)\
+	FIELD_GET(ANA_AGGR_CFG_AC_IP6_FLOW_LBL_ENA, x)
+
+#define ANA_AGGR_CFG_AC_IP6_TCPUDP_ENA           BIT(2)
+#define ANA_AGGR_CFG_AC_IP6_TCPUDP_ENA_SET(x)\
+	FIELD_PREP(ANA_AGGR_CFG_AC_IP6_TCPUDP_ENA, x)
+#define ANA_AGGR_CFG_AC_IP6_TCPUDP_ENA_GET(x)\
+	FIELD_GET(ANA_AGGR_CFG_AC_IP6_TCPUDP_ENA, x)
+
+#define ANA_AGGR_CFG_AC_IP4_SIPDIP_ENA           BIT(1)
+#define ANA_AGGR_CFG_AC_IP4_SIPDIP_ENA_SET(x)\
+	FIELD_PREP(ANA_AGGR_CFG_AC_IP4_SIPDIP_ENA, x)
+#define ANA_AGGR_CFG_AC_IP4_SIPDIP_ENA_GET(x)\
+	FIELD_GET(ANA_AGGR_CFG_AC_IP4_SIPDIP_ENA, x)
+
+#define ANA_AGGR_CFG_AC_IP4_TCPUDP_ENA           BIT(0)
+#define ANA_AGGR_CFG_AC_IP4_TCPUDP_ENA_SET(x)\
+	FIELD_PREP(ANA_AGGR_CFG_AC_IP4_TCPUDP_ENA, x)
+#define ANA_AGGR_CFG_AC_IP4_TCPUDP_ENA_GET(x)\
+	FIELD_GET(ANA_AGGR_CFG_AC_IP4_TCPUDP_ENA, x)
+
+/*      ANA:COMMON:CPUQ_CFG */
+#define ANA_CPUQ_CFG              __REG(TARGET_ANA,\
+					0, 1, 29248, 0, 1, 552, 4, 0, 1, 4)
+
+#define ANA_CPUQ_CFG_CPUQ_MLD                    GENMASK(29, 27)
+#define ANA_CPUQ_CFG_CPUQ_MLD_SET(x)\
+	FIELD_PREP(ANA_CPUQ_CFG_CPUQ_MLD, x)
+#define ANA_CPUQ_CFG_CPUQ_MLD_GET(x)\
+	FIELD_GET(ANA_CPUQ_CFG_CPUQ_MLD, x)
+
+#define ANA_CPUQ_CFG_CPUQ_IGMP                   GENMASK(26, 24)
+#define ANA_CPUQ_CFG_CPUQ_IGMP_SET(x)\
+	FIELD_PREP(ANA_CPUQ_CFG_CPUQ_IGMP, x)
+#define ANA_CPUQ_CFG_CPUQ_IGMP_GET(x)\
+	FIELD_GET(ANA_CPUQ_CFG_CPUQ_IGMP, x)
+
+#define ANA_CPUQ_CFG_CPUQ_IPMC_CTRL              GENMASK(23, 21)
+#define ANA_CPUQ_CFG_CPUQ_IPMC_CTRL_SET(x)\
+	FIELD_PREP(ANA_CPUQ_CFG_CPUQ_IPMC_CTRL, x)
+#define ANA_CPUQ_CFG_CPUQ_IPMC_CTRL_GET(x)\
+	FIELD_GET(ANA_CPUQ_CFG_CPUQ_IPMC_CTRL, x)
+
+#define ANA_CPUQ_CFG_CPUQ_ALLBRIDGE              GENMASK(20, 18)
+#define ANA_CPUQ_CFG_CPUQ_ALLBRIDGE_SET(x)\
+	FIELD_PREP(ANA_CPUQ_CFG_CPUQ_ALLBRIDGE, x)
+#define ANA_CPUQ_CFG_CPUQ_ALLBRIDGE_GET(x)\
+	FIELD_GET(ANA_CPUQ_CFG_CPUQ_ALLBRIDGE, x)
+
+#define ANA_CPUQ_CFG_CPUQ_LOCKED_PORTMOVE        GENMASK(17, 15)
+#define ANA_CPUQ_CFG_CPUQ_LOCKED_PORTMOVE_SET(x)\
+	FIELD_PREP(ANA_CPUQ_CFG_CPUQ_LOCKED_PORTMOVE, x)
+#define ANA_CPUQ_CFG_CPUQ_LOCKED_PORTMOVE_GET(x)\
+	FIELD_GET(ANA_CPUQ_CFG_CPUQ_LOCKED_PORTMOVE, x)
+
+#define ANA_CPUQ_CFG_CPUQ_SRC_COPY               GENMASK(14, 12)
+#define ANA_CPUQ_CFG_CPUQ_SRC_COPY_SET(x)\
+	FIELD_PREP(ANA_CPUQ_CFG_CPUQ_SRC_COPY, x)
+#define ANA_CPUQ_CFG_CPUQ_SRC_COPY_GET(x)\
+	FIELD_GET(ANA_CPUQ_CFG_CPUQ_SRC_COPY, x)
+
+#define ANA_CPUQ_CFG_CPUQ_MAC_COPY               GENMASK(11, 9)
+#define ANA_CPUQ_CFG_CPUQ_MAC_COPY_SET(x)\
+	FIELD_PREP(ANA_CPUQ_CFG_CPUQ_MAC_COPY, x)
+#define ANA_CPUQ_CFG_CPUQ_MAC_COPY_GET(x)\
+	FIELD_GET(ANA_CPUQ_CFG_CPUQ_MAC_COPY, x)
+
+#define ANA_CPUQ_CFG_CPUQ_LRN                    GENMASK(8, 6)
+#define ANA_CPUQ_CFG_CPUQ_LRN_SET(x)\
+	FIELD_PREP(ANA_CPUQ_CFG_CPUQ_LRN, x)
+#define ANA_CPUQ_CFG_CPUQ_LRN_GET(x)\
+	FIELD_GET(ANA_CPUQ_CFG_CPUQ_LRN, x)
+
+#define ANA_CPUQ_CFG_CPUQ_MIRROR                 GENMASK(5, 3)
+#define ANA_CPUQ_CFG_CPUQ_MIRROR_SET(x)\
+	FIELD_PREP(ANA_CPUQ_CFG_CPUQ_MIRROR, x)
+#define ANA_CPUQ_CFG_CPUQ_MIRROR_GET(x)\
+	FIELD_GET(ANA_CPUQ_CFG_CPUQ_MIRROR, x)
+
+#define ANA_CPUQ_CFG_CPUQ_SFLOW                  GENMASK(2, 0)
+#define ANA_CPUQ_CFG_CPUQ_SFLOW_SET(x)\
+	FIELD_PREP(ANA_CPUQ_CFG_CPUQ_SFLOW, x)
+#define ANA_CPUQ_CFG_CPUQ_SFLOW_GET(x)\
+	FIELD_GET(ANA_CPUQ_CFG_CPUQ_SFLOW, x)
+
+/*      ANA:COMMON:CPUQ_8021_CFG */
+#define ANA_CPUQ_8021_CFG(r)      __REG(TARGET_ANA,\
+					0, 1, 29248, 0, 1, 552, 12, r, 16, 4)
+
+#define ANA_CPUQ_8021_CFG_CPUQ_BPDU_VAL          GENMASK(8, 6)
+#define ANA_CPUQ_8021_CFG_CPUQ_BPDU_VAL_SET(x)\
+	FIELD_PREP(ANA_CPUQ_8021_CFG_CPUQ_BPDU_VAL, x)
+#define ANA_CPUQ_8021_CFG_CPUQ_BPDU_VAL_GET(x)\
+	FIELD_GET(ANA_CPUQ_8021_CFG_CPUQ_BPDU_VAL, x)
+
+#define ANA_CPUQ_8021_CFG_CPUQ_GARP_VAL          GENMASK(5, 3)
+#define ANA_CPUQ_8021_CFG_CPUQ_GARP_VAL_SET(x)\
+	FIELD_PREP(ANA_CPUQ_8021_CFG_CPUQ_GARP_VAL, x)
+#define ANA_CPUQ_8021_CFG_CPUQ_GARP_VAL_GET(x)\
+	FIELD_GET(ANA_CPUQ_8021_CFG_CPUQ_GARP_VAL, x)
+
+#define ANA_CPUQ_8021_CFG_CPUQ_CCM_VAL           GENMASK(2, 0)
+#define ANA_CPUQ_8021_CFG_CPUQ_CCM_VAL_SET(x)\
+	FIELD_PREP(ANA_CPUQ_8021_CFG_CPUQ_CCM_VAL, x)
+#define ANA_CPUQ_8021_CFG_CPUQ_CCM_VAL_GET(x)\
+	FIELD_GET(ANA_CPUQ_8021_CFG_CPUQ_CCM_VAL, x)
+
+/*      CHIP_TOP:CUPHY_CFG:CUPHY_PORT_CFG */
+#define CHIP_TOP_CUPHY_PORT_CFG(r) __REG(TARGET_CHIP_TOP,\
+					0, 1, 12, 0, 1, 64, 20, r, 5, 4)
+
+#define CHIP_TOP_CUPHY_PORT_CFG_AUTO_SQUELCH_ENA BIT(7)
+#define CHIP_TOP_CUPHY_PORT_CFG_AUTO_SQUELCH_ENA_SET(x)\
+	FIELD_PREP(CHIP_TOP_CUPHY_PORT_CFG_AUTO_SQUELCH_ENA, x)
+#define CHIP_TOP_CUPHY_PORT_CFG_AUTO_SQUELCH_ENA_GET(x)\
+	FIELD_GET(CHIP_TOP_CUPHY_PORT_CFG_AUTO_SQUELCH_ENA, x)
+
+#define CHIP_TOP_CUPHY_PORT_CFG_COMA_MODE        BIT(6)
+#define CHIP_TOP_CUPHY_PORT_CFG_COMA_MODE_SET(x)\
+	FIELD_PREP(CHIP_TOP_CUPHY_PORT_CFG_COMA_MODE, x)
+#define CHIP_TOP_CUPHY_PORT_CFG_COMA_MODE_GET(x)\
+	FIELD_GET(CHIP_TOP_CUPHY_PORT_CFG_COMA_MODE, x)
+
+#define CHIP_TOP_CUPHY_PORT_CFG_MODE             GENMASK(5, 1)
+#define CHIP_TOP_CUPHY_PORT_CFG_MODE_SET(x)\
+	FIELD_PREP(CHIP_TOP_CUPHY_PORT_CFG_MODE, x)
+#define CHIP_TOP_CUPHY_PORT_CFG_MODE_GET(x)\
+	FIELD_GET(CHIP_TOP_CUPHY_PORT_CFG_MODE, x)
+
+#define CHIP_TOP_CUPHY_PORT_CFG_GTX_CLK_ENA      BIT(0)
+#define CHIP_TOP_CUPHY_PORT_CFG_GTX_CLK_ENA_SET(x)\
+	FIELD_PREP(CHIP_TOP_CUPHY_PORT_CFG_GTX_CLK_ENA, x)
+#define CHIP_TOP_CUPHY_PORT_CFG_GTX_CLK_ENA_GET(x)\
+	FIELD_GET(CHIP_TOP_CUPHY_PORT_CFG_GTX_CLK_ENA, x)
+
+/*      CHIP_TOP:CUPHY_CFG:CUPHY_LED_CFG */
+#define CHIP_TOP_CUPHY_LED_CFG(r) __REG(TARGET_CHIP_TOP,\
+					0, 1, 12, 0, 1, 64, 40, r, 5, 4)
+
+#define CHIP_TOP_CUPHY_LED_CFG_LED_ECO_DIS       BIT(11)
+#define CHIP_TOP_CUPHY_LED_CFG_LED_ECO_DIS_SET(x)\
+	FIELD_PREP(CHIP_TOP_CUPHY_LED_CFG_LED_ECO_DIS, x)
+#define CHIP_TOP_CUPHY_LED_CFG_LED_ECO_DIS_GET(x)\
+	FIELD_GET(CHIP_TOP_CUPHY_LED_CFG_LED_ECO_DIS, x)
+
+#define CHIP_TOP_CUPHY_LED_CFG_LED_EEE_MODE      BIT(10)
+#define CHIP_TOP_CUPHY_LED_CFG_LED_EEE_MODE_SET(x)\
+	FIELD_PREP(CHIP_TOP_CUPHY_LED_CFG_LED_EEE_MODE, x)
+#define CHIP_TOP_CUPHY_LED_CFG_LED_EEE_MODE_GET(x)\
+	FIELD_GET(CHIP_TOP_CUPHY_LED_CFG_LED_EEE_MODE, x)
+
+#define CHIP_TOP_CUPHY_LED_CFG_LED_TEST_MODE     GENMASK(9, 8)
+#define CHIP_TOP_CUPHY_LED_CFG_LED_TEST_MODE_SET(x)\
+	FIELD_PREP(CHIP_TOP_CUPHY_LED_CFG_LED_TEST_MODE, x)
+#define CHIP_TOP_CUPHY_LED_CFG_LED_TEST_MODE_GET(x)\
+	FIELD_GET(CHIP_TOP_CUPHY_LED_CFG_LED_TEST_MODE, x)
+
+#define CHIP_TOP_CUPHY_LED_CFG_LED_TEST_VAL      GENMASK(7, 6)
+#define CHIP_TOP_CUPHY_LED_CFG_LED_TEST_VAL_SET(x)\
+	FIELD_PREP(CHIP_TOP_CUPHY_LED_CFG_LED_TEST_VAL, x)
+#define CHIP_TOP_CUPHY_LED_CFG_LED_TEST_VAL_GET(x)\
+	FIELD_GET(CHIP_TOP_CUPHY_LED_CFG_LED_TEST_VAL, x)
+
+#define CHIP_TOP_CUPHY_LED_CFG_LED_POLARITY      GENMASK(5, 4)
+#define CHIP_TOP_CUPHY_LED_CFG_LED_POLARITY_SET(x)\
+	FIELD_PREP(CHIP_TOP_CUPHY_LED_CFG_LED_POLARITY, x)
+#define CHIP_TOP_CUPHY_LED_CFG_LED_POLARITY_GET(x)\
+	FIELD_GET(CHIP_TOP_CUPHY_LED_CFG_LED_POLARITY, x)
+
+#define CHIP_TOP_CUPHY_LED_CFG_LED_DRIVE_MODE    GENMASK(3, 2)
+#define CHIP_TOP_CUPHY_LED_CFG_LED_DRIVE_MODE_SET(x)\
+	FIELD_PREP(CHIP_TOP_CUPHY_LED_CFG_LED_DRIVE_MODE, x)
+#define CHIP_TOP_CUPHY_LED_CFG_LED_DRIVE_MODE_GET(x)\
+	FIELD_GET(CHIP_TOP_CUPHY_LED_CFG_LED_DRIVE_MODE, x)
+
+#define CHIP_TOP_CUPHY_LED_CFG_LED_BLINK_MODE    GENMASK(1, 0)
+#define CHIP_TOP_CUPHY_LED_CFG_LED_BLINK_MODE_SET(x)\
+	FIELD_PREP(CHIP_TOP_CUPHY_LED_CFG_LED_BLINK_MODE, x)
+#define CHIP_TOP_CUPHY_LED_CFG_LED_BLINK_MODE_GET(x)\
+	FIELD_GET(CHIP_TOP_CUPHY_LED_CFG_LED_BLINK_MODE, x)
+
+/*      DEV:PORT_MODE:CLOCK_CFG */
+#define DEV_CLOCK_CFG(t)          __REG(TARGET_DEV,\
+					t, 9, 0, 0, 1, 20, 0, 0, 1, 4)
+
+#define DEV_CLOCK_CFG_MAC_TX_RST                 BIT(7)
+#define DEV_CLOCK_CFG_MAC_TX_RST_SET(x)\
+	FIELD_PREP(DEV_CLOCK_CFG_MAC_TX_RST, x)
+#define DEV_CLOCK_CFG_MAC_TX_RST_GET(x)\
+	FIELD_GET(DEV_CLOCK_CFG_MAC_TX_RST, x)
+
+#define DEV_CLOCK_CFG_MAC_RX_RST                 BIT(6)
+#define DEV_CLOCK_CFG_MAC_RX_RST_SET(x)\
+	FIELD_PREP(DEV_CLOCK_CFG_MAC_RX_RST, x)
+#define DEV_CLOCK_CFG_MAC_RX_RST_GET(x)\
+	FIELD_GET(DEV_CLOCK_CFG_MAC_RX_RST, x)
+
+#define DEV_CLOCK_CFG_PCS_TX_RST                 BIT(5)
+#define DEV_CLOCK_CFG_PCS_TX_RST_SET(x)\
+	FIELD_PREP(DEV_CLOCK_CFG_PCS_TX_RST, x)
+#define DEV_CLOCK_CFG_PCS_TX_RST_GET(x)\
+	FIELD_GET(DEV_CLOCK_CFG_PCS_TX_RST, x)
+
+#define DEV_CLOCK_CFG_PCS_RX_RST                 BIT(4)
+#define DEV_CLOCK_CFG_PCS_RX_RST_SET(x)\
+	FIELD_PREP(DEV_CLOCK_CFG_PCS_RX_RST, x)
+#define DEV_CLOCK_CFG_PCS_RX_RST_GET(x)\
+	FIELD_GET(DEV_CLOCK_CFG_PCS_RX_RST, x)
+
+#define DEV_CLOCK_CFG_PORT_RST                   BIT(3)
+#define DEV_CLOCK_CFG_PORT_RST_SET(x)\
+	FIELD_PREP(DEV_CLOCK_CFG_PORT_RST, x)
+#define DEV_CLOCK_CFG_PORT_RST_GET(x)\
+	FIELD_GET(DEV_CLOCK_CFG_PORT_RST, x)
+
+#define DEV_CLOCK_CFG_PHY_RST                    BIT(2)
+#define DEV_CLOCK_CFG_PHY_RST_SET(x)\
+	FIELD_PREP(DEV_CLOCK_CFG_PHY_RST, x)
+#define DEV_CLOCK_CFG_PHY_RST_GET(x)\
+	FIELD_GET(DEV_CLOCK_CFG_PHY_RST, x)
+
+#define DEV_CLOCK_CFG_LINK_SPEED                 GENMASK(1, 0)
+#define DEV_CLOCK_CFG_LINK_SPEED_SET(x)\
+	FIELD_PREP(DEV_CLOCK_CFG_LINK_SPEED, x)
+#define DEV_CLOCK_CFG_LINK_SPEED_GET(x)\
+	FIELD_GET(DEV_CLOCK_CFG_LINK_SPEED, x)
+
+/*      DEV:MAC_CFG_STATUS:MAC_ENA_CFG */
+#define DEV_MAC_ENA_CFG(t)        __REG(TARGET_DEV,\
+					t, 9, 20, 0, 1, 44, 0, 0, 1, 4)
+
+#define DEV_MAC_ENA_CFG_RX_ENA                   BIT(4)
+#define DEV_MAC_ENA_CFG_RX_ENA_SET(x)\
+	FIELD_PREP(DEV_MAC_ENA_CFG_RX_ENA, x)
+#define DEV_MAC_ENA_CFG_RX_ENA_GET(x)\
+	FIELD_GET(DEV_MAC_ENA_CFG_RX_ENA, x)
+
+#define DEV_MAC_ENA_CFG_TX_ENA                   BIT(0)
+#define DEV_MAC_ENA_CFG_TX_ENA_SET(x)\
+	FIELD_PREP(DEV_MAC_ENA_CFG_TX_ENA, x)
+#define DEV_MAC_ENA_CFG_TX_ENA_GET(x)\
+	FIELD_GET(DEV_MAC_ENA_CFG_TX_ENA, x)
+
+/*      DEV:MAC_CFG_STATUS:MAC_MODE_CFG */
+#define DEV_MAC_MODE_CFG(t)       __REG(TARGET_DEV,\
+					t, 9, 20, 0, 1, 44, 4, 0, 1, 4)
+
+#define DEV_MAC_MODE_CFG_FC_WORD_SYNC_ENA        BIT(8)
+#define DEV_MAC_MODE_CFG_FC_WORD_SYNC_ENA_SET(x)\
+	FIELD_PREP(DEV_MAC_MODE_CFG_FC_WORD_SYNC_ENA, x)
+#define DEV_MAC_MODE_CFG_FC_WORD_SYNC_ENA_GET(x)\
+	FIELD_GET(DEV_MAC_MODE_CFG_FC_WORD_SYNC_ENA, x)
+
+#define DEV_MAC_MODE_CFG_GIGA_MODE_ENA           BIT(4)
+#define DEV_MAC_MODE_CFG_GIGA_MODE_ENA_SET(x)\
+	FIELD_PREP(DEV_MAC_MODE_CFG_GIGA_MODE_ENA, x)
+#define DEV_MAC_MODE_CFG_GIGA_MODE_ENA_GET(x)\
+	FIELD_GET(DEV_MAC_MODE_CFG_GIGA_MODE_ENA, x)
+
+#define DEV_MAC_MODE_CFG_FDX_ENA                 BIT(0)
+#define DEV_MAC_MODE_CFG_FDX_ENA_SET(x)\
+	FIELD_PREP(DEV_MAC_MODE_CFG_FDX_ENA, x)
+#define DEV_MAC_MODE_CFG_FDX_ENA_GET(x)\
+	FIELD_GET(DEV_MAC_MODE_CFG_FDX_ENA, x)
+
+/*      DEV:MAC_CFG_STATUS:MAC_MAXLEN_CFG */
+#define DEV_MAC_MAXLEN_CFG(t)     __REG(TARGET_DEV,\
+					t, 9, 20, 0, 1, 44, 8, 0, 1, 4)
+
+#define DEV_MAC_MAXLEN_CFG_MAX_LEN               GENMASK(15, 0)
+#define DEV_MAC_MAXLEN_CFG_MAX_LEN_SET(x)\
+	FIELD_PREP(DEV_MAC_MAXLEN_CFG_MAX_LEN, x)
+#define DEV_MAC_MAXLEN_CFG_MAX_LEN_GET(x)\
+	FIELD_GET(DEV_MAC_MAXLEN_CFG_MAX_LEN, x)
+
+/*      DEV:MAC_CFG_STATUS:MAC_TAGS_CFG */
+#define DEV_MAC_TAGS_CFG(t)       __REG(TARGET_DEV,\
+					t, 9, 20, 0, 1, 44, 12, 0, 1, 4)
+
+#define DEV_MAC_TAGS_CFG_TAG_ID                  GENMASK(31, 16)
+#define DEV_MAC_TAGS_CFG_TAG_ID_SET(x)\
+	FIELD_PREP(DEV_MAC_TAGS_CFG_TAG_ID, x)
+#define DEV_MAC_TAGS_CFG_TAG_ID_GET(x)\
+	FIELD_GET(DEV_MAC_TAGS_CFG_TAG_ID, x)
+
+#define DEV_MAC_TAGS_CFG_PB_ENA                  BIT(1)
+#define DEV_MAC_TAGS_CFG_PB_ENA_SET(x)\
+	FIELD_PREP(DEV_MAC_TAGS_CFG_PB_ENA, x)
+#define DEV_MAC_TAGS_CFG_PB_ENA_GET(x)\
+	FIELD_GET(DEV_MAC_TAGS_CFG_PB_ENA, x)
+
+#define DEV_MAC_TAGS_CFG_VLAN_AWR_ENA            BIT(0)
+#define DEV_MAC_TAGS_CFG_VLAN_AWR_ENA_SET(x)\
+	FIELD_PREP(DEV_MAC_TAGS_CFG_VLAN_AWR_ENA, x)
+#define DEV_MAC_TAGS_CFG_VLAN_AWR_ENA_GET(x)\
+	FIELD_GET(DEV_MAC_TAGS_CFG_VLAN_AWR_ENA, x)
+
+#define DEV_MAC_TAGS_CFG_VLAN_LEN_AWR_ENA        BIT(2)
+#define DEV_MAC_TAGS_CFG_VLAN_LEN_AWR_ENA_SET(x)\
+	FIELD_PREP(DEV_MAC_TAGS_CFG_VLAN_LEN_AWR_ENA, x)
+#define DEV_MAC_TAGS_CFG_VLAN_LEN_AWR_ENA_GET(x)\
+	FIELD_GET(DEV_MAC_TAGS_CFG_VLAN_LEN_AWR_ENA, x)
+
+/*      DEV:MAC_CFG_STATUS:MAC_IFG_CFG */
+#define DEV_MAC_IFG_CFG(t)        __REG(TARGET_DEV,\
+					t, 9, 20, 0, 1, 44, 20, 0, 1, 4)
+
+#define DEV_MAC_IFG_CFG_OLD_IPG_CHECK            BIT(17)
+#define DEV_MAC_IFG_CFG_OLD_IPG_CHECK_SET(x)\
+	FIELD_PREP(DEV_MAC_IFG_CFG_OLD_IPG_CHECK, x)
+#define DEV_MAC_IFG_CFG_OLD_IPG_CHECK_GET(x)\
+	FIELD_GET(DEV_MAC_IFG_CFG_OLD_IPG_CHECK, x)
+
+#define DEV_MAC_IFG_CFG_REDUCED_TX_IFG           BIT(16)
+#define DEV_MAC_IFG_CFG_REDUCED_TX_IFG_SET(x)\
+	FIELD_PREP(DEV_MAC_IFG_CFG_REDUCED_TX_IFG, x)
+#define DEV_MAC_IFG_CFG_REDUCED_TX_IFG_GET(x)\
+	FIELD_GET(DEV_MAC_IFG_CFG_REDUCED_TX_IFG, x)
+
+#define DEV_MAC_IFG_CFG_TX_IFG                   GENMASK(12, 8)
+#define DEV_MAC_IFG_CFG_TX_IFG_SET(x)\
+	FIELD_PREP(DEV_MAC_IFG_CFG_TX_IFG, x)
+#define DEV_MAC_IFG_CFG_TX_IFG_GET(x)\
+	FIELD_GET(DEV_MAC_IFG_CFG_TX_IFG, x)
+
+#define DEV_MAC_IFG_CFG_RX_IFG2                  GENMASK(7, 4)
+#define DEV_MAC_IFG_CFG_RX_IFG2_SET(x)\
+	FIELD_PREP(DEV_MAC_IFG_CFG_RX_IFG2, x)
+#define DEV_MAC_IFG_CFG_RX_IFG2_GET(x)\
+	FIELD_GET(DEV_MAC_IFG_CFG_RX_IFG2, x)
+
+#define DEV_MAC_IFG_CFG_RX_IFG1                  GENMASK(3, 0)
+#define DEV_MAC_IFG_CFG_RX_IFG1_SET(x)\
+	FIELD_PREP(DEV_MAC_IFG_CFG_RX_IFG1, x)
+#define DEV_MAC_IFG_CFG_RX_IFG1_GET(x)\
+	FIELD_GET(DEV_MAC_IFG_CFG_RX_IFG1, x)
+
+/*      DEV:MAC_CFG_STATUS:MAC_HDX_CFG */
+#define DEV_MAC_HDX_CFG(t)        __REG(TARGET_DEV,\
+					t, 9, 20, 0, 1, 44, 24, 0, 1, 4)
+
+#define DEV_MAC_HDX_CFG_BYPASS_COL_SYNC          BIT(26)
+#define DEV_MAC_HDX_CFG_BYPASS_COL_SYNC_SET(x)\
+	FIELD_PREP(DEV_MAC_HDX_CFG_BYPASS_COL_SYNC, x)
+#define DEV_MAC_HDX_CFG_BYPASS_COL_SYNC_GET(x)\
+	FIELD_GET(DEV_MAC_HDX_CFG_BYPASS_COL_SYNC, x)
+
+#define DEV_MAC_HDX_CFG_OB_ENA                   BIT(25)
+#define DEV_MAC_HDX_CFG_OB_ENA_SET(x)\
+	FIELD_PREP(DEV_MAC_HDX_CFG_OB_ENA, x)
+#define DEV_MAC_HDX_CFG_OB_ENA_GET(x)\
+	FIELD_GET(DEV_MAC_HDX_CFG_OB_ENA, x)
+
+#define DEV_MAC_HDX_CFG_WEXC_DIS                 BIT(24)
+#define DEV_MAC_HDX_CFG_WEXC_DIS_SET(x)\
+	FIELD_PREP(DEV_MAC_HDX_CFG_WEXC_DIS, x)
+#define DEV_MAC_HDX_CFG_WEXC_DIS_GET(x)\
+	FIELD_GET(DEV_MAC_HDX_CFG_WEXC_DIS, x)
+
+#define DEV_MAC_HDX_CFG_SEED                     GENMASK(23, 16)
+#define DEV_MAC_HDX_CFG_SEED_SET(x)\
+	FIELD_PREP(DEV_MAC_HDX_CFG_SEED, x)
+#define DEV_MAC_HDX_CFG_SEED_GET(x)\
+	FIELD_GET(DEV_MAC_HDX_CFG_SEED, x)
+
+#define DEV_MAC_HDX_CFG_SEED_LOAD                BIT(12)
+#define DEV_MAC_HDX_CFG_SEED_LOAD_SET(x)\
+	FIELD_PREP(DEV_MAC_HDX_CFG_SEED_LOAD, x)
+#define DEV_MAC_HDX_CFG_SEED_LOAD_GET(x)\
+	FIELD_GET(DEV_MAC_HDX_CFG_SEED_LOAD, x)
+
+#define DEV_MAC_HDX_CFG_RETRY_EXC_COL_ENA        BIT(8)
+#define DEV_MAC_HDX_CFG_RETRY_EXC_COL_ENA_SET(x)\
+	FIELD_PREP(DEV_MAC_HDX_CFG_RETRY_EXC_COL_ENA, x)
+#define DEV_MAC_HDX_CFG_RETRY_EXC_COL_ENA_GET(x)\
+	FIELD_GET(DEV_MAC_HDX_CFG_RETRY_EXC_COL_ENA, x)
+
+#define DEV_MAC_HDX_CFG_LATE_COL_POS             GENMASK(6, 0)
+#define DEV_MAC_HDX_CFG_LATE_COL_POS_SET(x)\
+	FIELD_PREP(DEV_MAC_HDX_CFG_LATE_COL_POS, x)
+#define DEV_MAC_HDX_CFG_LATE_COL_POS_GET(x)\
+	FIELD_GET(DEV_MAC_HDX_CFG_LATE_COL_POS, x)
+
+/*      DEV:MAC_CFG_STATUS:MAC_FC_MAC_LOW_CFG */
+#define DEV_FC_MAC_LOW_CFG(t)     __REG(TARGET_DEV,\
+					t, 9, 20, 0, 1, 44, 32, 0, 1, 4)
+
+#define DEV_FC_MAC_LOW_CFG_MAC_LOW               GENMASK(23, 0)
+#define DEV_FC_MAC_LOW_CFG_MAC_LOW_SET(x)\
+	FIELD_PREP(DEV_FC_MAC_LOW_CFG_MAC_LOW, x)
+#define DEV_FC_MAC_LOW_CFG_MAC_LOW_GET(x)\
+	FIELD_GET(DEV_FC_MAC_LOW_CFG_MAC_LOW, x)
+
+/*      DEV:MAC_CFG_STATUS:MAC_FC_MAC_HIGH_CFG */
+#define DEV_FC_MAC_HIGH_CFG(t)    __REG(TARGET_DEV,\
+					t, 9, 20, 0, 1, 44, 36, 0, 1, 4)
+
+#define DEV_FC_MAC_HIGH_CFG_MAC_HIGH             GENMASK(23, 0)
+#define DEV_FC_MAC_HIGH_CFG_MAC_HIGH_SET(x)\
+	FIELD_PREP(DEV_FC_MAC_HIGH_CFG_MAC_HIGH, x)
+#define DEV_FC_MAC_HIGH_CFG_MAC_HIGH_GET(x)\
+	FIELD_GET(DEV_FC_MAC_HIGH_CFG_MAC_HIGH, x)
+
+/*      DEV:PCS1G_CFG_STATUS:PCS1G_CFG */
+#define DEV_PCS1G_CFG(t)          __REG(TARGET_DEV,\
+					t, 9, 64, 0, 1, 68, 0, 0, 1, 4)
+
+#define DEV_PCS1G_CFG_LINK_STATUS_TYPE           BIT(4)
+#define DEV_PCS1G_CFG_LINK_STATUS_TYPE_SET(x)\
+	FIELD_PREP(DEV_PCS1G_CFG_LINK_STATUS_TYPE, x)
+#define DEV_PCS1G_CFG_LINK_STATUS_TYPE_GET(x)\
+	FIELD_GET(DEV_PCS1G_CFG_LINK_STATUS_TYPE, x)
+
+#define DEV_PCS1G_CFG_AN_LINK_CTRL_ENA           BIT(1)
+#define DEV_PCS1G_CFG_AN_LINK_CTRL_ENA_SET(x)\
+	FIELD_PREP(DEV_PCS1G_CFG_AN_LINK_CTRL_ENA, x)
+#define DEV_PCS1G_CFG_AN_LINK_CTRL_ENA_GET(x)\
+	FIELD_GET(DEV_PCS1G_CFG_AN_LINK_CTRL_ENA, x)
+
+#define DEV_PCS1G_CFG_PCS_ENA                    BIT(0)
+#define DEV_PCS1G_CFG_PCS_ENA_SET(x)\
+	FIELD_PREP(DEV_PCS1G_CFG_PCS_ENA, x)
+#define DEV_PCS1G_CFG_PCS_ENA_GET(x)\
+	FIELD_GET(DEV_PCS1G_CFG_PCS_ENA, x)
+
+/*      DEV:PCS1G_CFG_STATUS:PCS1G_SD_CFG */
+#define DEV_PCS1G_SD_CFG(t)       __REG(TARGET_DEV,\
+					t, 9, 64, 0, 1, 68, 8, 0, 1, 4)
+
+#define DEV_PCS1G_SD_CFG_SD_SEL                  BIT(8)
+#define DEV_PCS1G_SD_CFG_SD_SEL_SET(x)\
+	FIELD_PREP(DEV_PCS1G_SD_CFG_SD_SEL, x)
+#define DEV_PCS1G_SD_CFG_SD_SEL_GET(x)\
+	FIELD_GET(DEV_PCS1G_SD_CFG_SD_SEL, x)
+
+#define DEV_PCS1G_SD_CFG_SD_POL                  BIT(4)
+#define DEV_PCS1G_SD_CFG_SD_POL_SET(x)\
+	FIELD_PREP(DEV_PCS1G_SD_CFG_SD_POL, x)
+#define DEV_PCS1G_SD_CFG_SD_POL_GET(x)\
+	FIELD_GET(DEV_PCS1G_SD_CFG_SD_POL, x)
+
+#define DEV_PCS1G_SD_CFG_SD_ENA                  BIT(0)
+#define DEV_PCS1G_SD_CFG_SD_ENA_SET(x)\
+	FIELD_PREP(DEV_PCS1G_SD_CFG_SD_ENA, x)
+#define DEV_PCS1G_SD_CFG_SD_ENA_GET(x)\
+	FIELD_GET(DEV_PCS1G_SD_CFG_SD_ENA, x)
+
+/*      DEVCPU_GCB:CHIP_REGS:FEAT_DISABLE */
+#define GCB_FEAT_DISABLE          __REG(TARGET_GCB,\
+					0, 1, 0, 0, 1, 28, 20, 0, 1, 4)
+
+#define GCB_FEAT_DISABLE_FEAT_EEPROM_CFG_DIS     BIT(0)
+#define GCB_FEAT_DISABLE_FEAT_EEPROM_CFG_DIS_SET(x)\
+	FIELD_PREP(GCB_FEAT_DISABLE_FEAT_EEPROM_CFG_DIS, x)
+#define GCB_FEAT_DISABLE_FEAT_EEPROM_CFG_DIS_GET(x)\
+	FIELD_GET(GCB_FEAT_DISABLE_FEAT_EEPROM_CFG_DIS, x)
+
+#define GCB_FEAT_DISABLE_FEAT_EEPROM_BOOT_DIS    BIT(1)
+#define GCB_FEAT_DISABLE_FEAT_EEPROM_BOOT_DIS_SET(x)\
+	FIELD_PREP(GCB_FEAT_DISABLE_FEAT_EEPROM_BOOT_DIS, x)
+#define GCB_FEAT_DISABLE_FEAT_EEPROM_BOOT_DIS_GET(x)\
+	FIELD_GET(GCB_FEAT_DISABLE_FEAT_EEPROM_BOOT_DIS, x)
+
+#define GCB_FEAT_DISABLE_FEAT_EEPROM_FW_PATCH_DIS BIT(2)
+#define GCB_FEAT_DISABLE_FEAT_EEPROM_FW_PATCH_DIS_SET(x)\
+	FIELD_PREP(GCB_FEAT_DISABLE_FEAT_EEPROM_FW_PATCH_DIS, x)
+#define GCB_FEAT_DISABLE_FEAT_EEPROM_FW_PATCH_DIS_GET(x)\
+	FIELD_GET(GCB_FEAT_DISABLE_FEAT_EEPROM_FW_PATCH_DIS, x)
+
+#define GCB_FEAT_DISABLE_FEAT_MMU_BACKDOOR_WR_DIS BIT(3)
+#define GCB_FEAT_DISABLE_FEAT_MMU_BACKDOOR_WR_DIS_SET(x)\
+	FIELD_PREP(GCB_FEAT_DISABLE_FEAT_MMU_BACKDOOR_WR_DIS, x)
+#define GCB_FEAT_DISABLE_FEAT_MMU_BACKDOOR_WR_DIS_GET(x)\
+	FIELD_GET(GCB_FEAT_DISABLE_FEAT_MMU_BACKDOOR_WR_DIS, x)
+
+#define GCB_FEAT_DISABLE_FEAT_MGMT_IF_WR_DIS     BIT(4)
+#define GCB_FEAT_DISABLE_FEAT_MGMT_IF_WR_DIS_SET(x)\
+	FIELD_PREP(GCB_FEAT_DISABLE_FEAT_MGMT_IF_WR_DIS, x)
+#define GCB_FEAT_DISABLE_FEAT_MGMT_IF_WR_DIS_GET(x)\
+	FIELD_GET(GCB_FEAT_DISABLE_FEAT_MGMT_IF_WR_DIS, x)
+
+#define GCB_FEAT_DISABLE_FEAT_DD_DIS             BIT(5)
+#define GCB_FEAT_DISABLE_FEAT_DD_DIS_SET(x)\
+	FIELD_PREP(GCB_FEAT_DISABLE_FEAT_DD_DIS, x)
+#define GCB_FEAT_DISABLE_FEAT_DD_DIS_GET(x)\
+	FIELD_GET(GCB_FEAT_DISABLE_FEAT_DD_DIS, x)
+
+#define GCB_FEAT_DISABLE_FEAT_TSN_DIS            BIT(6)
+#define GCB_FEAT_DISABLE_FEAT_TSN_DIS_SET(x)\
+	FIELD_PREP(GCB_FEAT_DISABLE_FEAT_TSN_DIS, x)
+#define GCB_FEAT_DISABLE_FEAT_TSN_DIS_GET(x)\
+	FIELD_GET(GCB_FEAT_DISABLE_FEAT_TSN_DIS, x)
+
+#define GCB_FEAT_DISABLE_FEAT_PTP_DIS            BIT(7)
+#define GCB_FEAT_DISABLE_FEAT_PTP_DIS_SET(x)\
+	FIELD_PREP(GCB_FEAT_DISABLE_FEAT_PTP_DIS, x)
+#define GCB_FEAT_DISABLE_FEAT_PTP_DIS_GET(x)\
+	FIELD_GET(GCB_FEAT_DISABLE_FEAT_PTP_DIS, x)
+
+#define GCB_FEAT_DISABLE_FEAT_FRER_DIS           BIT(8)
+#define GCB_FEAT_DISABLE_FEAT_FRER_DIS_SET(x)\
+	FIELD_PREP(GCB_FEAT_DISABLE_FEAT_FRER_DIS, x)
+#define GCB_FEAT_DISABLE_FEAT_FRER_DIS_GET(x)\
+	FIELD_GET(GCB_FEAT_DISABLE_FEAT_FRER_DIS, x)
+
+#define GCB_FEAT_DISABLE_FEAT_NUM_PORTS_DIS      GENMASK(14, 12)
+#define GCB_FEAT_DISABLE_FEAT_NUM_PORTS_DIS_SET(x)\
+	FIELD_PREP(GCB_FEAT_DISABLE_FEAT_NUM_PORTS_DIS, x)
+#define GCB_FEAT_DISABLE_FEAT_NUM_PORTS_DIS_GET(x)\
+	FIELD_GET(GCB_FEAT_DISABLE_FEAT_NUM_PORTS_DIS, x)
+
+#define GCB_FEAT_DISABLE_FEAT_NUM_CU_DIS         GENMASK(16, 15)
+#define GCB_FEAT_DISABLE_FEAT_NUM_CU_DIS_SET(x)\
+	FIELD_PREP(GCB_FEAT_DISABLE_FEAT_NUM_CU_DIS, x)
+#define GCB_FEAT_DISABLE_FEAT_NUM_CU_DIS_GET(x)\
+	FIELD_GET(GCB_FEAT_DISABLE_FEAT_NUM_CU_DIS, x)
+
+#define GCB_FEAT_DISABLE_FEAT_RGMII_DIS          GENMASK(18, 17)
+#define GCB_FEAT_DISABLE_FEAT_RGMII_DIS_SET(x)\
+	FIELD_PREP(GCB_FEAT_DISABLE_FEAT_RGMII_DIS, x)
+#define GCB_FEAT_DISABLE_FEAT_RGMII_DIS_GET(x)\
+	FIELD_GET(GCB_FEAT_DISABLE_FEAT_RGMII_DIS, x)
+
+#define GCB_FEAT_DISABLE_FEAT_SGMII_DIS          GENMASK(20, 19)
+#define GCB_FEAT_DISABLE_FEAT_SGMII_DIS_SET(x)\
+	FIELD_PREP(GCB_FEAT_DISABLE_FEAT_SGMII_DIS, x)
+#define GCB_FEAT_DISABLE_FEAT_SGMII_DIS_GET(x)\
+	FIELD_GET(GCB_FEAT_DISABLE_FEAT_SGMII_DIS, x)
+
+#define GCB_FEAT_DISABLE_FEAT_QSGMII_DIS         BIT(21)
+#define GCB_FEAT_DISABLE_FEAT_QSGMII_DIS_SET(x)\
+	FIELD_PREP(GCB_FEAT_DISABLE_FEAT_QSGMII_DIS, x)
+#define GCB_FEAT_DISABLE_FEAT_QSGMII_DIS_GET(x)\
+	FIELD_GET(GCB_FEAT_DISABLE_FEAT_QSGMII_DIS, x)
+
+#define GCB_FEAT_DISABLE_FEAT_NUM_CU_FIXED       BIT(22)
+#define GCB_FEAT_DISABLE_FEAT_NUM_CU_FIXED_SET(x)\
+	FIELD_PREP(GCB_FEAT_DISABLE_FEAT_NUM_CU_FIXED, x)
+#define GCB_FEAT_DISABLE_FEAT_NUM_CU_FIXED_GET(x)\
+	FIELD_GET(GCB_FEAT_DISABLE_FEAT_NUM_CU_FIXED, x)
+
+#define GCB_FEAT_DISABLE_FEAT_WDT_ENABLED        BIT(25)
+#define GCB_FEAT_DISABLE_FEAT_WDT_ENABLED_SET(x)\
+	FIELD_PREP(GCB_FEAT_DISABLE_FEAT_WDT_ENABLED, x)
+#define GCB_FEAT_DISABLE_FEAT_WDT_ENABLED_GET(x)\
+	FIELD_GET(GCB_FEAT_DISABLE_FEAT_WDT_ENABLED, x)
+
+/*      HSIO:HW_CFGSTAT:HW_CFG */
+#define HSIO_HW_CFG               __REG(TARGET_HSIO,\
+					0, 1, 72, 0, 1, 44, 0, 0, 1, 4)
+
+#define HSIO_HW_CFG_RGMII_0_CFG                  BIT(10)
+#define HSIO_HW_CFG_RGMII_0_CFG_SET(x)\
+	FIELD_PREP(HSIO_HW_CFG_RGMII_0_CFG, x)
+#define HSIO_HW_CFG_RGMII_0_CFG_GET(x)\
+	FIELD_GET(HSIO_HW_CFG_RGMII_0_CFG, x)
+
+#define HSIO_HW_CFG_GMII_ENA                     GENMASK(9, 1)
+#define HSIO_HW_CFG_GMII_ENA_SET(x)\
+	FIELD_PREP(HSIO_HW_CFG_GMII_ENA, x)
+#define HSIO_HW_CFG_GMII_ENA_GET(x)\
+	FIELD_GET(HSIO_HW_CFG_GMII_ENA, x)
+
+#define HSIO_HW_CFG_QSGMII_ENA                   BIT(0)
+#define HSIO_HW_CFG_QSGMII_ENA_SET(x)\
+	FIELD_PREP(HSIO_HW_CFG_QSGMII_ENA, x)
+#define HSIO_HW_CFG_QSGMII_ENA_GET(x)\
+	FIELD_GET(HSIO_HW_CFG_QSGMII_ENA, x)
+
+/*      HSIO:HW_CFGSTAT:RGMII_CFG */
+#define HSIO_RGMII_CFG(r)         __REG(TARGET_HSIO,\
+					0, 1, 72, 0, 1, 44, 12, r, 2, 4)
+
+#define HSIO_RGMII_CFG_IB_RX_LINK_STATUS         BIT(15)
+#define HSIO_RGMII_CFG_IB_RX_LINK_STATUS_SET(x)\
+	FIELD_PREP(HSIO_RGMII_CFG_IB_RX_LINK_STATUS, x)
+#define HSIO_RGMII_CFG_IB_RX_LINK_STATUS_GET(x)\
+	FIELD_GET(HSIO_RGMII_CFG_IB_RX_LINK_STATUS, x)
+
+#define HSIO_RGMII_CFG_IB_RX_DUPLEX              BIT(14)
+#define HSIO_RGMII_CFG_IB_RX_DUPLEX_SET(x)\
+	FIELD_PREP(HSIO_RGMII_CFG_IB_RX_DUPLEX, x)
+#define HSIO_RGMII_CFG_IB_RX_DUPLEX_GET(x)\
+	FIELD_GET(HSIO_RGMII_CFG_IB_RX_DUPLEX, x)
+
+#define HSIO_RGMII_CFG_IB_RX_SPEED               GENMASK(13, 12)
+#define HSIO_RGMII_CFG_IB_RX_SPEED_SET(x)\
+	FIELD_PREP(HSIO_RGMII_CFG_IB_RX_SPEED, x)
+#define HSIO_RGMII_CFG_IB_RX_SPEED_GET(x)\
+	FIELD_GET(HSIO_RGMII_CFG_IB_RX_SPEED, x)
+
+#define HSIO_RGMII_CFG_IB_TX_LINK_STATUS         BIT(11)
+#define HSIO_RGMII_CFG_IB_TX_LINK_STATUS_SET(x)\
+	FIELD_PREP(HSIO_RGMII_CFG_IB_TX_LINK_STATUS, x)
+#define HSIO_RGMII_CFG_IB_TX_LINK_STATUS_GET(x)\
+	FIELD_GET(HSIO_RGMII_CFG_IB_TX_LINK_STATUS, x)
+
+#define HSIO_RGMII_CFG_IB_TX_FDX                 BIT(10)
+#define HSIO_RGMII_CFG_IB_TX_FDX_SET(x)\
+	FIELD_PREP(HSIO_RGMII_CFG_IB_TX_FDX, x)
+#define HSIO_RGMII_CFG_IB_TX_FDX_GET(x)\
+	FIELD_GET(HSIO_RGMII_CFG_IB_TX_FDX, x)
+
+#define HSIO_RGMII_CFG_IB_TX_MII_SPD             BIT(9)
+#define HSIO_RGMII_CFG_IB_TX_MII_SPD_SET(x)\
+	FIELD_PREP(HSIO_RGMII_CFG_IB_TX_MII_SPD, x)
+#define HSIO_RGMII_CFG_IB_TX_MII_SPD_GET(x)\
+	FIELD_GET(HSIO_RGMII_CFG_IB_TX_MII_SPD, x)
+
+#define HSIO_RGMII_CFG_IB_TX_SPD_1G              BIT(8)
+#define HSIO_RGMII_CFG_IB_TX_SPD_1G_SET(x)\
+	FIELD_PREP(HSIO_RGMII_CFG_IB_TX_SPD_1G, x)
+#define HSIO_RGMII_CFG_IB_TX_SPD_1G_GET(x)\
+	FIELD_GET(HSIO_RGMII_CFG_IB_TX_SPD_1G, x)
+
+#define HSIO_RGMII_CFG_IB_TX_ENA                 BIT(7)
+#define HSIO_RGMII_CFG_IB_TX_ENA_SET(x)\
+	FIELD_PREP(HSIO_RGMII_CFG_IB_TX_ENA, x)
+#define HSIO_RGMII_CFG_IB_TX_ENA_GET(x)\
+	FIELD_GET(HSIO_RGMII_CFG_IB_TX_ENA, x)
+
+#define HSIO_RGMII_CFG_IB_RX_ENA                 BIT(6)
+#define HSIO_RGMII_CFG_IB_RX_ENA_SET(x)\
+	FIELD_PREP(HSIO_RGMII_CFG_IB_RX_ENA, x)
+#define HSIO_RGMII_CFG_IB_RX_ENA_GET(x)\
+	FIELD_GET(HSIO_RGMII_CFG_IB_RX_ENA, x)
+
+#define HSIO_RGMII_CFG_IB_ENA                    BIT(5)
+#define HSIO_RGMII_CFG_IB_ENA_SET(x)\
+	FIELD_PREP(HSIO_RGMII_CFG_IB_ENA, x)
+#define HSIO_RGMII_CFG_IB_ENA_GET(x)\
+	FIELD_GET(HSIO_RGMII_CFG_IB_ENA, x)
+
+#define HSIO_RGMII_CFG_TX_CLK_CFG                GENMASK(4, 2)
+#define HSIO_RGMII_CFG_TX_CLK_CFG_SET(x)\
+	FIELD_PREP(HSIO_RGMII_CFG_TX_CLK_CFG, x)
+#define HSIO_RGMII_CFG_TX_CLK_CFG_GET(x)\
+	FIELD_GET(HSIO_RGMII_CFG_TX_CLK_CFG, x)
+
+#define HSIO_RGMII_CFG_RGMII_TX_RST              BIT(1)
+#define HSIO_RGMII_CFG_RGMII_TX_RST_SET(x)\
+	FIELD_PREP(HSIO_RGMII_CFG_RGMII_TX_RST, x)
+#define HSIO_RGMII_CFG_RGMII_TX_RST_GET(x)\
+	FIELD_GET(HSIO_RGMII_CFG_RGMII_TX_RST, x)
+
+#define HSIO_RGMII_CFG_RGMII_RX_RST              BIT(0)
+#define HSIO_RGMII_CFG_RGMII_RX_RST_SET(x)\
+	FIELD_PREP(HSIO_RGMII_CFG_RGMII_RX_RST, x)
+#define HSIO_RGMII_CFG_RGMII_RX_RST_GET(x)\
+	FIELD_GET(HSIO_RGMII_CFG_RGMII_RX_RST, x)
+
+/*      HSIO:HW_CFGSTAT:DLL_CFG */
+#define HSIO_DLL_CFG(r)           __REG(TARGET_HSIO,\
+					0, 1, 72, 0, 1, 44, 28, r, 4, 4)
+
+#define HSIO_DLL_CFG_DLL_CLK_ENA                 BIT(20)
+#define HSIO_DLL_CFG_DLL_CLK_ENA_SET(x)\
+	FIELD_PREP(HSIO_DLL_CFG_DLL_CLK_ENA, x)
+#define HSIO_DLL_CFG_DLL_CLK_ENA_GET(x)\
+	FIELD_GET(HSIO_DLL_CFG_DLL_CLK_ENA, x)
+
+#define HSIO_DLL_CFG_BIST_PASS                   BIT(19)
+#define HSIO_DLL_CFG_BIST_PASS_SET(x)\
+	FIELD_PREP(HSIO_DLL_CFG_BIST_PASS, x)
+#define HSIO_DLL_CFG_BIST_PASS_GET(x)\
+	FIELD_GET(HSIO_DLL_CFG_BIST_PASS, x)
+
+#define HSIO_DLL_CFG_BIST_END                    BIT(18)
+#define HSIO_DLL_CFG_BIST_END_SET(x)\
+	FIELD_PREP(HSIO_DLL_CFG_BIST_END, x)
+#define HSIO_DLL_CFG_BIST_END_GET(x)\
+	FIELD_GET(HSIO_DLL_CFG_BIST_END, x)
+
+#define HSIO_DLL_CFG_BIST_START                  BIT(17)
+#define HSIO_DLL_CFG_BIST_START_SET(x)\
+	FIELD_PREP(HSIO_DLL_CFG_BIST_START, x)
+#define HSIO_DLL_CFG_BIST_START_GET(x)\
+	FIELD_GET(HSIO_DLL_CFG_BIST_START, x)
+
+#define HSIO_DLL_CFG_TAP_SEL                     GENMASK(16, 10)
+#define HSIO_DLL_CFG_TAP_SEL_SET(x)\
+	FIELD_PREP(HSIO_DLL_CFG_TAP_SEL, x)
+#define HSIO_DLL_CFG_TAP_SEL_GET(x)\
+	FIELD_GET(HSIO_DLL_CFG_TAP_SEL, x)
+
+#define HSIO_DLL_CFG_TAP_ADJ                     GENMASK(9, 3)
+#define HSIO_DLL_CFG_TAP_ADJ_SET(x)\
+	FIELD_PREP(HSIO_DLL_CFG_TAP_ADJ, x)
+#define HSIO_DLL_CFG_TAP_ADJ_GET(x)\
+	FIELD_GET(HSIO_DLL_CFG_TAP_ADJ, x)
+
+#define HSIO_DLL_CFG_DELAY_ENA                   BIT(2)
+#define HSIO_DLL_CFG_DELAY_ENA_SET(x)\
+	FIELD_PREP(HSIO_DLL_CFG_DELAY_ENA, x)
+#define HSIO_DLL_CFG_DELAY_ENA_GET(x)\
+	FIELD_GET(HSIO_DLL_CFG_DELAY_ENA, x)
+
+#define HSIO_DLL_CFG_DLL_ENA                     BIT(1)
+#define HSIO_DLL_CFG_DLL_ENA_SET(x)\
+	FIELD_PREP(HSIO_DLL_CFG_DLL_ENA, x)
+#define HSIO_DLL_CFG_DLL_ENA_GET(x)\
+	FIELD_GET(HSIO_DLL_CFG_DLL_ENA, x)
+
+#define HSIO_DLL_CFG_DLL_RST                     BIT(0)
+#define HSIO_DLL_CFG_DLL_RST_SET(x)\
+	FIELD_PREP(HSIO_DLL_CFG_DLL_RST, x)
+#define HSIO_DLL_CFG_DLL_RST_GET(x)\
+	FIELD_GET(HSIO_DLL_CFG_DLL_RST, x)
+
+/*      DEVCPU_QS:XTR:XTR_FLUSH */
+#define QS_XTR_FLUSH              __REG(TARGET_QS,\
+					0, 1, 0, 0, 1, 36, 24, 0, 1, 4)
+
+#define QS_XTR_FLUSH_FLUSH                       GENMASK(1, 0)
+#define QS_XTR_FLUSH_FLUSH_SET(x)\
+	FIELD_PREP(QS_XTR_FLUSH_FLUSH, x)
+#define QS_XTR_FLUSH_FLUSH_GET(x)\
+	FIELD_GET(QS_XTR_FLUSH_FLUSH, x)
+
+/*      DEVCPU_QS:INJ:INJ_GRP_CFG */
+#define QS_INJ_GRP_CFG(r)         __REG(TARGET_QS,\
+					0, 1, 36, 0, 1, 40, 0, r, 2, 4)
+
+#define QS_INJ_GRP_CFG_MODE                      GENMASK(3, 2)
+#define QS_INJ_GRP_CFG_MODE_SET(x)\
+	FIELD_PREP(QS_INJ_GRP_CFG_MODE, x)
+#define QS_INJ_GRP_CFG_MODE_GET(x)\
+	FIELD_GET(QS_INJ_GRP_CFG_MODE, x)
+
+#define QS_INJ_GRP_CFG_BYTE_SWAP                 BIT(0)
+#define QS_INJ_GRP_CFG_BYTE_SWAP_SET(x)\
+	FIELD_PREP(QS_INJ_GRP_CFG_BYTE_SWAP, x)
+#define QS_INJ_GRP_CFG_BYTE_SWAP_GET(x)\
+	FIELD_GET(QS_INJ_GRP_CFG_BYTE_SWAP, x)
+
+/*      DEVCPU_QS:INJ:INJ_CTRL */
+#define QS_INJ_CTRL(r)            __REG(TARGET_QS,\
+					0, 1, 36, 0, 1, 40, 16, r, 2, 4)
+
+#define QS_INJ_CTRL_GAP_SIZE                     GENMASK(24, 21)
+#define QS_INJ_CTRL_GAP_SIZE_SET(x)\
+	FIELD_PREP(QS_INJ_CTRL_GAP_SIZE, x)
+#define QS_INJ_CTRL_GAP_SIZE_GET(x)\
+	FIELD_GET(QS_INJ_CTRL_GAP_SIZE, x)
+
+#define QS_INJ_CTRL_ABORT                        BIT(20)
+#define QS_INJ_CTRL_ABORT_SET(x)\
+	FIELD_PREP(QS_INJ_CTRL_ABORT, x)
+#define QS_INJ_CTRL_ABORT_GET(x)\
+	FIELD_GET(QS_INJ_CTRL_ABORT, x)
+
+#define QS_INJ_CTRL_EOF                          BIT(19)
+#define QS_INJ_CTRL_EOF_SET(x)\
+	FIELD_PREP(QS_INJ_CTRL_EOF, x)
+#define QS_INJ_CTRL_EOF_GET(x)\
+	FIELD_GET(QS_INJ_CTRL_EOF, x)
+
+#define QS_INJ_CTRL_SOF                          BIT(18)
+#define QS_INJ_CTRL_SOF_SET(x)\
+	FIELD_PREP(QS_INJ_CTRL_SOF, x)
+#define QS_INJ_CTRL_SOF_GET(x)\
+	FIELD_GET(QS_INJ_CTRL_SOF, x)
+
+#define QS_INJ_CTRL_VLD_BYTES                    GENMASK(17, 16)
+#define QS_INJ_CTRL_VLD_BYTES_SET(x)\
+	FIELD_PREP(QS_INJ_CTRL_VLD_BYTES, x)
+#define QS_INJ_CTRL_VLD_BYTES_GET(x)\
+	FIELD_GET(QS_INJ_CTRL_VLD_BYTES, x)
+
+/*      QSYS:SYSTEM:PORT_MODE */
+#define QSYS_PORT_MODE(r)         __REG(TARGET_QSYS,\
+					0, 1, 14336, 0, 1, 240, 0, r, 11, 4)
+
+#define QSYS_PORT_MODE_DEQUEUE_DIS               BIT(1)
+#define QSYS_PORT_MODE_DEQUEUE_DIS_SET(x)\
+	FIELD_PREP(QSYS_PORT_MODE_DEQUEUE_DIS, x)
+#define QSYS_PORT_MODE_DEQUEUE_DIS_GET(x)\
+	FIELD_GET(QSYS_PORT_MODE_DEQUEUE_DIS, x)
+
+#define QSYS_PORT_MODE_DEQUEUE_LATE              BIT(0)
+#define QSYS_PORT_MODE_DEQUEUE_LATE_SET(x)\
+	FIELD_PREP(QSYS_PORT_MODE_DEQUEUE_LATE, x)
+#define QSYS_PORT_MODE_DEQUEUE_LATE_GET(x)\
+	FIELD_GET(QSYS_PORT_MODE_DEQUEUE_LATE, x)
+
+/*      QSYS:SYSTEM:SWITCH_PORT_MODE */
+#define QSYS_SW_PORT_MODE(r)      __REG(TARGET_QSYS,\
+					0, 1, 14336, 0, 1, 240, 88, r, 10, 4)
+
+#define QSYS_SW_PORT_MODE_PORT_ENA               BIT(19)
+#define QSYS_SW_PORT_MODE_PORT_ENA_SET(x)\
+	FIELD_PREP(QSYS_SW_PORT_MODE_PORT_ENA, x)
+#define QSYS_SW_PORT_MODE_PORT_ENA_GET(x)\
+	FIELD_GET(QSYS_SW_PORT_MODE_PORT_ENA, x)
+
+#define QSYS_SW_PORT_MODE_IDEQ_DIS               BIT(18)
+#define QSYS_SW_PORT_MODE_IDEQ_DIS_SET(x)\
+	FIELD_PREP(QSYS_SW_PORT_MODE_IDEQ_DIS, x)
+#define QSYS_SW_PORT_MODE_IDEQ_DIS_GET(x)\
+	FIELD_GET(QSYS_SW_PORT_MODE_IDEQ_DIS, x)
+
+#define QSYS_SW_PORT_MODE_SCH_NEXT_CFG           GENMASK(17, 15)
+#define QSYS_SW_PORT_MODE_SCH_NEXT_CFG_SET(x)\
+	FIELD_PREP(QSYS_SW_PORT_MODE_SCH_NEXT_CFG, x)
+#define QSYS_SW_PORT_MODE_SCH_NEXT_CFG_GET(x)\
+	FIELD_GET(QSYS_SW_PORT_MODE_SCH_NEXT_CFG, x)
+
+#define QSYS_SW_PORT_MODE_YEL_RSRVD              BIT(14)
+#define QSYS_SW_PORT_MODE_YEL_RSRVD_SET(x)\
+	FIELD_PREP(QSYS_SW_PORT_MODE_YEL_RSRVD, x)
+#define QSYS_SW_PORT_MODE_YEL_RSRVD_GET(x)\
+	FIELD_GET(QSYS_SW_PORT_MODE_YEL_RSRVD, x)
+
+#define QSYS_SW_PORT_MODE_INGRESS_DROP_MODE      BIT(13)
+#define QSYS_SW_PORT_MODE_INGRESS_DROP_MODE_SET(x)\
+	FIELD_PREP(QSYS_SW_PORT_MODE_INGRESS_DROP_MODE, x)
+#define QSYS_SW_PORT_MODE_INGRESS_DROP_MODE_GET(x)\
+	FIELD_GET(QSYS_SW_PORT_MODE_INGRESS_DROP_MODE, x)
+
+#define QSYS_SW_PORT_MODE_TX_PFC_ENA             GENMASK(12, 5)
+#define QSYS_SW_PORT_MODE_TX_PFC_ENA_SET(x)\
+	FIELD_PREP(QSYS_SW_PORT_MODE_TX_PFC_ENA, x)
+#define QSYS_SW_PORT_MODE_TX_PFC_ENA_GET(x)\
+	FIELD_GET(QSYS_SW_PORT_MODE_TX_PFC_ENA, x)
+
+#define QSYS_SW_PORT_MODE_TX_PFC_MODE            BIT(4)
+#define QSYS_SW_PORT_MODE_TX_PFC_MODE_SET(x)\
+	FIELD_PREP(QSYS_SW_PORT_MODE_TX_PFC_MODE, x)
+#define QSYS_SW_PORT_MODE_TX_PFC_MODE_GET(x)\
+	FIELD_GET(QSYS_SW_PORT_MODE_TX_PFC_MODE, x)
+
+#define QSYS_SW_PORT_MODE_FWD_TWOCYCLE_MODE      BIT(3)
+#define QSYS_SW_PORT_MODE_FWD_TWOCYCLE_MODE_SET(x)\
+	FIELD_PREP(QSYS_SW_PORT_MODE_FWD_TWOCYCLE_MODE, x)
+#define QSYS_SW_PORT_MODE_FWD_TWOCYCLE_MODE_GET(x)\
+	FIELD_GET(QSYS_SW_PORT_MODE_FWD_TWOCYCLE_MODE, x)
+
+#define QSYS_SW_PORT_MODE_AGING_MODE             GENMASK(2, 1)
+#define QSYS_SW_PORT_MODE_AGING_MODE_SET(x)\
+	FIELD_PREP(QSYS_SW_PORT_MODE_AGING_MODE, x)
+#define QSYS_SW_PORT_MODE_AGING_MODE_GET(x)\
+	FIELD_GET(QSYS_SW_PORT_MODE_AGING_MODE, x)
+
+#define QSYS_SW_PORT_MODE_HOL_PROTECTED          BIT(0)
+#define QSYS_SW_PORT_MODE_HOL_PROTECTED_SET(x)\
+	FIELD_PREP(QSYS_SW_PORT_MODE_HOL_PROTECTED, x)
+#define QSYS_SW_PORT_MODE_HOL_PROTECTED_GET(x)\
+	FIELD_GET(QSYS_SW_PORT_MODE_HOL_PROTECTED, x)
+
+/*      QSYS:SYSTEM:EGR_NO_SHARING */
+#define QSYS_EGR_NO_SHARING       __REG(TARGET_QSYS,\
+					0, 1, 14336, 0, 1, 240, 176, 0, 1, 4)
+
+#define QSYS_EGR_NO_SHARING_EGR_NO_SHARING       GENMASK(9, 0)
+#define QSYS_EGR_NO_SHARING_EGR_NO_SHARING_SET(x)\
+	FIELD_PREP(QSYS_EGR_NO_SHARING_EGR_NO_SHARING, x)
+#define QSYS_EGR_NO_SHARING_EGR_NO_SHARING_GET(x)\
+	FIELD_GET(QSYS_EGR_NO_SHARING_EGR_NO_SHARING, x)
+
+/*      QSYS:SYSTEM:SW_STATUS */
+#define QSYS_SW_STATUS(r)         __REG(TARGET_QSYS,\
+					0, 1, 14336, 0, 1, 240, 180, r, 10, 4)
+
+#define QSYS_SW_STATUS_EQ_AVAIL                  GENMASK(7, 0)
+#define QSYS_SW_STATUS_EQ_AVAIL_SET(x)\
+	FIELD_PREP(QSYS_SW_STATUS_EQ_AVAIL, x)
+#define QSYS_SW_STATUS_EQ_AVAIL_GET(x)\
+	FIELD_GET(QSYS_SW_STATUS_EQ_AVAIL, x)
+
+/*      QSYS:SYSTEM:EXT_CPU_CFG */
+#define QSYS_EXT_CPU_CFG          __REG(TARGET_QSYS,\
+					0, 1, 14336, 0, 1, 240, 220, 0, 1, 4)
+
+#define QSYS_EXT_CPU_CFG_EXT_CPU_KILL_ENA        BIT(14)
+#define QSYS_EXT_CPU_CFG_EXT_CPU_KILL_ENA_SET(x)\
+	FIELD_PREP(QSYS_EXT_CPU_CFG_EXT_CPU_KILL_ENA, x)
+#define QSYS_EXT_CPU_CFG_EXT_CPU_KILL_ENA_GET(x)\
+	FIELD_GET(QSYS_EXT_CPU_CFG_EXT_CPU_KILL_ENA, x)
+
+#define QSYS_EXT_CPU_CFG_INT_CPU_KILL_ENA        BIT(13)
+#define QSYS_EXT_CPU_CFG_INT_CPU_KILL_ENA_SET(x)\
+	FIELD_PREP(QSYS_EXT_CPU_CFG_INT_CPU_KILL_ENA, x)
+#define QSYS_EXT_CPU_CFG_INT_CPU_KILL_ENA_GET(x)\
+	FIELD_GET(QSYS_EXT_CPU_CFG_INT_CPU_KILL_ENA, x)
+
+#define QSYS_EXT_CPU_CFG_EXT_CPU_PORT            GENMASK(12, 8)
+#define QSYS_EXT_CPU_CFG_EXT_CPU_PORT_SET(x)\
+	FIELD_PREP(QSYS_EXT_CPU_CFG_EXT_CPU_PORT, x)
+#define QSYS_EXT_CPU_CFG_EXT_CPU_PORT_GET(x)\
+	FIELD_GET(QSYS_EXT_CPU_CFG_EXT_CPU_PORT, x)
+
+#define QSYS_EXT_CPU_CFG_EXT_CPUQ_MSK            GENMASK(7, 0)
+#define QSYS_EXT_CPU_CFG_EXT_CPUQ_MSK_SET(x)\
+	FIELD_PREP(QSYS_EXT_CPU_CFG_EXT_CPUQ_MSK, x)
+#define QSYS_EXT_CPU_CFG_EXT_CPUQ_MSK_GET(x)\
+	FIELD_GET(QSYS_EXT_CPU_CFG_EXT_CPUQ_MSK, x)
+
+/*      QSYS:SYSTEM:CPU_GROUP_MAP */
+#define QSYS_CPU_GROUP_MAP        __REG(TARGET_QSYS,\
+					0, 1, 14336, 0, 1, 240, 224, 0, 1, 4)
+
+#define QSYS_CPU_GROUP_MAP_CPU_GROUP_MAP         GENMASK(7, 0)
+#define QSYS_CPU_GROUP_MAP_CPU_GROUP_MAP_SET(x)\
+	FIELD_PREP(QSYS_CPU_GROUP_MAP_CPU_GROUP_MAP, x)
+#define QSYS_CPU_GROUP_MAP_CPU_GROUP_MAP_GET(x)\
+	FIELD_GET(QSYS_CPU_GROUP_MAP_CPU_GROUP_MAP, x)
+
+/*      QSYS:RES_CTRL:RES_CFG */
+#define QSYS_RES_CFG(g)           __REG(TARGET_QSYS,\
+					0, 1, 16384, g, 1024, 8, 0, 0, 1, 4)
+
+#define QSYS_RES_CFG_WM_HIGH                     GENMASK(8, 0)
+#define QSYS_RES_CFG_WM_HIGH_SET(x)\
+	FIELD_PREP(QSYS_RES_CFG_WM_HIGH, x)
+#define QSYS_RES_CFG_WM_HIGH_GET(x)\
+	FIELD_GET(QSYS_RES_CFG_WM_HIGH, x)
+
+/*      QSYS:DROP_CFG:EGR_DROP_MODE */
+#define QSYS_EGR_DROP_MODE        __REG(TARGET_QSYS,\
+					0, 1, 12736, 0, 1, 8, 0, 0, 1, 4)
+
+#define QSYS_EGR_DROP_MODE_EGRESS_DROP_MODE      GENMASK(9, 0)
+#define QSYS_EGR_DROP_MODE_EGRESS_DROP_MODE_SET(x)\
+	FIELD_PREP(QSYS_EGR_DROP_MODE_EGRESS_DROP_MODE, x)
+#define QSYS_EGR_DROP_MODE_EGRESS_DROP_MODE_GET(x)\
+	FIELD_GET(QSYS_EGR_DROP_MODE_EGRESS_DROP_MODE, x)
+
+/*      REW:PORT:PORT_VLAN_CFG */
+#define REW_PORT_VLAN_CFG(g)      __REG(TARGET_REW,\
+					0, 1, 4096, g, 11, 128, 0, 0, 1, 4)
+
+#define REW_PORT_VLAN_CFG_PORT_TPID              GENMASK(31, 16)
+#define REW_PORT_VLAN_CFG_PORT_TPID_SET(x)\
+	FIELD_PREP(REW_PORT_VLAN_CFG_PORT_TPID, x)
+#define REW_PORT_VLAN_CFG_PORT_TPID_GET(x)\
+	FIELD_GET(REW_PORT_VLAN_CFG_PORT_TPID, x)
+
+#define REW_PORT_VLAN_CFG_PORT_DEI               BIT(15)
+#define REW_PORT_VLAN_CFG_PORT_DEI_SET(x)\
+	FIELD_PREP(REW_PORT_VLAN_CFG_PORT_DEI, x)
+#define REW_PORT_VLAN_CFG_PORT_DEI_GET(x)\
+	FIELD_GET(REW_PORT_VLAN_CFG_PORT_DEI, x)
+
+#define REW_PORT_VLAN_CFG_PORT_PCP               GENMASK(14, 12)
+#define REW_PORT_VLAN_CFG_PORT_PCP_SET(x)\
+	FIELD_PREP(REW_PORT_VLAN_CFG_PORT_PCP, x)
+#define REW_PORT_VLAN_CFG_PORT_PCP_GET(x)\
+	FIELD_GET(REW_PORT_VLAN_CFG_PORT_PCP, x)
+
+#define REW_PORT_VLAN_CFG_PORT_VID               GENMASK(11, 0)
+#define REW_PORT_VLAN_CFG_PORT_VID_SET(x)\
+	FIELD_PREP(REW_PORT_VLAN_CFG_PORT_VID, x)
+#define REW_PORT_VLAN_CFG_PORT_VID_GET(x)\
+	FIELD_GET(REW_PORT_VLAN_CFG_PORT_VID, x)
+
+/*      REW:PORT:TAG_CFG */
+#define REW_TAG_CFG(g)            __REG(TARGET_REW,\
+					0, 1, 4096, g, 11, 128, 4, 0, 1, 4)
+
+#define REW_TAG_CFG_TAG_CFG                      GENMASK(8, 7)
+#define REW_TAG_CFG_TAG_CFG_SET(x)\
+	FIELD_PREP(REW_TAG_CFG_TAG_CFG, x)
+#define REW_TAG_CFG_TAG_CFG_GET(x)\
+	FIELD_GET(REW_TAG_CFG_TAG_CFG, x)
+
+#define REW_TAG_CFG_TAG_TPID_CFG                 GENMASK(6, 5)
+#define REW_TAG_CFG_TAG_TPID_CFG_SET(x)\
+	FIELD_PREP(REW_TAG_CFG_TAG_TPID_CFG, x)
+#define REW_TAG_CFG_TAG_TPID_CFG_GET(x)\
+	FIELD_GET(REW_TAG_CFG_TAG_TPID_CFG, x)
+
+#define REW_TAG_CFG_TAG_VID_CFG                  BIT(4)
+#define REW_TAG_CFG_TAG_VID_CFG_SET(x)\
+	FIELD_PREP(REW_TAG_CFG_TAG_VID_CFG, x)
+#define REW_TAG_CFG_TAG_VID_CFG_GET(x)\
+	FIELD_GET(REW_TAG_CFG_TAG_VID_CFG, x)
+
+#define REW_TAG_CFG_TAG_PCP_CFG                  GENMASK(3, 2)
+#define REW_TAG_CFG_TAG_PCP_CFG_SET(x)\
+	FIELD_PREP(REW_TAG_CFG_TAG_PCP_CFG, x)
+#define REW_TAG_CFG_TAG_PCP_CFG_GET(x)\
+	FIELD_GET(REW_TAG_CFG_TAG_PCP_CFG, x)
+
+#define REW_TAG_CFG_TAG_DEI_CFG                  GENMASK(1, 0)
+#define REW_TAG_CFG_TAG_DEI_CFG_SET(x)\
+	FIELD_PREP(REW_TAG_CFG_TAG_DEI_CFG, x)
+#define REW_TAG_CFG_TAG_DEI_CFG_GET(x)\
+	FIELD_GET(REW_TAG_CFG_TAG_DEI_CFG, x)
+
+/*      REW:PORT:PORT_CFG */
+#define REW_PORT_CFG(g)           __REG(TARGET_REW,\
+					0, 1, 4096, g, 11, 128, 8, 0, 1, 4)
+
+#define REW_PORT_CFG_ES0_EN                      BIT(4)
+#define REW_PORT_CFG_ES0_EN_SET(x)\
+	FIELD_PREP(REW_PORT_CFG_ES0_EN, x)
+#define REW_PORT_CFG_ES0_EN_GET(x)\
+	FIELD_GET(REW_PORT_CFG_ES0_EN, x)
+
+#define REW_PORT_CFG_FCS_UPDATE_NONCPU_CFG       GENMASK(3, 2)
+#define REW_PORT_CFG_FCS_UPDATE_NONCPU_CFG_SET(x)\
+	FIELD_PREP(REW_PORT_CFG_FCS_UPDATE_NONCPU_CFG, x)
+#define REW_PORT_CFG_FCS_UPDATE_NONCPU_CFG_GET(x)\
+	FIELD_GET(REW_PORT_CFG_FCS_UPDATE_NONCPU_CFG, x)
+
+#define REW_PORT_CFG_FCS_UPDATE_CPU_ENA          BIT(1)
+#define REW_PORT_CFG_FCS_UPDATE_CPU_ENA_SET(x)\
+	FIELD_PREP(REW_PORT_CFG_FCS_UPDATE_CPU_ENA, x)
+#define REW_PORT_CFG_FCS_UPDATE_CPU_ENA_GET(x)\
+	FIELD_GET(REW_PORT_CFG_FCS_UPDATE_CPU_ENA, x)
+
+#define REW_PORT_CFG_NO_REWRITE                  BIT(0)
+#define REW_PORT_CFG_NO_REWRITE_SET(x)\
+	FIELD_PREP(REW_PORT_CFG_NO_REWRITE, x)
+#define REW_PORT_CFG_NO_REWRITE_GET(x)\
+	FIELD_GET(REW_PORT_CFG_NO_REWRITE, x)
+
+/*      SYS:SYSTEM:RESET_CFG */
+#define SYS_RESET_CFG             __REG(TARGET_SYS,\
+					0, 1, 4160, 0, 1, 184, 0, 0, 1, 4)
+
+#define SYS_RESET_CFG_CORE_ENA                   BIT(0)
+#define SYS_RESET_CFG_CORE_ENA_SET(x)\
+	FIELD_PREP(SYS_RESET_CFG_CORE_ENA, x)
+#define SYS_RESET_CFG_CORE_ENA_GET(x)\
+	FIELD_GET(SYS_RESET_CFG_CORE_ENA, x)
+
+/*      SYS:SYSTEM:PORT_MODE */
+#define SYS_PORT_MODE(r)          __REG(TARGET_SYS,\
+					0, 1, 4160, 0, 1, 184, 48, r, 11, 4)
+
+#define SYS_PORT_MODE_PRP_LANID                  BIT(8)
+#define SYS_PORT_MODE_PRP_LANID_SET(x)\
+	FIELD_PREP(SYS_PORT_MODE_PRP_LANID, x)
+#define SYS_PORT_MODE_PRP_LANID_GET(x)\
+	FIELD_GET(SYS_PORT_MODE_PRP_LANID, x)
+
+#define SYS_PORT_MODE_PRP_ENA                    BIT(7)
+#define SYS_PORT_MODE_PRP_ENA_SET(x)\
+	FIELD_PREP(SYS_PORT_MODE_PRP_ENA, x)
+#define SYS_PORT_MODE_PRP_ENA_GET(x)\
+	FIELD_GET(SYS_PORT_MODE_PRP_ENA, x)
+
+#define SYS_PORT_MODE_INCL_INJ_HDR               GENMASK(6, 5)
+#define SYS_PORT_MODE_INCL_INJ_HDR_SET(x)\
+	FIELD_PREP(SYS_PORT_MODE_INCL_INJ_HDR, x)
+#define SYS_PORT_MODE_INCL_INJ_HDR_GET(x)\
+	FIELD_GET(SYS_PORT_MODE_INCL_INJ_HDR, x)
+
+#define SYS_PORT_MODE_INCL_XTR_HDR               GENMASK(4, 3)
+#define SYS_PORT_MODE_INCL_XTR_HDR_SET(x)\
+	FIELD_PREP(SYS_PORT_MODE_INCL_XTR_HDR, x)
+#define SYS_PORT_MODE_INCL_XTR_HDR_GET(x)\
+	FIELD_GET(SYS_PORT_MODE_INCL_XTR_HDR, x)
+
+#define SYS_PORT_MODE_INJ_HDR_ERR                BIT(2)
+#define SYS_PORT_MODE_INJ_HDR_ERR_SET(x)\
+	FIELD_PREP(SYS_PORT_MODE_INJ_HDR_ERR, x)
+#define SYS_PORT_MODE_INJ_HDR_ERR_GET(x)\
+	FIELD_GET(SYS_PORT_MODE_INJ_HDR_ERR, x)
+
+#define SYS_PORT_MODE_PAD_DIS                    BIT(1)
+#define SYS_PORT_MODE_PAD_DIS_SET(x)\
+	FIELD_PREP(SYS_PORT_MODE_PAD_DIS, x)
+#define SYS_PORT_MODE_PAD_DIS_GET(x)\
+	FIELD_GET(SYS_PORT_MODE_PAD_DIS, x)
+
+#define SYS_PORT_MODE_RTAG_CLEAR                 BIT(0)
+#define SYS_PORT_MODE_RTAG_CLEAR_SET(x)\
+	FIELD_PREP(SYS_PORT_MODE_RTAG_CLEAR, x)
+#define SYS_PORT_MODE_RTAG_CLEAR_GET(x)\
+	FIELD_GET(SYS_PORT_MODE_RTAG_CLEAR, x)
+
+/*      SYS:SYSTEM:FRONT_PORT_MODE */
+#define SYS_FRONT_PORT_MODE(r)    __REG(TARGET_SYS,\
+					0, 1, 4160, 0, 1, 184, 92, r, 9, 4)
+
+#define SYS_FRONT_PORT_MODE_HDX_MODE             BIT(1)
+#define SYS_FRONT_PORT_MODE_HDX_MODE_SET(x)\
+	FIELD_PREP(SYS_FRONT_PORT_MODE_HDX_MODE, x)
+#define SYS_FRONT_PORT_MODE_HDX_MODE_GET(x)\
+	FIELD_GET(SYS_FRONT_PORT_MODE_HDX_MODE, x)
+
+#define SYS_FRONT_PORT_MODE_ADD_FRAG_SIZE        GENMASK(9, 8)
+#define SYS_FRONT_PORT_MODE_ADD_FRAG_SIZE_SET(x)\
+	FIELD_PREP(SYS_FRONT_PORT_MODE_ADD_FRAG_SIZE, x)
+#define SYS_FRONT_PORT_MODE_ADD_FRAG_SIZE_GET(x)\
+	FIELD_GET(SYS_FRONT_PORT_MODE_ADD_FRAG_SIZE, x)
+
+#define SYS_FRONT_PORT_MODE_DONT_WAIT_FOR_TS     BIT(0)
+#define SYS_FRONT_PORT_MODE_DONT_WAIT_FOR_TS_SET(x)\
+	FIELD_PREP(SYS_FRONT_PORT_MODE_DONT_WAIT_FOR_TS, x)
+#define SYS_FRONT_PORT_MODE_DONT_WAIT_FOR_TS_GET(x)\
+	FIELD_GET(SYS_FRONT_PORT_MODE_DONT_WAIT_FOR_TS, x)
+
+/*      SYS:SYSTEM:FRM_AGING */
+#define SYS_FRM_AGING             __REG(TARGET_SYS,\
+					0, 1, 4160, 0, 1, 184, 128, 0, 1, 4)
+
+#define SYS_FRM_AGING_AGE_TX_ENA                 BIT(20)
+#define SYS_FRM_AGING_AGE_TX_ENA_SET(x)\
+	FIELD_PREP(SYS_FRM_AGING_AGE_TX_ENA, x)
+#define SYS_FRM_AGING_AGE_TX_ENA_GET(x)\
+	FIELD_GET(SYS_FRM_AGING_AGE_TX_ENA, x)
+
+#define SYS_FRM_AGING_MAX_AGE                    GENMASK(19, 0)
+#define SYS_FRM_AGING_MAX_AGE_SET(x)\
+	FIELD_PREP(SYS_FRM_AGING_MAX_AGE, x)
+#define SYS_FRM_AGING_MAX_AGE_GET(x)\
+	FIELD_GET(SYS_FRM_AGING_MAX_AGE, x)
+
+/*      SYS:SYSTEM:STAT_CFG */
+#define SYS_STAT_CFG              __REG(TARGET_SYS,\
+					0, 1, 4160, 0, 1, 184, 132, 0, 1, 4)
+
+#define SYS_STAT_CFG_STAT_CLEAR_SHOT             GENMASK(16, 10)
+#define SYS_STAT_CFG_STAT_CLEAR_SHOT_SET(x)\
+	FIELD_PREP(SYS_STAT_CFG_STAT_CLEAR_SHOT, x)
+#define SYS_STAT_CFG_STAT_CLEAR_SHOT_GET(x)\
+	FIELD_GET(SYS_STAT_CFG_STAT_CLEAR_SHOT, x)
+
+#define SYS_STAT_CFG_STAT_VIEW                   GENMASK(9, 0)
+#define SYS_STAT_CFG_STAT_VIEW_SET(x)\
+	FIELD_PREP(SYS_STAT_CFG_STAT_VIEW, x)
+#define SYS_STAT_CFG_STAT_VIEW_GET(x)\
+	FIELD_GET(SYS_STAT_CFG_STAT_VIEW, x)
+
+/*      SYS:SYSTEM:SW_STATUS */
+#define SYS_SW_STATUS(r)          __REG(TARGET_SYS,\
+					0, 1, 4160, 0, 1, 184, 136, r, 10, 4)
+
+#define SYS_SW_STATUS_PORT_RX_PAUSED             BIT(0)
+#define SYS_SW_STATUS_PORT_RX_PAUSED_SET(x)\
+	FIELD_PREP(SYS_SW_STATUS_PORT_RX_PAUSED, x)
+#define SYS_SW_STATUS_PORT_RX_PAUSED_GET(x)\
+	FIELD_GET(SYS_SW_STATUS_PORT_RX_PAUSED, x)
+
+/*      SYS:PAUSE_CFG:PAUSE_CFG */
+#define SYS_PAUSE_CFG(r)          __REG(TARGET_SYS,\
+					0, 1, 4344, 0, 1, 124, 0, r, 10, 4)
+
+#define SYS_PAUSE_CFG_PAUSE_START                GENMASK(18, 10)
+#define SYS_PAUSE_CFG_PAUSE_START_SET(x)\
+	FIELD_PREP(SYS_PAUSE_CFG_PAUSE_START, x)
+#define SYS_PAUSE_CFG_PAUSE_START_GET(x)\
+	FIELD_GET(SYS_PAUSE_CFG_PAUSE_START, x)
+
+#define SYS_PAUSE_CFG_PAUSE_STOP                 GENMASK(9, 1)
+#define SYS_PAUSE_CFG_PAUSE_STOP_SET(x)\
+	FIELD_PREP(SYS_PAUSE_CFG_PAUSE_STOP, x)
+#define SYS_PAUSE_CFG_PAUSE_STOP_GET(x)\
+	FIELD_GET(SYS_PAUSE_CFG_PAUSE_STOP, x)
+
+#define SYS_PAUSE_CFG_PAUSE_ENA                  BIT(0)
+#define SYS_PAUSE_CFG_PAUSE_ENA_SET(x)\
+	FIELD_PREP(SYS_PAUSE_CFG_PAUSE_ENA, x)
+#define SYS_PAUSE_CFG_PAUSE_ENA_GET(x)\
+	FIELD_GET(SYS_PAUSE_CFG_PAUSE_ENA, x)
+
+/*      SYS:PAUSE_CFG:ATOP */
+#define SYS_ATOP(r)               __REG(TARGET_SYS,\
+					0, 1, 4344, 0, 1, 124, 44, r, 10, 4)
+
+#define SYS_ATOP_ATOP                            GENMASK(8, 0)
+#define SYS_ATOP_ATOP_SET(x)\
+	FIELD_PREP(SYS_ATOP_ATOP, x)
+#define SYS_ATOP_ATOP_GET(x)\
+	FIELD_GET(SYS_ATOP_ATOP, x)
+
+/*      SYS:PAUSE_CFG:ATOP_TOT_CFG */
+#define SYS_ATOP_TOT_CFG          __REG(TARGET_SYS,\
+					0, 1, 4344, 0, 1, 124, 84, 0, 1, 4)
+
+#define SYS_ATOP_TOT_CFG_ATOP_TOT                GENMASK(8, 0)
+#define SYS_ATOP_TOT_CFG_ATOP_TOT_SET(x)\
+	FIELD_PREP(SYS_ATOP_TOT_CFG_ATOP_TOT, x)
+#define SYS_ATOP_TOT_CFG_ATOP_TOT_GET(x)\
+	FIELD_GET(SYS_ATOP_TOT_CFG_ATOP_TOT, x)
+
+/*      SYS:PAUSE_CFG:MAC_FC_CFG */
+#define SYS_MAC_FC_CFG(r)         __REG(TARGET_SYS,\
+					0, 1, 4344, 0, 1, 124, 88, r, 9, 4)
+
+#define SYS_MAC_FC_CFG_FC_LINK_SPEED             GENMASK(27, 26)
+#define SYS_MAC_FC_CFG_FC_LINK_SPEED_SET(x)\
+	FIELD_PREP(SYS_MAC_FC_CFG_FC_LINK_SPEED, x)
+#define SYS_MAC_FC_CFG_FC_LINK_SPEED_GET(x)\
+	FIELD_GET(SYS_MAC_FC_CFG_FC_LINK_SPEED, x)
+
+#define SYS_MAC_FC_CFG_FC_LATENCY_CFG            GENMASK(25, 20)
+#define SYS_MAC_FC_CFG_FC_LATENCY_CFG_SET(x)\
+	FIELD_PREP(SYS_MAC_FC_CFG_FC_LATENCY_CFG, x)
+#define SYS_MAC_FC_CFG_FC_LATENCY_CFG_GET(x)\
+	FIELD_GET(SYS_MAC_FC_CFG_FC_LATENCY_CFG, x)
+
+#define SYS_MAC_FC_CFG_ZERO_PAUSE_ENA            BIT(18)
+#define SYS_MAC_FC_CFG_ZERO_PAUSE_ENA_SET(x)\
+	FIELD_PREP(SYS_MAC_FC_CFG_ZERO_PAUSE_ENA, x)
+#define SYS_MAC_FC_CFG_ZERO_PAUSE_ENA_GET(x)\
+	FIELD_GET(SYS_MAC_FC_CFG_ZERO_PAUSE_ENA, x)
+
+#define SYS_MAC_FC_CFG_TX_FC_ENA                 BIT(17)
+#define SYS_MAC_FC_CFG_TX_FC_ENA_SET(x)\
+	FIELD_PREP(SYS_MAC_FC_CFG_TX_FC_ENA, x)
+#define SYS_MAC_FC_CFG_TX_FC_ENA_GET(x)\
+	FIELD_GET(SYS_MAC_FC_CFG_TX_FC_ENA, x)
+
+#define SYS_MAC_FC_CFG_RX_FC_ENA                 BIT(16)
+#define SYS_MAC_FC_CFG_RX_FC_ENA_SET(x)\
+	FIELD_PREP(SYS_MAC_FC_CFG_RX_FC_ENA, x)
+#define SYS_MAC_FC_CFG_RX_FC_ENA_GET(x)\
+	FIELD_GET(SYS_MAC_FC_CFG_RX_FC_ENA, x)
+
+#define SYS_MAC_FC_CFG_PAUSE_VAL_CFG             GENMASK(15, 0)
+#define SYS_MAC_FC_CFG_PAUSE_VAL_CFG_SET(x)\
+	FIELD_PREP(SYS_MAC_FC_CFG_PAUSE_VAL_CFG, x)
+#define SYS_MAC_FC_CFG_PAUSE_VAL_CFG_GET(x)\
+	FIELD_GET(SYS_MAC_FC_CFG_PAUSE_VAL_CFG, x)
+
+/*      SYS:STAT:CNT */
+#define SYS_CNT(g)                __REG(TARGET_SYS,\
+					0, 1, 0, g, 896, 4, 0, 0, 1, 4)
+
+/*      SYS:RAM_CTRL:RAM_INIT */
+#define SYS_RAM_INIT              __REG(TARGET_SYS,\
+					0, 1, 4492, 0, 1, 4, 0, 0, 1, 4)
+
+#define SYS_RAM_INIT_RAM_TEST_OPT                GENMASK(4, 2)
+#define SYS_RAM_INIT_RAM_TEST_OPT_SET(x)\
+	FIELD_PREP(SYS_RAM_INIT_RAM_TEST_OPT, x)
+#define SYS_RAM_INIT_RAM_TEST_OPT_GET(x)\
+	FIELD_GET(SYS_RAM_INIT_RAM_TEST_OPT, x)
+
+#define SYS_RAM_INIT_RAM_INIT                    BIT(1)
+#define SYS_RAM_INIT_RAM_INIT_SET(x)\
+	FIELD_PREP(SYS_RAM_INIT_RAM_INIT, x)
+#define SYS_RAM_INIT_RAM_INIT_GET(x)\
+	FIELD_GET(SYS_RAM_INIT_RAM_INIT, x)
+
+#define SYS_RAM_INIT_RAM_CFG_HOOK                BIT(0)
+#define SYS_RAM_INIT_RAM_CFG_HOOK_SET(x)\
+	FIELD_PREP(SYS_RAM_INIT_RAM_CFG_HOOK, x)
+#define SYS_RAM_INIT_RAM_CFG_HOOK_GET(x)\
+	FIELD_GET(SYS_RAM_INIT_RAM_CFG_HOOK, x)
+
+/*      SYS:PTPPORT:PTP_RXDLY_CFG */
+#define SYS_PTP_RXDLY_CFG(g)      __REG(TARGET_SYS,\
+					0, 1, 4512, g, 11, 28, 8, 0, 1, 4)
+
+#define SYS_PTP_RXDLY_CFG_PTP_RX_IO_DLY          GENMASK(23, 0)
+#define SYS_PTP_RXDLY_CFG_PTP_RX_IO_DLY_SET(x)\
+	FIELD_PREP(SYS_PTP_RXDLY_CFG_PTP_RX_IO_DLY, x)
+#define SYS_PTP_RXDLY_CFG_PTP_RX_IO_DLY_GET(x)\
+	FIELD_GET(SYS_PTP_RXDLY_CFG_PTP_RX_IO_DLY, x)
+
+/*      SYS:PTPPORT:PTP_TXDLY_CFG */
+#define SYS_PTP_TXDLY_CFG(g)      __REG(TARGET_SYS,\
+					0, 1, 4512, g, 11, 28, 12, 0, 1, 4)
+
+#define SYS_PTP_TXDLY_CFG_PTP_TX_IO_DLY          GENMASK(23, 0)
+#define SYS_PTP_TXDLY_CFG_PTP_TX_IO_DLY_SET(x)\
+	FIELD_PREP(SYS_PTP_TXDLY_CFG_PTP_TX_IO_DLY, x)
+#define SYS_PTP_TXDLY_CFG_PTP_TX_IO_DLY_GET(x)\
+	FIELD_GET(SYS_PTP_TXDLY_CFG_PTP_TX_IO_DLY, x)
+
+#endif /* _LAN9645X_REGS_H_ */
-- 
2.52.0

[PATCH net-next v12 5/9] net: dsa: lan9645x: add vlan support

From: Jens Emil Schulz Østergaard <hidden>
Date: 2026-09-08 07:41:33

Add VLAN support for VLAN-aware and VLAN-unaware bridges.

VID 4095 (HOST_PVID) is reserved and used internally as the port
VLAN for standalone (non-bridged) ports so their FDB domain is
isolated from bridged traffic.

VID 0 is used internally as the port VLAN for VLAN-unaware bridge ports
(UNAWARE_PVID).

The VLAN table and the per port VLAN configuration are serialized with
fwd_domain_lock, since .port_setup runs without rtnl held.

There are two chip limitations. Only one egress-untagged VLAN is
supported on a port that also has tagged VLANs. The rewriter can exempt
a single PORT_VID from tagging, so adding a second untagged VLAN
alongside tagged ones is rejected with -EBUSY.

Both TPIDs are recognized as VLAN tags unconditionally. There is no per
port control to classify one TPID as tagged while treating the other as
untagged customer data.

Only bridge vlan_protocol 802.1Q is offloaded. DSA does not pass
SWITCHDEV_ATTR_ID_BRIDGE_VLAN_PROTOCOL to drivers, so an 802.1ad bridge
cannot be refused, and the classifier does not honour it.

S-tag and C-tag are always treated as interchangeable. Full C-component
conformance would need S-tagged frames treated as untagged by a VLAN
aware bridge, dropped on a port without a pvid and classified to the
pvid on a port with one.

Of those, only the drop is expressible, through
ANA_DROP_CFG.DROP_S_TAGGED_ENA. The reclassification needs the VCAP to
match on tag type and overwrite the classified VID, see
ocelot_update_vlan_reclassify_rule().

We program neither, since dropping without reclassifying is less
consistent than treating both tag types alike.

Reviewed-by: Steen Hegelund <Steen.Hegelund@microchip.com>
Signed-off-by: Jens Emil Schulz Østergaard <redacted>
---
Changes in v12:
- Serialize the VLAN table with fwd_domain_lock, and assert it in the
  lan9645x_vlan.c entry points. .port_setup runs outside rtnl, so a
  registered user port can be bridged while a later port is still being
  set up.
- Move this patch before bridge support. It now carries bridge_mask,
  fwd_domain_lock and lan9645x_port_is_bridged().
- Move lan9645x_vlan_clear_hostmode() to the bridge patch, together with
  its only caller.
- Describe the untagged VLAN limit, and why S-tag and C-tag are treated
  as interchangeable.
- Say that only bridge vlan_protocol 802.1Q is offloaded.
- lan9645x_teardown(): destroy fwd_domain_lock before the NPI port deinit,
  so teardown unwinds in reverse order of setup.
- Drop the six unused lan9645x_vlan bitfields.
- Spell VLAN in upper case in the two dev_err() strings.
- Split the extack in lan9645x_vlan_port_add_vlan(). The condition also
  fires when a tagged VLAN is added to a port with several untagged ones,
  where there is no native VLAN to report.

Changes in v8:
- Use lan9645x->num_phys_ports instead of CPU_PORT for the CPU port
  module, and reword the portmask comment.

Changes in v5:
- switch -EBUSY to -EINVAL for vlan add/del in the reserved range.
- remove reserved HSR vlan
- update commit message

Changes in v4:
- fix clear HOST_PVID vlan membership when a port joins a bridge
- explicit default value write to tag type register for untagged frames
- use lan_rmw for ANA_DROP_CFG
- add comment for error path in lan9645x_vlan_hw_wr
- use dsa_switch_for_each_user_port to iterate ports

Changes in v3:
- use SET register macros in vlan_hw_wr
- add vlan id bounds check to vlan_del
- return vlan_hw_wr timeout err on init
- move cpu vlan action after bounds check

Changes in v2:
- redesign based on selftests which rely on changing vlan_default_pvid.
  Our HW limitations were too forward. Following Vladimirs changes to
  ocelot VLAN implementation, we now dynamically change egress tag
  configuration, allowing more states.
- selftests are passing, except an expected failure w.r.t ctag/stag
  conformance, which is a hw limitation.
---
 drivers/net/dsa/microchip/lan9645x/Makefile        |   1 +
 drivers/net/dsa/microchip/lan9645x/lan9645x_main.c |  69 ++++
 drivers/net/dsa/microchip/lan9645x/lan9645x_main.h |  32 ++
 drivers/net/dsa/microchip/lan9645x/lan9645x_port.c |   6 +
 drivers/net/dsa/microchip/lan9645x/lan9645x_vlan.c | 402 +++++++++++++++++++++
 5 files changed, 510 insertions(+)
diff --git a/drivers/net/dsa/microchip/lan9645x/Makefile b/drivers/net/dsa/microchip/lan9645x/Makefile
index 7cc0ae0ada40..e049114b3563 100644
--- a/drivers/net/dsa/microchip/lan9645x/Makefile
+++ b/drivers/net/dsa/microchip/lan9645x/Makefile
@@ -6,3 +6,4 @@ mchp-lan9645x-objs := \
 	lan9645x_npi.o \
 	lan9645x_phylink.o \
 	lan9645x_port.o \
+	lan9645x_vlan.o \
diff --git a/drivers/net/dsa/microchip/lan9645x/lan9645x_main.c b/drivers/net/dsa/microchip/lan9645x/lan9645x_main.c
index c7de3836e696..6818b91f4bed 100644
--- a/drivers/net/dsa/microchip/lan9645x/lan9645x_main.c
+++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_main.c
@@ -74,6 +74,7 @@ static void lan9645x_teardown(struct dsa_switch *ds)
 {
 	struct lan9645x *lan9645x = ds->priv;
 
+	mutex_destroy(&lan9645x->fwd_domain_lock);
 	lan9645x_npi_port_deinit(lan9645x, lan9645x->npi);
 }
 
@@ -157,6 +158,11 @@ static int lan9645x_setup(struct dsa_switch *ds)
 		return err;
 	}
 
+	mutex_init(&lan9645x->fwd_domain_lock);
+	err = lan9645x_vlan_init(lan9645x);
+	if (err)
+		goto err_mutex;
+
 	/* Link Aggregation Mode: NETDEV_LAG_HASH_L2 */
 	lan_wr(ANA_AGGR_CFG_AC_SMAC_ENA |
 	       ANA_AGGR_CFG_AC_DMAC_ENA,
@@ -288,6 +294,11 @@ static int lan9645x_setup(struct dsa_switch *ds)
 		 lan9645x->num_phys_ports - lan9645x->num_port_dis);
 
 	return 0;
+
+err_mutex:
+	mutex_destroy(&lan9645x->fwd_domain_lock);
+	lan9645x_npi_port_deinit(lan9645x, lan9645x->npi);
+	return err;
 }
 
 static void lan9645x_port_phylink_get_caps(struct dsa_switch *ds, int port,
@@ -296,6 +307,59 @@ static void lan9645x_port_phylink_get_caps(struct dsa_switch *ds, int port,
 	lan9645x_phylink_get_caps(ds->priv, port, config);
 }
 
+static int lan9645x_port_vlan_filtering(struct dsa_switch *ds, int port,
+					bool enabled,
+					struct netlink_ext_ack *extack)
+{
+	struct lan9645x *lan9645x = ds->priv;
+	struct lan9645x_port *p;
+
+	/* DSA core does not call this for the CPU port */
+	p = lan9645x_to_port(lan9645x, port);
+	mutex_lock(&lan9645x->fwd_domain_lock);
+	p->vlan_aware = enabled;
+	lan9645x_vlan_port_apply(p);
+	mutex_unlock(&lan9645x->fwd_domain_lock);
+
+	return 0;
+}
+
+static int lan9645x_port_vlan_add(struct dsa_switch *ds, int port,
+				  const struct switchdev_obj_port_vlan *vlan,
+				  struct netlink_ext_ack *extack)
+{
+	struct lan9645x *lan9645x = ds->priv;
+	struct lan9645x_port *p;
+	bool pvid, untagged;
+	int err;
+
+	p = lan9645x_to_port(lan9645x, port);
+	pvid = !!(vlan->flags & BRIDGE_VLAN_INFO_PVID);
+	untagged = !!(vlan->flags & BRIDGE_VLAN_INFO_UNTAGGED);
+
+	mutex_lock(&lan9645x->fwd_domain_lock);
+	err = lan9645x_vlan_port_add_vlan(p, vlan->vid, pvid, untagged, extack);
+	mutex_unlock(&lan9645x->fwd_domain_lock);
+
+	return err;
+}
+
+static int lan9645x_port_vlan_del(struct dsa_switch *ds, int port,
+				  const struct switchdev_obj_port_vlan *vlan)
+{
+	struct lan9645x *lan9645x = ds->priv;
+	struct lan9645x_port *p;
+	int err;
+
+	p = lan9645x_to_port(lan9645x, port);
+
+	mutex_lock(&lan9645x->fwd_domain_lock);
+	err = lan9645x_vlan_port_del_vlan(p, vlan->vid);
+	mutex_unlock(&lan9645x->fwd_domain_lock);
+
+	return err;
+}
+
 static const struct dsa_switch_ops lan9645x_switch_ops = {
 	.get_tag_protocol		= lan9645x_get_tag_protocol,
 
@@ -309,6 +373,11 @@ static const struct dsa_switch_ops lan9645x_switch_ops = {
 	/* MTU  */
 	.port_change_mtu		= lan9645x_change_mtu,
 	.port_max_mtu			= lan9645x_get_max_mtu,
+
+	/* VLAN integration */
+	.port_vlan_filtering		= lan9645x_port_vlan_filtering,
+	.port_vlan_add			= lan9645x_port_vlan_add,
+	.port_vlan_del			= lan9645x_port_vlan_del,
 };
 
 static int lan9645x_request_target_regmaps(struct lan9645x *lan9645x)
diff --git a/drivers/net/dsa/microchip/lan9645x/lan9645x_main.h b/drivers/net/dsa/microchip/lan9645x/lan9645x_main.h
index 81acacca7f2d..b8565a6f2bda 100644
--- a/drivers/net/dsa/microchip/lan9645x/lan9645x_main.h
+++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_main.h
@@ -7,6 +7,7 @@
 
 #include <linux/dsa/lan9645x.h>
 #include <linux/if_bridge.h>
+#include <linux/if_vlan.h>
 #include <linux/regmap.h>
 #include <net/dsa.h>
 
@@ -171,6 +172,11 @@ enum lan9645x_vlan_port_tag {
 	LAN9645X_TAG_ALL = 3,
 };
 
+struct lan9645x_vlan {
+	u32 portmask: 10, /* ports 0-8 + CPU port module */
+	    untagged: 9; /* ports 0-8 */
+};
+
 struct lan9645x {
 	struct device *dev;
 	struct dsa_switch *ds;
@@ -182,7 +188,15 @@ struct lan9645x {
 	u8 num_phys_ports;
 	struct lan9645x_port **ports;
 
+	/* Forwarding Database */
+	u16 bridge_mask; /* Mask for bridged ports */
+	/* lock forwarding configuration and vlan table */
+	struct mutex fwd_domain_lock;
+
 	int num_port_dis;
+
+	/* VLAN entries */
+	struct lan9645x_vlan vlans[VLAN_N_VID];
 };
 
 struct lan9645x_port {
@@ -190,6 +204,9 @@ struct lan9645x_port {
 
 	u8 chip_port;
 
+	bool vlan_aware;
+	u16 pvid;
+
 	bool rx_internal_delay;
 	bool tx_internal_delay;
 };
@@ -238,6 +255,11 @@ static inline struct lan9645x_port *lan9645x_to_port(struct lan9645x *lan9645x,
 	return lan9645x->ports[port];
 }
 
+static inline bool lan9645x_port_is_bridged(struct lan9645x_port *p)
+{
+	return p->lan9645x->bridge_mask & BIT(p->chip_port);
+}
+
 static inline struct regmap *lan_tgt2rmap(struct lan9645x *lan9645x,
 					  enum lan9645x_target t, int tinst)
 {
@@ -344,4 +366,14 @@ void lan9645x_phylink_get_caps(struct lan9645x *lan9645x, int port,
 			       struct phylink_config *c);
 void lan9645x_phylink_port_down(struct lan9645x *lan9645x, int port);
 
+/* VLAN lan9645x_vlan.c */
+int lan9645x_vlan_init(struct lan9645x *lan9645x);
+u16 lan9645x_vlan_unaware_pvid(bool is_bridged);
+void lan9645x_vlan_port_apply(struct lan9645x_port *p);
+int lan9645x_vlan_port_add_vlan(struct lan9645x_port *p, u16 vid, bool pvid,
+				bool untagged,
+				struct netlink_ext_ack *extack);
+int lan9645x_vlan_port_del_vlan(struct lan9645x_port *p, u16 vid);
+void lan9645x_vlan_set_hostmode(struct lan9645x_port *p);
+
 #endif /* __LAN9645X_MAIN_H__ */
diff --git a/drivers/net/dsa/microchip/lan9645x/lan9645x_port.c b/drivers/net/dsa/microchip/lan9645x/lan9645x_port.c
index 2491c51ba7e5..563bffdd20c0 100644
--- a/drivers/net/dsa/microchip/lan9645x/lan9645x_port.c
+++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_port.c
@@ -157,5 +157,11 @@ int lan9645x_port_setup(struct dsa_switch *ds, int port)
 		ANA_PORT_CFG_PORTID_VAL,
 		lan9645x, ANA_PORT_CFG(p->chip_port));
 
+	if (p->chip_port != lan9645x->npi) {
+		mutex_lock(&lan9645x->fwd_domain_lock);
+		lan9645x_vlan_set_hostmode(p);
+		mutex_unlock(&lan9645x->fwd_domain_lock);
+	}
+
 	return 0;
 }
diff --git a/drivers/net/dsa/microchip/lan9645x/lan9645x_vlan.c b/drivers/net/dsa/microchip/lan9645x/lan9645x_vlan.c
new file mode 100644
index 000000000000..6d28a7cf1442
--- /dev/null
+++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_vlan.c
@@ -0,0 +1,402 @@
+// SPDX-License-Identifier: GPL-2.0+
+/* Copyright (C) 2026 Microchip Technology Inc.
+ */
+
+#include "lan9645x_main.h"
+
+#define VLANACCESS_CMD_IDLE		0
+#define VLANACCESS_CMD_READ		1
+#define VLANACCESS_CMD_WRITE		2
+#define VLANACCESS_CMD_INIT		3
+
+struct lan9645x_vlan_port_info {
+	int untagged;
+	int tagged;
+	u16 untagged_vid;
+};
+
+/* Calculate VLAN state of a port, across all VLANS. */
+static void lan9645x_vlan_port_get_info(struct lan9645x *lan9645x, int port,
+					struct lan9645x_vlan_port_info *info)
+{
+	u16 vid;
+
+	info->untagged = 0;
+	info->tagged = 0;
+	info->untagged_vid = 0;
+
+	for (vid = 1; vid <= VLAN_MAX; vid++) {
+		struct lan9645x_vlan *v = &lan9645x->vlans[vid];
+
+		if (!(v->portmask & BIT(port)))
+			continue;
+
+		if (v->untagged & BIT(port)) {
+			info->untagged++;
+			info->untagged_vid = vid;
+		} else {
+			info->tagged++;
+		}
+
+		/* VLAN composition is invalid, so break early. */
+		if (info->untagged > 1 && info->tagged)
+			break;
+	}
+}
+
+static int lan9645x_vlan_wait_for_completion(struct lan9645x *lan9645x)
+{
+	u32 val;
+
+	return lan9645x_rd_poll_timeout(lan9645x, ANA_VLANACCESS, val,
+					ANA_VLANACCESS_VLAN_TBL_CMD_GET(val) ==
+					VLANACCESS_CMD_IDLE);
+}
+
+static int lan9645x_vlan_hw_wr(struct lan9645x *lan9645x, u16 vid)
+{
+	struct lan9645x_vlan *v = &lan9645x->vlans[vid];
+	bool cpu_dis = !(v->portmask & BIT(lan9645x->num_phys_ports));
+	u32 val;
+	int err;
+
+	val = ANA_VLANTIDX_VLAN_PGID_CPU_DIS_SET(cpu_dis) |
+	      ANA_VLANTIDX_V_INDEX_SET(vid);
+
+	lan_wr(val, lan9645x, ANA_VLANTIDX);
+	lan_wr(ANA_VLAN_PORT_MASK_VLAN_PORT_MASK_SET(v->portmask),
+	       lan9645x, ANA_VLAN_PORT_MASK);
+	lan_wr(ANA_VLANACCESS_VLAN_TBL_CMD_SET(VLANACCESS_CMD_WRITE),
+	       lan9645x, ANA_VLANACCESS);
+
+	/* The VLAN access engine completes in a fixed ~1us vs the polling
+	 * timeout of 100_000 us. A timeout here therefore likely means the
+	 * register bus itself is dead, not that the VLAN op failed. There is no
+	 * meaningful recovery at runtime, so this function logs via dev_err()
+	 * and runtime callers discard the return value. Only
+	 * lan9645x_vlan_init() treats this as fatal so that probe fails early
+	 * on a broken bus.
+	 */
+	err = lan9645x_vlan_wait_for_completion(lan9645x);
+	if (err)
+		dev_err(lan9645x->dev, "VLAN set mask failed\n");
+
+	return err;
+}
+
+u16 lan9645x_vlan_unaware_pvid(bool is_bridged)
+{
+	return is_bridged ? UNAWARE_PVID : HOST_PVID;
+}
+
+static u16 lan9645x_vlan_port_get_pvid(struct lan9645x_port *port)
+{
+	bool is_bridged = lan9645x_port_is_bridged(port);
+
+	if (is_bridged && port->vlan_aware)
+		return port->pvid;
+	else
+		return lan9645x_vlan_unaware_pvid(is_bridged);
+}
+
+/* Dynamically choose the egress tagging mode based on the port vlan state:
+ *
+ * Standalone:
+ * TAG_NO_PVID_NO_UNAWARE with PORT_VID=HOST_PVID. This avoids leaking the
+ * internal HOST_PVID tag on ingress mirrored frames while leaving normal
+ * egress frames untagged.
+ *
+ * Bridged, VLAN-aware:
+ *  - N untagged, 0 tagged: TAG_DISABLED
+ *  - 1 untagged, N tagged: TAG_NO_PVID_NO_UNAWARE
+ *  - 0 untagged, N tagged: TAG_ALL
+ *
+ * Bridged, VLAN-unaware:
+ *   TAG_DISABLED
+ */
+static void
+lan9645x_vlan_port_apply_egress(struct lan9645x_port *p,
+				struct lan9645x_vlan_port_info *info)
+{
+	struct lan9645x *lan9645x = p->lan9645x;
+	enum lan9645x_vlan_port_tag tag_cfg;
+	u16 port_vid = UNAWARE_PVID;
+
+	if (!lan9645x_port_is_bridged(p)) {
+		tag_cfg = LAN9645X_TAG_NO_PVID_NO_UNAWARE;
+		port_vid = HOST_PVID;
+	} else if (p->vlan_aware) {
+		struct lan9645x_vlan_port_info _info;
+
+		if (!info) {
+			lan9645x_vlan_port_get_info(lan9645x, p->chip_port,
+						    &_info);
+			info = &_info;
+		}
+
+		if (info->untagged == 1 && info->tagged) {
+			tag_cfg = LAN9645X_TAG_NO_PVID_NO_UNAWARE;
+			port_vid = info->untagged_vid;
+		} else if (info->untagged) {
+			tag_cfg = LAN9645X_TAG_DISABLED;
+		} else {
+			tag_cfg = LAN9645X_TAG_ALL;
+		}
+	} else {
+		tag_cfg = LAN9645X_TAG_DISABLED;
+	}
+
+	/* TAG_TPID_CFG encoding:
+	 *
+	 * 0: Use 0x8100.
+	 * 1: Use 0x88A8.
+	 * 2: Use custom value from PORT_VLAN_CFG.PORT_TPID.
+	 * 3: Use PORT_VLAN_CFG.PORT_TPID, unless ingress tag was a C-tag
+	 *    (EtherType = 0x8100)
+	 *
+	 * Use 3 and PORT_VLAN_CFG.PORT_TPID=0x88a8 to ensure stags are not
+	 * rewritten to ctags on egress.
+	 */
+	lan_rmw(REW_TAG_CFG_TAG_TPID_CFG_SET(3) |
+		REW_TAG_CFG_TAG_CFG_SET(tag_cfg),
+		REW_TAG_CFG_TAG_TPID_CFG |
+		REW_TAG_CFG_TAG_CFG,
+		lan9645x, REW_TAG_CFG(p->chip_port));
+
+	lan_rmw(REW_PORT_VLAN_CFG_PORT_TPID_SET(ETH_P_8021AD) |
+		REW_PORT_VLAN_CFG_PORT_VID_SET(port_vid),
+		REW_PORT_VLAN_CFG_PORT_TPID |
+		REW_PORT_VLAN_CFG_PORT_VID,
+		lan9645x, REW_PORT_VLAN_CFG(p->chip_port));
+}
+
+static void lan9645x_vlan_port_apply_ingress(struct lan9645x_port *p)
+{
+	struct lan9645x *lan9645x = p->lan9645x;
+	u16 pvid;
+	u32 val;
+
+	pvid = lan9645x_vlan_port_get_pvid(p);
+
+	/* Default vlan to classify for untagged frames (may be zero), and set
+	 * their tag type to C-tag.
+	 */
+	val = ANA_VLAN_CFG_VLAN_VID_SET(pvid) |
+	      ANA_VLAN_CFG_VLAN_TAG_TYPE_SET(0);
+	if (p->vlan_aware)
+		val |= ANA_VLAN_CFG_VLAN_AWARE_ENA_SET(1) |
+		       ANA_VLAN_CFG_VLAN_POP_CNT_SET(1);
+
+	lan_rmw(val,
+		ANA_VLAN_CFG_VLAN_VID |
+		ANA_VLAN_CFG_VLAN_AWARE_ENA |
+		ANA_VLAN_CFG_VLAN_POP_CNT |
+		ANA_VLAN_CFG_VLAN_TAG_TYPE,
+		lan9645x, ANA_VLAN_CFG(p->chip_port));
+
+	val = 0;
+	if (p->vlan_aware && !pvid)
+		/* If port is vlan-aware and tagged, drop untagged and priority
+		 * tagged frames.
+		 */
+		val = ANA_DROP_CFG_DROP_UNTAGGED_ENA_SET(1) |
+		      ANA_DROP_CFG_DROP_PRIO_S_TAGGED_ENA_SET(1) |
+		      ANA_DROP_CFG_DROP_PRIO_C_TAGGED_ENA_SET(1);
+
+	lan_rmw(val,
+		ANA_DROP_CFG_DROP_UNTAGGED_ENA |
+		ANA_DROP_CFG_DROP_PRIO_S_TAGGED_ENA |
+		ANA_DROP_CFG_DROP_PRIO_C_TAGGED_ENA,
+		lan9645x, ANA_DROP_CFG(p->chip_port));
+}
+
+void lan9645x_vlan_port_apply(struct lan9645x_port *p)
+{
+	lockdep_assert_held(&p->lan9645x->fwd_domain_lock);
+
+	lan9645x_vlan_port_apply_ingress(p);
+	lan9645x_vlan_port_apply_egress(p, NULL);
+}
+
+static struct lan9645x_vlan *lan9645x_vlan_port_modify(struct lan9645x_port *p,
+						       u16 vid, bool pvid,
+						       bool untagged)
+{
+	struct lan9645x_vlan *v = &p->lan9645x->vlans[vid];
+
+	if (untagged)
+		v->untagged |= BIT(p->chip_port);
+	else
+		v->untagged &= ~BIT(p->chip_port);
+
+	if (pvid)
+		p->pvid = vid;
+	else if (p->pvid == vid)
+		p->pvid = 0;
+
+	return v;
+}
+
+static int lan9645x_vlan_cpu_add(struct lan9645x_port *p, u16 vid)
+{
+	struct lan9645x_vlan *v = &p->lan9645x->vlans[vid];
+
+	v->portmask |= BIT(p->lan9645x->num_phys_ports) | BIT(p->chip_port);
+	lan9645x_vlan_hw_wr(p->lan9645x, vid);
+
+	return 0;
+}
+
+int lan9645x_vlan_port_add_vlan(struct lan9645x_port *p, u16 vid, bool pvid,
+				bool untagged, struct netlink_ext_ack *extack)
+{
+	struct lan9645x *lan9645x = p->lan9645x;
+	struct lan9645x_vlan_port_info info;
+	struct lan9645x_vlan old_vlan;
+	struct lan9645x_vlan *v;
+	u16 old_pvid;
+
+	lockdep_assert_held(&lan9645x->fwd_domain_lock);
+
+	/* Kernel VLAN core adds vid 0, which collides with our UNAWARE_PVID.
+	 * Hardware needs no entry for it: basic VLAN classification replaces a
+	 * VID of 0 with VLAN_CFG.VLAN_VID, keeping the frame's PCP and DEI, so
+	 * a priority tagged frame is classified to the port pvid. Ports with no
+	 * pvid instead drop it, see lan9645x_vlan_port_apply_ingress().
+	 */
+	if (!vid)
+		return 0;
+
+	if (vid > VLAN_MAX) {
+		NL_SET_ERR_MSG_MOD(extack, "VLAN 4095 reserved");
+		return -EINVAL;
+	}
+
+	if (p->chip_port == lan9645x->npi)
+		return lan9645x_vlan_cpu_add(p, vid);
+
+	old_vlan = lan9645x->vlans[vid];
+	old_pvid = p->pvid;
+
+	v = lan9645x_vlan_port_modify(p, vid, pvid, untagged);
+	v->portmask |= BIT(p->chip_port);
+
+	lan9645x_vlan_port_get_info(lan9645x, p->chip_port, &info);
+
+	if (info.untagged > 1 && info.tagged) {
+		*v = old_vlan;
+		p->pvid = old_pvid;
+		if (untagged)
+			NL_SET_ERR_MSG_MOD(extack,
+					   "Port with egress-tagged VLANs cannot have more than one egress-untagged VLAN");
+		else
+			NL_SET_ERR_MSG_MOD(extack,
+					   "Port with more than one egress-untagged VLAN cannot have egress-tagged VLANs");
+		return -EBUSY;
+	}
+
+	lan9645x_vlan_hw_wr(lan9645x, vid);
+	lan9645x_vlan_port_apply_ingress(p);
+	lan9645x_vlan_port_apply_egress(p, &info);
+
+	return 0;
+}
+
+static int lan9645x_vlan_cpu_del(struct lan9645x_port *p, u16 vid)
+{
+	struct lan9645x_vlan *v = &p->lan9645x->vlans[vid];
+
+	v->portmask &= ~(BIT(p->lan9645x->num_phys_ports) |
+			 BIT(p->chip_port));
+	lan9645x_vlan_hw_wr(p->lan9645x, vid);
+
+	return 0;
+}
+
+int lan9645x_vlan_port_del_vlan(struct lan9645x_port *p, u16 vid)
+{
+	struct lan9645x *lan9645x = p->lan9645x;
+	struct lan9645x_vlan *v;
+
+	lockdep_assert_held(&lan9645x->fwd_domain_lock);
+
+	if (!vid)
+		return 0;
+
+	if (vid > VLAN_MAX)
+		return -EINVAL;
+
+	if (p->chip_port == lan9645x->npi)
+		return lan9645x_vlan_cpu_del(p, vid);
+
+	v = lan9645x_vlan_port_modify(p, vid, false, false);
+	v->portmask &= ~BIT(p->chip_port);
+	lan9645x_vlan_hw_wr(lan9645x, vid);
+	lan9645x_vlan_port_apply(p);
+
+	return 0;
+}
+
+void lan9645x_vlan_set_hostmode(struct lan9645x_port *p)
+{
+	lockdep_assert_held(&p->lan9645x->fwd_domain_lock);
+
+	p->vlan_aware = false;
+	p->lan9645x->vlans[HOST_PVID].portmask |= BIT(p->chip_port);
+	lan9645x_vlan_hw_wr(p->lan9645x, HOST_PVID);
+	lan9645x_vlan_port_apply(p);
+}
+
+int lan9645x_vlan_init(struct lan9645x *lan9645x)
+{
+	u32 all_phys_ports, all_ports;
+	struct dsa_port *dp;
+	u16 vid;
+	int err;
+
+	all_phys_ports = GENMASK(lan9645x->num_phys_ports - 1, 0);
+	all_ports = all_phys_ports | BIT(lan9645x->num_phys_ports);
+
+	/* Clear VLAN table, by default all ports are members of all VLANS */
+	lan_wr(ANA_VLANACCESS_VLAN_TBL_CMD_SET(VLANACCESS_CMD_INIT),
+	       lan9645x, ANA_VLANACCESS);
+
+	err = lan9645x_vlan_wait_for_completion(lan9645x);
+	if (err) {
+		dev_err(lan9645x->dev, "VLAN clear table failed\n");
+		return err;
+	}
+
+	for (vid = 1; vid < VLAN_N_VID; vid++) {
+		err = lan9645x_vlan_hw_wr(lan9645x, vid);
+		if (err)
+			return err;
+	}
+
+	/* Set all the ports + cpu to be part of HOST_PVID and UNAWARE_PVID */
+	lan9645x->vlans[HOST_PVID].portmask = all_ports;
+	err = lan9645x_vlan_hw_wr(lan9645x, HOST_PVID);
+	if (err)
+		return err;
+
+	lan9645x->vlans[UNAWARE_PVID].portmask = all_ports;
+	err = lan9645x_vlan_hw_wr(lan9645x, UNAWARE_PVID);
+	if (err)
+		return err;
+
+	/* Configure the CPU port module to be vlan aware */
+	lan_wr(ANA_VLAN_CFG_VLAN_VID_SET(UNAWARE_PVID) |
+	       ANA_VLAN_CFG_VLAN_AWARE_ENA_SET(1) |
+	       ANA_VLAN_CFG_VLAN_POP_CNT_SET(1),
+	       lan9645x, ANA_VLAN_CFG(lan9645x->num_phys_ports));
+
+	/* Set vlan ingress filter mask to all ports */
+	lan_wr(all_ports, lan9645x, ANA_VLANMASK);
+
+	dsa_switch_for_each_user_port(dp, lan9645x->ds) {
+		lan_wr(0, lan9645x, REW_PORT_VLAN_CFG(dp->index));
+		lan_wr(0, lan9645x, REW_TAG_CFG(dp->index));
+	}
+
+	return 0;
+}
-- 
2.52.0

[PATCH net-next v12 4/9] net: dsa: lan9645x: add basic dsa driver for LAN9645X

From: Jens Emil Schulz Østergaard <hidden>
Date: 2026-09-08 07:41:34

Add the LAN9645X basic DSA driver with initialization, parent regmap
requests, port module initialization for NPI, CPU ports and front ports,
and phylink integration for MAC side configuration.

This patch only wires up the internal CuPHYs (ports 0-4) and the RGMII
paths for ports 7 and 8. The supported SerDes-based modes: SGMII,
QSGMII, 1000BASE-X and 2500BASE-X on ports 5-8 are deferred, and support
will be added in follow-up patches. Port 5-6 have no usable interface
mode in this patch, and should not be declared in DT until SerDes
support lands.

If port 5 or 6 is declared in DT as a user port, phylink_create()
rejects the empty supported_interfaces and dsa_tree_setup_ports() falls
back to setting the port up as unused, so the switch still probes.
DSA logs an error including the port number. Declared as a CPU port
there is no such fallback, and the probe fails, and our driver logs an
error including the port number.

The intended way to bind this driver is using a parent MFD driver,
responsible for the register IO protocol, and distributing regmaps to
child devices. The goal is to use the same approach as the MFD driver in
drivers/mfd/ocelot-spi.c, which also resets the chip before it
instantiates its children.

This driver expects to request named regmaps from a parent device. This
approach is similar to the DSA driver

drivers/net/dsa/ocelot/ocelot_ext.c

which supports being driven by an external CPU via SPI with parent
device drivers/mfd/ocelot-spi.c.

The MFD driver will come in a later series, because there are
requirements on the number of child devices before a driver qualifies as
a MFD device.

Reviewed-by: Steen Hegelund <Steen.Hegelund@microchip.com>
Signed-off-by: Jens Emil Schulz Østergaard <redacted>
---
Changes in v12:
- Drop lan9645x_port_set_tail_drop_wm(). This tail dropping config
  is only active under cut through forwarding, which we never enable.
- lan9645x_reset_switch(): skip the memory init when the switch core is
  already enabled. We expect parent device to reset before probe, but if
  it does not, we must skip memory init if switch core is already
  enabled.
- Drop unused shared_queue_sz field and LAN9645X_BUFFER_MEMORY define.
- Set ds->max_num_bridges = 1 next to ds->fdb_isolation. We support a
  single offloaded bridge, so let the core enforce it.
- Reject an NPI port with no usable interface mode.
- Kconfig: select REGMAP.
- Include the CPU port module in the unicast and multicast flood masks.
  Host address filtering needs the fdb and mdb operations, and unknown
  traffic needs port_set_host_flood(), so until bridge support lands the
  host depends on flooding to receive anything.
- Describe in the commit what happens if port 5 or 6 is declared in DT.
- Drop unused register writes from lan9645x_port_cpu_init().
- Update PGID table comment.
- Drop the unused PGID_AGGR_NUM, LAN9645X_ISDX_MAX, LAN9645X_ESDX_MAX and
  LAN9645X_SFID_MAX defines, along with PGID_ENTRIES.
- Follow the LAN9645X_IFH_LEN rename in the tag driver.
- Drop ds->dscp_prio_mapping_is_global. It is only consulted after
  port_add_dscp_prio() or port_del_dscp_prio() succeeds, neither of which
  this series implements.
- Program QSYS_RES_CFG through lan9645x_wm_enc().
- Return -ENOMEM directly, not via dev_err_probe(), and add the missing
  newlines to the remaining probe messages.
- lan9645x_npi_port_deinit(): restore the NPI bit in the CPU port module
  source PGID that lan9645x_npi_port_init() cleared.
- Spell the part LAN9645x in the Kconfig prompt, the help text and
  MODULE_DESCRIPTION.
- Drop the unread NULL sentinel from lan9645x_resource_names[].
- Resolve the regmap and the register address once in
  lan9645x_rd_poll_timeout() and lan9645x_rd_poll_slow().
- lan9645x_get_max_mtu(): drop the NPI case. DSA only asks for the user
  port limit and adds the tag overhead back itself when it sizes the
  conduit MTU.
- lan9645x_phylink_mac_prepare(): drop the port range check. Ports 5 and 6
  get no phylink instance, and ds->num_ports bounds the rest.
- lan9645x_phylink_mac_link_up(): drop the unreachable SPEED_2500 case,
  and the DEV_CLOCK_CFG.LINK_SPEED fixup it forced.
  lan9645x_phylink_get_caps() does not advertise MAC_2500FD.
- Fix DEV_MAC_IFG_CFG.TX_IFG, which had its full and half duplex values
  swapped.
- Drop the unused afi target regmap request.
- Name the first per port regmap dev0 rather than dev, so the nine read
  uniformly.
- Bound the ANA_PGID_CFG.OBEY_VLAN loop at PGID_AGGR.
- lan9645x_phylink_mac_prepare(): drop the HSIO_HW_CFG.RGMII_0_CFG write.
  The field resets to DEV7 and nothing else writes it.
- lan9645x_phylink_port_down(): restore QSYS_SW_PORT_MODE.AGING_MODE to 2
  after the flush, which is the reset value, rather than 1. Mode 1 exempts
  frames from the CPU interface from the transmit ageing that
  lan9645x_setup() enables, and every port sat in it from probe.

Changes in v9:
- Drop the DEV4 RGMII muxing support.

Changes in v8:
- Remove the CPU_PORT macro, which named the chips internal CPU port
  module and collided with the DSA CPU port terminology. Reference the
  module as lan9645x->num_phys_ports.
- Rework the port map comment to distinguish the chip CPU port modules
  (indices 9-10) from the DSA CPU port (the NPI front port).
- Derive PGID_GP_START from NUM_PHYS_PORTS instead of CPU_PORT.
- Add a port_mux_lock mutex to serialize port mux arbitration in
  lan9645x_phylink_mac_prepare().

Changes in v7:
- use cpu queue LAN9645X_CPUQ_TRAP for bpdu frames

Changes in v5:
- remove dd_dis and tsn_dis, and add SKU supported port validation
  during setup
- phylink: remove MAC_2500FD
- phylink: add comment about empty supported_interfaces for port 5-6.
- phylink: fix 2:1 rgmii port muxing for port module 4 and 7 to be fully
  dynamic and validate requested mux settings.
- phylink: add comment about 2:1 rgmii port muxing for port module 4
  and 7.
- rx/tx-internal-delay-ps checked against support 2ns value
- update commit message
- init lan9645x->npi = -1 at probe and check port < 0 in npi_deinit

Changes in v4:
- add comment to QSYS_RES_CFG configuration
- phylink_mac_prepare: fix to make sure we can dynamically change rgmii
  on port 4
- move ports allocation to probe
- tag_npi_setup: reject cascaded setups
- skip WARN_ON in lan9645x_to_port

Changes in v3:
- move DEV_MAC_TAGS_CFG config to port setup, for vlan overhead in port
  frame maxlen
- remove code disabling ipv6 on conduit
- use of_property_read_u32 for {rx,tx}-internal-delay-ps
- use dsa_user_ports(ds) instead of
  GENMASK(lan9645x->num_phys_ports - 1, 0) as base flood mask.
- update obey vlan comment

Changes in v2:
- source Kconfig from drivers/net/dsa/Kconfig
- sorting in Kconfig and Makefiles
- remove unused struct fields
- remote path delays
- use port_setup and dp->dn instead of DTS parsing
- phylink: split rgmii setup into dll and speed config
- phylink: remove pcs/sgmii/qsgmii related code
- phylink: simplify mac_prepare
- phylink: remove phylink_ops wrappers
- phylink: remove unrelated config from link up
- phylink: reorder functions according to phylink call order
---
 drivers/net/dsa/Kconfig                            |   2 +
 drivers/net/dsa/microchip/Makefile                 |   1 +
 drivers/net/dsa/microchip/lan9645x/Kconfig         |  12 +
 drivers/net/dsa/microchip/lan9645x/Makefile        |   8 +
 drivers/net/dsa/microchip/lan9645x/lan9645x_main.c | 456 +++++++++++++++++++++
 drivers/net/dsa/microchip/lan9645x/lan9645x_main.h | 347 ++++++++++++++++
 drivers/net/dsa/microchip/lan9645x/lan9645x_npi.c  |  90 ++++
 .../net/dsa/microchip/lan9645x/lan9645x_phylink.c  | 361 ++++++++++++++++
 drivers/net/dsa/microchip/lan9645x/lan9645x_port.c | 161 ++++++++
 9 files changed, 1438 insertions(+)
diff --git a/drivers/net/dsa/Kconfig b/drivers/net/dsa/Kconfig
index 8d8edcf89f10..e7e28abb33d8 100644
--- a/drivers/net/dsa/Kconfig
+++ b/drivers/net/dsa/Kconfig
@@ -82,6 +82,8 @@ config NET_DSA_MV88E6060
 
 source "drivers/net/dsa/microchip/Kconfig"
 
+source "drivers/net/dsa/microchip/lan9645x/Kconfig"
+
 source "drivers/net/dsa/motorcomm/Kconfig"
 
 source "drivers/net/dsa/mv88e6xxx/Kconfig"
diff --git a/drivers/net/dsa/microchip/Makefile b/drivers/net/dsa/microchip/Makefile
index 9347cfb3d0b5..e75f17888f75 100644
--- a/drivers/net/dsa/microchip/Makefile
+++ b/drivers/net/dsa/microchip/Makefile
@@ -12,3 +12,4 @@ endif
 obj-$(CONFIG_NET_DSA_MICROCHIP_KSZ9477_I2C)	+= ksz9477_i2c.o
 obj-$(CONFIG_NET_DSA_MICROCHIP_KSZ_SPI)		+= ksz_spi.o
 obj-$(CONFIG_NET_DSA_MICROCHIP_KSZ8863_SMI)	+= ksz8863_smi.o
+obj-$(CONFIG_NET_DSA_MICROCHIP_LAN9645X)	+= lan9645x/
diff --git a/drivers/net/dsa/microchip/lan9645x/Kconfig b/drivers/net/dsa/microchip/lan9645x/Kconfig
new file mode 100644
index 000000000000..4e3e0d2f5846
--- /dev/null
+++ b/drivers/net/dsa/microchip/lan9645x/Kconfig
@@ -0,0 +1,12 @@
+# SPDX-License-Identifier: GPL-2.0-only
+config NET_DSA_MICROCHIP_LAN9645X
+	tristate "Microchip LAN9645x switch support"
+	depends on NET_DSA
+	select NET_DSA_TAG_LAN9645X
+	select REGMAP
+	help
+	  This driver adds DSA support for Microchip LAN9645x switch chips.
+	  The lan9645x switch is a multi-port Gigabit AVB/TSN Ethernet Switch
+	  with five integrated 10/100/1000Base-T PHYs. In addition to the
+	  integrated PHYs, it supports up to 2 RGMII/RMII, up to 2
+	  BASE-X/SERDES/2.5GBASE-X and one Quad-SGMII/Quad-USGMII interfaces.
diff --git a/drivers/net/dsa/microchip/lan9645x/Makefile b/drivers/net/dsa/microchip/lan9645x/Makefile
new file mode 100644
index 000000000000..7cc0ae0ada40
--- /dev/null
+++ b/drivers/net/dsa/microchip/lan9645x/Makefile
@@ -0,0 +1,8 @@
+# SPDX-License-Identifier: GPL-2.0-only
+obj-$(CONFIG_NET_DSA_MICROCHIP_LAN9645X) += mchp-lan9645x.o
+
+mchp-lan9645x-objs := \
+	lan9645x_main.o \
+	lan9645x_npi.o \
+	lan9645x_phylink.o \
+	lan9645x_port.o \
diff --git a/drivers/net/dsa/microchip/lan9645x/lan9645x_main.c b/drivers/net/dsa/microchip/lan9645x/lan9645x_main.c
new file mode 100644
index 000000000000..c7de3836e696
--- /dev/null
+++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_main.c
@@ -0,0 +1,456 @@
+// SPDX-License-Identifier: GPL-2.0+
+/* Copyright (C) 2026 Microchip Technology Inc.
+ */
+
+#include <linux/platform_device.h>
+
+#include "lan9645x_main.h"
+
+static const char *lan9645x_resource_names[NUM_TARGETS] = {
+	[TARGET_GCB]          = "gcb",
+	[TARGET_QS]           = "qs",
+	[TARGET_CHIP_TOP]     = "chip_top",
+	[TARGET_REW]          = "rew",
+	[TARGET_SYS]          = "sys",
+	[TARGET_HSIO]         = "hsio",
+	[TARGET_DEV + 0]      = "dev0",
+	[TARGET_DEV + 1]      = "dev1",
+	[TARGET_DEV + 2]      = "dev2",
+	[TARGET_DEV + 3]      = "dev3",
+	[TARGET_DEV + 4]      = "dev4",
+	[TARGET_DEV + 5]      = "dev5",
+	[TARGET_DEV + 6]      = "dev6",
+	[TARGET_DEV + 7]      = "dev7",
+	[TARGET_DEV + 8]      = "dev8",
+	[TARGET_QSYS]         = "qsys",
+	[TARGET_ANA]          = "ana",
+};
+
+static int lan9645x_tag_npi_setup(struct dsa_switch *ds)
+{
+	struct dsa_port *dp, *first_cpu_dp = NULL;
+	struct lan9645x *lan9645x = ds->priv;
+	struct phylink_config c = {};
+
+	dsa_switch_for_each_user_port(dp, ds) {
+		if (dp->cpu_dp->ds != ds) {
+			dev_err(ds->dev,
+				"NPI port on a remote switch is not supported\n");
+			return -EINVAL;
+		}
+
+		if (first_cpu_dp && dp->cpu_dp != first_cpu_dp) {
+			dev_err(ds->dev, "Multiple NPI ports not supported\n");
+			return -EINVAL;
+		}
+
+		first_cpu_dp = dp->cpu_dp;
+	}
+
+	if (!first_cpu_dp)
+		return -EINVAL;
+
+	lan9645x_phylink_get_caps(lan9645x, first_cpu_dp->index, &c);
+	if (phy_interface_empty(c.supported_interfaces)) {
+		dev_err(ds->dev,
+			"Port %d has no usable interface mode, cannot be NPI port\n",
+			first_cpu_dp->index);
+		return -EINVAL;
+	}
+
+	lan9645x_npi_port_init(lan9645x, first_cpu_dp);
+
+	return 0;
+}
+
+static enum dsa_tag_protocol lan9645x_get_tag_protocol(struct dsa_switch *ds,
+						       int port,
+						       enum dsa_tag_protocol tp)
+{
+	return DSA_TAG_PROTO_LAN9645X;
+}
+
+static void lan9645x_teardown(struct dsa_switch *ds)
+{
+	struct lan9645x *lan9645x = ds->priv;
+
+	lan9645x_npi_port_deinit(lan9645x, lan9645x->npi);
+}
+
+static int lan9645x_change_mtu(struct dsa_switch *ds, int port, int new_mtu)
+{
+	return lan9645x_port_set_maxlen(ds->priv, port, new_mtu);
+}
+
+static int lan9645x_get_max_mtu(struct dsa_switch *ds, int port)
+{
+	/* Actual MAC max MTU is around 16KB. We set 10000 - overhead which
+	 * should be sufficient for all jumbo frames. Larger frames can cause
+	 * problems especially with flow control, since we only have 160K queue
+	 * buffer.
+	 *
+	 * DSA only asks for the user port limit, and adds the tag overhead
+	 * back when it sizes the conduit MTU, so the CPU port is not a case
+	 * here.
+	 */
+	return 10000 - ETH_HLEN - ETH_FCS_LEN;
+}
+
+static int lan9645x_reset_switch(struct lan9645x *lan9645x)
+{
+	u32 val = 0;
+	int err;
+
+	/* The parent must reset the whole chip before instantiating its
+	 * children, so the switch core is disabled when we get here. The
+	 * sequence below must not run more than once: initializing the
+	 * memories on a running core corrupts pointers inside the queue
+	 * system, which eventually fills up and stops passing traffic. If the
+	 * core is already enabled the chip was not reset, so skip it.
+	 */
+	if (SYS_RESET_CFG_CORE_ENA_GET(lan_rd(lan9645x, SYS_RESET_CFG)))
+		return 0;
+
+	lan_wr(SYS_RESET_CFG_CORE_ENA_SET(0), lan9645x, SYS_RESET_CFG);
+	lan_wr(SYS_RAM_INIT_RAM_INIT_SET(1), lan9645x, SYS_RAM_INIT);
+	err = lan9645x_rd_poll_timeout(lan9645x, SYS_RAM_INIT, val,
+				       SYS_RAM_INIT_RAM_INIT_GET(val) == 0);
+	if (err) {
+		dev_err(lan9645x->dev, "Failed to init chip RAM.\n");
+		return err;
+	}
+	lan_wr(SYS_RESET_CFG_CORE_ENA_SET(1), lan9645x, SYS_RESET_CFG);
+
+	return 0;
+}
+
+static int lan9645x_setup(struct dsa_switch *ds)
+{
+	struct lan9645x *lan9645x = ds->priv;
+	struct device *dev = lan9645x->dev;
+	u32 front_ports, res_wm;
+	int supported, used = 0;
+	struct dsa_port *dp;
+	int err;
+
+	lan9645x->num_phys_ports = ds->num_ports;
+	front_ports = dsa_user_ports(ds);
+	supported = lan9645x->num_phys_ports - lan9645x->num_port_dis;
+
+	dsa_switch_for_each_available_port(dp, ds)
+		used++;
+
+	if (used > supported) {
+		dev_err(ds->dev,
+			"%d physical ports in use in DT, SKU supports at most %d\n",
+			used, supported);
+		return -EINVAL;
+	}
+
+	err = lan9645x_reset_switch(lan9645x);
+	if (err)
+		return err;
+
+	err = lan9645x_tag_npi_setup(ds);
+	if (err) {
+		dev_err(dev, "Failed to setup NPI port.\n");
+		return err;
+	}
+
+	/* Link Aggregation Mode: NETDEV_LAG_HASH_L2 */
+	lan_wr(ANA_AGGR_CFG_AC_SMAC_ENA |
+	       ANA_AGGR_CFG_AC_DMAC_ENA,
+	       lan9645x, ANA_AGGR_CFG);
+
+	/* Flush queues */
+	lan_wr(GENMASK(1, 0), lan9645x, QS_XTR_FLUSH);
+
+	/* Allow to drain */
+	usleep_range(1000, 2000);
+
+	/* All Queues normal */
+	lan_wr(0x0, lan9645x, QS_XTR_FLUSH);
+
+	/* Set MAC age time to default value, the entry is aged after
+	 * 2 * AGE_PERIOD
+	 */
+	lan_wr(ANA_AUTOAGE_AGE_PERIOD_SET(BR_DEFAULT_AGEING_TIME / 2 / HZ),
+	       lan9645x, ANA_AUTOAGE);
+
+	/* Disable learning for frames discarded by VLAN ingress filtering */
+	lan_rmw(ANA_ADVLEARN_VLAN_CHK_SET(1),
+		ANA_ADVLEARN_VLAN_CHK,
+		lan9645x, ANA_ADVLEARN);
+
+	/* Queue system frame ageing. We target 2s ageing.
+	 *
+	 * Register unit is 1024 cycles.
+	 *
+	 * ASIC: 165.625 Mhz  ~ 6.0377 ns period
+	 *
+	 * 1024 * 6.0377 ns =~ 6182 ns
+	 * val = 2000000000ns / 6182ns
+	 */
+	lan_wr(SYS_FRM_AGING_AGE_TX_ENA_SET(1) |
+	       SYS_FRM_AGING_MAX_AGE_SET((2000000000 / 6182)),
+	       lan9645x,  SYS_FRM_AGING);
+
+	/* Setup flooding PGIDs for IPv4/IPv6 multicast. Control and dataplane
+	 * use the same masks. Control frames are redirected to CPU, and
+	 * the network stack is responsible for forwarding these.
+	 * The dataplane is forwarding according to the offloaded MDB entries.
+	 */
+	lan_wr(ANA_FLOODING_IPMC_FLD_MC4_DATA_SET(PGID_MCIPV4) |
+	       ANA_FLOODING_IPMC_FLD_MC4_CTRL_SET(PGID_MC) |
+	       ANA_FLOODING_IPMC_FLD_MC6_DATA_SET(PGID_MCIPV6) |
+	       ANA_FLOODING_IPMC_FLD_MC6_CTRL_SET(PGID_MC),
+	       lan9645x, ANA_FLOODING_IPMC);
+
+	for (int prio = 0; prio < NUM_PRIO_QUEUES; ++prio)
+		lan_wr(ANA_FLOODING_FLD_MULTICAST_SET(PGID_MC) |
+		       ANA_FLOODING_FLD_UNICAST_SET(PGID_UC) |
+		       ANA_FLOODING_FLD_BROADCAST_SET(PGID_BC),
+		       lan9645x, ANA_FLOODING(prio));
+
+	/* Allow VLAN table to control whether cpu copy from the pgid table is
+	 * enabled. Only the destination analysis resolves a CPU copy, so the
+	 * aggregation and source regions are left at their reset value.
+	 */
+	for (int i = 0; i < PGID_AGGR; ++i)
+		lan_wr(ANA_PGID_CFG_OBEY_VLAN_SET(1),
+		       lan9645x, ANA_PGID_CFG(i));
+
+	/* Disable bridging by default */
+	for (int p = 0; p < lan9645x->num_phys_ports; p++) {
+		lan_wr(0, lan9645x, ANA_PGID(PGID_SRC + p));
+
+		/* Do not forward BPDU frames to the front ports and trap them
+		 * to CPU
+		 */
+		lan_wr(ANA_CPU_FWD_BPDU_CFG_BPDU_REDIR_ENA,
+		       lan9645x, ANA_CPU_FWD_BPDU_CFG(p));
+	}
+
+	for (int i = 0; i < 16; i++) {
+		/* The register instance number corresponds to the
+		 * address of the extracted frame. For instance:
+		 * CPUQ_8021_CFG[4].CPUQ_BPDU_VAL
+		 * is the cpu extraction queue used for BPDU frames
+		 * with address 01-80-C2-00-00-04
+		 */
+		lan_rmw(ANA_CPUQ_8021_CFG_CPUQ_BPDU_VAL_SET(LAN9645X_CPUQ_TRAP),
+			ANA_CPUQ_8021_CFG_CPUQ_BPDU_VAL,
+			lan9645x, ANA_CPUQ_8021_CFG(i));
+	}
+
+	/* Reserve 1664 bytes, 26 buffer cells, per (port, prio) for source
+	 * tracking (resource 0, indices 0..95) and destination tracking
+	 * (resource 2, indices 512..607). These are access watermarks, not
+	 * pre-allocations: a flow draws from its reservation first, then
+	 * from the shared pool. Keeping the reservation above a max-size
+	 * Ethernet frame prevents a single frame from spilling into the
+	 * shared pool, and cause pause frames to be emitted without actual
+	 * congestion.
+	 */
+	res_wm = QSYS_RES_CFG_WM_HIGH_SET(lan9645x_wm_enc(QS_SRC_BUF_RSV));
+	for (int i = 0; i <= QSYS_Q_RSRV; ++i) {
+		lan_wr(res_wm, lan9645x, QSYS_RES_CFG(i));
+		lan_wr(res_wm, lan9645x, QSYS_RES_CFG(512 + i));
+	}
+
+	lan9645x_port_cpu_init(lan9645x);
+
+	/* Multicast to cpu and all front ports */
+	lan_wr(BIT(lan9645x->num_phys_ports) | front_ports, lan9645x,
+	       ANA_PGID(PGID_MC));
+
+	/* IP multicast to cpu and all front ports */
+	lan_wr(BIT(lan9645x->num_phys_ports) | front_ports, lan9645x,
+	       ANA_PGID(PGID_MCIPV4));
+	lan_wr(BIT(lan9645x->num_phys_ports) | front_ports, lan9645x,
+	       ANA_PGID(PGID_MCIPV6));
+
+	/* Unicast to cpu and all front ports */
+	lan_wr(BIT(lan9645x->num_phys_ports) | front_ports, lan9645x,
+	       ANA_PGID(PGID_UC));
+
+	/* Broadcast to cpu and all front ports */
+	lan_wr(BIT(lan9645x->num_phys_ports) | front_ports, lan9645x,
+	       ANA_PGID(PGID_BC));
+
+	ds->mtu_enforcement_ingress = true;
+	ds->assisted_learning_on_cpu_port = true;
+	ds->fdb_isolation = true;
+	ds->max_num_bridges = 1;
+
+	dev_info(lan9645x->dev,
+		 "SKU features: max_ports=%d\n",
+		 lan9645x->num_phys_ports - lan9645x->num_port_dis);
+
+	return 0;
+}
+
+static void lan9645x_port_phylink_get_caps(struct dsa_switch *ds, int port,
+					   struct phylink_config *config)
+{
+	lan9645x_phylink_get_caps(ds->priv, port, config);
+}
+
+static const struct dsa_switch_ops lan9645x_switch_ops = {
+	.get_tag_protocol		= lan9645x_get_tag_protocol,
+
+	.setup				= lan9645x_setup,
+	.teardown			= lan9645x_teardown,
+	.port_setup			= lan9645x_port_setup,
+
+	/* Phylink integration */
+	.phylink_get_caps		= lan9645x_port_phylink_get_caps,
+
+	/* MTU  */
+	.port_change_mtu		= lan9645x_change_mtu,
+	.port_max_mtu			= lan9645x_get_max_mtu,
+};
+
+static int lan9645x_request_target_regmaps(struct lan9645x *lan9645x)
+{
+	const char *resource_name;
+	struct regmap *tgt_map;
+
+	for (int i = 0; i < NUM_TARGETS; i++) {
+		resource_name = lan9645x_resource_names[i];
+		if (!resource_name)
+			continue;
+
+		tgt_map = dev_get_regmap(lan9645x->dev->parent, resource_name);
+		if (IS_ERR_OR_NULL(tgt_map)) {
+			dev_err(lan9645x->dev, "Failed to get regmap=%s\n",
+				resource_name);
+			return -ENODEV;
+		}
+
+		lan9645x->rmap[i] = tgt_map;
+	}
+
+	return 0;
+}
+
+static void lan9645x_set_feat_dis(struct lan9645x *lan9645x)
+{
+	u32 feat_dis;
+
+	/* The features which can be physically disabled on some SKUs are:
+	 * 1) Number of ports can be 5, 7 or 9. Any ports can be used, the chip
+	 *    tracks how many are active.
+	 * 2) HSR/PRP. The duplicate discard table can be disabled.
+	 * 3) TAS, frame preemption and PSFP can be disabled.
+	 */
+	feat_dis = lan_rd(lan9645x, GCB_FEAT_DISABLE);
+
+	lan9645x->num_port_dis =
+		GCB_FEAT_DISABLE_FEAT_NUM_PORTS_DIS_GET(feat_dis);
+}
+
+static int lan9645x_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct lan9645x *lan9645x;
+	struct dsa_switch *ds;
+	int err = 0;
+
+	lan9645x = devm_kzalloc(dev, sizeof(*lan9645x), GFP_KERNEL);
+	if (!lan9645x)
+		return -ENOMEM;
+
+	dev_set_drvdata(dev, lan9645x);
+	lan9645x->dev = dev;
+
+	err = lan9645x_request_target_regmaps(lan9645x);
+	if (err)
+		return dev_err_probe(dev, err, "Failed to request regmaps\n");
+
+	ds = devm_kzalloc(dev, sizeof(*ds), GFP_KERNEL);
+	if (!ds)
+		return -ENOMEM;
+
+	lan9645x->ports = devm_kcalloc(lan9645x->dev, NUM_PHYS_PORTS,
+				       sizeof(struct lan9645x_port *),
+				       GFP_KERNEL);
+	if (!lan9645x->ports)
+		return -ENOMEM;
+
+	for (int port = 0; port < NUM_PHYS_PORTS; port++) {
+		struct lan9645x_port *p;
+
+		p = devm_kzalloc(lan9645x->dev, sizeof(*p), GFP_KERNEL);
+		if (!p)
+			return -ENOMEM;
+
+		p->lan9645x = lan9645x;
+		p->chip_port = port;
+		lan9645x->ports[port] = p;
+	}
+
+	ds->dev = dev;
+	ds->num_ports = NUM_PHYS_PORTS;
+	ds->num_tx_queues = NUM_PRIO_QUEUES;
+
+	ds->ops = &lan9645x_switch_ops;
+	ds->phylink_mac_ops = &lan9645x_phylink_mac_ops;
+	ds->priv = lan9645x;
+
+	lan9645x->ds = ds;
+	lan9645x->npi = -1;
+
+	lan9645x_set_feat_dis(lan9645x);
+
+	err = dsa_register_switch(ds);
+	if (err)
+		return dev_err_probe(dev, err, "Failed to register DSA switch\n");
+
+	return 0;
+}
+
+static void lan9645x_remove(struct platform_device *pdev)
+{
+	struct lan9645x *lan9645x = dev_get_drvdata(&pdev->dev);
+
+	if (!lan9645x)
+		return;
+
+	/* Calls lan9645x DSA .teardown */
+	dsa_unregister_switch(lan9645x->ds);
+	dev_set_drvdata(&pdev->dev, NULL);
+}
+
+static void lan9645x_shutdown(struct platform_device *pdev)
+{
+	struct lan9645x *lan9645x = dev_get_drvdata(&pdev->dev);
+
+	if (!lan9645x)
+		return;
+
+	dsa_switch_shutdown(lan9645x->ds);
+
+	dev_set_drvdata(&pdev->dev, NULL);
+}
+
+static const struct of_device_id lan9645x_switch_of_match[] = {
+	{ .compatible = "microchip,lan96455s-switch" },
+	{},
+};
+MODULE_DEVICE_TABLE(of, lan9645x_switch_of_match);
+
+static struct platform_driver lan9645x_switch_driver = {
+	.driver = {
+		.name = "lan96455s-switch",
+		.of_match_table = lan9645x_switch_of_match,
+	},
+	.probe = lan9645x_probe,
+	.remove = lan9645x_remove,
+	.shutdown = lan9645x_shutdown,
+};
+module_platform_driver(lan9645x_switch_driver);
+
+MODULE_DESCRIPTION("LAN9645x Switch Driver");
+MODULE_AUTHOR("Jens Emil Schulz Østergaard <jensemil.schulzostergaard@microchip.com>");
+MODULE_LICENSE("GPL");
diff --git a/drivers/net/dsa/microchip/lan9645x/lan9645x_main.h b/drivers/net/dsa/microchip/lan9645x/lan9645x_main.h
new file mode 100644
index 000000000000..81acacca7f2d
--- /dev/null
+++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_main.h
@@ -0,0 +1,347 @@
+/* SPDX-License-Identifier: GPL-2.0+ */
+/* Copyright (C) 2026 Microchip Technology Inc.
+ */
+
+#ifndef __LAN9645X_MAIN_H__
+#define __LAN9645X_MAIN_H__
+
+#include <linux/dsa/lan9645x.h>
+#include <linux/if_bridge.h>
+#include <linux/regmap.h>
+#include <net/dsa.h>
+
+#include "lan9645x_regs.h"
+
+/* Port modules 0-8 are front (user) ports. The chip additionally has two
+ * logical CPU port modules at indices 9 and 10. These are not the DSA CPU port.
+ * The CPU port modules are logical ports in the chip intended for management.
+ *
+ * The frame delivery mechanism can vary: direct register injection/extraction,
+ * or a front port can be used as the management port, called a Node Processor
+ * Interface (NPI) in the datasheet.
+ *
+ * LAN9645X uses the NPI approach, so the DSA CPU port is a front port
+ * (see lan9645x->npi) configured as NPI port.
+ *
+ * Therefore the CPU datapath has two port module indices of interest,
+ * lan9645x->npi and the cpu port module at index 9.
+ */
+#define NUM_PHYS_PORTS		9
+
+#define NUM_PRIO_QUEUES		8
+
+#define QS_SRC_BUF_RSV		1664
+
+/* Reserved amount for (SRC, PRIO) at index 8*SRC + PRIO
+ * See QSYS:RES_CTRL[*]:RES_CFG description
+ */
+#define QSYS_Q_RSRV			95
+
+/* Reserved VLAN IDs. */
+#define UNAWARE_PVID			0
+#define HOST_PVID			4095
+#define VLAN_MAX			(HOST_PVID - 1)
+
+/* Port Group Identifiers (PGID) are port-masks applied to all frames.
+ *
+ * The forwarding engine outputs two masks for the forwarding decision, DEST and
+ * CPUQ. DEST determines which front ports will receive a frame, and CPUQ
+ * whether the frame is forwarded to the CPU.
+ *
+ * The replicated registers are organized like so in HW:
+ *
+ * 0-63:         Destination analysis.
+ * 64-79:        Aggregation analysis
+ * 80-(80+10-1): Source port analysis
+ *
+ * Destination: Destination PGIDs are the result of mac table lookups or
+ * flooding decisions. The resolved destination PGID will feed into DEST and set
+ * the bit for ANA_PGID_CFG.CPUQ_DST_PGID in CPUQ, if the CPU port module bit is
+ * set in the PGID. This is how PGID_CPU and host flooding work.
+ *
+ * The first NUM_PHYS_PORTS destination PGIDs can not be used freely, because
+ * dynamic learning writes mac table entries with DEST_IDX taken from the
+ * ingress ports ANA_PORT_CFG.PORTID_VAL. They come out of reset as BIT(i)
+ * for that reason.
+ *
+ * PGID NUM_PHYS_PORTS..63 come out of reset as zero and have no reserved
+ * role. We use them for L2 Multicast and flooding (see FLOODING and
+ * FLOODING_IPMC) and reserve a few at the end of the range. The CPU port module
+ * has no reserved destination PGID. It does have a reserved source PGID at
+ * PGID_SRC + NUM_PHYS_PORTS.
+ *
+ * Aggregation: Each frame receives a link aggregation code from the analyzer,
+ * based on a configurable algorithm. This code picks out one of the 16
+ * aggregation PGIDs to mask out ports in DEST. If no aggregation is configured,
+ * these are all-ones. The CPU port module bit is inert here.
+ *
+ * Source: Control which ports a given source port can forward to. A frame that
+ * is received on port n, uses entry PGID_SRC + n to mask out DEST.
+ * The default values are all bits set except for the index itself
+ * (no loopback). The CPU port module bit is inert here.
+ */
+
+#define PGID_AGGR			64
+#define PGID_SRC			80
+
+/* General purpose PGIDs. */
+#define PGID_GP_START			NUM_PHYS_PORTS
+#define PGID_GP_END			PGID_MRP
+
+/* Reserved PGIDs.
+ * PGID_MRP is a blackhole PGID
+ */
+#define PGID_MRP			(PGID_AGGR - 7)
+#define PGID_CPU			(PGID_AGGR - 6)
+#define PGID_UC				(PGID_AGGR - 5)
+#define PGID_BC				(PGID_AGGR - 4)
+#define PGID_MC				(PGID_AGGR - 3)
+#define PGID_MCIPV4			(PGID_AGGR - 2)
+#define PGID_MCIPV6			(PGID_AGGR - 1)
+
+/* Flooding PGIDS:
+ * PGID_UC
+ * PGID_MC*
+ * PGID_BC
+ */
+
+#define GWM_MULTIPLIER_BIT		BIT(8)
+#define LAN9645X_BUFFER_CELL_SZ		64
+
+#define RD_SLEEP_US			3
+#define RD_SLEEPTIMEOUT_US		100000
+#define SLOW_RD_SLEEP_US		1000
+#define SLOW_RD_SLEEPTIMEOUT_US		4000000
+
+/* regmap_read_poll_timeout() re-evaluates its map and address arguments on
+ * every iteration, so resolve both once up front.
+ */
+#define lan9645x_rd_poll_timeout(_lan9645x, _reg_macro, _val, _cond)	\
+({									\
+	struct regmap *__map = lan_rmap((_lan9645x), _reg_macro);	\
+	u32 __addr = lan_rel_addr(_reg_macro);				\
+									\
+	regmap_read_poll_timeout(__map, __addr, (_val), (_cond),		\
+				 RD_SLEEP_US, RD_SLEEPTIMEOUT_US);	\
+})
+
+#define lan9645x_rd_poll_slow(_lan9645x, _reg_macro, _val, _cond)	\
+({									\
+	struct regmap *__map = lan_rmap((_lan9645x), _reg_macro);	\
+	u32 __addr = lan_rel_addr(_reg_macro);				\
+									\
+	regmap_read_poll_timeout(__map, __addr, (_val), (_cond),		\
+				 SLOW_RD_SLEEP_US,			\
+				 SLOW_RD_SLEEPTIMEOUT_US);		\
+})
+
+/* NPI port prefix config encoding
+ *
+ * 0: No CPU extraction header (normal frames)
+ * 1: CPU extraction header without prefix
+ * 2: CPU extraction header with short prefix
+ * 3: CPU extraction header with long prefix
+ */
+enum lan9645x_tag_prefix {
+	LAN9645X_TAG_PREFIX_DISABLED = 0,
+	LAN9645X_TAG_PREFIX_NONE = 1,
+	LAN9645X_TAG_PREFIX_SHORT = 2,
+	LAN9645X_TAG_PREFIX_LONG = 3,
+};
+
+enum {
+	LAN9645X_SPEED_DISABLED = 0,
+	LAN9645X_SPEED_10 = 1,
+	LAN9645X_SPEED_100 = 2,
+	LAN9645X_SPEED_1000 = 3,
+	LAN9645X_SPEED_2500 = 4,
+};
+
+/* Rewriter VLAN port tagging encoding for REW:PORT[0-10]:TAG_CFG.TAG_CFG
+ *
+ * 0: Port tagging disabled.
+ * 1: Tag all frames, except when VID=PORT_VLAN_CFG.PORT_VID or VID=0.
+ * 2: Tag all frames, except when VID=0.
+ * 3: Tag all frames.
+ */
+enum lan9645x_vlan_port_tag {
+	LAN9645X_TAG_DISABLED = 0,
+	LAN9645X_TAG_NO_PVID_NO_UNAWARE = 1,
+	LAN9645X_TAG_NO_UNAWARE = 2,
+	LAN9645X_TAG_ALL = 3,
+};
+
+struct lan9645x {
+	struct device *dev;
+	struct dsa_switch *ds;
+	struct regmap *rmap[NUM_TARGETS];
+
+	/* NPI chip_port */
+	int npi;
+
+	u8 num_phys_ports;
+	struct lan9645x_port **ports;
+
+	int num_port_dis;
+};
+
+struct lan9645x_port {
+	struct lan9645x *lan9645x;
+
+	u8 chip_port;
+
+	bool rx_internal_delay;
+	bool tx_internal_delay;
+};
+
+extern const struct phylink_mac_ops lan9645x_phylink_mac_ops;
+
+/* PFC_CFG.FC_LINK_SPEED encoding */
+static inline int lan9645x_speed_fc_enc(int speed)
+{
+	switch (speed) {
+	case LAN9645X_SPEED_10:
+		return 3;
+	case LAN9645X_SPEED_100:
+		return 2;
+	case LAN9645X_SPEED_1000:
+		return 1;
+	case LAN9645X_SPEED_2500:
+		return 0;
+	default:
+		WARN_ON_ONCE(1);
+		return 1;
+	}
+}
+
+/* Watermark encode. See QSYS:RES_CTRL[*]:RES_CFG.WM_HIGH for details.
+ * Returns lowest encoded number which will fit request/ is larger than request.
+ * Or the maximum representable value, if request is too large.
+ */
+static inline u32 lan9645x_wm_enc(u32 value)
+{
+	value = DIV_ROUND_UP(value, LAN9645X_BUFFER_CELL_SZ);
+
+	if (value >= GWM_MULTIPLIER_BIT) {
+		value = DIV_ROUND_UP(value, 16);
+		if (value >= GWM_MULTIPLIER_BIT)
+			value = (GWM_MULTIPLIER_BIT - 1);
+		value |= GWM_MULTIPLIER_BIT;
+	}
+
+	return value;
+}
+
+static inline struct lan9645x_port *lan9645x_to_port(struct lan9645x *lan9645x,
+						     int port)
+{
+	return lan9645x->ports[port];
+}
+
+static inline struct regmap *lan_tgt2rmap(struct lan9645x *lan9645x,
+					  enum lan9645x_target t, int tinst)
+{
+	return lan9645x->rmap[t + tinst];
+}
+
+static inline u32 __lan_rel_addr(int gbase, int ginst, int gcnt,
+				 int gwidth, int raddr, int rinst,
+				 int rcnt, int rwidth)
+{
+	WARN_ON(ginst >= gcnt);
+	WARN_ON(rinst >= rcnt);
+	return gbase + ginst * gwidth + raddr + rinst * rwidth;
+}
+
+/* Get register address relative to target instance */
+static inline u32 lan_rel_addr(enum lan9645x_target t, int tinst, int tcnt,
+			       int gbase, int ginst, int gcnt, int gwidth,
+			       int raddr, int rinst, int rcnt, int rwidth)
+{
+	WARN_ON(tinst >= tcnt);
+	return __lan_rel_addr(gbase, ginst, gcnt, gwidth, raddr, rinst,
+			      rcnt, rwidth);
+}
+
+static inline u32 lan_rd(struct lan9645x *lan9645x, enum lan9645x_target t,
+			 int tinst, int tcnt, int gbase, int ginst,
+			 int gcnt, int gwidth, int raddr, int rinst,
+			 int rcnt, int rwidth)
+{
+	u32 addr, val = 0;
+
+	addr = lan_rel_addr(t, tinst, tcnt, gbase, ginst, gcnt, gwidth,
+			    raddr, rinst, rcnt, rwidth);
+
+	WARN_ON_ONCE(regmap_read(lan_tgt2rmap(lan9645x, t, tinst), addr, &val));
+
+	return val;
+}
+
+static inline int lan_bulk_rd(void *val, size_t val_count,
+			      struct lan9645x *lan9645x,
+			      enum lan9645x_target t, int tinst, int tcnt,
+			      int gbase, int ginst, int gcnt, int gwidth,
+			      int raddr, int rinst, int rcnt, int rwidth)
+{
+	u32 addr;
+
+	addr = lan_rel_addr(t, tinst, tcnt, gbase, ginst, gcnt, gwidth,
+			    raddr, rinst, rcnt, rwidth);
+
+	return regmap_bulk_read(lan_tgt2rmap(lan9645x, t, tinst), addr, val,
+				val_count);
+}
+
+static inline struct regmap *lan_rmap(struct lan9645x *lan9645x,
+				      enum lan9645x_target t, int tinst,
+				      int tcnt, int gbase, int ginst,
+				      int gcnt, int gwidth, int raddr,
+				      int rinst, int rcnt, int rwidth)
+{
+	return lan_tgt2rmap(lan9645x, t, tinst);
+}
+
+static inline void lan_wr(u32 val, struct lan9645x *lan9645x,
+			  enum lan9645x_target t, int tinst, int tcnt,
+			  int gbase, int ginst, int gcnt, int gwidth,
+			  int raddr, int rinst, int rcnt, int rwidth)
+{
+	u32 addr;
+
+	addr = lan_rel_addr(t, tinst, tcnt, gbase, ginst, gcnt, gwidth,
+			    raddr, rinst, rcnt, rwidth);
+
+	WARN_ON_ONCE(regmap_write(lan_tgt2rmap(lan9645x, t, tinst), addr, val));
+}
+
+static inline void lan_rmw(u32 val, u32 mask, struct lan9645x *lan9645x,
+			   enum lan9645x_target t, int tinst, int tcnt,
+			   int gbase, int ginst, int gcnt, int gwidth,
+			   int raddr, int rinst, int rcnt, int rwidth)
+{
+	u32 addr;
+
+	addr = lan_rel_addr(t, tinst, tcnt, gbase, ginst, gcnt, gwidth,
+			    raddr, rinst, rcnt, rwidth);
+
+	WARN_ON_ONCE(regmap_update_bits(lan_tgt2rmap(lan9645x, t, tinst),
+					addr, mask, val));
+}
+
+/* lan9645x_npi.c */
+void lan9645x_npi_port_init(struct lan9645x *lan9645x,
+			    struct dsa_port *cpu_port);
+void lan9645x_npi_port_deinit(struct lan9645x *lan9645x, int port);
+
+/* lan9645x_port.c */
+int lan9645x_port_setup(struct dsa_switch *ds, int port);
+int lan9645x_port_set_maxlen(struct lan9645x *lan9645x, int port, size_t sdu);
+void lan9645x_port_cpu_init(struct lan9645x *lan9645x);
+
+/* lan9645x_phylink.c */
+void lan9645x_phylink_get_caps(struct lan9645x *lan9645x, int port,
+			       struct phylink_config *c);
+void lan9645x_phylink_port_down(struct lan9645x *lan9645x, int port);
+
+#endif /* __LAN9645X_MAIN_H__ */
diff --git a/drivers/net/dsa/microchip/lan9645x/lan9645x_npi.c b/drivers/net/dsa/microchip/lan9645x/lan9645x_npi.c
new file mode 100644
index 000000000000..5da09ca8d1dd
--- /dev/null
+++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_npi.c
@@ -0,0 +1,90 @@
+// SPDX-License-Identifier: GPL-2.0+
+/* Copyright (C) 2026 Microchip Technology Inc.
+ */
+
+#include "lan9645x_main.h"
+
+void lan9645x_npi_port_init(struct lan9645x *lan9645x,
+			    struct dsa_port *cpu_port)
+{
+	int port = cpu_port->index;
+	struct lan9645x_port *p;
+
+	p = lan9645x_to_port(lan9645x, port);
+	lan9645x->npi = port;
+
+	dev_dbg(lan9645x->dev, "NPI port=%d\n", port);
+
+	/* Any CPU extraction queue frames, are sent to external CPU on given
+	 * port. Never send injected frames back to cpu.
+	 */
+	lan_wr(QSYS_EXT_CPU_CFG_EXT_CPUQ_MSK |
+	       QSYS_EXT_CPU_CFG_EXT_CPU_PORT_SET(p->chip_port) |
+	       QSYS_EXT_CPU_CFG_EXT_CPU_KILL_ENA_SET(1) |
+	       QSYS_EXT_CPU_CFG_INT_CPU_KILL_ENA_SET(1),
+	       lan9645x, QSYS_EXT_CPU_CFG);
+
+	/* Configure IFH prefix mode for NPI port. We can not use an injection
+	 * prefix, because it requires all frames sent on the port to contain
+	 * the prefix. Frames without the prefix would get stuck in the queue
+	 * system rendering the port unusable. Since we do not control
+	 * what is sent to the NPI port, no prefix is our only option.
+	 */
+	lan_rmw(SYS_PORT_MODE_INCL_XTR_HDR_SET(LAN9645X_TAG_PREFIX_LONG) |
+		SYS_PORT_MODE_INCL_INJ_HDR_SET(LAN9645X_TAG_PREFIX_NONE),
+		SYS_PORT_MODE_INCL_XTR_HDR |
+		SYS_PORT_MODE_INCL_INJ_HDR,
+		lan9645x,
+		SYS_PORT_MODE(p->chip_port));
+
+	/* Rewriting and extraction with IFH does not play nice together. A VLAN
+	 * tag pushed into the frame by REW will cause 4 bytes at the end of the
+	 * extraction header to be overwritten with the top 4 bytes of the DMAC.
+	 *
+	 * We can not use REW_PORT_CFG_NO_REWRITE=1 as that disabled RTAGD
+	 * setting in the IFH
+	 */
+	lan_rmw(REW_TAG_CFG_TAG_CFG_SET(LAN9645X_TAG_DISABLED),
+		REW_TAG_CFG_TAG_CFG, lan9645x, REW_TAG_CFG(p->chip_port));
+
+	/* Clear rewriter port vid */
+	lan_wr(0, lan9645x, REW_PORT_VLAN_CFG(p->chip_port));
+
+	/* Make sure frames with src_port=<CPU port module> are not reflected
+	 * back via the NPI port. This could happen if a frame is flooded for
+	 * instance. The *_CPU_KILL_ENA flags above only have an effect when a
+	 * frame is output due to a CPU forwarding decision such as trapping or
+	 * cpu copy.
+	 */
+	lan_rmw(0, BIT(port), lan9645x,
+		ANA_PGID(PGID_SRC + lan9645x->num_phys_ports));
+}
+
+void lan9645x_npi_port_deinit(struct lan9645x *lan9645x, int port)
+{
+	u32 no_cpu_port = FIELD_MAX(QSYS_EXT_CPU_CFG_EXT_CPU_PORT);
+	struct lan9645x_port *p;
+
+	if (port < 0)
+		return;
+
+	lan9645x->npi = -1;
+	p = lan9645x_to_port(lan9645x, port);
+
+	lan_wr(QSYS_EXT_CPU_CFG_EXT_CPUQ_MSK_SET(0) |
+	       QSYS_EXT_CPU_CFG_EXT_CPU_PORT_SET(no_cpu_port) |
+	       QSYS_EXT_CPU_CFG_EXT_CPU_KILL_ENA_SET(1) |
+	       QSYS_EXT_CPU_CFG_INT_CPU_KILL_ENA_SET(1),
+	       lan9645x, QSYS_EXT_CPU_CFG);
+
+	lan_rmw(SYS_PORT_MODE_INCL_XTR_HDR_SET(LAN9645X_TAG_PREFIX_DISABLED) |
+		SYS_PORT_MODE_INCL_INJ_HDR_SET(LAN9645X_TAG_PREFIX_DISABLED),
+		SYS_PORT_MODE_INCL_XTR_HDR |
+		SYS_PORT_MODE_INCL_INJ_HDR,
+		lan9645x,
+		SYS_PORT_MODE(p->chip_port));
+
+	lan_rmw(BIT(p->chip_port),
+		BIT(p->chip_port), lan9645x,
+		ANA_PGID(PGID_SRC + lan9645x->num_phys_ports));
+}
diff --git a/drivers/net/dsa/microchip/lan9645x/lan9645x_phylink.c b/drivers/net/dsa/microchip/lan9645x/lan9645x_phylink.c
new file mode 100644
index 000000000000..3d704b963d6b
--- /dev/null
+++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_phylink.c
@@ -0,0 +1,361 @@
+// SPDX-License-Identifier: GPL-2.0+
+/* Copyright (C) 2026 Microchip Technology Inc.
+ */
+
+#include <linux/phy.h>
+
+#include "lan9645x_main.h"
+
+/* Port 7 is RGMII_0 and port 8 is RGMII_1 */
+#define RGMII_IDX(port) ((port) == 8 ? 1 : 0)
+
+void lan9645x_phylink_get_caps(struct lan9645x *lan9645x, int port,
+			       struct phylink_config *c)
+{
+	c->mac_capabilities = MAC_ASYM_PAUSE | MAC_SYM_PAUSE | MAC_10 |
+			      MAC_100 | MAC_1000FD;
+
+	switch (port) {
+	case 0 ... 4:
+		__set_bit(PHY_INTERFACE_MODE_GMII, c->supported_interfaces);
+		break;
+	case 5 ... 6:
+		/* SerDes ports: QSGMII/SGMII/1000BASEX/2500BASEX modes
+		 * require PCS support which is not yet implemented. With an
+		 * empty supported_interfaces, phylink_create() fails and
+		 * dsa_tree_setup_ports() retries the port as unused, so a
+		 * user port declared in DT is only disabled. A CPU port has
+		 * no such fallback and fails the probe, which
+		 * lan9645x_tag_npi_setup() rejects explicitly.
+		 */
+		break;
+	case 7 ... 8:
+		/* QSGMII mode on ports 7-8 requires SerDes PCS support,
+		 * which is not yet implemented.
+		 */
+		phy_interface_set_rgmii(c->supported_interfaces);
+		break;
+	default:
+		break;
+	}
+}
+
+static void lan9645x_rgmii_set_speed(struct lan9645x *lan9645x, int port,
+				     int speed)
+{
+	u8 tx_clk;
+
+	tx_clk = speed == SPEED_1000 ? 1 :
+		 speed == SPEED_100  ? 2 :
+		 speed == SPEED_10   ? 3 : 0;
+
+	lan_rmw(HSIO_RGMII_CFG_RGMII_RX_RST_SET(0) |
+		HSIO_RGMII_CFG_RGMII_TX_RST_SET(0) |
+		HSIO_RGMII_CFG_TX_CLK_CFG_SET(tx_clk),
+		HSIO_RGMII_CFG_RGMII_RX_RST |
+		HSIO_RGMII_CFG_RGMII_TX_RST |
+		HSIO_RGMII_CFG_TX_CLK_CFG,
+		lan9645x, HSIO_RGMII_CFG(RGMII_IDX(port)));
+}
+
+static void lan9645x_rgmii_dll_config(struct lan9645x_port *p)
+{
+	u32 rx_idx, tx_idx;
+
+	/* DLL register layout:
+	 * (N*2):   RGMII_N_RX
+	 * (N*2)+1: RGMII_N_TX
+	 */
+	rx_idx = RGMII_IDX(p->chip_port) * 2;
+	tx_idx = RGMII_IDX(p->chip_port) * 2 + 1;
+
+	/* Enable DLL in RGMII clock paths, deassert DLL reset, and start the
+	 * delay tune FSM.
+	 */
+	lan_rmw(HSIO_DLL_CFG_DLL_CLK_ENA_SET(1) |
+		HSIO_DLL_CFG_DLL_RST_SET(0) |
+		HSIO_DLL_CFG_DLL_ENA_SET(p->rx_internal_delay) |
+		HSIO_DLL_CFG_DELAY_ENA_SET(p->rx_internal_delay),
+		HSIO_DLL_CFG_DLL_CLK_ENA |
+		HSIO_DLL_CFG_DLL_RST |
+		HSIO_DLL_CFG_DLL_ENA |
+		HSIO_DLL_CFG_DELAY_ENA,
+		p->lan9645x, HSIO_DLL_CFG(rx_idx));
+
+	lan_rmw(HSIO_DLL_CFG_DLL_CLK_ENA_SET(1) |
+		HSIO_DLL_CFG_DLL_RST_SET(0) |
+		HSIO_DLL_CFG_DLL_ENA_SET(p->tx_internal_delay) |
+		HSIO_DLL_CFG_DELAY_ENA_SET(p->tx_internal_delay),
+		HSIO_DLL_CFG_DLL_CLK_ENA |
+		HSIO_DLL_CFG_DLL_RST |
+		HSIO_DLL_CFG_DLL_ENA |
+		HSIO_DLL_CFG_DELAY_ENA,
+		p->lan9645x, HSIO_DLL_CFG(tx_idx));
+}
+
+static struct lan9645x_port *
+lan9645x_phylink_config_to_port(struct phylink_config *config)
+{
+	struct dsa_port *dp = dsa_phylink_to_port(config);
+
+	return lan9645x_to_port(dp->ds->priv, dp->index);
+}
+
+static int lan9645x_phylink_mac_prepare(struct phylink_config *config,
+					unsigned int mode,
+					phy_interface_t iface)
+{
+	struct lan9645x_port *p = lan9645x_phylink_config_to_port(config);
+	struct lan9645x *lan9645x = p->lan9645x;
+	int port = p->chip_port;
+
+	lan_rmw(HSIO_HW_CFG_GMII_ENA_SET(BIT(port)),
+		HSIO_HW_CFG_GMII_ENA_SET(BIT(port)), lan9645x, HSIO_HW_CFG);
+
+	return 0;
+}
+
+static void lan9645x_phylink_mac_config(struct phylink_config *config,
+					unsigned int mode,
+					const struct phylink_link_state *state)
+{
+	struct lan9645x_port *p = lan9645x_phylink_config_to_port(config);
+
+	if (phy_interface_mode_is_rgmii(state->interface))
+		lan9645x_rgmii_dll_config(p);
+}
+
+static bool lan9645x_port_is_cuphy(struct lan9645x *lan9645x, int port,
+				   phy_interface_t interface)
+{
+	return port >= 0 && port <= 4 && interface == PHY_INTERFACE_MODE_GMII;
+}
+
+void lan9645x_phylink_port_down(struct lan9645x *lan9645x, int port)
+{
+	struct lan9645x_port *p = lan9645x_to_port(lan9645x, port);
+	u32 val;
+
+	/* Disable MAC frame reception */
+	lan_rmw(DEV_MAC_ENA_CFG_RX_ENA_SET(0),
+		DEV_MAC_ENA_CFG_RX_ENA,
+		lan9645x, DEV_MAC_ENA_CFG(p->chip_port));
+
+	/* Disable traffic being sent to or from switch port */
+	lan_rmw(QSYS_SW_PORT_MODE_PORT_ENA_SET(0),
+		QSYS_SW_PORT_MODE_PORT_ENA,
+		lan9645x, QSYS_SW_PORT_MODE(p->chip_port));
+
+	/* Disable dequeuing from the egress queues  */
+	lan_rmw(QSYS_PORT_MODE_DEQUEUE_DIS_SET(1),
+		QSYS_PORT_MODE_DEQUEUE_DIS,
+		lan9645x, QSYS_PORT_MODE(p->chip_port));
+
+	/* Disable Flowcontrol */
+	lan_rmw(SYS_PAUSE_CFG_PAUSE_ENA_SET(0),
+		SYS_PAUSE_CFG_PAUSE_ENA,
+		lan9645x, SYS_PAUSE_CFG(p->chip_port));
+
+	/* Wait a worst case time 8ms (10K jumbo/10Mbit) */
+	usleep_range(8 * USEC_PER_MSEC, 9 * USEC_PER_MSEC);
+
+	/* Disable HDX backpressure. */
+	lan_rmw(SYS_FRONT_PORT_MODE_HDX_MODE_SET(0),
+		SYS_FRONT_PORT_MODE_HDX_MODE,
+		lan9645x, SYS_FRONT_PORT_MODE(p->chip_port));
+
+	/* Flush the queues associated with the port */
+	lan_rmw(QSYS_SW_PORT_MODE_AGING_MODE_SET(3),
+		QSYS_SW_PORT_MODE_AGING_MODE,
+		lan9645x, QSYS_SW_PORT_MODE(p->chip_port));
+
+	/* Enable dequeuing from the egress queues */
+	lan_rmw(QSYS_PORT_MODE_DEQUEUE_DIS_SET(0),
+		QSYS_PORT_MODE_DEQUEUE_DIS,
+		lan9645x, QSYS_PORT_MODE(p->chip_port));
+
+	/* Wait until flushing is complete */
+	if (lan9645x_rd_poll_slow(lan9645x, QSYS_SW_STATUS(p->chip_port),
+				  val, !QSYS_SW_STATUS_EQ_AVAIL_GET(val)))
+		dev_err(lan9645x->dev, "Flush timeout chip port %u\n",
+			p->chip_port);
+
+	/* Disable MAC tx */
+	lan_rmw(DEV_MAC_ENA_CFG_TX_ENA_SET(0),
+		DEV_MAC_ENA_CFG_TX_ENA,
+		lan9645x, DEV_MAC_ENA_CFG(p->chip_port));
+
+	/* Reset the Port and MAC clock domains */
+	lan_rmw(DEV_CLOCK_CFG_PORT_RST_SET(1),
+		DEV_CLOCK_CFG_PORT_RST,
+		lan9645x, DEV_CLOCK_CFG(p->chip_port));
+
+	/* Wait before resetting MAC clock domains. */
+	usleep_range(USEC_PER_MSEC, 2 * USEC_PER_MSEC);
+
+	lan_rmw(DEV_CLOCK_CFG_MAC_TX_RST_SET(1) |
+		DEV_CLOCK_CFG_MAC_RX_RST_SET(1) |
+		DEV_CLOCK_CFG_PORT_RST_SET(1),
+		DEV_CLOCK_CFG_MAC_TX_RST |
+		DEV_CLOCK_CFG_MAC_RX_RST |
+		DEV_CLOCK_CFG_PORT_RST,
+		lan9645x, DEV_CLOCK_CFG(p->chip_port));
+
+	/* Clear flushing */
+	lan_rmw(QSYS_SW_PORT_MODE_AGING_MODE_SET(2),
+		QSYS_SW_PORT_MODE_AGING_MODE,
+		lan9645x, QSYS_SW_PORT_MODE(p->chip_port));
+}
+
+static void lan9645x_phylink_mac_link_down(struct phylink_config *config,
+					   unsigned int link_an_mode,
+					   phy_interface_t interface)
+{
+	struct lan9645x_port *p = lan9645x_phylink_config_to_port(config);
+	struct lan9645x *lan9645x = p->lan9645x;
+
+	lan9645x_phylink_port_down(lan9645x, p->chip_port);
+}
+
+static void lan9645x_phylink_mac_link_up(struct phylink_config *config,
+					 struct phy_device *phydev,
+					 unsigned int link_an_mode,
+					 phy_interface_t interface, int speed,
+					 int duplex, bool tx_pause,
+					 bool rx_pause)
+{
+	struct lan9645x_port *p = lan9645x_phylink_config_to_port(config);
+	int rx_ifg1, rx_ifg2, tx_ifg, gtx_clk = 0;
+	struct lan9645x *lan9645x = p->lan9645x;
+	int gspeed = LAN9645X_SPEED_DISABLED;
+	int port = p->chip_port;
+	int mode = 0;
+	int fc_spd;
+	bool fdx;
+
+	/* Configure RGMII TX clock for the negotiated speed */
+	if (phy_interface_mode_is_rgmii(interface))
+		lan9645x_rgmii_set_speed(lan9645x, port, speed);
+
+	fdx = duplex == DUPLEX_FULL || speed == SPEED_1000;
+
+	if (fdx)
+		mode |= DEV_MAC_MODE_CFG_FDX_ENA_SET(1);
+
+	tx_ifg = fdx ? 0x6 : 0x5;
+	rx_ifg2 = 0x2;
+
+	switch (speed) {
+	case SPEED_10:
+		rx_ifg1 = 0x2;
+		gspeed = LAN9645X_SPEED_10;
+		break;
+	case SPEED_100:
+		rx_ifg1 = 0x1;
+		gspeed = LAN9645X_SPEED_100;
+		break;
+	case SPEED_1000:
+		gspeed = LAN9645X_SPEED_1000;
+		mode |= DEV_MAC_MODE_CFG_GIGA_MODE_ENA_SET(1);
+		rx_ifg1 = 0x1;
+		gtx_clk = 1;
+		break;
+	default:
+		dev_err(lan9645x->dev, "Unsupported speed on port %d: %d\n",
+			p->chip_port, speed);
+		return;
+	}
+
+	fc_spd = lan9645x_speed_fc_enc(gspeed);
+
+	lan_rmw(mode,
+		DEV_MAC_MODE_CFG_FDX_ENA |
+		DEV_MAC_MODE_CFG_GIGA_MODE_ENA,
+		lan9645x, DEV_MAC_MODE_CFG(p->chip_port));
+
+	lan_rmw(DEV_MAC_IFG_CFG_TX_IFG_SET(tx_ifg) |
+		DEV_MAC_IFG_CFG_RX_IFG1_SET(rx_ifg1) |
+		DEV_MAC_IFG_CFG_RX_IFG2_SET(rx_ifg2),
+		DEV_MAC_IFG_CFG_TX_IFG |
+		DEV_MAC_IFG_CFG_RX_IFG1 |
+		DEV_MAC_IFG_CFG_RX_IFG2,
+		lan9645x, DEV_MAC_IFG_CFG(p->chip_port));
+
+	if (lan9645x_port_is_cuphy(lan9645x, port, interface)) {
+		lan_rmw(CHIP_TOP_CUPHY_PORT_CFG_GTX_CLK_ENA_SET(gtx_clk),
+			CHIP_TOP_CUPHY_PORT_CFG_GTX_CLK_ENA, lan9645x,
+			CHIP_TOP_CUPHY_PORT_CFG(p->chip_port));
+	}
+
+	lan_rmw(SYS_PAUSE_CFG_PAUSE_ENA_SET(1),
+		SYS_PAUSE_CFG_PAUSE_ENA,
+		lan9645x, SYS_PAUSE_CFG(p->chip_port));
+
+	/* Flow control */
+	lan_rmw(SYS_MAC_FC_CFG_FC_LINK_SPEED_SET(fc_spd) |
+		SYS_MAC_FC_CFG_FC_LATENCY_CFG_SET(0x7) |
+		SYS_MAC_FC_CFG_ZERO_PAUSE_ENA_SET(1) |
+		SYS_MAC_FC_CFG_PAUSE_VAL_CFG_SET(0xffff) |
+		SYS_MAC_FC_CFG_RX_FC_ENA_SET(rx_pause ? 1 : 0) |
+		SYS_MAC_FC_CFG_TX_FC_ENA_SET(tx_pause ? 1 : 0),
+		SYS_MAC_FC_CFG_FC_LINK_SPEED |
+		SYS_MAC_FC_CFG_FC_LATENCY_CFG |
+		SYS_MAC_FC_CFG_ZERO_PAUSE_ENA |
+		SYS_MAC_FC_CFG_PAUSE_VAL_CFG |
+		SYS_MAC_FC_CFG_RX_FC_ENA |
+		SYS_MAC_FC_CFG_TX_FC_ENA,
+		lan9645x, SYS_MAC_FC_CFG(p->chip_port));
+
+	/* Enable MAC module */
+	lan_wr(DEV_MAC_ENA_CFG_RX_ENA_SET(1) |
+	       DEV_MAC_ENA_CFG_TX_ENA_SET(1),
+	       lan9645x, DEV_MAC_ENA_CFG(p->chip_port));
+
+	/* port _must_ be taken out of reset before MAC. */
+	lan_rmw(DEV_CLOCK_CFG_PORT_RST_SET(0),
+		DEV_CLOCK_CFG_PORT_RST,
+		lan9645x, DEV_CLOCK_CFG(p->chip_port));
+
+	/* Take out the clock from reset. Note this write will set all these
+	 * fields to zero:
+	 *
+	 * DEV_CLOCK_CFG[*].MAC_TX_RST
+	 * DEV_CLOCK_CFG[*].MAC_RX_RST
+	 * DEV_CLOCK_CFG[*].PCS_TX_RST
+	 * DEV_CLOCK_CFG[*].PCS_RX_RST
+	 * DEV_CLOCK_CFG[*].PORT_RST
+	 * DEV_CLOCK_CFG[*].PHY_RST
+	 *
+	 * Note link_down will assert PORT_RST, MAC_RX_RST and MAC_TX_RST, so
+	 * we are effectively taking the mac tx/rx clocks out of reset.
+	 *
+	 * This linkspeed field has a slightly different encoding from others:
+	 *
+	 * - 0 is no-link
+	 * - 1 is both 2500/1000
+	 * - 2 is 100mbit
+	 * - 3 is 10mbit
+	 *
+	 */
+	lan_wr(DEV_CLOCK_CFG_LINK_SPEED_SET(fc_spd),
+	       lan9645x,
+	       DEV_CLOCK_CFG(p->chip_port));
+
+	/* Core: Enable port for frame transfer */
+	lan_rmw(QSYS_SW_PORT_MODE_PORT_ENA_SET(1) |
+		QSYS_SW_PORT_MODE_SCH_NEXT_CFG_SET(1) |
+		QSYS_SW_PORT_MODE_INGRESS_DROP_MODE_SET(1) |
+		QSYS_SW_PORT_MODE_TX_PFC_ENA_SET(0),
+		QSYS_SW_PORT_MODE_PORT_ENA |
+		QSYS_SW_PORT_MODE_SCH_NEXT_CFG |
+		QSYS_SW_PORT_MODE_INGRESS_DROP_MODE |
+		QSYS_SW_PORT_MODE_TX_PFC_ENA,
+		lan9645x, QSYS_SW_PORT_MODE(p->chip_port));
+}
+
+const struct phylink_mac_ops lan9645x_phylink_mac_ops = {
+	.mac_prepare			= lan9645x_phylink_mac_prepare,
+	.mac_config			= lan9645x_phylink_mac_config,
+	.mac_link_down			= lan9645x_phylink_mac_link_down,
+	.mac_link_up			= lan9645x_phylink_mac_link_up,
+};
diff --git a/drivers/net/dsa/microchip/lan9645x/lan9645x_port.c b/drivers/net/dsa/microchip/lan9645x/lan9645x_port.c
new file mode 100644
index 000000000000..2491c51ba7e5
--- /dev/null
+++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_port.c
@@ -0,0 +1,161 @@
+// SPDX-License-Identifier: GPL-2.0+
+/* Copyright (C) 2026 Microchip Technology Inc.
+ */
+
+#include <linux/of_net.h>
+
+#include "lan9645x_main.h"
+
+void lan9645x_port_cpu_init(struct lan9645x *lan9645x)
+{
+	u32 cpu_module_mask = BIT(lan9645x->num_phys_ports);
+
+	/* Map the 8 CPU extraction queues to the CPU port module (datasheet is
+	 * wrong)
+	 */
+	lan_wr(0, lan9645x, QSYS_CPU_GROUP_MAP);
+
+	/* The CPU will only use its reserved buffer in the shared queue system
+	 * and none of the shared buffer space, therefore we disable resource
+	 * sharing in egress direction. We must not disable resource sharing in
+	 * the ingress direction, because some traffic test scenarios require
+	 * loads of buffer memory for frames initiated by the CPU.
+	 */
+	lan_rmw(QSYS_EGR_NO_SHARING_EGR_NO_SHARING_SET(cpu_module_mask),
+		QSYS_EGR_NO_SHARING_EGR_NO_SHARING_SET(cpu_module_mask),
+		lan9645x, QSYS_EGR_NO_SHARING);
+
+	/* The CPU should also discard frames forwarded to it if it has run
+	 * out of the reserved buffer space. Otherwise they will be held back
+	 * in the ingress queues with potential head-of-line blocking effects.
+	 */
+	lan_rmw(QSYS_EGR_DROP_MODE_EGRESS_DROP_MODE_SET(cpu_module_mask),
+		QSYS_EGR_DROP_MODE_EGRESS_DROP_MODE_SET(cpu_module_mask),
+		lan9645x, QSYS_EGR_DROP_MODE);
+
+	lan_wr(cpu_module_mask, lan9645x, ANA_PGID(PGID_CPU));
+
+	/* Enable switching to/from cpu port. Keep default aging-mode. */
+	lan_rmw(QSYS_SW_PORT_MODE_PORT_ENA_SET(1) |
+		QSYS_SW_PORT_MODE_SCH_NEXT_CFG_SET(1) |
+		QSYS_SW_PORT_MODE_INGRESS_DROP_MODE_SET(1),
+		QSYS_SW_PORT_MODE_PORT_ENA |
+		QSYS_SW_PORT_MODE_SCH_NEXT_CFG |
+		QSYS_SW_PORT_MODE_INGRESS_DROP_MODE,
+		lan9645x, QSYS_SW_PORT_MODE(lan9645x->num_phys_ports));
+}
+
+/* VLAN tag overhead is handled by DEV_MAC_TAGS_CFG */
+int lan9645x_port_set_maxlen(struct lan9645x *lan9645x, int port, size_t sdu)
+{
+	struct lan9645x_port *p = lan9645x_to_port(lan9645x, port);
+	int maxlen = sdu + ETH_HLEN + ETH_FCS_LEN;
+
+	if (port == lan9645x->npi) {
+		maxlen += LAN9645X_IFH_LEN_BYTES;
+		maxlen += LAN9645X_LONG_PREFIX_LEN;
+	}
+
+	lan_wr(DEV_MAC_MAXLEN_CFG_MAX_LEN_SET(maxlen), lan9645x,
+	       DEV_MAC_MAXLEN_CFG(p->chip_port));
+
+	/* Set Pause WM hysteresis */
+	lan_rmw(SYS_PAUSE_CFG_PAUSE_STOP_SET(lan9645x_wm_enc(4 * maxlen)) |
+		SYS_PAUSE_CFG_PAUSE_START_SET(lan9645x_wm_enc(6 * maxlen)),
+		SYS_PAUSE_CFG_PAUSE_START |
+		SYS_PAUSE_CFG_PAUSE_STOP,
+		lan9645x,
+		SYS_PAUSE_CFG(p->chip_port));
+
+	return 0;
+}
+
+static void lan9645x_parse_mac_delay(struct lan9645x *lan9645x, int port,
+				     struct device_node *dn, const char *name,
+				     bool *out)
+{
+	u32 val;
+
+	if (of_property_read_u32(dn, name, &val))
+		return;
+
+	if (val != 0 && val != 2000) {
+		dev_warn(lan9645x->dev,
+			 "port %d: %s only supports a 2ns delay (on/off), got %u ps\n",
+			 port, name, val);
+		return;
+	}
+
+	*out = val == 2000;
+}
+
+int lan9645x_port_setup(struct dsa_switch *ds, int port)
+{
+	struct dsa_port *dp = dsa_to_port(ds, port);
+	struct lan9645x *lan9645x = ds->priv;
+	struct lan9645x_port *p;
+
+	p = lan9645x_to_port(lan9645x, port);
+
+	if (dp->dn) {
+		lan9645x_parse_mac_delay(lan9645x, port, dp->dn,
+					 "rx-internal-delay-ps",
+					 &p->rx_internal_delay);
+
+		lan9645x_parse_mac_delay(lan9645x, port, dp->dn,
+					 "tx-internal-delay-ps",
+					 &p->tx_internal_delay);
+	}
+
+	/* Disable learning on port */
+	lan_rmw(ANA_PORT_CFG_LEARN_ENA_SET(0),
+		ANA_PORT_CFG_LEARN_ENA,
+		lan9645x, ANA_PORT_CFG(p->chip_port));
+
+	lan9645x_port_set_maxlen(lan9645x, port, ETH_DATA_LEN);
+
+	/* Load HDX backoff seed (fixed per-port, one-shot strobe) */
+	lan_rmw(DEV_MAC_HDX_CFG_SEED_SET(p->chip_port) |
+		DEV_MAC_HDX_CFG_SEED_LOAD_SET(1),
+		DEV_MAC_HDX_CFG_SEED |
+		DEV_MAC_HDX_CFG_SEED_LOAD, lan9645x,
+		DEV_MAC_HDX_CFG(p->chip_port));
+
+	lan_rmw(DEV_MAC_HDX_CFG_SEED_LOAD_SET(0),
+		DEV_MAC_HDX_CFG_SEED_LOAD, lan9645x,
+		DEV_MAC_HDX_CFG(p->chip_port));
+
+	/* Set SMAC of Pause frame (00:00:00:00:00:00) */
+	lan_wr(0, lan9645x, DEV_FC_MAC_LOW_CFG(p->chip_port));
+	lan_wr(0, lan9645x, DEV_FC_MAC_HIGH_CFG(p->chip_port));
+
+	lan9645x_phylink_port_down(lan9645x, port);
+
+	/* Drop frames with multicast source address */
+	lan_rmw(ANA_DROP_CFG_DROP_MC_SMAC_ENA_SET(1),
+		ANA_DROP_CFG_DROP_MC_SMAC_ENA, lan9645x,
+		ANA_DROP_CFG(p->chip_port));
+
+	lan_rmw(DEV_MAC_TAGS_CFG_VLAN_AWR_ENA_SET(1) |
+		DEV_MAC_TAGS_CFG_PB_ENA_SET(1) |
+		DEV_MAC_TAGS_CFG_VLAN_LEN_AWR_ENA_SET(1) |
+		DEV_MAC_TAGS_CFG_TAG_ID_SET(ETH_P_8021AD),
+		DEV_MAC_TAGS_CFG_VLAN_AWR_ENA |
+		DEV_MAC_TAGS_CFG_PB_ENA |
+		DEV_MAC_TAGS_CFG_VLAN_LEN_AWR_ENA |
+		DEV_MAC_TAGS_CFG_TAG_ID,
+		lan9645x, DEV_MAC_TAGS_CFG(p->chip_port));
+
+	/* Enable receiving frames on the port, and activate auto-learning of
+	 * MAC addresses. LEARNAUTO is ignored when LEARN_ENA=0.
+	 */
+	lan_rmw(ANA_PORT_CFG_LEARNAUTO_SET(1) |
+		ANA_PORT_CFG_RECV_ENA_SET(1) |
+		ANA_PORT_CFG_PORTID_VAL_SET(p->chip_port),
+		ANA_PORT_CFG_LEARNAUTO |
+		ANA_PORT_CFG_RECV_ENA |
+		ANA_PORT_CFG_PORTID_VAL,
+		lan9645x, ANA_PORT_CFG(p->chip_port));
+
+	return 0;
+}
-- 
2.52.0

[PATCH net-next v12 6/9] net: dsa: lan9645x: add mac table integration

From: Jens Emil Schulz Østergaard <hidden>
Date: 2026-09-08 07:41:37

Add MAC table support, and dsa fdb callback integration. The mactable is
keyed on (vid,mac) and each row has 4 buckets. A mac table entry
typically points to a PGID index, the first 9 of which represent a front
port.

Mac table entries for L2 multicast will use a PGID containing a group
port mask. For IP multicast entries in the mac table a trick is used,
where the group port mask is packed into the MAC data, exploiting the
fact that the top bits are fixed, and that the number of switch ports is
small enough to fit in the redundant bits.

Therefore, we can avoid using sparse PGID resources for IP multicast
entries in the mac table.

Reviewed-by: Steen Hegelund <Steen.Hegelund@microchip.com>
Signed-off-by: Jens Emil Schulz Østergaard <redacted>
---
Changes in v12:
- Log a failure from lan9645x_mact_flush() in port_fast_age(), which
  returns void and cannot propagate it.
- Fix a comment to name PGID_CPU rather than the CPU port module.
- Move this patch before bridge support, so .port_fast_age exists when
  port_stp_state_set() is introduced.
- Give lan9645x_mact_learn() a cpu_copy argument, and drop
  lan9645x_mac_ports_use_cpu() and the wrapper that used it to derive
  the flag from the encoded mac. The helper could only return false,
  since its one caller passes ENTRYTYPE_LOCKED, and the mdb path needs
  the flag passed in rather than derived.
- Bound the CMD_GET_NEXT walk in lan9645x_mact_dsa_dump() at the mac table
  size plus one, derived from ANA_MACTINDX. Log and return -EIO on overrun.
- lan9645x_mact_dsa_dump(): leave automatic ageing enabled during the
  GET_NEXT walk, as it does not disturb the walk, and toggling ageing
  resets the timer.
- lan9645x_teardown(): deinit the mac table before fwd_domain_lock.
- Spell the mac argument as mac[ETH_ALEN] and the vid as unsigned int.

Changes in v8:
- Pass lan9645x into lan9645x_mac_ports_use_cpu() and reference the CPU
  port module as lan9645x->num_phys_ports instead of CPU_PORT.

Changes in v5:
- lan9645x_mac_init returns error on table init timeout
- add comment about skipping LOCKED entries on fdb dump

Changes in v4:
- remove mac_entries list and just do direct IO to mac table from
  fdb_add/fdb_del

Changes in v3:
- avoid mac add/del dealloc when mac table writes fail
- add mact_lock to change ageing time
- dealloc all mac_entries on deinit
- dsa_dump returns mac table timeout error

Changes in v2:
- use a single lock for hw and sw
- remove unused row struct field and define
- remove list element INIT_LIST_HEAD
- consistent use of err vs ret
- remove mutex_lock in init
- use empty initializer { 0 } -> {}
- do not move fwd_domain_lock init to this unit
- add newline to dev_* log statements
---
 drivers/net/dsa/microchip/lan9645x/Makefile        |   1 +
 drivers/net/dsa/microchip/lan9645x/lan9645x_mac.c  | 268 +++++++++++++++++++++
 drivers/net/dsa/microchip/lan9645x/lan9645x_main.c |  85 +++++++
 drivers/net/dsa/microchip/lan9645x/lan9645x_main.h |  27 +++
 4 files changed, 381 insertions(+)
diff --git a/drivers/net/dsa/microchip/lan9645x/Makefile b/drivers/net/dsa/microchip/lan9645x/Makefile
index e049114b3563..70815edca5b9 100644
--- a/drivers/net/dsa/microchip/lan9645x/Makefile
+++ b/drivers/net/dsa/microchip/lan9645x/Makefile
@@ -2,6 +2,7 @@
 obj-$(CONFIG_NET_DSA_MICROCHIP_LAN9645X) += mchp-lan9645x.o
 
 mchp-lan9645x-objs := \
+	lan9645x_mac.o \
 	lan9645x_main.o \
 	lan9645x_npi.o \
 	lan9645x_phylink.o \
diff --git a/drivers/net/dsa/microchip/lan9645x/lan9645x_mac.c b/drivers/net/dsa/microchip/lan9645x/lan9645x_mac.c
new file mode 100644
index 000000000000..50ce1378b9da
--- /dev/null
+++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_mac.c
@@ -0,0 +1,268 @@
+// SPDX-License-Identifier: GPL-2.0+
+/* Copyright (C) 2026 Microchip Technology Inc.
+ */
+
+#include "lan9645x_main.h"
+
+#define CMD_IDLE		0
+#define CMD_LEARN		1
+#define CMD_FORGET		2
+#define CMD_AGE			3
+#define CMD_GET_NEXT		4
+#define CMD_INIT		5
+#define CMD_READ		6
+#define CMD_WRITE		7
+#define CMD_SYNC_GET_NEXT	8
+
+/* The chip has 2048 * 4 = 8192 total entries in the mac table */
+#define MACT_ROWS		(FIELD_MAX(ANA_MACTINDX_M_INDEX) + 1)
+#define MACT_BUCKETS		(FIELD_MAX(ANA_MACTINDX_BUCKET) + 1)
+#define MACT_ENTRIES		(MACT_ROWS * MACT_BUCKETS)
+#define MACT_GET_NEXT_MAX	(MACT_ENTRIES + 1)
+
+static int lan9645x_mac_wait_for_completion(struct lan9645x *lan9645x,
+					    u32 *maca)
+{
+	u32 val = 0;
+	int err;
+
+	lockdep_assert_held(&lan9645x->mact_lock);
+
+	err = lan9645x_rd_poll_timeout(lan9645x, ANA_MACACCESS, val,
+				       ANA_MACACCESS_MAC_TABLE_CMD_GET(val) ==
+				       CMD_IDLE);
+	if (err)
+		return err;
+
+	if (maca)
+		*maca = val;
+
+	return 0;
+}
+
+static void lan9645x_mac_select(struct lan9645x *lan9645x,
+				const unsigned char mac[ETH_ALEN],
+				unsigned int vid)
+{
+	u64 maddr = ether_addr_to_u64(mac);
+
+	lockdep_assert_held(&lan9645x->mact_lock);
+
+	lan_wr(ANA_MACHDATA_VID_SET(vid) |
+	       ANA_MACHDATA_MACHDATA_SET(upper_32_bits(maddr)),
+	       lan9645x,
+	       ANA_MACHDATA);
+
+	lan_wr(lower_32_bits(maddr),
+	       lan9645x,
+	       ANA_MACLDATA);
+}
+
+static int __lan9645x_mact_forget(struct lan9645x *lan9645x,
+				  const unsigned char mac[ETH_ALEN],
+				  unsigned int vid,
+				  enum macaccess_entry_type type)
+{
+	lockdep_assert_held(&lan9645x->mact_lock);
+
+	lan9645x_mac_select(lan9645x, mac, vid);
+
+	lan_wr(ANA_MACACCESS_ENTRYTYPE_SET(type) |
+	       ANA_MACACCESS_MAC_TABLE_CMD_SET(CMD_FORGET),
+	       lan9645x,
+	       ANA_MACACCESS);
+
+	return lan9645x_mac_wait_for_completion(lan9645x, NULL);
+}
+
+int lan9645x_mact_forget(struct lan9645x *lan9645x,
+			 const unsigned char mac[ETH_ALEN], unsigned int vid,
+			 enum macaccess_entry_type type)
+{
+	int err;
+
+	mutex_lock(&lan9645x->mact_lock);
+	err = __lan9645x_mact_forget(lan9645x, mac, vid, type);
+	mutex_unlock(&lan9645x->mact_lock);
+
+	return err;
+}
+
+static int __lan9645x_mact_learn(struct lan9645x *lan9645x, int port,
+				 const unsigned char mac[ETH_ALEN],
+				 unsigned int vid,
+				 enum macaccess_entry_type type,
+				 bool cpu_copy)
+{
+	lockdep_assert_held(&lan9645x->mact_lock);
+
+	lan9645x_mac_select(lan9645x, mac, vid);
+
+	lan_wr(ANA_MACACCESS_VALID_SET(1) |
+	       ANA_MACACCESS_DEST_IDX_SET(port) |
+	       ANA_MACACCESS_MAC_CPU_COPY_SET(cpu_copy) |
+	       ANA_MACACCESS_ENTRYTYPE_SET(type) |
+	       ANA_MACACCESS_MAC_TABLE_CMD_SET(CMD_LEARN),
+	       lan9645x, ANA_MACACCESS);
+
+	return lan9645x_mac_wait_for_completion(lan9645x, NULL);
+}
+
+int lan9645x_mact_learn(struct lan9645x *lan9645x, int port,
+			const unsigned char mac[ETH_ALEN], unsigned int vid,
+			enum macaccess_entry_type type, bool cpu_copy)
+{
+	int err;
+
+	mutex_lock(&lan9645x->mact_lock);
+	err = __lan9645x_mact_learn(lan9645x, port, mac, vid, type, cpu_copy);
+	mutex_unlock(&lan9645x->mact_lock);
+
+	return err;
+}
+
+int lan9645x_mact_flush(struct lan9645x *lan9645x, int port)
+{
+	int err;
+
+	mutex_lock(&lan9645x->mact_lock);
+	/* MAC table entries with dst index matching port are aged on scan. */
+	lan_wr(ANA_ANAGEFIL_PID_EN_SET(1) |
+	       ANA_ANAGEFIL_PID_VAL_SET(port),
+	       lan9645x, ANA_ANAGEFIL);
+
+	/* Flushing requires two scans. First sets AGE_FLAG=1, second removes
+	 * entries with AGE_FLAG=1.
+	 */
+	lan_wr(ANA_MACACCESS_MAC_TABLE_CMD_SET(CMD_AGE),
+	       lan9645x,
+	       ANA_MACACCESS);
+
+	err = lan9645x_mac_wait_for_completion(lan9645x, NULL);
+	if (err)
+		goto mact_unlock;
+
+	lan_wr(ANA_MACACCESS_MAC_TABLE_CMD_SET(CMD_AGE),
+	       lan9645x,
+	       ANA_MACACCESS);
+
+	err = lan9645x_mac_wait_for_completion(lan9645x, NULL);
+
+mact_unlock:
+	lan_wr(0, lan9645x, ANA_ANAGEFIL);
+	mutex_unlock(&lan9645x->mact_lock);
+	return err;
+}
+
+int lan9645x_mac_init(struct lan9645x *lan9645x)
+{
+	u32 val;
+	int err;
+
+	/* Clear the MAC table */
+	lan_wr(ANA_MACACCESS_MAC_TABLE_CMD_SET(CMD_INIT), lan9645x,
+	       ANA_MACACCESS);
+
+	err = lan9645x_rd_poll_timeout(lan9645x, ANA_MACACCESS, val,
+				       ANA_MACACCESS_MAC_TABLE_CMD_GET(val) ==
+				       CMD_IDLE);
+	if (err) {
+		dev_err(lan9645x->dev, "MAC table clear timeout\n");
+		return err;
+	}
+
+	mutex_init(&lan9645x->mact_lock);
+	return 0;
+}
+
+void lan9645x_mac_deinit(struct lan9645x *lan9645x)
+{
+	mutex_destroy(&lan9645x->mact_lock);
+}
+
+int lan9645x_mact_dsa_dump(struct lan9645x *lan9645x, int port,
+			   dsa_fdb_dump_cb_t *cb, void *data)
+{
+	u8 mac[ETH_ALEN] __aligned(2);
+	u32 mach, macl, maca;
+	int err = 0;
+	u64 addr;
+	int iter;
+	u16 vid;
+	u8 type;
+
+	mutex_lock(&lan9645x->mact_lock);
+
+	/* The aging filter works both for aging scans and GET_NEXT table scans.
+	 * With it, the HW table iteration only stops at entries matching our
+	 * filter. Since DSA calls us for each port on a table dump, this helps
+	 * avoid unnecessary work.
+	 *
+	 * Setup agefilter on our port
+	 */
+	lan_wr(ANA_ANAGEFIL_PID_EN_SET(1) |
+	       ANA_ANAGEFIL_PID_VAL_SET(port),
+	       lan9645x, ANA_ANAGEFIL);
+
+	lan_wr(0, lan9645x, ANA_MACHDATA);
+	lan_wr(0, lan9645x, ANA_MACLDATA);
+
+	type = ENTRYTYPE_NORMAL;
+
+	for (iter = 0; iter < MACT_GET_NEXT_MAX; iter++) {
+		/* CMD_GET_NEXT returns the smallest entry ordered above the one
+		 * left in the registers.
+		 * We rely on mach, macl and type being set correctly in
+		 * the registers from previous round, vis a vis the GET_NEXT
+		 * semantics, so locking entire loop is important.
+		 */
+		lan_wr(ANA_MACACCESS_MAC_TABLE_CMD_SET(CMD_GET_NEXT) |
+		       ANA_MACACCESS_ENTRYTYPE_SET(type),
+		       lan9645x, ANA_MACACCESS);
+
+		err = lan9645x_mac_wait_for_completion(lan9645x, &maca);
+		if (err)
+			break;
+
+		if (ANA_MACACCESS_VALID_GET(maca) == 0)
+			break;
+
+		type = ANA_MACACCESS_ENTRYTYPE_GET(maca);
+		mach = lan_rd(lan9645x, ANA_MACHDATA);
+		macl = lan_rd(lan9645x, ANA_MACLDATA);
+
+		/* Only dynamic entries are surfaced through the user port dump.
+		 * ENTRYTYPE_LOCKED entries are already reported by the bridge
+		 * master's ndo_fdb_dump as NTF_MASTER, so we avoid duplicating
+		 * them as NTF_SELF.
+		 * Entries toward the host (NTF_SELF) have DEST_IDX == PGID_CPU
+		 * and are filtered out by the DEST_IDX check.
+		 */
+		if (ANA_MACACCESS_DEST_IDX_GET(maca) == port &&
+		    type == ENTRYTYPE_NORMAL) {
+			addr = (u64)ANA_MACHDATA_MACHDATA_GET(mach) << 32 |
+			       macl;
+			u64_to_ether_addr(addr, mac);
+			vid = ANA_MACHDATA_VID_GET(mach);
+			if (vid > VLAN_MAX)
+				vid = 0;
+
+			err = cb(mac, vid, false, data);
+			if (err)
+				break;
+		}
+	}
+
+	if (iter == MACT_GET_NEXT_MAX) {
+		dev_err(lan9645x->dev,
+			"MAC table walk on port %d did not terminate\n", port);
+		err = -EIO;
+	}
+
+	/* Remove aging filters */
+	lan_wr(0, lan9645x, ANA_ANAGEFIL);
+
+	mutex_unlock(&lan9645x->mact_lock);
+
+	return err;
+}
diff --git a/drivers/net/dsa/microchip/lan9645x/lan9645x_main.c b/drivers/net/dsa/microchip/lan9645x/lan9645x_main.c
index 6818b91f4bed..5cb4f5fe1374 100644
--- a/drivers/net/dsa/microchip/lan9645x/lan9645x_main.c
+++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_main.c
@@ -74,6 +74,7 @@ static void lan9645x_teardown(struct dsa_switch *ds)
 {
 	struct lan9645x *lan9645x = ds->priv;
 
+	lan9645x_mac_deinit(lan9645x);
 	mutex_destroy(&lan9645x->fwd_domain_lock);
 	lan9645x_npi_port_deinit(lan9645x, lan9645x->npi);
 }
@@ -160,6 +161,9 @@ static int lan9645x_setup(struct dsa_switch *ds)
 
 	mutex_init(&lan9645x->fwd_domain_lock);
 	err = lan9645x_vlan_init(lan9645x);
+	if (err)
+		goto err_mutex;
+	err = lan9645x_mac_init(lan9645x);
 	if (err)
 		goto err_mutex;
 
@@ -360,6 +364,81 @@ static int lan9645x_port_vlan_del(struct dsa_switch *ds, int port,
 	return err;
 }
 
+static void lan9645x_port_fast_age(struct dsa_switch *ds, int port)
+{
+	int err;
+
+	err = lan9645x_mact_flush(ds->priv, port);
+	if (err)
+		dev_err(ds->dev, "Flushing MAC table on port %d returned %pe\n",
+			port, ERR_PTR(err));
+}
+
+static int lan9645x_fdb_dump(struct dsa_switch *ds, int port,
+			     dsa_fdb_dump_cb_t *cb, void *data)
+{
+	return lan9645x_mact_dsa_dump(ds->priv, port, cb, data);
+}
+
+static struct net_device *lan9645x_db2bridge(struct dsa_db db)
+{
+	switch (db.type) {
+	case DSA_DB_PORT:
+	case DSA_DB_LAG:
+		return NULL;
+	case DSA_DB_BRIDGE:
+		return db.bridge.dev;
+	default:
+		return ERR_PTR(-EOPNOTSUPP);
+	}
+}
+
+static int lan9645x_fdb_add(struct dsa_switch *ds, int port,
+			    const unsigned char *addr, u16 vid,
+			    struct dsa_db db)
+{
+	struct net_device *br = lan9645x_db2bridge(db);
+	struct dsa_port *dp = dsa_to_port(ds, port);
+	struct lan9645x *lan9645x = ds->priv;
+	int dest;
+
+	if (IS_ERR(br))
+		return PTR_ERR(br);
+
+	if (dsa_port_is_cpu(dp) && !br &&
+	    dsa_fdb_present_in_other_db(ds, port, addr, vid, db))
+		return 0;
+
+	if (!vid)
+		vid = lan9645x_vlan_unaware_pvid(!!br);
+
+	dest = dsa_port_is_cpu(dp) ? PGID_CPU : port;
+
+	return lan9645x_mact_learn(lan9645x, dest, addr, vid, ENTRYTYPE_LOCKED,
+				   false);
+}
+
+static int lan9645x_fdb_del(struct dsa_switch *ds, int port,
+			    const unsigned char *addr, u16 vid,
+			    struct dsa_db db)
+{
+	struct net_device *br = lan9645x_db2bridge(db);
+	struct dsa_port *dp = dsa_to_port(ds, port);
+	struct lan9645x *lan9645x = ds->priv;
+
+	if (IS_ERR(br))
+		return PTR_ERR(br);
+
+	if (dsa_port_is_cpu(dp) && !br &&
+	    dsa_fdb_present_in_other_db(ds, port, addr, vid, db))
+		return 0;
+
+	if (!vid)
+		vid = lan9645x_vlan_unaware_pvid(!!br);
+
+	return lan9645x_mact_forget(lan9645x, addr, vid, ENTRYTYPE_LOCKED);
+}
+
 static const struct dsa_switch_ops lan9645x_switch_ops = {
 	.get_tag_protocol		= lan9645x_get_tag_protocol,
 
@@ -378,6 +457,12 @@ static const struct dsa_switch_ops lan9645x_switch_ops = {
 	.port_vlan_filtering		= lan9645x_port_vlan_filtering,
 	.port_vlan_add			= lan9645x_port_vlan_add,
 	.port_vlan_del			= lan9645x_port_vlan_del,
+
+	/* MAC table integration */
+	.port_fast_age			= lan9645x_port_fast_age,
+	.port_fdb_dump			= lan9645x_fdb_dump,
+	.port_fdb_add			= lan9645x_fdb_add,
+	.port_fdb_del			= lan9645x_fdb_del,
 };
 
 static int lan9645x_request_target_regmaps(struct lan9645x *lan9645x)
diff --git a/drivers/net/dsa/microchip/lan9645x/lan9645x_main.h b/drivers/net/dsa/microchip/lan9645x/lan9645x_main.h
index b8565a6f2bda..4f077eac1d82 100644
--- a/drivers/net/dsa/microchip/lan9645x/lan9645x_main.h
+++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_main.h
@@ -177,6 +177,19 @@ struct lan9645x_vlan {
 	    untagged: 9; /* ports 0-8 */
 };
 
+/* MAC table entry types.
+ * ENTRYTYPE_NORMAL is subject to aging.
+ * ENTRYTYPE_LOCKED is not subject to aging.
+ * ENTRYTYPE_MACV4 is not subject to aging. For IPv4 multicast.
+ * ENTRYTYPE_MACV6 is not subject to aging. For IPv6 multicast.
+ */
+enum macaccess_entry_type {
+	ENTRYTYPE_NORMAL = 0,
+	ENTRYTYPE_LOCKED,
+	ENTRYTYPE_MACV4,
+	ENTRYTYPE_MACV6,
+};
+
 struct lan9645x {
 	struct device *dev;
 	struct dsa_switch *ds;
@@ -192,6 +205,7 @@ struct lan9645x {
 	u16 bridge_mask; /* Mask for bridged ports */
 	/* lock forwarding configuration and vlan table */
 	struct mutex fwd_domain_lock;
+	struct mutex mact_lock; /* serialize mac table register access */
 
 	int num_port_dis;
 
@@ -376,4 +390,17 @@ int lan9645x_vlan_port_add_vlan(struct lan9645x_port *p, u16 vid, bool pvid,
 int lan9645x_vlan_port_del_vlan(struct lan9645x_port *p, u16 vid);
 void lan9645x_vlan_set_hostmode(struct lan9645x_port *p);
 
+/* MAC table: lan9645x_mac.c */
+int lan9645x_mact_flush(struct lan9645x *lan9645x, int port);
+int lan9645x_mact_learn(struct lan9645x *lan9645x, int port,
+			const unsigned char mac[ETH_ALEN], unsigned int vid,
+			enum macaccess_entry_type type, bool cpu_copy);
+int lan9645x_mact_forget(struct lan9645x *lan9645x,
+			 const unsigned char mac[ETH_ALEN], unsigned int vid,
+			 enum macaccess_entry_type type);
+int lan9645x_mac_init(struct lan9645x *lan9645x);
+void lan9645x_mac_deinit(struct lan9645x *lan9645x);
+int lan9645x_mact_dsa_dump(struct lan9645x *lan9645x, int port,
+			   dsa_fdb_dump_cb_t *cb, void *data);
+
 #endif /* __LAN9645X_MAIN_H__ */
-- 
2.52.0

[PATCH net-next v12 7/9] net: dsa: lan9645x: add mdb management

From: Jens Emil Schulz Østergaard <hidden>
Date: 2026-09-08 07:41:40

Add support for dsa mdb callbacks. L2 multicast and IP multicast is
handled differently. IP multicast stores the port group mask inline in
the mac table. L2 multicast points at a PGID, whose destination mask is
the port group.

CPU port module membership is carried by the mac table entry's
MAC_CPU_COPY bit rather than by the PGID destination mask. This differs
from mscc/ocelot, which keeps the CPU port module bit inside the PGID
mask, and it lets groups that differ only in host membership share a
PGID.

The PGID table is finite, so it is possible to exhaust it and be unable to
service an L2 MC mdb request. The two directions then behave differently on
purpose.

On add we report the error and leave the group forwarding to the port group
it already had, so the caller learns the port was not joined and the
existing members are undisturbed.

On delete we always report success. Reporting a failure has negative
effects. The switchdev_port_obj_del_deferred() path only logs the error,
while br_switchdev_mdb_replay() abandons the remaining entries after the
first failure, so failing one delete during a bridge leave would leave
the other groups programmed. The group falls back to forwarding
according to the flood masks and host membership instead.

IGMP and MLD frames are redirected to the CPU, and IPv4 multicast
control frames are copied to it, so that the bridge can snoop them.

Reviewed-by: Steen Hegelund <Steen.Hegelund@microchip.com>
Signed-off-by: Jens Emil Schulz Østergaard <redacted>
---
Changes in v12:
- Move this patch before bridge support.
- lan9645x_teardown(): deinit the mdb before the mac table, so teardown
  unwinds in reverse order of init.
- Move the IGMP, MLD and IPMC CPU queue configuration from
  lan9645x_mdb_init() to lan9645x_setup(), next to the other CPUQ setup.
- __lan9645x_mdb_del(): do not bail out when no PGID is free. If the
  entry owns its PGID outright, narrow the port mask in place.
  Otherwise fall back to the L2 MC flood mask.
- Split lan9645x_mdb_update_dest() into lan9645x_mdb_widen_dest() for the
  add direction and lan9645x_mdb_narrow_dest() for the delete direction.
  The bridge reads the return value of a .port_mdb_add but not of a
  .port_mdb_del, so the two need opposite error handling.
- __lan9645x_mdb_add(): let a group already on the flood mask absorb
  membership changes, so the host can still join it.
- Note that IGMP and MLD are redirected to the CPU and IPv4 multicast
  control frames copied, so the bridge can snoop them.
- __lan9645x_mdb_del(): log a failing CMD_FORGET but drop the software
  entry anyway. The bridge never repeats a delete, so a kept entry
  would be wedged.
- Issue a CMD_FORGET before discarding a freshly allocated mdb entry whose
  CMD_LEARN reported an error, so a bus failure between the command and
  the status read cannot leave an unreachable entry behind in hardware.
- Use GPL-2.0+ in the SPDX identifier, matching the rest of the driver.
- Assert mdb_lock in every helper that walks the mdb or pgid lists.
- Replace the PGID_INDEX() macro with lan9645x_pgid_idx().
- Spell the mac argument as mac[ETH_ALEN] and the vid as unsigned int.
- Keep the PGID reference when a mac table write fails, both in
  lan9645x_mdb_narrow_dest() and when the CMD_FORGET fails for the last
  port leaving a group, so the index cannot be handed to an unrelated
  group while hardware may still point at it.
- lan9645x_mdb_deinit(): drain pgid_entries, which can now outlive every
  mdb entry.
- Note why the delete side reports success. One switchdev path ignores
  the error and br_switchdev_mdb_replay() truncates on it.
- Classify IPv4 multicast on the 01:00:5E prefix alone rather than with
  ether_addr_is_ipv4_mcast().
- Name the DSA ops lan9645x_port_mdb_add()/_del() after the
  .port_mdb_add hooks, rename the lan9645x_mdb.c entry points to
  lan9645x_mdb_add()/_del(), and fold the mdb_lock wrappers into them.
  The two files previously defined the same names at different levels
  of the same call chain.
- lan9645x_mdb_pgid_entry_get(): drop the unreachable !ports test, so
  a NULL return means an IP entry and nothing else.

Changes in v8:
- Use lan9645x->num_phys_ports instead of CPU_PORT for the CPU port
  module, and reword the related comments.

Changes in v7:
- refactor IGMP/MLD/IPMC_CTRL to use new LAN9645X_CPUQ_DEF,
  LAN9645X_CPUQ_TRAP and LAN9645X_CPUQ_COPY queues
- add __aligned(2) to mac variable in lan9645x_mdb_update_dest

Changes in v5:
- skip igmp/mld redir for npi port

Changes in v4:
- clean up fresh mdb when hw mac table write fails.
- avoid void return in lan9645x_offload_fwd_mark

Changes in v3:
- avoid mdb add/del dealloc when mac table writes fail
- dealloc mdb entries on deinit

Changes in v2:
- New file: selftests required implementation of the mdb callbacks.
---
 drivers/net/dsa/microchip/lan9645x/Makefile        |   1 +
 drivers/net/dsa/microchip/lan9645x/lan9645x_main.c |  72 +++
 drivers/net/dsa/microchip/lan9645x/lan9645x_main.h |  18 +
 drivers/net/dsa/microchip/lan9645x/lan9645x_mdb.c  | 569 +++++++++++++++++++++
 drivers/net/dsa/microchip/lan9645x/lan9645x_port.c |   8 +
 5 files changed, 668 insertions(+)
diff --git a/drivers/net/dsa/microchip/lan9645x/Makefile b/drivers/net/dsa/microchip/lan9645x/Makefile
index 70815edca5b9..2413d11fe849 100644
--- a/drivers/net/dsa/microchip/lan9645x/Makefile
+++ b/drivers/net/dsa/microchip/lan9645x/Makefile
@@ -4,6 +4,7 @@ obj-$(CONFIG_NET_DSA_MICROCHIP_LAN9645X) += mchp-lan9645x.o
 mchp-lan9645x-objs := \
 	lan9645x_mac.o \
 	lan9645x_main.o \
+	lan9645x_mdb.o \
 	lan9645x_npi.o \
 	lan9645x_phylink.o \
 	lan9645x_port.o \
diff --git a/drivers/net/dsa/microchip/lan9645x/lan9645x_main.c b/drivers/net/dsa/microchip/lan9645x/lan9645x_main.c
index 5cb4f5fe1374..cc85e18c5a3b 100644
--- a/drivers/net/dsa/microchip/lan9645x/lan9645x_main.c
+++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_main.c
@@ -74,6 +74,7 @@ static void lan9645x_teardown(struct dsa_switch *ds)
 {
 	struct lan9645x *lan9645x = ds->priv;
 
+	lan9645x_mdb_deinit(lan9645x);
 	lan9645x_mac_deinit(lan9645x);
 	mutex_destroy(&lan9645x->fwd_domain_lock);
 	lan9645x_npi_port_deinit(lan9645x, lan9645x->npi);
@@ -166,6 +167,7 @@ static int lan9645x_setup(struct dsa_switch *ds)
 	err = lan9645x_mac_init(lan9645x);
 	if (err)
 		goto err_mutex;
+	lan9645x_mdb_init(lan9645x);
 
 	/* Link Aggregation Mode: NETDEV_LAG_HASH_L2 */
 	lan_wr(ANA_AGGR_CFG_AC_SMAC_ENA |
@@ -253,6 +255,17 @@ static int lan9645x_setup(struct dsa_switch *ds)
 			lan9645x, ANA_CPUQ_8021_CFG(i));
 	}
 
+	/* Use CPU queues to communicate frame classification to the CPU */
+	lan_rmw(ANA_CPUQ_CFG_CPUQ_MAC_COPY_SET(LAN9645X_CPUQ_DEF) |
+		ANA_CPUQ_CFG_CPUQ_IGMP_SET(LAN9645X_CPUQ_TRAP) |
+		ANA_CPUQ_CFG_CPUQ_MLD_SET(LAN9645X_CPUQ_TRAP) |
+		ANA_CPUQ_CFG_CPUQ_IPMC_CTRL_SET(LAN9645X_CPUQ_COPY),
+		ANA_CPUQ_CFG_CPUQ_MAC_COPY |
+		ANA_CPUQ_CFG_CPUQ_IGMP |
+		ANA_CPUQ_CFG_CPUQ_MLD |
+		ANA_CPUQ_CFG_CPUQ_IPMC_CTRL,
+		lan9645x, ANA_CPUQ_CFG);
+
 	/* Reserve 1664 bytes, 26 buffer cells, per (port, prio) for source
 	 * tracking (resource 0, indices 0..95) and destination tracking
 	 * (resource 2, indices 512..607). These are access watermarks, not
@@ -439,6 +452,61 @@ static int lan9645x_fdb_del(struct dsa_switch *ds, int port,
 	return lan9645x_mact_forget(lan9645x, addr, vid, ENTRYTYPE_LOCKED);
 }
 
+static int lan9645x_port_mdb_add(struct dsa_switch *ds, int port,
+				 const struct switchdev_obj_port_mdb *mdb,
+				 struct dsa_db db)
+{
+	struct net_device *bridge_dev = lan9645x_db2bridge(db);
+	struct lan9645x *lan9645x = ds->priv;
+
+	dev_dbg(lan9645x->dev, "port=%d addr=%pM vid=%u\n", port, mdb->addr,
+		mdb->vid);
+
+	if (IS_ERR(bridge_dev))
+		return PTR_ERR(bridge_dev);
+
+	if (dsa_is_cpu_port(ds, port) && !bridge_dev &&
+	    dsa_mdb_present_in_other_db(ds, port, mdb, db))
+		return 0;
+
+	if (port == lan9645x->npi)
+		port = lan9645x->num_phys_ports;
+
+	return lan9645x_mdb_add(lan9645x, port, mdb, bridge_dev);
+}
+
+static int lan9645x_port_mdb_del(struct dsa_switch *ds, int port,
+				 const struct switchdev_obj_port_mdb *mdb,
+				 struct dsa_db db)
+{
+	struct net_device *bridge_dev = lan9645x_db2bridge(db);
+	struct lan9645x *lan9645x = ds->priv;
+	int err;
+
+	dev_dbg(lan9645x->dev, "port=%d addr=%pM vid=%u\n", port, mdb->addr,
+		mdb->vid);
+
+	if (IS_ERR(bridge_dev))
+		return PTR_ERR(bridge_dev);
+
+	if (dsa_is_cpu_port(ds, port) && !bridge_dev &&
+	    dsa_mdb_present_in_other_db(ds, port, mdb, db))
+		return 0;
+
+	if (port == lan9645x->npi)
+		port = lan9645x->num_phys_ports;
+
+	err = lan9645x_mdb_del(lan9645x, port, mdb, bridge_dev);
+	if (err == -ENOENT) {
+		dev_dbg(lan9645x->dev,
+			"mdb not found port=%d addr=%pM vid=%u\n", port,
+			mdb->addr, mdb->vid);
+		return 0;
+	}
+
+	return err;
+}
+
 static const struct dsa_switch_ops lan9645x_switch_ops = {
 	.get_tag_protocol		= lan9645x_get_tag_protocol,
 
@@ -463,6 +531,10 @@ static const struct dsa_switch_ops lan9645x_switch_ops = {
 	.port_fdb_dump			= lan9645x_fdb_dump,
 	.port_fdb_add			= lan9645x_fdb_add,
 	.port_fdb_del			= lan9645x_fdb_del,
+
+	/* Multicast database */
+	.port_mdb_add			= lan9645x_port_mdb_add,
+	.port_mdb_del			= lan9645x_port_mdb_del,
 };
 
 static int lan9645x_request_target_regmaps(struct lan9645x *lan9645x)
diff --git a/drivers/net/dsa/microchip/lan9645x/lan9645x_main.h b/drivers/net/dsa/microchip/lan9645x/lan9645x_main.h
index 4f077eac1d82..b320fbcc7312 100644
--- a/drivers/net/dsa/microchip/lan9645x/lan9645x_main.h
+++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_main.h
@@ -207,6 +207,14 @@ struct lan9645x {
 	struct mutex fwd_domain_lock;
 	struct mutex mact_lock; /* serialize mac table register access */
 
+	/* Multicast Forwarding Database */
+	struct list_head mdb_entries;
+	struct list_head pgid_entries;
+	/* lock for mdb_entries and pgid_entries. Must be taken before mact_lock
+	 * if both are taken.
+	 */
+	struct mutex mdb_lock;
+
 	int num_port_dis;
 
 	/* VLAN entries */
@@ -403,4 +411,14 @@ void lan9645x_mac_deinit(struct lan9645x *lan9645x);
 int lan9645x_mact_dsa_dump(struct lan9645x *lan9645x, int port,
 			   dsa_fdb_dump_cb_t *cb, void *data);
 
+/* Multicast Database lan9645x_mdb.c */
+int lan9645x_mdb_add(struct lan9645x *lan9645x, int port,
+		     const struct switchdev_obj_port_mdb *mdb,
+		     struct net_device *bridge);
+int lan9645x_mdb_del(struct lan9645x *lan9645x, int port,
+		     const struct switchdev_obj_port_mdb *mdb,
+		     struct net_device *bridge);
+void lan9645x_mdb_init(struct lan9645x *lan9645x);
+void lan9645x_mdb_deinit(struct lan9645x *lan9645x);
+
 #endif /* __LAN9645X_MAIN_H__ */
diff --git a/drivers/net/dsa/microchip/lan9645x/lan9645x_mdb.c b/drivers/net/dsa/microchip/lan9645x/lan9645x_mdb.c
new file mode 100644
index 000000000000..92ab5f55f791
--- /dev/null
+++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_mdb.c
@@ -0,0 +1,569 @@
+// SPDX-License-Identifier: GPL-2.0+
+/* Copyright (C) 2026 Microchip Technology Inc.
+ */
+
+#include "lan9645x_main.h"
+
+/* IPv4/IPv6 types the mac entry dest_idx is not used for forwarding. The
+ * datasheet recommends using 0 as the dummy index.
+ */
+#define IP_ENTRY_PGID		0
+
+struct lan9645x_pgid_entry {
+	struct list_head list;
+	int index;
+	refcount_t refcount;
+	u16 ports;
+};
+
+struct lan9645x_mdb_entry {
+	struct list_head list;
+	unsigned char mac[ETH_ALEN];
+	u16 vid;
+	u16 ports;
+	struct lan9645x_pgid_entry *pgid;
+};
+
+static int lan9645x_pgid_idx(const struct lan9645x_pgid_entry *pgid)
+{
+	return pgid ? pgid->index : IP_ENTRY_PGID;
+}
+
+void lan9645x_mdb_init(struct lan9645x *lan9645x)
+{
+	INIT_LIST_HEAD(&lan9645x->mdb_entries);
+	INIT_LIST_HEAD(&lan9645x->pgid_entries);
+	mutex_init(&lan9645x->mdb_lock);
+}
+
+static enum macaccess_entry_type lan9645x_mdb_classify(const unsigned char *mac)
+{
+	if (mac[0] == 0x01 && mac[1] == 0x00 && mac[2] == 0x5e)
+		return ENTRYTYPE_MACV4;
+	if (mac[0] == 0x33 && mac[1] == 0x33)
+		return ENTRYTYPE_MACV6;
+	return ENTRYTYPE_LOCKED;
+}
+
+static struct lan9645x_mdb_entry *
+lan9645x_mdb_entry_lookup(struct lan9645x *lan9645x, const unsigned char *mac,
+			  u16 vid)
+{
+	struct lan9645x_mdb_entry *mdb;
+
+	lockdep_assert_held(&lan9645x->mdb_lock);
+
+	list_for_each_entry(mdb, &lan9645x->mdb_entries, list) {
+		if (ether_addr_equal(mdb->mac, mac) && mdb->vid == vid)
+			return mdb;
+	}
+
+	return NULL;
+}
+
+static struct lan9645x_mdb_entry *
+lan9645x_mdb_entry_alloc(struct lan9645x *lan9645x,
+			 const unsigned char addr[ETH_ALEN], u16 vid)
+{
+	struct lan9645x_mdb_entry *mdb_entry;
+
+	lockdep_assert_held(&lan9645x->mdb_lock);
+
+	mdb_entry = kzalloc_obj(*mdb_entry);
+	if (!mdb_entry)
+		return ERR_PTR(-ENOMEM);
+
+	ether_addr_copy(mdb_entry->mac, addr);
+	mdb_entry->vid = vid;
+
+	list_add_tail(&mdb_entry->list, &lan9645x->mdb_entries);
+
+	dev_dbg(lan9645x->dev, "vid=%u addr=%pM\n", mdb_entry->vid,
+		mdb_entry->mac);
+
+	return mdb_entry;
+}
+
+static void lan9645x_mdb_encode_mac(unsigned char *dst,
+				    const unsigned char *mac, u16 ports,
+				    enum macaccess_entry_type type)
+{
+	ether_addr_copy(dst, mac);
+
+	/* The HW encodes the portmask in the high bits of the mac for ip
+	 * multicast entries, to save on the limited PGID resources.
+	 *
+	 * IPv4 Multicast DMAC: 0x01005Exxxxxx
+	 * IPv6 Multicast DMAC: 0x3333xxxxxxxx
+	 *
+	 * which gives us 24 or 16 bits to encode the portmask.
+	 */
+	if (type == ENTRYTYPE_MACV4) {
+		dst[0] = 0;
+		dst[1] = ports >> 8;
+		dst[2] = ports & 0xff;
+	} else if (type == ENTRYTYPE_MACV6) {
+		dst[0] = ports >> 8;
+		dst[1] = ports & 0xff;
+	}
+}
+
+static void lan9645x_pgid_entry_put(struct lan9645x *lan9645x,
+				    struct lan9645x_pgid_entry *pgid_entry)
+{
+	lockdep_assert_held(&lan9645x->mdb_lock);
+
+	if (!pgid_entry)
+		return;
+
+	if (!refcount_dec_and_test(&pgid_entry->refcount))
+		return;
+
+	dev_dbg(lan9645x->dev, "pgid=%d ports=0x%x\n", pgid_entry->index,
+		pgid_entry->ports);
+	/* Leave the destination mask programmed in HW. Reusing the index
+	 * rewrites the mask in lan9645x_pgid_entry_alloc() before anything
+	 * points at it.
+	 */
+	list_del(&pgid_entry->list);
+	kfree(pgid_entry);
+}
+
+static void lan9645x_mdb_entry_dealloc(struct lan9645x *lan9645x,
+				       struct lan9645x_mdb_entry *mdb_entry)
+{
+	lockdep_assert_held(&lan9645x->mdb_lock);
+
+	dev_dbg(lan9645x->dev, "vid=%u addr=%pM\n", mdb_entry->vid,
+		mdb_entry->mac);
+	list_del(&mdb_entry->list);
+	lan9645x_pgid_entry_put(lan9645x, mdb_entry->pgid);
+	kfree(mdb_entry);
+}
+
+static struct lan9645x_pgid_entry *
+lan9645x_mdb_pgid_entry_lookup(struct lan9645x *lan9645x, u16 ports)
+{
+	struct lan9645x_pgid_entry *pgid_entry;
+
+	lockdep_assert_held(&lan9645x->mdb_lock);
+
+	list_for_each_entry(pgid_entry, &lan9645x->pgid_entries, list) {
+		if (pgid_entry->ports == ports) {
+			refcount_inc(&pgid_entry->refcount);
+			return pgid_entry;
+		}
+	}
+
+	return NULL;
+}
+
+static struct lan9645x_pgid_entry *
+lan9645x_pgid_entry_alloc(struct lan9645x *lan9645x, int index, u16 ports)
+{
+	struct lan9645x_pgid_entry *pgid_entry;
+
+	lockdep_assert_held(&lan9645x->mdb_lock);
+
+	pgid_entry = kzalloc_obj(*pgid_entry);
+	if (!pgid_entry)
+		return ERR_PTR(-ENOMEM);
+
+	pgid_entry->ports = ports;
+	pgid_entry->index = index;
+	refcount_set(&pgid_entry->refcount, 1);
+
+	list_add_tail(&pgid_entry->list, &lan9645x->pgid_entries);
+
+	dev_dbg(lan9645x->dev, "index=%d ports=0x%x\n", pgid_entry->index,
+		pgid_entry->ports);
+
+	lan_rmw(ANA_PGID_PGID_SET(pgid_entry->ports),
+		ANA_PGID_PGID, lan9645x,
+		ANA_PGID(pgid_entry->index));
+
+	return pgid_entry;
+}
+
+static struct lan9645x_pgid_entry *
+lan9645x_mdb_pgid_entry_create(struct lan9645x *lan9645x, u16 ports)
+{
+	struct lan9645x_pgid_entry *pgid_entry;
+	int index;
+
+	lockdep_assert_held(&lan9645x->mdb_lock);
+
+	for (index = PGID_GP_START; index < PGID_GP_END; index++) {
+		bool used = false;
+
+		list_for_each_entry(pgid_entry, &lan9645x->pgid_entries, list) {
+			if (pgid_entry->index == index) {
+				used = true;
+				break;
+			}
+		}
+
+		if (!used)
+			return lan9645x_pgid_entry_alloc(lan9645x, index,
+							 ports);
+	}
+
+	return ERR_PTR(-ENOSPC);
+}
+
+static struct lan9645x_pgid_entry *
+lan9645x_mdb_pgid_entry_get(struct lan9645x *lan9645x, u16 ports,
+			    enum macaccess_entry_type type)
+{
+	struct lan9645x_pgid_entry *pgid_entry;
+	u16 pgid_ports;
+
+	lockdep_assert_held(&lan9645x->mdb_lock);
+
+	if (type == ENTRYTYPE_MACV4 || type == ENTRYTYPE_MACV6)
+		return NULL;
+
+	/* CPU port module forwarding is handled by cpu_copy flag on mac table
+	 * entry. So we can strip the CPU port module here to allow better PGID
+	 * sharing.
+	 */
+	pgid_ports = ports & ~BIT(lan9645x->num_phys_ports);
+
+	pgid_entry = lan9645x_mdb_pgid_entry_lookup(lan9645x, pgid_ports);
+	if (!pgid_entry)
+		return lan9645x_mdb_pgid_entry_create(lan9645x, pgid_ports);
+
+	return pgid_entry;
+}
+
+/* An L2 MC group without a pgid is one that fell back to the flood mask in
+ * __lan9645x_mdb_del().
+ */
+static bool
+lan9645x_mdb_on_flood_mask(const struct lan9645x_mdb_entry *mdb_entry,
+			   enum macaccess_entry_type type)
+{
+	return type == ENTRYTYPE_LOCKED && mdb_entry->ports && !mdb_entry->pgid;
+}
+
+/* Point the hardware mac table entry at pgid_index with the port mask
+ * new_ports. Does not touch mdb_entry, the caller owns the software state.
+ */
+static int lan9645x_mdb_write_dest(struct lan9645x *lan9645x,
+				   struct lan9645x_mdb_entry *mdb_entry,
+				   enum macaccess_entry_type type,
+				   int pgid_index, u16 new_ports)
+{
+	unsigned char mac[ETH_ALEN] __aligned(2);
+	bool cpu_copy;
+
+	lockdep_assert_held(&lan9645x->mdb_lock);
+
+	lan9645x_mdb_encode_mac(mac, mdb_entry->mac, new_ports, type);
+	cpu_copy = !!(new_ports & BIT(lan9645x->num_phys_ports));
+
+	/* For IP multicast, the hardware lookup uses the DMAC
+	 * (01:00:5E:.. / 33:33:..) as the (mac, vid) key, not the encoded mac.
+	 * Therefore, this CMD_LEARN will atomically rewrite the existing
+	 * hardware entry. We intentionally do not do a forget before learn
+	 * sequence, as that would not be atomic, and leave a forwarding gap.
+	 */
+	return lan9645x_mact_learn(lan9645x, pgid_index, mac, mdb_entry->vid,
+				   type, cpu_copy);
+}
+
+/* Grow the group. A port can be refused membership, and the bridge reads the
+ * return value of a SWITCHDEV_PORT_OBJ_ADD, so a failure must leave the
+ * software entry exactly as it was and drop the reference taken on new_pgid.
+ */
+static int lan9645x_mdb_widen_dest(struct lan9645x *lan9645x,
+				   struct lan9645x_mdb_entry *mdb_entry,
+				   enum macaccess_entry_type type,
+				   struct lan9645x_pgid_entry *new_pgid,
+				   int pgid_index, u16 new_ports)
+{
+	struct lan9645x_pgid_entry *old_pgid = mdb_entry->pgid;
+	int err;
+
+	lockdep_assert_held(&lan9645x->mdb_lock);
+
+	err = lan9645x_mdb_write_dest(lan9645x, mdb_entry, type, pgid_index,
+				      new_ports);
+	if (err) {
+		lan9645x_pgid_entry_put(lan9645x, new_pgid);
+		return err;
+	}
+	mdb_entry->pgid = new_pgid;
+	mdb_entry->ports = new_ports;
+	lan9645x_pgid_entry_put(lan9645x, old_pgid);
+	return 0;
+}
+
+/* Shrink the group. A port cannot be refused leaving, and reporting a failure
+ * is worse than useless. One del path ignores the error:
+ * switchdev_port_obj_del_deferred() only logs it, and unlike RTM_NEWMDB the
+ * RTM_DELMDB path installs no obj.complete. The other truncates on it:
+ * br_switchdev_mdb_replay() abandons the remaining entries after the first
+ * failure, so failing one delete during a bridge leave would leave the other
+ * groups programmed. The bridge has already dropped the group by the time we
+ * run, so it will never ask again.
+ *
+ * Commit the software state first, then program hardware and only log a
+ * failure. That keeps both invariants the rest of this file relies on:
+ *
+ *   mdb_entry->ports stays a subset of the bridge's view of the group, so a
+ *   later delete can still drive it to zero and reclaim the entry, and
+ *
+ *   mdb_entry->pgid->ports stays equal to
+ *   mdb_entry->ports & ~BIT(num_phys_ports), which is what lets
+ *   __lan9645x_mdb_del() conclude that a departing port reaching the
+ *   overwrite in place branch cannot be the CPU port module.
+ */
+static void lan9645x_mdb_narrow_dest(struct lan9645x *lan9645x,
+				     struct lan9645x_mdb_entry *mdb_entry,
+				     enum macaccess_entry_type type,
+				     struct lan9645x_pgid_entry *new_pgid,
+				     int pgid_index, u16 new_ports)
+{
+	struct lan9645x_pgid_entry *old_pgid = mdb_entry->pgid;
+	int err;
+
+	lockdep_assert_held(&lan9645x->mdb_lock);
+
+	mdb_entry->pgid = new_pgid;
+	mdb_entry->ports = new_ports;
+
+	err = lan9645x_mdb_write_dest(lan9645x, mdb_entry, type, pgid_index,
+				      new_ports);
+	if (err) {
+		dev_err(lan9645x->dev,
+			"Narrowing %pM vid %u to mask 0x%x returned %pe\n",
+			mdb_entry->mac, mdb_entry->vid, new_ports,
+			ERR_PTR(err));
+		return;
+	}
+
+	lan9645x_pgid_entry_put(lan9645x, old_pgid);
+}
+
+static int __lan9645x_mdb_add(struct lan9645x *lan9645x, int chip_port,
+			      const unsigned char addr[ETH_ALEN], u16 vid,
+			      enum macaccess_entry_type type)
+{
+	struct lan9645x_pgid_entry *new_pgid;
+	struct lan9645x_mdb_entry *mdb_entry;
+	u16 new_ports;
+	int err;
+
+	lockdep_assert_held(&lan9645x->mdb_lock);
+
+	mdb_entry = lan9645x_mdb_entry_lookup(lan9645x, addr, vid);
+	if (!mdb_entry) {
+		mdb_entry = lan9645x_mdb_entry_alloc(lan9645x, addr, vid);
+		if (IS_ERR(mdb_entry))
+			return PTR_ERR(mdb_entry);
+	}
+
+	if (mdb_entry->ports & BIT(chip_port))
+		return 0;
+
+	new_ports = mdb_entry->ports | BIT(chip_port);
+
+	/* Update PGID ptr for non-IP entries (L2 multicast) */
+	new_pgid = lan9645x_mdb_pgid_entry_get(lan9645x, new_ports, type);
+	if (IS_ERR(new_pgid)) {
+		/* Out of PGIDs or mem. Remove a fresh mdb_entry again. */
+		if (!mdb_entry->ports) {
+			lan9645x_mdb_entry_dealloc(lan9645x, mdb_entry);
+			return PTR_ERR(new_pgid);
+		}
+
+		/* For a L2 MC group already on the flood mask, we keep it there
+		 * so that the host can still join/leave a group we had to give
+		 * up offloading.
+		 */
+		if (lan9645x_mdb_on_flood_mask(mdb_entry, type))
+			return lan9645x_mdb_widen_dest(lan9645x, mdb_entry,
+						       type, NULL, PGID_MC,
+						       new_ports);
+
+		/* Continue forwarding to old port group. */
+		return PTR_ERR(new_pgid);
+	}
+
+	err = lan9645x_mdb_widen_dest(lan9645x, mdb_entry, type, new_pgid,
+				      lan9645x_pgid_idx(new_pgid), new_ports);
+	if (err && !mdb_entry->ports) {
+		/* We are about to drop a fresh entry, so make sure the hardware
+		 * does not keep one we can no longer reach. The mac commands
+		 * complete in a few microseconds, so the only way to get here
+		 * with an entry actually written is for the register bus to
+		 * fail between the command and the status read. That is close
+		 * to impossible, but the forget costs nothing: it is a noop if
+		 * the entry was never written, and it fails harmlessly if the
+		 * bus is still down.
+		 */
+		lan9645x_mact_forget(lan9645x, mdb_entry->mac,
+				     mdb_entry->vid, type);
+		lan9645x_mdb_entry_dealloc(lan9645x, mdb_entry);
+	}
+
+	return err;
+}
+
+static int __lan9645x_mdb_del(struct lan9645x *lan9645x, int chip_port,
+			      const unsigned char addr[ETH_ALEN], u16 vid,
+			      enum macaccess_entry_type type)
+{
+	struct lan9645x_pgid_entry *new_pgid;
+	struct lan9645x_mdb_entry *mdb_entry;
+	u16 new_ports;
+	int err;
+
+	lockdep_assert_held(&lan9645x->mdb_lock);
+
+	mdb_entry = lan9645x_mdb_entry_lookup(lan9645x, addr, vid);
+	if (!mdb_entry)
+		return -ENOENT;
+
+	if (!(mdb_entry->ports & BIT(chip_port)))
+		return 0;
+
+	new_ports = mdb_entry->ports & ~BIT(chip_port);
+
+	if (!new_ports) {
+		/* The encoded bytes are not part of the (mac, vid) key used for
+		 * lookups in the mactable, for entries of type 2
+		 * (IPv4 Multicast) and type 3 (IPv6 Multicast). For these types
+		 * the mac used in the key is the 'real' mac e.g.
+		 *
+		 * Type 2 (IPv4 multicast):
+		 * KEY_MAC = 0x01005E000000 | MACLDATA[23:0]
+		 *
+		 * Type 3 (IPv6 multicast):
+		 * KEY_MAC = 0x333300000000 | MACLDATA[31:0]
+		 *
+		 * This holds for both the datapath and the CPU access path.
+		 * Therefore, it is not necessary to encode the mac before a
+		 * CMD_FORGET, because the bytes it changes are unused for the
+		 * lookup.
+		 */
+		err = lan9645x_mact_forget(lan9645x, mdb_entry->mac,
+					   mdb_entry->vid, type);
+		if (err) {
+			dev_err(lan9645x->dev,
+				"Forgetting %pM vid %u on port %d returned %pe\n",
+				mdb_entry->mac, mdb_entry->vid, chip_port,
+				ERR_PTR(err));
+			mdb_entry->pgid = NULL;
+		}
+		lan9645x_mdb_entry_dealloc(lan9645x, mdb_entry);
+		return 0;
+	}
+
+	/* Update PGID ptr for non-IP entries (L2 multicast) */
+	new_pgid = lan9645x_mdb_pgid_entry_get(lan9645x, new_ports, type);
+	if (!IS_ERR(new_pgid)) {
+		lan9645x_mdb_narrow_dest(lan9645x, mdb_entry, type, new_pgid,
+					 lan9645x_pgid_idx(new_pgid),
+					 new_ports);
+		return 0;
+	}
+
+	/* Out of pgids for L2 MC.
+	 *
+	 * PGID is not shared, so we overwrite in place.
+	 *
+	 * We know the deleted port is not the CPU. Getting here means
+	 * lan9645x_mdb_pgid_entry_get() found no PGID holding the narrowed
+	 * mask, and for a delete of only the CPU port module bit the mask it
+	 * looks up is unchanged, since lan9645x_mdb_pgid_entry_get() strips
+	 * that bit. lan9645x_mdb_pgid_entry_lookup() would have returned the
+	 * PGID this entry already points at. So the departing port is a front
+	 * port, and the mac table entry, which only carries the PGID index and
+	 * MAC_CPU_COPY, does not need rewriting.
+	 *
+	 * refcount_read is safe because mdb_lock serializes every get and put
+	 * of a PGID entry.
+	 */
+	if (mdb_entry->pgid && refcount_read(&mdb_entry->pgid->refcount) == 1) {
+		mdb_entry->pgid->ports = new_ports &
+					 ~BIT(lan9645x->num_phys_ports);
+		mdb_entry->ports = new_ports;
+
+		lan_rmw(ANA_PGID_PGID_SET(mdb_entry->pgid->ports),
+			ANA_PGID_PGID,
+			lan9645x, ANA_PGID(mdb_entry->pgid->index));
+
+		return 0;
+	}
+
+	/* Shared PGID or on flood mask. Point the entry at the L2 MC flood mask
+	 * instead. This way our mac table entry survives and MAC_CPU_COPY
+	 * keeps host membership working.
+	 */
+	if (mdb_entry->pgid)
+		dev_warn_ratelimited(lan9645x->dev,
+				     "No PGID, flooding group %pM vid %u: %pe\n",
+				     mdb_entry->mac, mdb_entry->vid, new_pgid);
+
+	lan9645x_mdb_narrow_dest(lan9645x, mdb_entry, type, NULL, PGID_MC,
+				 new_ports);
+	return 0;
+}
+
+int lan9645x_mdb_add(struct lan9645x *lan9645x, int port,
+		     const struct switchdev_obj_port_mdb *mdb,
+		     struct net_device *bridge)
+{
+	enum macaccess_entry_type type;
+	u16 vid = mdb->vid;
+	int err;
+
+	type = lan9645x_mdb_classify(mdb->addr);
+
+	if (!vid)
+		vid = lan9645x_vlan_unaware_pvid(!!bridge);
+
+	mutex_lock(&lan9645x->mdb_lock);
+	err = __lan9645x_mdb_add(lan9645x, port, mdb->addr, vid, type);
+	mutex_unlock(&lan9645x->mdb_lock);
+	return err;
+}
+
+int lan9645x_mdb_del(struct lan9645x *lan9645x, int port,
+		     const struct switchdev_obj_port_mdb *mdb,
+		     struct net_device *bridge)
+{
+	enum macaccess_entry_type type;
+	u16 vid = mdb->vid;
+	int err;
+
+	type = lan9645x_mdb_classify(mdb->addr);
+
+	if (!vid)
+		vid = lan9645x_vlan_unaware_pvid(!!bridge);
+
+	mutex_lock(&lan9645x->mdb_lock);
+	err = __lan9645x_mdb_del(lan9645x, port, mdb->addr, vid, type);
+	mutex_unlock(&lan9645x->mdb_lock);
+	return err;
+}
+
+void lan9645x_mdb_deinit(struct lan9645x *lan9645x)
+{
+	struct lan9645x_pgid_entry *pgid, *pgid_tmp;
+	struct lan9645x_mdb_entry *mdb, *tmp;
+
+	mutex_lock(&lan9645x->mdb_lock);
+	list_for_each_entry_safe(mdb, tmp, &lan9645x->mdb_entries, list)
+		lan9645x_mdb_entry_dealloc(lan9645x, mdb);
+
+	list_for_each_entry_safe(pgid, pgid_tmp, &lan9645x->pgid_entries, list) {
+		list_del(&pgid->list);
+		kfree(pgid);
+	}
+	mutex_unlock(&lan9645x->mdb_lock);
+
+	mutex_destroy(&lan9645x->mdb_lock);
+}
diff --git a/drivers/net/dsa/microchip/lan9645x/lan9645x_port.c b/drivers/net/dsa/microchip/lan9645x/lan9645x_port.c
index 563bffdd20c0..4fe6c05601bf 100644
--- a/drivers/net/dsa/microchip/lan9645x/lan9645x_port.c
+++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_port.c
@@ -161,6 +161,14 @@ int lan9645x_port_setup(struct dsa_switch *ds, int port)
 		mutex_lock(&lan9645x->fwd_domain_lock);
 		lan9645x_vlan_set_hostmode(p);
 		mutex_unlock(&lan9645x->fwd_domain_lock);
+
+		lan_rmw(ANA_CPU_FWD_CFG_IGMP_REDIR_ENA_SET(1) |
+			ANA_CPU_FWD_CFG_MLD_REDIR_ENA_SET(1) |
+			ANA_CPU_FWD_CFG_IPMC_CTRL_COPY_ENA_SET(1),
+			ANA_CPU_FWD_CFG_IGMP_REDIR_ENA |
+			ANA_CPU_FWD_CFG_MLD_REDIR_ENA |
+			ANA_CPU_FWD_CFG_IPMC_CTRL_COPY_ENA,
+			lan9645x, ANA_CPU_FWD_CFG(p->chip_port));
 	}
 
 	return 0;
-- 
2.52.0

[PATCH net-next v12 8/9] net: dsa: lan9645x: add bridge support

From: Jens Emil Schulz Østergaard <hidden>
Date: 2026-09-08 07:41:45

Add support for hardware offloading of the bridge. We support a single
bridge device.

Mac entry ageing can be configured with a 2s granularity, and a
requested time is rounded down. An ANA_AUTOAGE.AGE_PERIOD of 0 stops the
analyzer from ageing at all, which is the opposite of what a bridge
ageing time of 0 asks for, so ds->ageing_time_min and
ds->ageing_time_max let the DSA core reject what the hardware cannot
express.

The flood masks are global and not per ingress port, so a host flood
request from one port causes unknown unicast or multicast from every
port to be forwarded to the CPU.

The bridge applies promiscuous mode to enslaved ports under most
conditions even when VLAN filtering is enabled. To protect a weak
external CPU, port_set_host_flood requests for offloaded bridged ports
are recorded, but not programmed until the port leaves the bridge.
Standalone ports get programmed, so software bridging works.

Reviewed-by: Steen Hegelund <Steen.Hegelund@microchip.com>
Signed-off-by: Jens Emil Schulz Østergaard <redacted>
---
Changes in v12:
- Move this patch after vlan, mac table and mdb support, so
  .port_fast_age exists for the STP state transitions and host address
  filtering is already advertised.
- Drop the open coded single bridge check now that ds->max_num_bridges
  makes the core reject a second bridge, and the now unused bridge field.
- Set ds->ageing_time_min and drop the clamp and the msecs == 0 case
  from lan9645x_set_ageing_time(). An ANA_AUTOAGE.AGE_PERIOD of 0 stops
  the analyzer from ageing, which is the opposite of what a bridge
  ageing_time of 0 asks for, so the DSA core now rejects out of range
  values instead.
- Drop mact_lock from lan9645x_set_ageing_time() after the fdb dump no
  longer toggles ageing.
- Remove the CPU port module from the unicast and multicast flood masks.
  Host address filtering covers the known addresses and
  port_set_host_flood() covers promiscuous and allmulti.
- Move lan9645x_vlan_clear_hostmode() here, with its caller.
- Describe the host flood compromise for offloaded bridged ports.
- lan9645x_port_set_learning() now takes fwd_domain_lock.
- port_set_host_flood(): drop the per port work and ordered workqueue
  and program the masks inline, since the ndo no longer runs in atomic
  context.
- Remove unreachable WARN_ON()s and the equally unreachable NPI port
  check in lan9645x_port_bridge_flags(). DSA does not call the port
  operations for the CPU port.

Changes in v9:
- Drop the port_mux_lock teardown from the setup error path, as the
  mutex has been removed.

Changes in v8:
- Use lan9645x->num_phys_ports instead of CPU_PORT for the CPU port
  module in the port pgid and host flood helpers.

Changes in v5:
- use ds->ageing_time_max
- use packed p->host_flood_req for atomic r/w
- fix typo in set_ageing_time comment
- include lan9645x->bridge deref under lock in brige_join

Changes in v4:
- set_host_flood changed to per port work to coalesce values and skip
  atomic allocations

Changes in v3:
- allow disabling aging with explicit zero parameters.
- fix non-forwarding stp states
- fix restore host_flood requests on bridge leave
- destroy fwd_domain_lock mutex on setup err path

Changes in v2:
- variable name consistency
- port_set_learning use stp_state before writing to hw
- add set_host_flood for selftests, which need promic/all_multi on
standalone interfaces
---
 drivers/net/dsa/microchip/lan9645x/lan9645x_main.c | 291 ++++++++++++++++++++-
 drivers/net/dsa/microchip/lan9645x/lan9645x_main.h |   7 +
 drivers/net/dsa/microchip/lan9645x/lan9645x_vlan.c |   9 +
 3 files changed, 296 insertions(+), 11 deletions(-)
diff --git a/drivers/net/dsa/microchip/lan9645x/lan9645x_main.c b/drivers/net/dsa/microchip/lan9645x/lan9645x_main.c
index cc85e18c5a3b..e41a73c20606 100644
--- a/drivers/net/dsa/microchip/lan9645x/lan9645x_main.c
+++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_main.c
@@ -283,19 +283,15 @@ static int lan9645x_setup(struct dsa_switch *ds)
 
 	lan9645x_port_cpu_init(lan9645x);
 
-	/* Multicast to cpu and all front ports */
-	lan_wr(BIT(lan9645x->num_phys_ports) | front_ports, lan9645x,
-	       ANA_PGID(PGID_MC));
+	/* Multicast to all front ports */
+	lan_wr(front_ports, lan9645x, ANA_PGID(PGID_MC));
 
-	/* IP multicast to cpu and all front ports */
-	lan_wr(BIT(lan9645x->num_phys_ports) | front_ports, lan9645x,
-	       ANA_PGID(PGID_MCIPV4));
-	lan_wr(BIT(lan9645x->num_phys_ports) | front_ports, lan9645x,
-	       ANA_PGID(PGID_MCIPV6));
+	/* IP multicast to all front ports */
+	lan_wr(front_ports, lan9645x, ANA_PGID(PGID_MCIPV4));
+	lan_wr(front_ports, lan9645x, ANA_PGID(PGID_MCIPV6));
 
-	/* Unicast to cpu and all front ports */
-	lan_wr(BIT(lan9645x->num_phys_ports) | front_ports, lan9645x,
-	       ANA_PGID(PGID_UC));
+	/* Unicast to all front ports */
+	lan_wr(front_ports, lan9645x, ANA_PGID(PGID_UC));
 
 	/* Broadcast to cpu and all front ports */
 	lan_wr(BIT(lan9645x->num_phys_ports) | front_ports, lan9645x,
@@ -306,6 +302,19 @@ static int lan9645x_setup(struct dsa_switch *ds)
 	ds->fdb_isolation = true;
 	ds->max_num_bridges = 1;
 
+	/* ANA_AUTOAGE_AGE_PERIOD is a seconds-based field and entries are
+	 * aged after 2 * AGE_PERIOD, giving (2 * FIELD_MAX) seconds of
+	 * maximum aging and 2 seconds of minimum aging.
+	 *
+	 * An AGE_PERIOD of 0 stops the analyzer from aging at all, which is
+	 * the opposite of what a bridge ageing_time of 0 asks for. The bridge
+	 * expires every dynamic entry immediately in that case and floods, so
+	 * reject it rather than keep learned addresses forever.
+	 */
+	ds->ageing_time_min = 2U * MSEC_PER_SEC;
+	ds->ageing_time_max = 2U * MSEC_PER_SEC *
+			      FIELD_MAX(ANA_AUTOAGE_AGE_PERIOD);
+
 	dev_info(lan9645x->dev,
 		 "SKU features: max_ports=%d\n",
 		 lan9645x->num_phys_ports - lan9645x->num_port_dis);
@@ -324,6 +333,257 @@ static void lan9645x_port_phylink_get_caps(struct dsa_switch *ds, int port,
 	lan9645x_phylink_get_caps(ds->priv, port, config);
 }
 
+static int lan9645x_set_ageing_time(struct dsa_switch *ds, unsigned int msecs)
+{
+	struct lan9645x *lan9645x = ds->priv;
+
+	/* Entry must suffer two aging scans before it is removed, so it is
+	 * aged after 2*AGE_PERIOD, and the unit is in seconds.
+	 */
+	lan_rmw(ANA_AUTOAGE_AGE_PERIOD_SET(msecs / (2 * MSEC_PER_SEC)),
+		ANA_AUTOAGE_AGE_PERIOD,
+		lan9645x, ANA_AUTOAGE);
+
+	return 0;
+}
+
+static int lan9645x_port_pre_bridge_flags(struct dsa_switch *ds, int port,
+					  struct switchdev_brport_flags flags,
+					  struct netlink_ext_ack *extack)
+{
+	if (flags.mask &
+	    ~(BR_LEARNING | BR_FLOOD | BR_MCAST_FLOOD | BR_BCAST_FLOOD))
+		return -EINVAL;
+
+	return 0;
+}
+
+static void lan9645x_port_pgid_set(struct lan9645x *lan9645x, u16 pgid,
+				   int chip_port, bool enabled)
+{
+	u32 reg_msk, port_msk;
+
+	port_msk = ANA_PGID_PGID_SET(enabled ? BIT(chip_port) : 0);
+	reg_msk = ANA_PGID_PGID_SET(BIT(chip_port));
+
+	lan_rmw(port_msk, reg_msk, lan9645x, ANA_PGID(pgid));
+}
+
+static void lan9645x_port_set_learning(struct lan9645x *lan9645x, int port,
+				       bool enabled)
+{
+	struct lan9645x_port *p = lan9645x_to_port(lan9645x, port);
+
+	mutex_lock(&lan9645x->fwd_domain_lock);
+	p->learn_ena = enabled;
+
+	enabled = enabled && (p->stp_state == BR_STATE_LEARNING ||
+			      p->stp_state == BR_STATE_FORWARDING);
+
+	lan_rmw(ANA_PORT_CFG_LEARN_ENA_SET(enabled), ANA_PORT_CFG_LEARN_ENA,
+		lan9645x, ANA_PORT_CFG(p->chip_port));
+	mutex_unlock(&lan9645x->fwd_domain_lock);
+}
+
+static int lan9645x_port_bridge_flags(struct dsa_switch *ds, int port,
+				      struct switchdev_brport_flags f,
+				      struct netlink_ext_ack *extack)
+{
+	struct lan9645x *lan9645x = ds->priv;
+
+	/* DSA core does not call this for the CPU port */
+	if (f.mask & BR_LEARNING)
+		lan9645x_port_set_learning(lan9645x, port,
+					   !!(f.val & BR_LEARNING));
+
+	if (f.mask & BR_FLOOD)
+		lan9645x_port_pgid_set(lan9645x, PGID_UC, port,
+				       !!(f.val & BR_FLOOD));
+
+	if (f.mask & BR_MCAST_FLOOD) {
+		bool ena = !!(f.val & BR_MCAST_FLOOD);
+
+		lan9645x_port_pgid_set(lan9645x, PGID_MC, port, ena);
+		lan9645x_port_pgid_set(lan9645x, PGID_MCIPV4, port, ena);
+		lan9645x_port_pgid_set(lan9645x, PGID_MCIPV6, port, ena);
+	}
+
+	if (f.mask & BR_BCAST_FLOOD)
+		lan9645x_port_pgid_set(lan9645x, PGID_BC, port,
+				       !!(f.val & BR_BCAST_FLOOD));
+
+	return 0;
+}
+
+static void lan9645x_update_fwd_mask(struct lan9645x *lan9645x)
+{
+	struct lan9645x_port *p;
+	struct dsa_port *dp;
+
+	lockdep_assert_held(&lan9645x->fwd_domain_lock);
+
+	/* Updates the source port PGIDs, making sure frames from p
+	 * are only forwarded to ports q != p, where q is relevant to forward
+	 */
+	dsa_switch_for_each_available_port(dp, lan9645x->ds) {
+		u32 mask = 0;
+
+		p = lan9645x_to_port(lan9645x, dp->index);
+
+		if (lan9645x_port_is_bridged(p) &&
+		    (lan9645x->bridge_fwd_mask & BIT(dp->index))) {
+			mask = lan9645x->bridge_mask &
+			       lan9645x->bridge_fwd_mask & ~BIT(dp->index);
+		}
+
+		lan_wr(ANA_PGID_PGID_SET(mask), lan9645x,
+		       ANA_PGID(PGID_SRC + dp->index));
+	}
+}
+
+static void __lan9645x_port_mark_host_flood(struct lan9645x *lan9645x, int port,
+					    bool uc, bool mc)
+{
+	lockdep_assert_held(&lan9645x->fwd_domain_lock);
+
+	if (uc)
+		lan9645x->host_flood_uc_mask |= BIT(port);
+	else
+		lan9645x->host_flood_uc_mask &= ~BIT(port);
+
+	if (mc)
+		lan9645x->host_flood_mc_mask |= BIT(port);
+	else
+		lan9645x->host_flood_mc_mask &= ~BIT(port);
+}
+
+static void __lan9645x_port_set_host_flood(struct lan9645x *lan9645x)
+{
+	bool mc_ena, uc_ena;
+	u16 unbridged;
+
+	lockdep_assert_held(&lan9645x->fwd_domain_lock);
+
+	/* We want promiscuous and all_multi to affect standalone ports, for
+	 * software bridging, debug and test purposes.
+	 *
+	 * However, the linux bridge is incredibly eager to put bridged ports in
+	 * promiscuous mode.
+	 *
+	 * This is unfortunate since lan9645x flood masks are global and not per
+	 * ingress port. When some port triggers unknown uc/mc to the CPU, the
+	 * traffic from any port is forwarded to the CPU.
+	 *
+	 * If the host CPU is weak, this can cause tremendous stress. Therefore,
+	 * we compromise by ignoring this host flood request for offloaded
+	 * bridged ports.
+	 */
+	unbridged = ~lan9645x->bridge_mask &
+		    GENMASK(lan9645x->num_phys_ports - 1, 0);
+
+	uc_ena = !!(lan9645x->host_flood_uc_mask & unbridged);
+	lan9645x_port_pgid_set(lan9645x, PGID_UC, lan9645x->num_phys_ports,
+			       uc_ena);
+
+	mc_ena = !!(lan9645x->host_flood_mc_mask & unbridged);
+	lan9645x_port_pgid_set(lan9645x, PGID_MC, lan9645x->num_phys_ports,
+			       mc_ena);
+	lan9645x_port_pgid_set(lan9645x, PGID_MCIPV4, lan9645x->num_phys_ports,
+			       mc_ena);
+	lan9645x_port_pgid_set(lan9645x, PGID_MCIPV6, lan9645x->num_phys_ports,
+			       mc_ena);
+}
+
+static void lan9645x_port_set_host_flood(struct dsa_switch *ds, int port,
+					 bool uc, bool mc)
+{
+	struct lan9645x *lan9645x = ds->priv;
+	struct lan9645x_port *p;
+
+	p = lan9645x_to_port(lan9645x, port);
+
+	mutex_lock(&lan9645x->fwd_domain_lock);
+	__lan9645x_port_mark_host_flood(lan9645x, p->chip_port, uc, mc);
+	__lan9645x_port_set_host_flood(lan9645x);
+	mutex_unlock(&lan9645x->fwd_domain_lock);
+}
+
+static int lan9645x_port_bridge_join(struct dsa_switch *ds, int port,
+				     struct dsa_bridge bridge,
+				     bool *tx_fwd_offload,
+				     struct netlink_ext_ack *extack)
+{
+	struct lan9645x *lan9645x = ds->priv;
+	struct lan9645x_port *p;
+
+	p = lan9645x_to_port(lan9645x, port);
+
+	mutex_lock(&lan9645x->fwd_domain_lock);
+
+	lan9645x->bridge_mask |= BIT(p->chip_port);
+	__lan9645x_port_set_host_flood(lan9645x);
+	lan9645x_vlan_clear_hostmode(p);
+
+	mutex_unlock(&lan9645x->fwd_domain_lock);
+
+	/* Forwarding is updated by .port_stp_state_set(), which the core
+	 * invokes after this returns:
+	 * dsa_port_bridge_join() -> dsa_port_switchdev_sync_attrs() ->
+	 * dsa_port_set_state()
+	 */
+
+	return 0;
+}
+
+static void lan9645x_port_bridge_stp_state_set(struct dsa_switch *ds, int port,
+					       u8 state)
+{
+	struct lan9645x *lan9645x;
+	struct lan9645x_port *p;
+	bool learn_ena;
+
+	lan9645x = ds->priv;
+	p = lan9645x_to_port(lan9645x, port);
+
+	mutex_lock(&lan9645x->fwd_domain_lock);
+
+	p->stp_state = state;
+
+	if (state == BR_STATE_FORWARDING)
+		lan9645x->bridge_fwd_mask |= BIT(p->chip_port);
+	else
+		lan9645x->bridge_fwd_mask &= ~BIT(p->chip_port);
+
+	learn_ena = (state == BR_STATE_LEARNING ||
+		     state == BR_STATE_FORWARDING) && p->learn_ena;
+
+	lan_rmw(ANA_PORT_CFG_LEARN_ENA_SET(learn_ena),
+		ANA_PORT_CFG_LEARN_ENA, lan9645x,
+		ANA_PORT_CFG(p->chip_port));
+
+	lan9645x_update_fwd_mask(lan9645x);
+	mutex_unlock(&lan9645x->fwd_domain_lock);
+}
+
+static void lan9645x_port_bridge_leave(struct dsa_switch *ds, int port,
+				       struct dsa_bridge bridge)
+{
+	struct lan9645x *lan9645x = ds->priv;
+	struct lan9645x_port *p;
+
+	p = lan9645x_to_port(lan9645x, port);
+
+	mutex_lock(&lan9645x->fwd_domain_lock);
+
+	lan9645x->bridge_mask &= ~BIT(p->chip_port);
+
+	__lan9645x_port_set_host_flood(lan9645x);
+	lan9645x_vlan_set_hostmode(p);
+	lan9645x_update_fwd_mask(lan9645x);
+
+	mutex_unlock(&lan9645x->fwd_domain_lock);
+}
+
 static int lan9645x_port_vlan_filtering(struct dsa_switch *ds, int port,
 					bool enabled,
 					struct netlink_ext_ack *extack)
@@ -521,6 +781,15 @@ static const struct dsa_switch_ops lan9645x_switch_ops = {
 	.port_change_mtu		= lan9645x_change_mtu,
 	.port_max_mtu			= lan9645x_get_max_mtu,
 
+	/* Bridge integration */
+	.set_ageing_time		= lan9645x_set_ageing_time,
+	.port_pre_bridge_flags		= lan9645x_port_pre_bridge_flags,
+	.port_bridge_flags		= lan9645x_port_bridge_flags,
+	.port_bridge_join		= lan9645x_port_bridge_join,
+	.port_bridge_leave		= lan9645x_port_bridge_leave,
+	.port_stp_state_set		= lan9645x_port_bridge_stp_state_set,
+	.port_set_host_flood		= lan9645x_port_set_host_flood,
+
 	/* VLAN integration */
 	.port_vlan_filtering		= lan9645x_port_vlan_filtering,
 	.port_vlan_add			= lan9645x_port_vlan_add,
diff --git a/drivers/net/dsa/microchip/lan9645x/lan9645x_main.h b/drivers/net/dsa/microchip/lan9645x/lan9645x_main.h
index b320fbcc7312..66fefd060fd8 100644
--- a/drivers/net/dsa/microchip/lan9645x/lan9645x_main.h
+++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_main.h
@@ -195,6 +195,9 @@ struct lan9645x {
 	struct dsa_switch *ds;
 	struct regmap *rmap[NUM_TARGETS];
 
+	u16 host_flood_uc_mask;
+	u16 host_flood_mc_mask;
+
 	/* NPI chip_port */
 	int npi;
 
@@ -203,6 +206,7 @@ struct lan9645x {
 
 	/* Forwarding Database */
 	u16 bridge_mask; /* Mask for bridged ports */
+	u16 bridge_fwd_mask; /* Mask for forwarding bridged ports */
 	/* lock forwarding configuration and vlan table */
 	struct mutex fwd_domain_lock;
 	struct mutex mact_lock; /* serialize mac table register access */
@@ -225,6 +229,8 @@ struct lan9645x_port {
 	struct lan9645x *lan9645x;
 
 	u8 chip_port;
+	u8 stp_state;
+	bool learn_ena;
 
 	bool vlan_aware;
 	u16 pvid;
@@ -397,6 +403,7 @@ int lan9645x_vlan_port_add_vlan(struct lan9645x_port *p, u16 vid, bool pvid,
 				struct netlink_ext_ack *extack);
 int lan9645x_vlan_port_del_vlan(struct lan9645x_port *p, u16 vid);
 void lan9645x_vlan_set_hostmode(struct lan9645x_port *p);
+void lan9645x_vlan_clear_hostmode(struct lan9645x_port *p);
 
 /* MAC table: lan9645x_mac.c */
 int lan9645x_mact_flush(struct lan9645x *lan9645x, int port);
diff --git a/drivers/net/dsa/microchip/lan9645x/lan9645x_vlan.c b/drivers/net/dsa/microchip/lan9645x/lan9645x_vlan.c
index 6d28a7cf1442..29d679c853dc 100644
--- a/drivers/net/dsa/microchip/lan9645x/lan9645x_vlan.c
+++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_vlan.c
@@ -347,6 +347,15 @@ void lan9645x_vlan_set_hostmode(struct lan9645x_port *p)
 	lan9645x_vlan_port_apply(p);
 }
 
+void lan9645x_vlan_clear_hostmode(struct lan9645x_port *p)
+{
+	lockdep_assert_held(&p->lan9645x->fwd_domain_lock);
+
+	p->lan9645x->vlans[HOST_PVID].portmask &= ~BIT(p->chip_port);
+	lan9645x_vlan_hw_wr(p->lan9645x, HOST_PVID);
+	lan9645x_vlan_port_apply(p);
+}
+
 int lan9645x_vlan_init(struct lan9645x *lan9645x)
 {
 	u32 all_phys_ports, all_ports;
-- 
2.52.0

[PATCH net-next v12 9/9] net: dsa: lan9645x: add port statistics

From: Jens Emil Schulz Østergaard <hidden>
Date: 2026-09-08 07:41:49

Add statistics support for the port counters. Chip registers are 32 bit,
so this unit is responsible maintaining a 64bit software cache, and
updating it frequently to handle overflows in hardware.

The eMAC and pMAC counter sources are decoded, but only the aggregate
source is reachable for now, until MAC merge support is added.

Reviewed-by: Steen Hegelund <Steen.Hegelund@microchip.com>
Signed-off-by: Jens Emil Schulz Østergaard <redacted>
---
Changes in v12:
- Clear hardware and software counters lan9645x_stats_init().
- get_stats64: add the missing SCNT_RX_PMAC_LONG to rx_packets.
- get_stats64: move SCNT_DR_TAIL from rx_dropped to rx_missed_errors.
- Drop the unimplemented ISDX, ESDX and SFID counter views, the MAC
  merge statistics, and the now unused view back pointer, type and name
  fields.
- Rename lan9645x_stats_add_cnt() to lan9645x_stats_update_counter().
- Move lan9645x_get_vstats() out of lan9645x_stats.h and into
  lan9645x_stats.c, and replace the STATS_INDEX and STAT_COUNTERS macros
  with static functions there.
- Drop the NULL return from lan9645x_get_vstats(), and the three guards it
  forced on its callers.
- Drop the unreachable mac merge related code.
- get_eth_mac_stats: leave InRangeLengthErrors and OutOfRangeLengthField
  unset. Both describe the 802.3 length/type field, which the MAC does not
  validate.
- get_eth_mac_stats: leave SingleCollisionFrames unset. SCNT_TX_COL
  counts collision events, charging 16 to a frame that hits the excessive
  collision limit, so it cannot express the attribute. It is still
  reported as rtnl_link_stats64.collisions.
- get_eth_mac_stats: add SCNT_RX_JABBER to FrameTooLongErrors, the
  too-long-with-bad-CRC half of the event.
- get_stats64: drop SCNT_RX_SYMBOL_ERR from rx_errors, it is a subset of
  SCNT_RX_CRC which rx_crc_errors already covers. Still reported as
  SymbolErrorDuringCarrier.
- get_stats64: report rx_length_errors and rx_crc_errors.
  rx_length_errors carries the FrameTooLongErrors counters, and runts
  are left out of it since they have no 802.3 length attribute.
  SCNT_RX_SHORT is reported in rx_errors and rmon undersize_pkts
  instead.
- Size the bulk read scratch buffer for a single index rather than all of
  them. hw_lock is held across the read and the transfer into the 64 bit
  counters, so only one index is ever live.
- Stop the statistics poller in .shutdown. dsa_switch_shutdown() does not
  call .teardown, so the delayed work stays armed and keeps reading
  registers after the parent holding the regmaps has been shut down.
- Drop dr_tail from the ethtool -S list. It is reported as
  rtnl_link_stats64.rx_missed_errors.
- lan9645x_teardown(): stop the counter poller first, so nothing reads
  registers while the rest unwinds.
- Stop using SCNT_RX_FRAG. It counts frames received after the port is
  paused rather than undersize frames, so it is dropped from rx_errors,
  rx_packets and the rmon fragment count.
- get_stats64: drop SCNT_RX_CAT_DROP and SCNT_RX_LONG from rx_dropped.
  Classifier drops cover ordinary ingress filtering, and an oversized
  frame is an error rather than a drop, so it is reported in rx_errors
  and rx_length_errors.
- lan9645x_stats.h: include what it uses, and spell the ethtool statistics
  argument u64, matching the definition.
- Rate limit the bulk read error. It sits under the three second poller and
  runs once per port.

Changes in v11:
- add comment about calling contexts of sw_lock in
  lan9645x_stats_get_stats64

Changes in v5:
- make lan9645x_stats_init void
- add SCNT_TX_BUFDROP to tx_dropped
- change rmon range {0,64} -> {64, 64}. Runt frames counted elsewhere.
- remove rx_crc, rx_symbol_err from rx_packets, as they are already
  counted in SZ_* buckets.
- add defensive cancel_delayed_work_sync in stats_free

Changes in v4:
- remove rx_uc and tx_uc from ethtool stats list, as they are derivable
  from the eth-mac group
- split stats_init into stats_alloc and stats_init, use alloc in probe
  and init in dsa_setup

Changes in v3:
- No changes

Changes in v2:
- introduce spinlock sw_lock protecting software counters and region
  buffer.
- ran Ioana's selftest for standard counters
        drivers/net/hw/ethtool_std_stats.sh
  along with
        selftests/drivers/net/hw/ethtool_rmon.sh
  they pass except for software injected pause frames.
- remove strings/counters covered by standard counters from
  get_strings/get_ethtool_stats.
- fix proper use of 'src' in standard counters
- remove static region table, and use stats_prepare_regions for dynamic
  region calculation inspired by ocelot.
- fix queue leak in error path.
---
 drivers/net/dsa/microchip/lan9645x/Makefile        |   1 +
 drivers/net/dsa/microchip/lan9645x/lan9645x_main.c |  82 +-
 drivers/net/dsa/microchip/lan9645x/lan9645x_main.h |   3 +
 .../net/dsa/microchip/lan9645x/lan9645x_stats.c    | 856 +++++++++++++++++++++
 .../net/dsa/microchip/lan9645x/lan9645x_stats.h    | 229 ++++++
 5 files changed, 1170 insertions(+), 1 deletion(-)
diff --git a/drivers/net/dsa/microchip/lan9645x/Makefile b/drivers/net/dsa/microchip/lan9645x/Makefile
index 2413d11fe849..cd994943c1c0 100644
--- a/drivers/net/dsa/microchip/lan9645x/Makefile
+++ b/drivers/net/dsa/microchip/lan9645x/Makefile
@@ -8,4 +8,5 @@ mchp-lan9645x-objs := \
 	lan9645x_npi.o \
 	lan9645x_phylink.o \
 	lan9645x_port.o \
+	lan9645x_stats.o \
 	lan9645x_vlan.o \
diff --git a/drivers/net/dsa/microchip/lan9645x/lan9645x_main.c b/drivers/net/dsa/microchip/lan9645x/lan9645x_main.c
index e41a73c20606..599099c72968 100644
--- a/drivers/net/dsa/microchip/lan9645x/lan9645x_main.c
+++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_main.c
@@ -5,6 +5,7 @@
 #include <linux/platform_device.h>
 
 #include "lan9645x_main.h"
+#include "lan9645x_stats.h"
 
 static const char *lan9645x_resource_names[NUM_TARGETS] = {
 	[TARGET_GCB]          = "gcb",
@@ -74,6 +75,7 @@ static void lan9645x_teardown(struct dsa_switch *ds)
 {
 	struct lan9645x *lan9645x = ds->priv;
 
+	lan9645x_stats_deinit(lan9645x);
 	lan9645x_mdb_deinit(lan9645x);
 	lan9645x_mac_deinit(lan9645x);
 	mutex_destroy(&lan9645x->fwd_domain_lock);
@@ -297,6 +299,8 @@ static int lan9645x_setup(struct dsa_switch *ds)
 	lan_wr(BIT(lan9645x->num_phys_ports) | front_ports, lan9645x,
 	       ANA_PGID(PGID_BC));
 
+	lan9645x_stats_init(lan9645x);
+
 	ds->mtu_enforcement_ingress = true;
 	ds->assisted_learning_on_cpu_port = true;
 	ds->fdb_isolation = true;
@@ -767,6 +771,62 @@ static int lan9645x_port_mdb_del(struct dsa_switch *ds, int port,
 	return err;
 }
 
+static void lan9645x_get_strings(struct dsa_switch *ds, int port, u32 stringset,
+				 uint8_t *data)
+{
+	lan9645x_stats_get_strings(ds->priv, port, stringset, data);
+}
+
+static void lan9645x_get_ethtool_stats(struct dsa_switch *ds, int port,
+				       uint64_t *data)
+{
+	lan9645x_stats_get_ethtool_stats(ds->priv, port, data);
+}
+
+static int lan9645x_get_sset_count(struct dsa_switch *ds, int port, int sset)
+{
+	return lan9645x_stats_get_sset_count(ds->priv, port, sset);
+}
+
+static void lan9645x_get_eth_mac_stats(struct dsa_switch *ds, int port,
+				       struct ethtool_eth_mac_stats *mac_stats)
+{
+	lan9645x_stats_get_eth_mac_stats(ds->priv, port, mac_stats);
+}
+
+static void
+lan9645x_get_rmon_stats(struct dsa_switch *ds, int port,
+			struct ethtool_rmon_stats *rmon_stats,
+			const struct ethtool_rmon_hist_range **ranges)
+{
+	lan9645x_stats_get_rmon_stats(ds->priv, port, rmon_stats, ranges);
+}
+
+static void lan9645x_get_stats64(struct dsa_switch *ds, int port,
+				 struct rtnl_link_stats64 *s)
+{
+	lan9645x_stats_get_stats64(ds->priv, port, s);
+}
+
+static void lan9645x_get_pause_stats(struct dsa_switch *ds, int port,
+				     struct ethtool_pause_stats *pause_stats)
+{
+	lan9645x_stats_get_pause_stats(ds->priv, port, pause_stats);
+}
+
+static void lan9645x_get_eth_phy_stats(struct dsa_switch *ds, int port,
+				       struct ethtool_eth_phy_stats *phy_stats)
+{
+	lan9645x_stats_get_eth_phy_stats(ds->priv, port, phy_stats);
+}
+
+static void
+lan9645x_get_eth_ctrl_stats(struct dsa_switch *ds, int port,
+			    struct ethtool_eth_ctrl_stats *ctrl_stats)
+{
+	lan9645x_stats_get_eth_ctrl_stats(ds->priv, port, ctrl_stats);
+}
+
 static const struct dsa_switch_ops lan9645x_switch_ops = {
 	.get_tag_protocol		= lan9645x_get_tag_protocol,
 
@@ -804,6 +864,17 @@ static const struct dsa_switch_ops lan9645x_switch_ops = {
 	/* Multicast database */
 	.port_mdb_add			= lan9645x_port_mdb_add,
 	.port_mdb_del			= lan9645x_port_mdb_del,
+
+	/* Port statistics counters. */
+	.get_strings			= lan9645x_get_strings,
+	.get_ethtool_stats		= lan9645x_get_ethtool_stats,
+	.get_sset_count			= lan9645x_get_sset_count,
+	.get_eth_mac_stats		= lan9645x_get_eth_mac_stats,
+	.get_rmon_stats			= lan9645x_get_rmon_stats,
+	.get_stats64			= lan9645x_get_stats64,
+	.get_pause_stats		= lan9645x_get_pause_stats,
+	.get_eth_phy_stats		= lan9645x_get_eth_phy_stats,
+	.get_eth_ctrl_stats		= lan9645x_get_eth_ctrl_stats,
 };
 
 static int lan9645x_request_target_regmaps(struct lan9645x *lan9645x)
@@ -898,9 +969,15 @@ static int lan9645x_probe(struct platform_device *pdev)
 
 	lan9645x_set_feat_dis(lan9645x);
 
-	err = dsa_register_switch(ds);
+	err = lan9645x_stats_alloc(lan9645x);
 	if (err)
+		return dev_err_probe(dev, err, "Failed to allocate stats\n");
+
+	err = dsa_register_switch(ds);
+	if (err) {
+		lan9645x_stats_free(lan9645x);
 		return dev_err_probe(dev, err, "Failed to register DSA switch\n");
+	}
 
 	return 0;
 }
@@ -914,6 +991,7 @@ static void lan9645x_remove(struct platform_device *pdev)
 
 	/* Calls lan9645x DSA .teardown */
 	dsa_unregister_switch(lan9645x->ds);
+	lan9645x_stats_free(lan9645x);
 	dev_set_drvdata(&pdev->dev, NULL);
 }
 
@@ -924,6 +1002,8 @@ static void lan9645x_shutdown(struct platform_device *pdev)
 	if (!lan9645x)
 		return;
 
+	lan9645x_stats_deinit(lan9645x);
+
 	dsa_switch_shutdown(lan9645x->ds);
 
 	dev_set_drvdata(&pdev->dev, NULL);
diff --git a/drivers/net/dsa/microchip/lan9645x/lan9645x_main.h b/drivers/net/dsa/microchip/lan9645x/lan9645x_main.h
index 66fefd060fd8..1044cb0f5682 100644
--- a/drivers/net/dsa/microchip/lan9645x/lan9645x_main.h
+++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_main.h
@@ -219,6 +219,9 @@ struct lan9645x {
 	 */
 	struct mutex mdb_lock;
 
+	/* Statistics  */
+	struct lan9645x_stats *stats;
+
 	int num_port_dis;
 
 	/* VLAN entries */
diff --git a/drivers/net/dsa/microchip/lan9645x/lan9645x_stats.c b/drivers/net/dsa/microchip/lan9645x/lan9645x_stats.c
new file mode 100644
index 000000000000..1253f7748993
--- /dev/null
+++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_stats.c
@@ -0,0 +1,856 @@
+// SPDX-License-Identifier: GPL-2.0+
+/* Copyright (C) 2026 Microchip Technology Inc.
+ */
+
+#include <linux/spinlock.h>
+#include <linux/workqueue.h>
+
+#include "lan9645x_main.h"
+#include "lan9645x_stats.h"
+
+#define LAN9645X_STATS_CHECK_DELAY	(3 * HZ)
+
+/* SYS_STAT_CFG.STAT_CLEAR_SHOT group bits selecting the counter groups that
+ * are indexed by port: 0 (Rx), 1 (Tx) and 2 (Drop).
+ * Bit 0: RX
+ * Bit 1: TX
+ * Bit 2: Drop
+ */
+#define LAN9645X_STAT_PORT_GROUPS	GENMASK(2, 0)
+
+static const u32 lan9645x_port_stats_layout[] = {
+	[SCNT_RX_OCT]              = 0x0,
+	[SCNT_RX_UC]               = 0x1,
+	[SCNT_RX_MC]               = 0x2,
+	[SCNT_RX_BC]               = 0x3,
+	[SCNT_RX_SHORT]            = 0x4,
+	[SCNT_RX_FRAG]             = 0x5,
+	[SCNT_RX_JABBER]           = 0x6,
+	[SCNT_RX_CRC]              = 0x7,
+	[SCNT_RX_SYMBOL_ERR]       = 0x8,
+	[SCNT_RX_SZ_64]            = 0x9,
+	[SCNT_RX_SZ_65_127]        = 0xa,
+	[SCNT_RX_SZ_128_255]       = 0xb,
+	[SCNT_RX_SZ_256_511]       = 0xc,
+	[SCNT_RX_SZ_512_1023]      = 0xd,
+	[SCNT_RX_SZ_1024_1526]     = 0xe,
+	[SCNT_RX_SZ_JUMBO]         = 0xf,
+	[SCNT_RX_PAUSE]            = 0x10,
+	[SCNT_RX_CONTROL]          = 0x11,
+	[SCNT_RX_LONG]             = 0x12,
+	[SCNT_RX_CAT_DROP]         = 0x13,
+	[SCNT_RX_RED_PRIO_0]       = 0x14,
+	[SCNT_RX_RED_PRIO_1]       = 0x15,
+	[SCNT_RX_RED_PRIO_2]       = 0x16,
+	[SCNT_RX_RED_PRIO_3]       = 0x17,
+	[SCNT_RX_RED_PRIO_4]       = 0x18,
+	[SCNT_RX_RED_PRIO_5]       = 0x19,
+	[SCNT_RX_RED_PRIO_6]       = 0x1a,
+	[SCNT_RX_RED_PRIO_7]       = 0x1b,
+	[SCNT_RX_YELLOW_PRIO_0]    = 0x1c,
+	[SCNT_RX_YELLOW_PRIO_1]    = 0x1d,
+	[SCNT_RX_YELLOW_PRIO_2]    = 0x1e,
+	[SCNT_RX_YELLOW_PRIO_3]    = 0x1f,
+	[SCNT_RX_YELLOW_PRIO_4]    = 0x20,
+	[SCNT_RX_YELLOW_PRIO_5]    = 0x21,
+	[SCNT_RX_YELLOW_PRIO_6]    = 0x22,
+	[SCNT_RX_YELLOW_PRIO_7]    = 0x23,
+	[SCNT_RX_GREEN_PRIO_0]     = 0x24,
+	[SCNT_RX_GREEN_PRIO_1]     = 0x25,
+	[SCNT_RX_GREEN_PRIO_2]     = 0x26,
+	[SCNT_RX_GREEN_PRIO_3]     = 0x27,
+	[SCNT_RX_GREEN_PRIO_4]     = 0x28,
+	[SCNT_RX_GREEN_PRIO_5]     = 0x29,
+	[SCNT_RX_GREEN_PRIO_6]     = 0x2a,
+	[SCNT_RX_GREEN_PRIO_7]     = 0x2b,
+	[SCNT_RX_ASSEMBLY_ERR]     = 0x2c,
+	[SCNT_RX_SMD_ERR]          = 0x2d,
+	[SCNT_RX_ASSEMBLY_OK]      = 0x2e,
+	[SCNT_RX_MERGE_FRAG]       = 0x2f,
+	[SCNT_RX_PMAC_OCT]         = 0x30,
+	[SCNT_RX_PMAC_UC]          = 0x31,
+	[SCNT_RX_PMAC_MC]          = 0x32,
+	[SCNT_RX_PMAC_BC]          = 0x33,
+	[SCNT_RX_PMAC_SHORT]       = 0x34,
+	[SCNT_RX_PMAC_FRAG]        = 0x35,
+	[SCNT_RX_PMAC_JABBER]      = 0x36,
+	[SCNT_RX_PMAC_CRC]         = 0x37,
+	[SCNT_RX_PMAC_SYMBOL_ERR]  = 0x38,
+	[SCNT_RX_PMAC_SZ_64]       = 0x39,
+	[SCNT_RX_PMAC_SZ_65_127]   = 0x3a,
+	[SCNT_RX_PMAC_SZ_128_255]  = 0x3b,
+	[SCNT_RX_PMAC_SZ_256_511]  = 0x3c,
+	[SCNT_RX_PMAC_SZ_512_1023] = 0x3d,
+	[SCNT_RX_PMAC_SZ_1024_1526] = 0x3e,
+	[SCNT_RX_PMAC_SZ_JUMBO]    = 0x3f,
+	[SCNT_RX_PMAC_PAUSE]       = 0x40,
+	[SCNT_RX_PMAC_CONTROL]     = 0x41,
+	[SCNT_RX_PMAC_LONG]        = 0x42,
+	[SCNT_TX_OCT]              = 0x80,
+	[SCNT_TX_UC]               = 0x81,
+	[SCNT_TX_MC]               = 0x82,
+	[SCNT_TX_BC]               = 0x83,
+	[SCNT_TX_COL]              = 0x84,
+	[SCNT_TX_DROP]             = 0x85,
+	[SCNT_TX_PAUSE]            = 0x86,
+	[SCNT_TX_SZ_64]            = 0x87,
+	[SCNT_TX_SZ_65_127]        = 0x88,
+	[SCNT_TX_SZ_128_255]       = 0x89,
+	[SCNT_TX_SZ_256_511]       = 0x8a,
+	[SCNT_TX_SZ_512_1023]      = 0x8b,
+	[SCNT_TX_SZ_1024_1526]     = 0x8c,
+	[SCNT_TX_SZ_JUMBO]         = 0x8d,
+	[SCNT_TX_YELLOW_PRIO_0]    = 0x8e,
+	[SCNT_TX_YELLOW_PRIO_1]    = 0x8f,
+	[SCNT_TX_YELLOW_PRIO_2]    = 0x90,
+	[SCNT_TX_YELLOW_PRIO_3]    = 0x91,
+	[SCNT_TX_YELLOW_PRIO_4]    = 0x92,
+	[SCNT_TX_YELLOW_PRIO_5]    = 0x93,
+	[SCNT_TX_YELLOW_PRIO_6]    = 0x94,
+	[SCNT_TX_YELLOW_PRIO_7]    = 0x95,
+	[SCNT_TX_GREEN_PRIO_0]     = 0x96,
+	[SCNT_TX_GREEN_PRIO_1]     = 0x97,
+	[SCNT_TX_GREEN_PRIO_2]     = 0x98,
+	[SCNT_TX_GREEN_PRIO_3]     = 0x99,
+	[SCNT_TX_GREEN_PRIO_4]     = 0x9a,
+	[SCNT_TX_GREEN_PRIO_5]     = 0x9b,
+	[SCNT_TX_GREEN_PRIO_6]     = 0x9c,
+	[SCNT_TX_GREEN_PRIO_7]     = 0x9d,
+	[SCNT_TX_AGED]             = 0x9e,
+	[SCNT_TX_LLCT]             = 0x9f,
+	[SCNT_TX_CT]               = 0xa0,
+	[SCNT_TX_BUFDROP]          = 0xa1,
+	[SCNT_TX_MM_HOLD]          = 0xa2,
+	[SCNT_TX_MERGE_FRAG]       = 0xa3,
+	[SCNT_TX_PMAC_OCT]         = 0xa4,
+	[SCNT_TX_PMAC_UC]          = 0xa5,
+	[SCNT_TX_PMAC_MC]          = 0xa6,
+	[SCNT_TX_PMAC_BC]          = 0xa7,
+	[SCNT_TX_PMAC_PAUSE]       = 0xa8,
+	[SCNT_TX_PMAC_SZ_64]       = 0xa9,
+	[SCNT_TX_PMAC_SZ_65_127]   = 0xaa,
+	[SCNT_TX_PMAC_SZ_128_255]  = 0xab,
+	[SCNT_TX_PMAC_SZ_256_511]  = 0xac,
+	[SCNT_TX_PMAC_SZ_512_1023] = 0xad,
+	[SCNT_TX_PMAC_SZ_1024_1526] = 0xae,
+	[SCNT_TX_PMAC_SZ_JUMBO]    = 0xaf,
+	[SCNT_DR_LOCAL]            = 0x100,
+	[SCNT_DR_TAIL]             = 0x101,
+	[SCNT_DR_YELLOW_PRIO_0]    = 0x102,
+	[SCNT_DR_YELLOW_PRIO_1]    = 0x103,
+	[SCNT_DR_YELLOW_PRIO_2]    = 0x104,
+	[SCNT_DR_YELLOW_PRIO_3]    = 0x105,
+	[SCNT_DR_YELLOW_PRIO_4]    = 0x106,
+	[SCNT_DR_YELLOW_PRIO_5]    = 0x107,
+	[SCNT_DR_YELLOW_PRIO_6]    = 0x108,
+	[SCNT_DR_YELLOW_PRIO_7]    = 0x109,
+	[SCNT_DR_GREEN_PRIO_0]     = 0x10a,
+	[SCNT_DR_GREEN_PRIO_1]     = 0x10b,
+	[SCNT_DR_GREEN_PRIO_2]     = 0x10c,
+	[SCNT_DR_GREEN_PRIO_3]     = 0x10d,
+	[SCNT_DR_GREEN_PRIO_4]     = 0x10e,
+	[SCNT_DR_GREEN_PRIO_5]     = 0x10f,
+	[SCNT_DR_GREEN_PRIO_6]     = 0x110,
+	[SCNT_DR_GREEN_PRIO_7]     = 0x111,
+};
+
+struct lan9645x_ethtool_stat {
+	char name[ETH_GSTRING_LEN];
+	u16 idx;
+};
+
+static const struct lan9645x_ethtool_stat lan9645x_port_ethtool_stats[] = {
+	{ "rx_cat_drop",        SCNT_RX_CAT_DROP },
+	{ "rx_red_prio_0",      SCNT_RX_RED_PRIO_0 },
+	{ "rx_red_prio_1",      SCNT_RX_RED_PRIO_1 },
+	{ "rx_red_prio_2",      SCNT_RX_RED_PRIO_2 },
+	{ "rx_red_prio_3",      SCNT_RX_RED_PRIO_3 },
+	{ "rx_red_prio_4",      SCNT_RX_RED_PRIO_4 },
+	{ "rx_red_prio_5",      SCNT_RX_RED_PRIO_5 },
+	{ "rx_red_prio_6",      SCNT_RX_RED_PRIO_6 },
+	{ "rx_red_prio_7",      SCNT_RX_RED_PRIO_7 },
+	{ "rx_yellow_prio_0",   SCNT_RX_YELLOW_PRIO_0 },
+	{ "rx_yellow_prio_1",   SCNT_RX_YELLOW_PRIO_1 },
+	{ "rx_yellow_prio_2",   SCNT_RX_YELLOW_PRIO_2 },
+	{ "rx_yellow_prio_3",   SCNT_RX_YELLOW_PRIO_3 },
+	{ "rx_yellow_prio_4",   SCNT_RX_YELLOW_PRIO_4 },
+	{ "rx_yellow_prio_5",   SCNT_RX_YELLOW_PRIO_5 },
+	{ "rx_yellow_prio_6",   SCNT_RX_YELLOW_PRIO_6 },
+	{ "rx_yellow_prio_7",   SCNT_RX_YELLOW_PRIO_7 },
+	{ "rx_green_prio_0",    SCNT_RX_GREEN_PRIO_0 },
+	{ "rx_green_prio_1",    SCNT_RX_GREEN_PRIO_1 },
+	{ "rx_green_prio_2",    SCNT_RX_GREEN_PRIO_2 },
+	{ "rx_green_prio_3",    SCNT_RX_GREEN_PRIO_3 },
+	{ "rx_green_prio_4",    SCNT_RX_GREEN_PRIO_4 },
+	{ "rx_green_prio_5",    SCNT_RX_GREEN_PRIO_5 },
+	{ "rx_green_prio_6",    SCNT_RX_GREEN_PRIO_6 },
+	{ "rx_green_prio_7",    SCNT_RX_GREEN_PRIO_7 },
+	{ "tx_drop",            SCNT_TX_DROP },
+	{ "tx_yellow_prio_0",   SCNT_TX_YELLOW_PRIO_0 },
+	{ "tx_yellow_prio_1",   SCNT_TX_YELLOW_PRIO_1 },
+	{ "tx_yellow_prio_2",   SCNT_TX_YELLOW_PRIO_2 },
+	{ "tx_yellow_prio_3",   SCNT_TX_YELLOW_PRIO_3 },
+	{ "tx_yellow_prio_4",   SCNT_TX_YELLOW_PRIO_4 },
+	{ "tx_yellow_prio_5",   SCNT_TX_YELLOW_PRIO_5 },
+	{ "tx_yellow_prio_6",   SCNT_TX_YELLOW_PRIO_6 },
+	{ "tx_yellow_prio_7",   SCNT_TX_YELLOW_PRIO_7 },
+	{ "tx_green_prio_0",    SCNT_TX_GREEN_PRIO_0 },
+	{ "tx_green_prio_1",    SCNT_TX_GREEN_PRIO_1 },
+	{ "tx_green_prio_2",    SCNT_TX_GREEN_PRIO_2 },
+	{ "tx_green_prio_3",    SCNT_TX_GREEN_PRIO_3 },
+	{ "tx_green_prio_4",    SCNT_TX_GREEN_PRIO_4 },
+	{ "tx_green_prio_5",    SCNT_TX_GREEN_PRIO_5 },
+	{ "tx_green_prio_6",    SCNT_TX_GREEN_PRIO_6 },
+	{ "tx_green_prio_7",    SCNT_TX_GREEN_PRIO_7 },
+	{ "tx_aged",            SCNT_TX_AGED },
+	{ "tx_bufdrop",         SCNT_TX_BUFDROP },
+	{ "dr_local",           SCNT_DR_LOCAL },
+	{ "dr_yellow_prio_0",   SCNT_DR_YELLOW_PRIO_0 },
+	{ "dr_yellow_prio_1",   SCNT_DR_YELLOW_PRIO_1 },
+	{ "dr_yellow_prio_2",   SCNT_DR_YELLOW_PRIO_2 },
+	{ "dr_yellow_prio_3",   SCNT_DR_YELLOW_PRIO_3 },
+	{ "dr_yellow_prio_4",   SCNT_DR_YELLOW_PRIO_4 },
+	{ "dr_yellow_prio_5",   SCNT_DR_YELLOW_PRIO_5 },
+	{ "dr_yellow_prio_6",   SCNT_DR_YELLOW_PRIO_6 },
+	{ "dr_yellow_prio_7",   SCNT_DR_YELLOW_PRIO_7 },
+	{ "dr_green_prio_0",    SCNT_DR_GREEN_PRIO_0 },
+	{ "dr_green_prio_1",    SCNT_DR_GREEN_PRIO_1 },
+	{ "dr_green_prio_2",    SCNT_DR_GREEN_PRIO_2 },
+	{ "dr_green_prio_3",    SCNT_DR_GREEN_PRIO_3 },
+	{ "dr_green_prio_4",    SCNT_DR_GREEN_PRIO_4 },
+	{ "dr_green_prio_5",    SCNT_DR_GREEN_PRIO_5 },
+	{ "dr_green_prio_6",    SCNT_DR_GREEN_PRIO_6 },
+	{ "dr_green_prio_7",    SCNT_DR_GREEN_PRIO_7 },
+};
+
+static const struct lan9645x_view_stats lan9645x_view_stat_cfgs[] = {
+	[LAN9645X_STAT_PORTS] = {
+		.layout = lan9645x_port_stats_layout,
+		.num_cnts = ARRAY_SIZE(lan9645x_port_stats_layout),
+		.num_indexes = NUM_PHYS_PORTS,
+	},
+};
+
+static_assert(ARRAY_SIZE(lan9645x_view_stat_cfgs) == LAN9645X_STAT_NUM);
+
+static struct lan9645x_view_stats *
+lan9645x_get_vstats(struct lan9645x *lan9645x,
+		    enum lan9645x_view_stat_type type)
+{
+	return &lan9645x->stats->view[type];
+}
+
+static u64 *lan9645x_stats_index(struct lan9645x_view_stats *vstats, int idx)
+{
+	return &vstats->cnts[vstats->num_cnts * idx];
+}
+
+static u64 *lan9645x_stat_counters(struct lan9645x *lan9645x,
+				   enum lan9645x_view_stat_type type, int idx)
+{
+	return lan9645x_stats_index(lan9645x_get_vstats(lan9645x, type), idx);
+}
+
+/* Update a 64 bit software counter from a wrapping 32 bit hardware counter.
+ * The low half is replaced, not accumulated, so both must start from the same
+ * baseline. lan9645x_stats_init() clears hardware and software together.
+ */
+static void lan9645x_stats_update_counter(u64 *sw, u32 hw)
+{
+	if (hw < (*sw & U32_MAX))
+		*sw += (u64)1 << 32; /* value has wrapped */
+
+	*sw = (*sw & ~(u64)U32_MAX) + hw;
+}
+
+static int __lan9645x_stats_view_idx_hw_read(struct lan9645x *lan9645x,
+					     enum lan9645x_view_stat_type vtype,
+					     int idx)
+{
+	struct lan9645x_stat_region region;
+	struct lan9645x_view_stats *vstats;
+	int err;
+
+	lockdep_assert_held(&lan9645x->stats->hw_lock);
+
+	vstats = lan9645x_get_vstats(lan9645x, vtype);
+	if (idx < 0 || idx >= vstats->num_indexes)
+		return -EINVAL;
+
+	lan_wr(SYS_STAT_CFG_STAT_VIEW_SET(idx), lan9645x, SYS_STAT_CFG);
+
+	/* Each region for this index contains counters which are at sequential
+	 * addresses, so we can use bulk reads to ease lock pressure a bit.
+	 */
+	for (int r = 0; r < vstats->num_regions; r++) {
+		region = vstats->regions[r];
+		err = lan_bulk_rd(&vstats->buf[region.cnts_base_idx],
+				  region.cnt, lan9645x,
+				  SYS_CNT(region.base_offset));
+		if (err) {
+			dev_err_ratelimited(lan9645x->dev,
+					    "stats bulk read err vtype=%d idx=%d err=%d\n",
+					    vtype, idx, err);
+			return err;
+		}
+	}
+
+	return 0;
+}
+
+static void
+__lan9645x_stats_view_idx_transfer(struct lan9645x *lan9645x,
+				   enum lan9645x_view_stat_type vtype, int idx)
+{
+	struct lan9645x_view_stats *vstats;
+	u64 *idx_counters;
+	int cntr;
+
+	lockdep_assert_held(&lan9645x->stats->sw_lock);
+
+	vstats = lan9645x_get_vstats(lan9645x, vtype);
+	if (idx < 0 || idx >= vstats->num_indexes)
+		return;
+
+	idx_counters = lan9645x_stats_index(vstats, idx);
+
+	for (cntr = 0; cntr < vstats->num_cnts; cntr++)
+		lan9645x_stats_update_counter(&idx_counters[cntr],
+					      vstats->buf[cntr]);
+}
+
+static void __lan9645x_stats_view_idx_update(struct lan9645x *lan9645x,
+					     enum lan9645x_view_stat_type vtype,
+					     int idx)
+{
+	struct lan9645x_stats *s = lan9645x->stats;
+
+	lockdep_assert_held(&s->hw_lock);
+
+	if (!__lan9645x_stats_view_idx_hw_read(lan9645x, vtype, idx)) {
+		spin_lock(&s->sw_lock);
+		__lan9645x_stats_view_idx_transfer(lan9645x, vtype, idx);
+		spin_unlock(&s->sw_lock);
+	}
+}
+
+static u64 *lan9645x_stats_view_idx_update(struct lan9645x *lan9645x,
+					   enum lan9645x_view_stat_type vtype,
+					   int idx)
+{
+	struct lan9645x_stats *s = lan9645x->stats;
+
+	mutex_lock(&s->hw_lock);
+	__lan9645x_stats_view_idx_update(lan9645x, vtype, idx);
+	mutex_unlock(&s->hw_lock);
+
+	return lan9645x_stat_counters(lan9645x, vtype, idx);
+}
+
+static void lan9645x_stats_view_update(struct lan9645x *lan9645x,
+				       enum lan9645x_view_stat_type vtype)
+{
+	struct lan9645x_stats *s = lan9645x->stats;
+	struct lan9645x_view_stats *vstats;
+	int idx;
+
+	vstats = lan9645x_get_vstats(lan9645x, vtype);
+
+	switch (vtype) {
+	case LAN9645X_STAT_PORTS:
+		mutex_lock(&s->hw_lock);
+		for (idx = 0; idx < vstats->num_indexes; idx++) {
+			if (dsa_is_unused_port(lan9645x->ds, idx))
+				continue;
+			__lan9645x_stats_view_idx_update(lan9645x, vtype, idx);
+		}
+		mutex_unlock(&s->hw_lock);
+		return;
+	default:
+		return;
+	}
+}
+
+static void lan9645x_stats_update(struct lan9645x *lan9645x)
+{
+	for (int vtype = 0; vtype < LAN9645X_STAT_NUM; vtype++)
+		lan9645x_stats_view_update(lan9645x, vtype);
+}
+
+void lan9645x_stats_get_strings(struct lan9645x *lan9645x, int port,
+				u32 stringset, u8 *data)
+{
+	int i;
+
+	if (stringset != ETH_SS_STATS)
+		return;
+
+	for (i = 0; i < ARRAY_SIZE(lan9645x_port_ethtool_stats); i++)
+		ethtool_puts(&data, lan9645x_port_ethtool_stats[i].name);
+}
+
+int lan9645x_stats_get_sset_count(struct lan9645x *lan9645x, int port, int sset)
+{
+	if (sset != ETH_SS_STATS)
+		return -EOPNOTSUPP;
+
+	return ARRAY_SIZE(lan9645x_port_ethtool_stats);
+}
+
+void lan9645x_stats_get_ethtool_stats(struct lan9645x *lan9645x, int port,
+				      u64 *data)
+{
+	struct lan9645x_stats *stats = lan9645x->stats;
+	u64 *c;
+	int i;
+
+	c = lan9645x_stats_view_idx_update(lan9645x, LAN9645X_STAT_PORTS, port);
+
+	spin_lock(&stats->sw_lock);
+	for (i = 0; i < ARRAY_SIZE(lan9645x_port_ethtool_stats); i++)
+		*data++ = c[lan9645x_port_ethtool_stats[i].idx];
+	spin_unlock(&stats->sw_lock);
+}
+
+static u64 *lan9645x_stats_port_update(struct lan9645x *lan9645x, int port)
+{
+	return lan9645x_stats_view_idx_update(lan9645x, LAN9645X_STAT_PORTS,
+					      port);
+}
+
+void lan9645x_stats_get_eth_mac_stats(struct lan9645x *lan9645x, int port,
+				      struct ethtool_eth_mac_stats *m)
+{
+	struct lan9645x_stats *s = lan9645x->stats;
+	u64 *c;
+
+	c = lan9645x_stats_port_update(lan9645x, port);
+
+	spin_lock(&s->sw_lock);
+
+	m->FramesTransmittedOK = c[SCNT_TX_UC] +
+				 c[SCNT_TX_MC] +
+				 c[SCNT_TX_BC] +
+				 c[SCNT_TX_PMAC_UC] +
+				 c[SCNT_TX_PMAC_MC] +
+				 c[SCNT_TX_PMAC_BC];
+	m->FramesReceivedOK = c[SCNT_RX_UC] +
+			      c[SCNT_RX_MC] +
+			      c[SCNT_RX_BC] +
+			      c[SCNT_RX_PMAC_UC] +
+			      c[SCNT_RX_PMAC_MC] +
+			      c[SCNT_RX_PMAC_BC];
+	m->FrameCheckSequenceErrors = c[SCNT_RX_CRC] +
+				      c[SCNT_RX_PMAC_CRC];
+	m->OctetsTransmittedOK = c[SCNT_TX_OCT] +
+				 c[SCNT_TX_PMAC_OCT];
+	m->OctetsReceivedOK = c[SCNT_RX_OCT] +
+			      c[SCNT_RX_PMAC_OCT];
+	m->MulticastFramesXmittedOK = c[SCNT_TX_MC] +
+				      c[SCNT_TX_PMAC_MC];
+	m->BroadcastFramesXmittedOK = c[SCNT_TX_BC] +
+				      c[SCNT_TX_PMAC_BC];
+	m->MulticastFramesReceivedOK = c[SCNT_RX_MC] +
+				       c[SCNT_RX_PMAC_MC];
+	m->BroadcastFramesReceivedOK = c[SCNT_RX_BC] +
+				       c[SCNT_RX_PMAC_BC];
+	m->FrameTooLongErrors = c[SCNT_RX_JABBER] +
+				c[SCNT_RX_LONG] +
+				c[SCNT_RX_PMAC_JABBER] +
+				c[SCNT_RX_PMAC_LONG];
+
+	spin_unlock(&s->sw_lock);
+}
+
+static const struct ethtool_rmon_hist_range lan9645x_rmon_ranges[] = {
+	{   64,     64 },
+	{   65,    127 },
+	{  128,    255 },
+	{  256,    511 },
+	{  512,   1023 },
+	{ 1024,   1526 },
+	{ 1527, 0xffff },
+	{}
+};
+
+void
+lan9645x_stats_get_rmon_stats(struct lan9645x *lan9645x, int port,
+			      struct ethtool_rmon_stats *r,
+			      const struct ethtool_rmon_hist_range **ranges)
+{
+	struct lan9645x_stats *s = lan9645x->stats;
+	u64 *c;
+
+	c = lan9645x_stats_port_update(lan9645x, port);
+
+	spin_lock(&s->sw_lock);
+
+	r->undersize_pkts = c[SCNT_RX_SHORT] +
+			    c[SCNT_RX_PMAC_SHORT];
+	r->oversize_pkts = c[SCNT_RX_LONG] +
+			   c[SCNT_RX_PMAC_LONG];
+	/* SCNT_RX_FRAG counts frames received after the port is paused, and
+	 * increments when pause frames arrive from the link partner.
+	 * It counts neither undersize frames nor errors, so it is left out here
+	 * and out of rx_packets, unlike lan966x which adds it to rx_errors.
+	 * SCNT_RX_PMAC_FRAG does count number of runt frames with invalid CRC.
+	 */
+	r->fragments = c[SCNT_RX_PMAC_FRAG];
+	r->jabbers = c[SCNT_RX_JABBER] +
+		     c[SCNT_RX_PMAC_JABBER];
+	r->hist[0] = c[SCNT_RX_SZ_64] +
+		     c[SCNT_RX_PMAC_SZ_64];
+	r->hist[1] = c[SCNT_RX_SZ_65_127] +
+		     c[SCNT_RX_PMAC_SZ_65_127];
+	r->hist[2] = c[SCNT_RX_SZ_128_255] +
+		     c[SCNT_RX_PMAC_SZ_128_255];
+	r->hist[3] = c[SCNT_RX_SZ_256_511] +
+		     c[SCNT_RX_PMAC_SZ_256_511];
+	r->hist[4] = c[SCNT_RX_SZ_512_1023] +
+		     c[SCNT_RX_PMAC_SZ_512_1023];
+	r->hist[5] = c[SCNT_RX_SZ_1024_1526] +
+		     c[SCNT_RX_PMAC_SZ_1024_1526];
+	r->hist[6] = c[SCNT_RX_SZ_JUMBO] +
+		     c[SCNT_RX_PMAC_SZ_JUMBO];
+	r->hist_tx[0] = c[SCNT_TX_SZ_64] +
+			c[SCNT_TX_PMAC_SZ_64];
+	r->hist_tx[1] = c[SCNT_TX_SZ_65_127] +
+			c[SCNT_TX_PMAC_SZ_65_127];
+	r->hist_tx[2] = c[SCNT_TX_SZ_128_255] +
+			c[SCNT_TX_PMAC_SZ_128_255];
+	r->hist_tx[3] = c[SCNT_TX_SZ_256_511] +
+			c[SCNT_TX_PMAC_SZ_256_511];
+	r->hist_tx[4] = c[SCNT_TX_SZ_512_1023] +
+			c[SCNT_TX_PMAC_SZ_512_1023];
+	r->hist_tx[5] = c[SCNT_TX_SZ_1024_1526] +
+			c[SCNT_TX_PMAC_SZ_1024_1526];
+	r->hist_tx[6] = c[SCNT_TX_SZ_JUMBO] +
+			c[SCNT_TX_PMAC_SZ_JUMBO];
+
+	spin_unlock(&s->sw_lock);
+
+	*ranges = lan9645x_rmon_ranges;
+}
+
+void lan9645x_stats_get_stats64(struct lan9645x *lan9645x, int port,
+				struct rtnl_link_stats64 *stats)
+{
+	struct lan9645x_stats *s = lan9645x->stats;
+	u64 *c;
+
+	c = lan9645x_stat_counters(lan9645x, LAN9645X_STAT_PORTS, port);
+
+	/* ndo_get_stats64 may run in non-sleepable context (under
+	 * rcu_read_lock, or with a callers spinlock held as in bonding), so
+	 * unlike the ethtool paths we must not take the hw_lock mutex or touch
+	 * hardware here.
+	 * sw_lock is never taken from softirq/IRQ context by any path, so a
+	 * plain spin_lock is sufficient, as in ocelot and ksz.
+	 */
+	spin_lock(&s->sw_lock);
+
+	stats->rx_bytes = c[SCNT_RX_OCT] + c[SCNT_RX_PMAC_OCT];
+
+	stats->rx_packets = c[SCNT_RX_SHORT] +
+			    c[SCNT_RX_JABBER] +
+			    c[SCNT_RX_SZ_64] +
+			    c[SCNT_RX_SZ_65_127] +
+			    c[SCNT_RX_SZ_128_255] +
+			    c[SCNT_RX_SZ_256_511] +
+			    c[SCNT_RX_SZ_512_1023] +
+			    c[SCNT_RX_SZ_1024_1526] +
+			    c[SCNT_RX_SZ_JUMBO] +
+			    c[SCNT_RX_LONG] +
+			    c[SCNT_RX_PMAC_SHORT] +
+			    c[SCNT_RX_PMAC_FRAG] +
+			    c[SCNT_RX_PMAC_JABBER] +
+			    c[SCNT_RX_PMAC_SZ_64] +
+			    c[SCNT_RX_PMAC_SZ_65_127] +
+			    c[SCNT_RX_PMAC_SZ_128_255] +
+			    c[SCNT_RX_PMAC_SZ_256_511] +
+			    c[SCNT_RX_PMAC_SZ_512_1023] +
+			    c[SCNT_RX_PMAC_SZ_1024_1526] +
+			    c[SCNT_RX_PMAC_SZ_JUMBO] +
+			    c[SCNT_RX_PMAC_LONG];
+
+	stats->multicast = c[SCNT_RX_MC] + c[SCNT_RX_PMAC_MC];
+
+	stats->rx_errors = c[SCNT_RX_SHORT] +
+			   c[SCNT_RX_JABBER] +
+			   c[SCNT_RX_CRC] +
+			   c[SCNT_RX_LONG] +
+			   c[SCNT_RX_PMAC_SHORT] +
+			   c[SCNT_RX_PMAC_JABBER] +
+			   c[SCNT_RX_PMAC_CRC] +
+			   c[SCNT_RX_PMAC_LONG];
+
+	stats->rx_length_errors = c[SCNT_RX_JABBER] +
+				  c[SCNT_RX_LONG] +
+				  c[SCNT_RX_PMAC_JABBER] +
+				  c[SCNT_RX_PMAC_LONG];
+
+	/* SCNT_RX_CRC counts CRC errors, alignment errors and RX_ER events at
+	 * any frame size, so a short frame with a bad CRC is counted here.
+	 */
+	stats->rx_crc_errors = c[SCNT_RX_CRC] + c[SCNT_RX_PMAC_CRC];
+
+	stats->rx_missed_errors = c[SCNT_DR_TAIL];
+
+	stats->rx_dropped = c[SCNT_DR_LOCAL];
+
+	for (int i = 0; i < NUM_PRIO_QUEUES; i++) {
+		stats->rx_dropped += c[SCNT_RX_RED_PRIO_0 + i] +
+				     c[SCNT_DR_YELLOW_PRIO_0 + i] +
+				     c[SCNT_DR_GREEN_PRIO_0 + i];
+	}
+
+	stats->tx_bytes = c[SCNT_TX_OCT] + c[SCNT_TX_PMAC_OCT];
+
+	stats->tx_packets = c[SCNT_TX_SZ_64] +
+			    c[SCNT_TX_SZ_65_127] +
+			    c[SCNT_TX_SZ_128_255] +
+			    c[SCNT_TX_SZ_256_511] +
+			    c[SCNT_TX_SZ_512_1023] +
+			    c[SCNT_TX_SZ_1024_1526] +
+			    c[SCNT_TX_SZ_JUMBO] +
+			    c[SCNT_TX_PMAC_SZ_64] +
+			    c[SCNT_TX_PMAC_SZ_65_127] +
+			    c[SCNT_TX_PMAC_SZ_128_255] +
+			    c[SCNT_TX_PMAC_SZ_256_511] +
+			    c[SCNT_TX_PMAC_SZ_512_1023] +
+			    c[SCNT_TX_PMAC_SZ_1024_1526] +
+			    c[SCNT_TX_PMAC_SZ_JUMBO];
+
+	stats->tx_dropped = c[SCNT_TX_DROP] +
+			    c[SCNT_TX_AGED] +
+			    c[SCNT_TX_BUFDROP];
+
+	stats->collisions = c[SCNT_TX_COL];
+
+	spin_unlock(&s->sw_lock);
+}
+
+void lan9645x_stats_get_eth_phy_stats(struct lan9645x *lan9645x, int port,
+				      struct ethtool_eth_phy_stats *p)
+{
+	struct lan9645x_stats *s = lan9645x->stats;
+	u64 *c;
+
+	c = lan9645x_stats_port_update(lan9645x, port);
+
+	spin_lock(&s->sw_lock);
+
+	p->SymbolErrorDuringCarrier = c[SCNT_RX_SYMBOL_ERR] +
+				      c[SCNT_RX_PMAC_SYMBOL_ERR];
+
+	spin_unlock(&s->sw_lock);
+}
+
+void
+lan9645x_stats_get_eth_ctrl_stats(struct lan9645x *lan9645x, int port,
+				  struct ethtool_eth_ctrl_stats *ctrl)
+{
+	struct lan9645x_stats *s = lan9645x->stats;
+	u64 *c;
+
+	c = lan9645x_stats_port_update(lan9645x, port);
+
+	spin_lock(&s->sw_lock);
+
+	ctrl->MACControlFramesReceived = c[SCNT_RX_CONTROL] +
+					 c[SCNT_RX_PMAC_CONTROL];
+
+	spin_unlock(&s->sw_lock);
+}
+
+void lan9645x_stats_get_pause_stats(struct lan9645x *lan9645x, int port,
+				    struct ethtool_pause_stats *ps)
+{
+	struct lan9645x_stats *s = lan9645x->stats;
+	u64 *c;
+
+	c = lan9645x_stats_port_update(lan9645x, port);
+
+	spin_lock(&s->sw_lock);
+
+	ps->tx_pause_frames = c[SCNT_TX_PAUSE] + c[SCNT_TX_PMAC_PAUSE];
+	ps->rx_pause_frames = c[SCNT_RX_PAUSE] + c[SCNT_RX_PMAC_PAUSE];
+
+	spin_unlock(&s->sw_lock);
+}
+
+static void lan9645x_check_stats_work(struct work_struct *work)
+{
+	struct delayed_work *del_work = to_delayed_work(work);
+	struct lan9645x_stats *stats;
+
+	stats = container_of(del_work, struct lan9645x_stats, work);
+
+	lan9645x_stats_update(stats->lan9645x);
+
+	queue_delayed_work(stats->queue, &stats->work,
+			   LAN9645X_STATS_CHECK_DELAY);
+}
+
+static int lan9645x_stats_prepare_regions(struct lan9645x *lan9645x,
+					  struct lan9645x_view_stats *vstat)
+{
+	struct lan9645x_stat_region *regions;
+	const u32 *layout = vstat->layout;
+	size_t num_regions = 1;
+	size_t r;
+	int i;
+
+	for (i = 1; i < vstat->num_cnts; i++)
+		if (layout[i] != layout[i - 1] + 1)
+			num_regions++;
+
+	regions = devm_kcalloc(lan9645x->dev, num_regions, sizeof(*regions),
+			       GFP_KERNEL);
+	if (!regions)
+		return -ENOMEM;
+
+	vstat->num_regions = num_regions;
+	vstat->regions = regions;
+
+	regions[0].base_offset = layout[0];
+	regions[0].cnts_base_idx = 0;
+	regions[0].cnt = 1;
+
+	for (i = 1, r = 0; i < vstat->num_cnts; i++) {
+		if (layout[i] != layout[i - 1] + 1) {
+			r++;
+			regions[r].base_offset = layout[i];
+			regions[r].cnts_base_idx = i;
+			regions[r].cnt = 1;
+		} else {
+			regions[r].cnt++;
+		}
+	}
+
+	return 0;
+}
+
+static int lan9645x_view_stat_init(struct lan9645x *lan9645x,
+				   struct lan9645x_view_stats *vstat,
+				   const struct lan9645x_view_stats *cfg)
+{
+	size_t total = cfg->num_cnts * cfg->num_indexes;
+	int err;
+
+	memcpy(vstat, cfg, sizeof(*cfg));
+
+	vstat->cnts = devm_kcalloc(lan9645x->dev, total, sizeof(u64),
+				   GFP_KERNEL);
+	if (!vstat->cnts)
+		return -ENOMEM;
+
+	/* Scratch for one index at a time. hw_lock is held across the bulk
+	 * read and the transfer into cnts, so no slot outlives an iteration.
+	 */
+	vstat->buf = devm_kcalloc(lan9645x->dev, cfg->num_cnts, sizeof(u32),
+				  GFP_KERNEL);
+	if (!vstat->buf)
+		return -ENOMEM;
+
+	err = lan9645x_stats_prepare_regions(lan9645x, vstat);
+	if (err)
+		return err;
+
+	return 0;
+}
+
+int lan9645x_stats_alloc(struct lan9645x *lan9645x)
+{
+	struct lan9645x_stats *stats;
+	int err;
+
+	lan9645x->stats = devm_kzalloc(lan9645x->dev, sizeof(*stats),
+				       GFP_KERNEL);
+	if (!lan9645x->stats)
+		return -ENOMEM;
+
+	stats = lan9645x->stats;
+	stats->lan9645x = lan9645x;
+
+	for (int t = 0; t < LAN9645X_STAT_NUM; t++) {
+		err = lan9645x_view_stat_init(lan9645x, &stats->view[t],
+					      &lan9645x_view_stat_cfgs[t]);
+		if (err)
+			return err;
+	}
+
+	stats->queue = alloc_ordered_workqueue("%s-stats", 0,
+					       dev_name(lan9645x->dev));
+	if (!stats->queue)
+		return -ENOMEM;
+
+	mutex_init(&stats->hw_lock);
+	spin_lock_init(&stats->sw_lock);
+	INIT_DELAYED_WORK(&stats->work, lan9645x_check_stats_work);
+
+	return 0;
+}
+
+void lan9645x_stats_free(struct lan9645x *lan9645x)
+{
+	cancel_delayed_work_sync(&lan9645x->stats->work);
+	destroy_workqueue(lan9645x->stats->queue);
+	mutex_destroy(&lan9645x->stats->hw_lock);
+}
+
+static int lan9645x_stats_clear_view(struct lan9645x *lan9645x, u32 idx,
+				     u32 groups)
+{
+	u32 val;
+
+	lockdep_assert_held(&lan9645x->stats->hw_lock);
+
+	lan_wr(SYS_STAT_CFG_STAT_VIEW_SET(idx) |
+	       SYS_STAT_CFG_STAT_CLEAR_SHOT_SET(groups),
+	       lan9645x, SYS_STAT_CFG);
+
+	/* STAT_CLEAR_SHOT self-clears when the clear completes, in about 1us.
+	 * Wait for it, so the next STAT_VIEW write cannot land while a
+	 * clear is still in flight.
+	 */
+	return lan9645x_rd_poll_timeout(lan9645x, SYS_STAT_CFG, val,
+					!SYS_STAT_CFG_STAT_CLEAR_SHOT_GET(val));
+}
+
+void lan9645x_stats_init(struct lan9645x *lan9645x)
+{
+	struct lan9645x_stats *stats = lan9645x->stats;
+	struct lan9645x_view_stats *vstat;
+
+	 /* Make sure software and hardware is zeroed on init. */
+	vstat = &stats->view[LAN9645X_STAT_PORTS];
+
+	mutex_lock(&stats->hw_lock);
+
+	/* Clear hardware before software. A failed clear leaves hardware
+	 * non-zero against a zeroed shadow, which the first poll in
+	 * lan9645x_stats_update_counter() simply adopts.
+	 */
+	for (u32 idx = 0; idx < vstat->num_indexes; idx++)
+		if (lan9645x_stats_clear_view(lan9645x, idx,
+					      LAN9645X_STAT_PORT_GROUPS))
+			dev_err(lan9645x->dev,
+				"Timeout clearing counters for port %u\n", idx);
+
+	spin_lock(&stats->sw_lock);
+	memset(vstat->cnts, 0,
+	       array3_size(vstat->num_indexes, vstat->num_cnts,
+			   sizeof(*vstat->cnts)));
+	spin_unlock(&stats->sw_lock);
+
+	mutex_unlock(&stats->hw_lock);
+
+	queue_delayed_work(stats->queue, &stats->work,
+			   LAN9645X_STATS_CHECK_DELAY);
+}
+
+void lan9645x_stats_deinit(struct lan9645x *lan9645x)
+{
+	cancel_delayed_work_sync(&lan9645x->stats->work);
+}
diff --git a/drivers/net/dsa/microchip/lan9645x/lan9645x_stats.h b/drivers/net/dsa/microchip/lan9645x/lan9645x_stats.h
new file mode 100644
index 000000000000..915e20af6521
--- /dev/null
+++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_stats.h
@@ -0,0 +1,229 @@
+/* SPDX-License-Identifier: GPL-2.0+ */
+/* Copyright (C) 2026 Microchip Technology Inc.
+ */
+
+#ifndef _LAN9645X_STATS_H_
+#define _LAN9645X_STATS_H_
+
+#include <linux/ethtool.h>
+#include <linux/mutex.h>
+#include <linux/spinlock.h>
+#include <linux/types.h>
+#include <linux/workqueue.h>
+
+#include "lan9645x_main.h"
+
+/* Counter indices into stat layout structs */
+#define SCNT_RX_OCT                      0
+#define SCNT_RX_UC                       1
+#define SCNT_RX_MC                       2
+#define SCNT_RX_BC                       3
+#define SCNT_RX_SHORT                    4
+#define SCNT_RX_FRAG                     5
+#define SCNT_RX_JABBER                   6
+#define SCNT_RX_CRC                      7
+#define SCNT_RX_SYMBOL_ERR               8
+#define SCNT_RX_SZ_64                    9
+#define SCNT_RX_SZ_65_127                10
+#define SCNT_RX_SZ_128_255               11
+#define SCNT_RX_SZ_256_511               12
+#define SCNT_RX_SZ_512_1023              13
+#define SCNT_RX_SZ_1024_1526             14
+#define SCNT_RX_SZ_JUMBO                 15
+#define SCNT_RX_PAUSE                    16
+#define SCNT_RX_CONTROL                  17
+#define SCNT_RX_LONG                     18
+#define SCNT_RX_CAT_DROP                 19
+#define SCNT_RX_RED_PRIO_0               20
+#define SCNT_RX_RED_PRIO_1               21
+#define SCNT_RX_RED_PRIO_2               22
+#define SCNT_RX_RED_PRIO_3               23
+#define SCNT_RX_RED_PRIO_4               24
+#define SCNT_RX_RED_PRIO_5               25
+#define SCNT_RX_RED_PRIO_6               26
+#define SCNT_RX_RED_PRIO_7               27
+#define SCNT_RX_YELLOW_PRIO_0            28
+#define SCNT_RX_YELLOW_PRIO_1            29
+#define SCNT_RX_YELLOW_PRIO_2            30
+#define SCNT_RX_YELLOW_PRIO_3            31
+#define SCNT_RX_YELLOW_PRIO_4            32
+#define SCNT_RX_YELLOW_PRIO_5            33
+#define SCNT_RX_YELLOW_PRIO_6            34
+#define SCNT_RX_YELLOW_PRIO_7            35
+#define SCNT_RX_GREEN_PRIO_0             36
+#define SCNT_RX_GREEN_PRIO_1             37
+#define SCNT_RX_GREEN_PRIO_2             38
+#define SCNT_RX_GREEN_PRIO_3             39
+#define SCNT_RX_GREEN_PRIO_4             40
+#define SCNT_RX_GREEN_PRIO_5             41
+#define SCNT_RX_GREEN_PRIO_6             42
+#define SCNT_RX_GREEN_PRIO_7             43
+#define SCNT_RX_ASSEMBLY_ERR             44
+#define SCNT_RX_SMD_ERR                  45
+#define SCNT_RX_ASSEMBLY_OK              46
+#define SCNT_RX_MERGE_FRAG               47
+#define SCNT_RX_PMAC_OCT                 48
+#define SCNT_RX_PMAC_UC                  49
+#define SCNT_RX_PMAC_MC                  50
+#define SCNT_RX_PMAC_BC                  51
+#define SCNT_RX_PMAC_SHORT               52
+#define SCNT_RX_PMAC_FRAG                53
+#define SCNT_RX_PMAC_JABBER              54
+#define SCNT_RX_PMAC_CRC                 55
+#define SCNT_RX_PMAC_SYMBOL_ERR          56
+#define SCNT_RX_PMAC_SZ_64               57
+#define SCNT_RX_PMAC_SZ_65_127           58
+#define SCNT_RX_PMAC_SZ_128_255          59
+#define SCNT_RX_PMAC_SZ_256_511          60
+#define SCNT_RX_PMAC_SZ_512_1023         61
+#define SCNT_RX_PMAC_SZ_1024_1526        62
+#define SCNT_RX_PMAC_SZ_JUMBO            63
+#define SCNT_RX_PMAC_PAUSE               64
+#define SCNT_RX_PMAC_CONTROL             65
+#define SCNT_RX_PMAC_LONG                66
+#define SCNT_TX_OCT                      67
+#define SCNT_TX_UC                       68
+#define SCNT_TX_MC                       69
+#define SCNT_TX_BC                       70
+#define SCNT_TX_COL                      71
+#define SCNT_TX_DROP                     72
+#define SCNT_TX_PAUSE                    73
+#define SCNT_TX_SZ_64                    74
+#define SCNT_TX_SZ_65_127                75
+#define SCNT_TX_SZ_128_255               76
+#define SCNT_TX_SZ_256_511               77
+#define SCNT_TX_SZ_512_1023              78
+#define SCNT_TX_SZ_1024_1526             79
+#define SCNT_TX_SZ_JUMBO                 80
+#define SCNT_TX_YELLOW_PRIO_0            81
+#define SCNT_TX_YELLOW_PRIO_1            82
+#define SCNT_TX_YELLOW_PRIO_2            83
+#define SCNT_TX_YELLOW_PRIO_3            84
+#define SCNT_TX_YELLOW_PRIO_4            85
+#define SCNT_TX_YELLOW_PRIO_5            86
+#define SCNT_TX_YELLOW_PRIO_6            87
+#define SCNT_TX_YELLOW_PRIO_7            88
+#define SCNT_TX_GREEN_PRIO_0             89
+#define SCNT_TX_GREEN_PRIO_1             90
+#define SCNT_TX_GREEN_PRIO_2             91
+#define SCNT_TX_GREEN_PRIO_3             92
+#define SCNT_TX_GREEN_PRIO_4             93
+#define SCNT_TX_GREEN_PRIO_5             94
+#define SCNT_TX_GREEN_PRIO_6             95
+#define SCNT_TX_GREEN_PRIO_7             96
+#define SCNT_TX_AGED                     97
+#define SCNT_TX_LLCT                     98
+#define SCNT_TX_CT                       99
+#define SCNT_TX_BUFDROP                  100
+#define SCNT_TX_MM_HOLD                  101
+#define SCNT_TX_MERGE_FRAG               102
+#define SCNT_TX_PMAC_OCT                 103
+#define SCNT_TX_PMAC_UC                  104
+#define SCNT_TX_PMAC_MC                  105
+#define SCNT_TX_PMAC_BC                  106
+#define SCNT_TX_PMAC_PAUSE               107
+#define SCNT_TX_PMAC_SZ_64               108
+#define SCNT_TX_PMAC_SZ_65_127           109
+#define SCNT_TX_PMAC_SZ_128_255          110
+#define SCNT_TX_PMAC_SZ_256_511          111
+#define SCNT_TX_PMAC_SZ_512_1023         112
+#define SCNT_TX_PMAC_SZ_1024_1526        113
+#define SCNT_TX_PMAC_SZ_JUMBO            114
+#define SCNT_DR_LOCAL                    115
+#define SCNT_DR_TAIL                     116
+#define SCNT_DR_YELLOW_PRIO_0            117
+#define SCNT_DR_YELLOW_PRIO_1            118
+#define SCNT_DR_YELLOW_PRIO_2            119
+#define SCNT_DR_YELLOW_PRIO_3            120
+#define SCNT_DR_YELLOW_PRIO_4            121
+#define SCNT_DR_YELLOW_PRIO_5            122
+#define SCNT_DR_YELLOW_PRIO_6            123
+#define SCNT_DR_YELLOW_PRIO_7            124
+#define SCNT_DR_GREEN_PRIO_0             125
+#define SCNT_DR_GREEN_PRIO_1             126
+#define SCNT_DR_GREEN_PRIO_2             127
+#define SCNT_DR_GREEN_PRIO_3             128
+#define SCNT_DR_GREEN_PRIO_4             129
+#define SCNT_DR_GREEN_PRIO_5             130
+#define SCNT_DR_GREEN_PRIO_6             131
+#define SCNT_DR_GREEN_PRIO_7             132
+
+enum lan9645x_view_stat_type {
+	LAN9645X_STAT_PORTS = 0,
+	/* Unimplemented views: ISDX, ESDX, SFID, FRER */
+
+	LAN9645X_STAT_NUM,
+};
+
+struct lan9645x_stat_region {
+	u32 base_offset;
+	u32 cnt;
+	u32 cnts_base_idx;
+};
+
+/* Counters are organized by indices/views such as
+ *
+ * - physical ports
+ * - isdx
+ * - esdx
+ * - frer
+ * - sfid
+ *
+ * Each view contains regions, which is a linear address range of related
+ * stats. I.e. the ports index has RX, TX and Drop regions, and you have a
+ * given counter replicated per index.
+ */
+struct lan9645x_view_stats {
+	/* HW register offsets indexed by SCNT_*, used for bulk reading */
+	const u32 *layout;
+	/* Region description for this view, used for bulk reading */
+	struct lan9645x_stat_region *regions;
+	/* 64bit software counters with the same addr layout hw */
+	u64 *cnts;
+	/* Buffer for bulk reading counter regions from hw */
+	u32 *buf;
+	/* Number of counters per index in view */
+	u32 num_cnts;
+	/* Number of indexes in view */
+	u32 num_indexes;
+	/* Number of counter regions with counters at sequential addresses */
+	size_t num_regions;
+};
+
+struct lan9645x_stats {
+	struct lan9645x *lan9645x;
+	struct mutex hw_lock; /* lock r/w to stat registers and u32 buf */
+	spinlock_t sw_lock; /* lock access to u64 software counters */
+	struct delayed_work work;
+	struct workqueue_struct *queue;
+
+	struct lan9645x_view_stats view[LAN9645X_STAT_NUM];
+};
+
+int lan9645x_stats_alloc(struct lan9645x *lan9645x);
+void lan9645x_stats_free(struct lan9645x *lan9645x);
+void lan9645x_stats_init(struct lan9645x *lan9645x);
+void lan9645x_stats_deinit(struct lan9645x *lan9645x);
+void lan9645x_stats_get_strings(struct lan9645x *lan9645x, int port,
+				u32 stringset, u8 *data);
+int lan9645x_stats_get_sset_count(struct lan9645x *lan9645x, int port,
+				  int sset);
+void lan9645x_stats_get_ethtool_stats(struct lan9645x *lan9645x, int port,
+				      u64 *data);
+void lan9645x_stats_get_eth_mac_stats(struct lan9645x *lan9645x, int port,
+				      struct ethtool_eth_mac_stats *mac_stats);
+void
+lan9645x_stats_get_rmon_stats(struct lan9645x *lan9645x, int port,
+			      struct ethtool_rmon_stats *rmon_stats,
+			      const struct ethtool_rmon_hist_range **ranges);
+void lan9645x_stats_get_stats64(struct lan9645x *lan9645x, int port,
+				struct rtnl_link_stats64 *s);
+void lan9645x_stats_get_pause_stats(struct lan9645x *lan9645x, int port,
+				    struct ethtool_pause_stats *ps);
+void
+lan9645x_stats_get_eth_ctrl_stats(struct lan9645x *lan9645x, int port,
+				  struct ethtool_eth_ctrl_stats *ctrl_stats);
+void lan9645x_stats_get_eth_phy_stats(struct lan9645x *lan9645x, int port,
+				      struct ethtool_eth_phy_stats *phy_stats);
+
+#endif
-- 
2.52.0

Re: [PATCH net-next v12 2/9] dt-bindings: net: lan9645x: add LAN9645X switch bindings

From: Andrew Lunn <andrew@lunn.ch>
Date: 2026-09-08 12:59:37

rx/tx-internal-delay-ps is used to model a direct MAC to MAC fixed link,
using RGMII, where exactly one of them must supply the delay.
Documentation/networking/phy.rst describes plain rgmii as the mode where
the MAC or the PCB adds the delay rather than the PHY.
This is not correct. phy-mode 'rgmii' in DT means the PCB adds the
delay.

If the value is not 'rgmii', then the MAC/PHY pair need to decided on
which adds the delay, and by convention, it is normally the PHY.

Where it gets interesting is MAC to MAC, which you do see in
switches. Using rx/tx-internal-delay-ps makes a lot of sense in this
setting.

The binding itself looks O.K, its just this commit message text which
is wrong.

	Andrew

Re: [PATCH net-next v12 2/9] dt-bindings: net: lan9645x: add LAN9645X switch bindings

From: Jens Emil Schulz Ostergaard <hidden>
Date: 2026-09-10 14:42:17

On Tue, 2026-09-08 at 14:59 +0200, Andrew Lunn wrote:
EXTERNAL EMAIL: Do not click links or open attachments unless you know the content is safe
quoted
rx/tx-internal-delay-ps is used to model a direct MAC to MAC fixed link,
using RGMII, where exactly one of them must supply the delay.
Documentation/networking/phy.rst describes plain rgmii as the mode where
the MAC or the PCB adds the delay rather than the PHY.
This is not correct. phy-mode 'rgmii' in DT means the PCB adds the
delay.

If the value is not 'rgmii', then the MAC/PHY pair need to decided on
which adds the delay, and by convention, it is normally the PHY.

Where it gets interesting is MAC to MAC, which you do see in
switches. Using rx/tx-internal-delay-ps makes a lot of sense in this
setting.

The binding itself looks O.K, its just this commit message text which
is wrong.

        Andrew
This is exactly intended to model the MAC to MAC fixed link situation.
So no PHYs between, and one side needs to supply the delays.

I had originally implemented this by using the phy interface mode to program
the MAC delays off of. This is what we had done previously in lan966x say.

But I was instructed that the modern approach is to use rx/tx-internal-delay-ps
along with phy interface mode rgmii. There are a few other examples of
dsa bindings doing this:

Documentation/devicetree/bindings/net/dsa/microchip,lan937x.yaml
Documentation/devicetree/bindings/net/dsa/realtek.yaml

I will remove the paragraph from the commit message if that is wrong. I have
misunderstood that part of phy.rst.

Thanks,
Emil
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help