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.
IPv6 is disabled on the conduit. When enabled, the RFC 4861 frames are
dispatched directly on the conduit bypassing the tag driver. The switch
parses these frames as if they have an IFH prepended, leading to a
garbage in garbage out situation. Therefore, IPv6 on the conduit is not
a sensible configuration.
Reviewed-by: Steen Hegelund <Steen.Hegelund@microchip.com>
Signed-off-by: Jens Emil Schulz Østergaard <jensemil.schulzostergaard@microchip.com>
---
drivers/net/dsa/microchip/Makefile | 1 +
drivers/net/dsa/microchip/lan9645x/Kconfig | 12 +
drivers/net/dsa/microchip/lan9645x/Makefile | 7 +
drivers/net/dsa/microchip/lan9645x/lan9645x_main.c | 435 +++++++++++++++++
drivers/net/dsa/microchip/lan9645x/lan9645x_main.h | 396 +++++++++++++++
drivers/net/dsa/microchip/lan9645x/lan9645x_npi.c | 99 ++++
.../net/dsa/microchip/lan9645x/lan9645x_phylink.c | 537 +++++++++++++++++++++
drivers/net/dsa/microchip/lan9645x/lan9645x_port.c | 289 +++++++++++
drivers/net/ethernet/microchip/Kconfig | 1 +
9 files changed, 1777 insertions(+)
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..8cbac1f9875d
--- /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
+ depends on NET_VENDOR_MICROCHIP
+ select NET_DSA_TAG_LAN9645X
+ 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..eea1edc5c0e3
--- /dev/null
+++ b/drivers/net/dsa/microchip/lan9645x/Makefile
@@ -0,0 +1,7 @@
+# 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_port.o \
+ lan9645x_phylink.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..739013f049d0
--- /dev/null
+++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_main.c
@@ -0,0 +1,435 @@
+// SPDX-License-Identifier: GPL-2.0+
+/* Copyright (C) 2026 Microchip Technology Inc.
+ */
+
+#include <linux/debugfs.h>
+#include <linux/platform_device.h>
+#include <linux/phy/phy.h>
+
+#include "lan9645x_main.h"
+
+static const char *lan9645x_resource_names[NUM_TARGETS + 1] = {
+ [TARGET_GCB] = "gcb",
+ [TARGET_QS] = "qs",
+ [TARGET_CHIP_TOP] = "chip_top",
+ [TARGET_REW] = "rew",
+ [TARGET_SYS] = "sys",
+ [TARGET_HSIO] = "hsio",
+ [TARGET_DEV] = "dev",
+ [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_AFI] = "afi",
+ [TARGET_ANA] = "ana",
+ [NUM_TARGETS] = NULL,
+};
+
+static int lan9645x_tag_npi_setup(struct dsa_switch *ds)
+{
+ struct dsa_port *dp, *first_cpu_dp = NULL;
+ struct lan9645x *lan9645x = ds->priv;
+
+ dsa_switch_for_each_user_port(dp, ds) {
+ 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_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)
+{
+ struct lan9645x *lan9645x = ds->priv;
+
+ return lan9645x->tag_proto;
+}
+
+static int lan9645x_connect_tag_protocol(struct dsa_switch *ds,
+ enum dsa_tag_protocol proto)
+{
+ switch (proto) {
+ case DSA_TAG_PROTO_LAN9645X:
+ return 0;
+ default:
+ return -EPROTONOSUPPORT;
+ }
+}
+
+static void lan9645x_teardown(struct dsa_switch *ds)
+{
+ struct lan9645x *lan9645x = ds->priv;
+
+ debugfs_remove_recursive(lan9645x->debugfs_root);
+ 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)
+{
+ struct lan9645x *lan9645x = ds->priv;
+ int max_mtu;
+
+ /* 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.
+ */
+ max_mtu = 10000 - ETH_HLEN - ETH_FCS_LEN;
+
+ if (port == lan9645x->npi) {
+ max_mtu -= LAN9645X_IFH_LEN;
+ max_mtu -= LAN9645X_LONG_PREFIX_LEN;
+ }
+
+ return max_mtu;
+}
+
+static int lan9645x_reset_switch(struct lan9645x *lan9645x)
+{
+ int val = 0;
+ int err;
+
+ 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, "Lan9645x setup: failed to init chip RAM.");
+ 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;
+ struct dsa_port *dp;
+ u32 all_phys_ports;
+ int err = 0;
+
+ lan9645x->num_phys_ports = ds->num_ports;
+ all_phys_ports = GENMASK(lan9645x->num_phys_ports - 1, 0);
+
+ err = lan9645x_reset_switch(lan9645x);
+ if (err)
+ return err;
+
+ lan9645x->debugfs_root = debugfs_create_dir("lan9645x_sw", NULL);
+
+ lan9645x->ports = devm_kcalloc(lan9645x->dev, lan9645x->num_phys_ports,
+ sizeof(struct lan9645x_port *),
+ GFP_KERNEL);
+ if (!lan9645x->ports)
+ return -ENOMEM;
+
+ for (int port = 0; port < lan9645x->num_phys_ports; port++) {
+ struct lan9645x_port *p;
+
+ p = devm_kzalloc(lan9645x->dev,
+ sizeof(struct lan9645x_port), GFP_KERNEL);
+ if (!p)
+ return -ENOMEM;
+
+ p->lan9645x = lan9645x;
+ p->chip_port = port;
+ lan9645x->ports[port] = p;
+ }
+
+ err = lan9645x_port_parse_ports_node(lan9645x);
+ if (err) {
+ dev_err(dev, "Lan9645x setup: failed to parse ports node.");
+ return err;
+ }
+
+ err = lan9645x_tag_npi_setup(ds);
+ if (err) {
+ dev_err(dev, "Lan9645x setup: 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 */
+ mdelay(1);
+
+ /* 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);
+
+ /* There are 8 priorities */
+ for (int prio = 0; prio < 8; ++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));
+
+ /* Set all the entries to obey VLAN. */
+ for (int i = 0; i < PGID_ENTRIES; ++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 copy them
+ * to CPU
+ */
+ lan_wr(ANA_CPU_FWD_BPDU_CFG_BPDU_REDIR_ENA,
+ lan9645x, ANA_CPU_FWD_BPDU_CFG(p));
+ }
+
+ /* Set source buffer size for each priority and port to ~1700 bytes */
+ for (int i = 0; i <= QSYS_Q_RSRV; ++i) {
+ lan_wr(QS_SRC_BUF_RSV / 64, lan9645x, QSYS_RES_CFG(i));
+ lan_wr(QS_SRC_BUF_RSV / 64, lan9645x, QSYS_RES_CFG(512 + i));
+ }
+
+ /* Configure and enable the CPU port */
+ lan9645x_port_cpu_init(lan9645x);
+
+ /* Multicast to all front ports */
+ lan_wr(all_phys_ports, lan9645x, ANA_PGID(PGID_MC));
+
+ /* IP multicast to all front ports */
+ lan_wr(all_phys_ports, lan9645x, ANA_PGID(PGID_MCIPV4));
+ lan_wr(all_phys_ports, lan9645x, ANA_PGID(PGID_MCIPV6));
+
+ /* Unicast to all front ports */
+ lan_wr(all_phys_ports, lan9645x, ANA_PGID(PGID_UC));
+
+ /* Broadcast to all ports */
+ lan_wr(BIT(CPU_PORT) | all_phys_ports, lan9645x, ANA_PGID(PGID_BC));
+
+ dsa_switch_for_each_available_port(dp, ds)
+ lan9645x_port_init(lan9645x, dp->index);
+
+ lan9645x_port_set_tail_drop_wm(lan9645x);
+
+ ds->mtu_enforcement_ingress = true;
+ ds->assisted_learning_on_cpu_port = true;
+ ds->fdb_isolation = true;
+
+ dev_info(lan9645x->dev,
+ "Setup complete. SKU features: tsn_dis=%d hsr_dis=%d max_ports=%d",
+ lan9645x->tsn_dis, lan9645x->dd_dis,
+ 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,
+ .connect_tag_protocol = lan9645x_connect_tag_protocol,
+
+ .setup = lan9645x_setup,
+ .teardown = lan9645x_teardown,
+
+ /* 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=%d", i);
+ 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);
+ lan9645x->dd_dis = GCB_FEAT_DISABLE_FEAT_DD_DIS_GET(feat_dis);
+ lan9645x->tsn_dis = GCB_FEAT_DISABLE_FEAT_TSN_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 dev_err_probe(dev, -ENOMEM,
+ "Failed to allocate LAN9645X");
+
+ 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");
+
+ ds = devm_kzalloc(dev, sizeof(*ds), GFP_KERNEL);
+ if (!ds)
+ return dev_err_probe(dev, -ENOMEM,
+ "Failed to allocate DSA switch");
+
+ ds->dev = dev;
+ ds->num_ports = NUM_PHYS_PORTS;
+ ds->num_tx_queues = NUM_PRIO_QUEUES;
+ ds->dscp_prio_mapping_is_global = true;
+
+ ds->ops = &lan9645x_switch_ops;
+ ds->phylink_mac_ops = &lan9645x_phylink_mac_ops;
+ ds->priv = lan9645x;
+
+ lan9645x->ds = ds;
+ lan9645x->tag_proto = DSA_TAG_PROTO_LAN9645X;
+ lan9645x->shared_queue_sz = LAN9645X_BUFFER_MEMORY;
+
+ lan9645x_set_feat_dis(lan9645x);
+
+ err = dsa_register_switch(ds);
+ if (err)
+ return dev_err_probe(dev, err, "Failed to register DSA switch");
+
+ 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..a51b637f28bf
--- /dev/null
+++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_main.h
@@ -0,0 +1,396 @@
+/* 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/regmap.h>
+#include <net/dsa.h>
+
+#include "lan9645x_regs.h"
+
+#define lan9645x_for_each_chipport(_lan9645x, _i) \
+ for ((_i) = 0; (_i) < (_lan9645x)->num_phys_ports; (_i)++)
+
+/* Ports 0-8 are front ports
+ * Ports 9-10 are CPU ports
+ *
+ * CPU ports are logical ports in the chip intended for management. The frame
+ * delivery mechanism can vary: direct register injection/extraction or one can
+ * use a front port as CPU port, called a Node Processor Interface (NPI).
+ *
+ * This is the approach used by LAN9645X.
+ */
+#define NUM_PHYS_PORTS 9
+#define CPU_PORT 9
+#define NUM_PRIO_QUEUES 8
+#define LAN9645X_NUM_TC 8
+
+#define QS_SRC_BUF_RSV 1700
+
+/* Reserved amount for (SRC, PRIO) at index 8*SRC + PRIO
+ * See QSYS:RES_CTRL[*]:RES_CFG description
+ */
+#define QSYS_Q_RSRV 95
+
+#define LAN9645X_ISDX_MAX 128
+#define LAN9645X_ESDX_MAX 128
+#define LAN9645X_SFID_MAX 128
+
+/* Reserved VLAN IDs. */
+#define UNAWARE_PVID 0
+#define HOST_PVID 4095
+#define VLAN_HSR_PRP 4094
+#define VLAN_MAX (VLAN_HSR_PRP - 1)
+
+/* VLAN flags for VLAN table defined in ANA_VLANTIDX */
+#define VLAN_SRC_CHK 0x01
+#define VLAN_MIR 0x02
+#define VLAN_LRN_DIS 0x04
+#define VLAN_PRV_VLAN 0x08
+#define VLAN_FLD_DIS 0x10
+#define VLAN_S_FWD_ENA 0x20
+
+/* 160KiB / 1.25Mbit */
+#define LAN9645X_BUFFER_MEMORY (160 * 1024)
+
+/* Port Group Identifiers (PGID) are port-masks applied to all frames.
+ * 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: By default the first 9 port masks == BIT(port_num). Never change
+ * these except for aggregation. Remaining dst masks are for L2 MC and
+ * flooding. (See FLOODING and FLOODING_IPMC).
+ *
+ * Aggregation: Used to pick a port within an aggregation group. If no
+ * aggregation is configured, these are all-ones.
+ *
+ * Source: Control which ports a given source port can forward to. A frame that
+ * is received on port n, uses mask 80+n as a mask to filter out destination
+ * ports. The default values are that all bits are set except for the index
+ * number (no loopback).
+ *
+ * We reserve destination PGIDs at the end of the range.
+ */
+
+#define PGID_AGGR 64
+#define PGID_SRC 80
+#define PGID_ENTRIES 89
+
+#define PGID_AGGR_NUM (PGID_SRC - PGID_AGGR)
+
+/* General purpose PGIDs. */
+#define PGID_GP_START CPU_PORT
+#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 RD_SLEEP_US 3
+#define RD_SLEEPTIMEOUT_US 100000
+#define SLOW_RD_SLEEP_US 1000
+#define SLOW_RD_SLEEPTIMEOUT_US 2000000
+
+#define lan9645x_rd_poll_timeout(_lan9645x, _reg_macro, _val, _cond) \
+ regmap_read_poll_timeout(lan_rmap((_lan9645x), _reg_macro), \
+ lan_rel_addr(_reg_macro), (_val), \
+ (_cond), RD_SLEEP_US, RD_SLEEPTIMEOUT_US)
+
+#define lan9645x_rd_poll_slow(_lan9645x, _reg_macro, _val, _cond) \
+ regmap_read_poll_timeout(lan_rmap((_lan9645x), _reg_macro), \
+ lan_rel_addr(_reg_macro), (_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;
+ enum dsa_tag_protocol tag_proto;
+ struct regmap *rmap[NUM_TARGETS];
+
+ int shared_queue_sz;
+
+ /* NPI chip_port */
+ int npi;
+
+ u8 num_phys_ports;
+ struct lan9645x_port **ports;
+
+ /* debugfs */
+ struct dentry *debugfs_root;
+
+ int num_port_dis;
+ bool dd_dis;
+ bool tsn_dis;
+};
+
+struct lan9645x_port {
+ struct lan9645x *lan9645x;
+ const char *name;
+
+ u16 pvid;
+ u16 untagged_vid;
+ u8 chip_port;
+ u8 stp_state;
+ bool vlan_aware;
+ bool learn_ena;
+
+ phy_interface_t phy_mode;
+
+ int speed; /* internal speed value LAN9645X_SPEED_* */
+ u8 duplex;
+ struct list_head path_delays;
+ u32 rx_delay;
+};
+
+struct lan9645x_path_delay {
+ struct list_head list;
+ u32 rx_delay;
+ u32 tx_delay;
+ u32 speed;
+};
+
+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)
+{
+#define GWM_MULTIPLIER_BIT BIT(8)
+#define LAN9645X_BUFFER_CELL_SZ 64
+ 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)
+{
+ if (WARN_ON(!(port >= 0 && port < lan9645x->num_phys_ports)))
+ return NULL;
+
+ return lan9645x->ports[port];
+}
+
+static inline struct net_device *lan9645x_port_to_ndev(struct lan9645x_port *p)
+{
+ struct lan9645x *lan9645x = p->lan9645x;
+ struct dsa_port *dp;
+
+ dp = dsa_to_port(lan9645x->ds, p->chip_port);
+ if (dp && dp->type == DSA_PORT_TYPE_USER)
+ return dp->user;
+
+ return NULL;
+}
+
+static inline struct net_device *
+lan9645x_chipport_to_ndev(struct lan9645x *lan9645x, int port)
+{
+ return lan9645x_port_to_ndev(lan9645x_to_port(lan9645x, port));
+}
+
+static inline bool lan9645x_port_is_used(struct lan9645x *lan9645x, int port)
+{
+ struct dsa_port *dp;
+
+ dp = dsa_to_port(lan9645x->ds, port);
+ if (!dp)
+ return false;
+
+ return dp->type != DSA_PORT_TYPE_UNUSED;
+}
+
+static inline struct regmap *lan_tgt2rmap(struct lan9645x *lan9645x,
+ enum lan9645x_target t, int tinst)
+{
+ WARN_ON_ONCE(!lan9645x->rmap[t + 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_init(struct lan9645x *lan9645x, int port);
+void lan9645x_port_cpu_init(struct lan9645x *lan9645x);
+void lan9645x_port_set_tail_drop_wm(struct lan9645x *lan9645x);
+int lan9645x_port_set_maxlen(struct lan9645x *lan9645x, int port, size_t sdu);
+int lan9645x_port_parse_ports_node(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..0ae8b9acb916
--- /dev/null
+++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_npi.c
@@ -0,0 +1,99 @@
+// SPDX-License-Identifier: GPL-2.0+
+/* Copyright (C) 2026 Microchip Technology Inc.
+ */
+#include <net/addrconf.h>
+
+#include "lan9645x_main.h"
+
+static void disable_conduit_ipv6(struct lan9645x *lan9645x,
+ struct net_device *conduit)
+{
+ struct inet6_dev *dev_v6;
+
+ if (!conduit)
+ return;
+
+ /* IPv6 on the conduit will generate frames bypassing our tag driver, so
+ * they lack an IFH. This will be garbage in garbage out and we want to
+ * avoid this.
+ */
+ rtnl_lock();
+ dev_v6 = __in6_dev_get(conduit);
+ if (dev_v6) {
+ WRITE_ONCE(dev_v6->cnf.disable_ipv6, 1);
+ dev_warn(lan9645x->dev, "Disabled IPv6 on conduit device: %s\n",
+ netdev_name(conduit));
+ }
+ rtnl_unlock();
+}
+
+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);
+
+ /* Enabling IPv6 on the conduit will send frames directly on the
+ * interface, without being intercepted by our tag driver. This causes a
+ * GIGO situation.
+ */
+ disable_conduit_ipv6(lan9645x, cpu_port->conduit);
+
+ /* 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. */
+ 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(port));
+
+ /* Make sure frames with src_port=CPU_PORT 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 + CPU_PORT));
+}
+
+void lan9645x_npi_port_deinit(struct lan9645x *lan9645x, int port)
+{
+ struct lan9645x_port *p = lan9645x_to_port(lan9645x, port);
+
+ lan9645x->npi = -1;
+
+ lan_wr(QSYS_EXT_CPU_CFG_EXT_CPU_PORT_SET(0x1f) |
+ 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));
+}
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..3acc48f12fae
--- /dev/null
+++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_phylink.c
@@ -0,0 +1,537 @@
+// SPDX-License-Identifier: GPL-2.0+
+/* Copyright (C) 2026 Microchip Technology Inc.
+ */
+
+#include <linux/phy.h>
+#include <linux/phy/phy.h>
+
+#include "lan9645x_main.h"
+
+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 | MAC_2500FD;
+
+ switch (port) {
+ case 0 ... 3:
+ __set_bit(PHY_INTERFACE_MODE_GMII, c->supported_interfaces);
+ break;
+ case 4:
+ __set_bit(PHY_INTERFACE_MODE_GMII, c->supported_interfaces);
+ phy_interface_set_rgmii(c->supported_interfaces);
+ break;
+ case 5 ... 6:
+ /* SerDes ports: QSGMII/SGMII/1000BASEX/2500BASEX modes
+ * require PCS support which is not yet implemented.
+ */
+ 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 int lan9645x_rgmii_setup(struct lan9645x *lan9645x, int port, int speed,
+ phy_interface_t mode)
+{
+ bool tx_delay = false, rx_delay = false;
+ u32 rx_idx, tx_idx;
+ u8 tx_clk;
+ int idx;
+
+ /* Port 4 or 7 is RGMII_0 and port 8 is RGMII_1 */
+ idx = port == 8 ? 1 : 0;
+
+ if (!phy_interface_mode_is_rgmii(mode))
+ return 0;
+
+ 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(idx));
+
+ /* We configure delays on the MAC side. When the PHY is not responsible
+ * for delays, the MAC is, which is why RGMII_TXID results in
+ * rx_delay=true.
+ */
+ if (mode == PHY_INTERFACE_MODE_RGMII ||
+ mode == PHY_INTERFACE_MODE_RGMII_TXID)
+ rx_delay = true;
+
+ if (mode == PHY_INTERFACE_MODE_RGMII ||
+ mode == PHY_INTERFACE_MODE_RGMII_RXID)
+ tx_delay = true;
+
+ /* Setup DLL configuration. Register layout:
+ * 0: RGMII_0_RX
+ * 1: RGMII_0_TX
+ * 2: RGMII_1_RX
+ * 3: RGMII_1_TX
+ * ...
+ * (N<<1) RGMII_N_RX,
+ * (N<<1)+1: RGMII_N_TX,
+ */
+ rx_idx = idx << 1;
+ tx_idx = rx_idx + 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(rx_delay) |
+ HSIO_DLL_CFG_DELAY_ENA_SET(rx_delay),
+ HSIO_DLL_CFG_DLL_CLK_ENA |
+ HSIO_DLL_CFG_DLL_RST |
+ HSIO_DLL_CFG_DLL_ENA |
+ HSIO_DLL_CFG_DELAY_ENA,
+ 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(tx_delay) |
+ HSIO_DLL_CFG_DELAY_ENA_SET(tx_delay),
+ HSIO_DLL_CFG_DLL_CLK_ENA |
+ HSIO_DLL_CFG_DLL_RST |
+ HSIO_DLL_CFG_DLL_ENA |
+ HSIO_DLL_CFG_DELAY_ENA,
+ lan9645x, HSIO_DLL_CFG(tx_idx));
+
+ return 0;
+}
+
+static void lan9645x_phylink_mac_config(struct lan9645x *lan9645x, int port,
+ unsigned int mode,
+ const struct phylink_link_state *state)
+{
+ if (phy_interface_mode_is_rgmii(state->interface))
+ lan9645x_rgmii_setup(lan9645x, port, state->speed,
+ state->interface);
+}
+
+static int lan9645x_phylink_mac_prepare(struct lan9645x *lan9645x, int port,
+ unsigned int mode,
+ phy_interface_t iface)
+{
+ switch (port) {
+ case 0 ... 3:
+ lan_rmw(HSIO_HW_CFG_GMII_ENA_SET(BIT(port)),
+ HSIO_HW_CFG_GMII_ENA_SET(BIT(port)), lan9645x,
+ HSIO_HW_CFG);
+ break;
+ case 4:
+ lan_rmw(HSIO_HW_CFG_GMII_ENA_SET(BIT(port)),
+ HSIO_HW_CFG_GMII_ENA_SET(BIT(port)), lan9645x,
+ HSIO_HW_CFG);
+
+ if (phy_interface_mode_is_rgmii(iface))
+ lan_rmw(HSIO_HW_CFG_RGMII_0_CFG_SET(1),
+ HSIO_HW_CFG_RGMII_0_CFG,
+ lan9645x, HSIO_HW_CFG);
+
+ break;
+ case 7 ... 8:
+ lan_rmw(HSIO_HW_CFG_GMII_ENA_SET(BIT(port)),
+ HSIO_HW_CFG_GMII_ENA_SET(BIT(port)), lan9645x,
+ HSIO_HW_CFG);
+ break;
+ default:
+ /* Ports 5-6 are SerDes-only and need PCS support (not yet
+ * implemented). They are excluded from phylink_get_caps.
+ */
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static int lan9645x_port_is_cuphy(struct lan9645x *lan9645x, int port,
+ phy_interface_t interface)
+{
+ return port >= 0 && port <= 4 && interface == PHY_INTERFACE_MODE_GMII;
+}
+
+static void lan9645x_phylink_mac_link_up(struct lan9645x *lan9645x, int port,
+ unsigned int link_an_mode,
+ phy_interface_t interface,
+ struct phy_device *phydev, int speed,
+ int duplex, bool tx_pause,
+ bool rx_pause)
+{
+ struct lan9645x_port *p = lan9645x_to_port(lan9645x, port);
+ int rx_ifg1, rx_ifg2, tx_ifg, gtx_clk = 0;
+ struct lan9645x_path_delay *path_delay;
+ int gspeed = LAN9645X_SPEED_DISABLED;
+ int mode = 0;
+ int fc_spd;
+
+ /* Configure speed for RGMII modules. */
+ if (phy_interface_mode_is_rgmii(interface))
+ lan9645x_rgmii_setup(lan9645x, port, speed, interface);
+
+ if (duplex == DUPLEX_FULL) {
+ mode |= DEV_MAC_MODE_CFG_FDX_ENA_SET(1);
+ rx_ifg2 = DEV_MAC_IFG_CFG_RX_IFG2_SET(0x2);
+ tx_ifg = DEV_MAC_IFG_CFG_TX_IFG_SET(0x5);
+
+ } else {
+ rx_ifg2 = DEV_MAC_IFG_CFG_RX_IFG2_SET(0x2);
+ tx_ifg = DEV_MAC_IFG_CFG_TX_IFG_SET(0x6);
+ }
+
+ switch (speed) {
+ case SPEED_10:
+ rx_ifg1 = DEV_MAC_IFG_CFG_RX_IFG1_SET(0x2);
+ gspeed = LAN9645X_SPEED_10;
+ break;
+ case SPEED_100:
+ rx_ifg1 = DEV_MAC_IFG_CFG_RX_IFG1_SET(0x1);
+ gspeed = LAN9645X_SPEED_100;
+ break;
+ case SPEED_1000:
+ gspeed = LAN9645X_SPEED_1000;
+ mode |= DEV_MAC_MODE_CFG_GIGA_MODE_ENA_SET(1);
+ mode |= DEV_MAC_MODE_CFG_FDX_ENA_SET(1);
+ rx_ifg1 = DEV_MAC_IFG_CFG_RX_IFG1_SET(0x1);
+ rx_ifg2 = DEV_MAC_IFG_CFG_RX_IFG2_SET(0x2);
+ tx_ifg = DEV_MAC_IFG_CFG_TX_IFG_SET(0x6);
+ gtx_clk = 1;
+ break;
+ case SPEED_2500:
+ gspeed = LAN9645X_SPEED_2500;
+ mode |= DEV_MAC_MODE_CFG_GIGA_MODE_ENA_SET(1);
+ mode |= DEV_MAC_MODE_CFG_FDX_ENA_SET(1);
+ rx_ifg1 = DEV_MAC_IFG_CFG_RX_IFG1_SET(0x1);
+ rx_ifg2 = DEV_MAC_IFG_CFG_RX_IFG2_SET(0x2);
+ tx_ifg = DEV_MAC_IFG_CFG_TX_IFG_SET(0x6);
+ break;
+ default:
+ dev_err(lan9645x->dev, "Unsupported speed on port %d: %d\n",
+ p->chip_port, speed);
+ return;
+ }
+
+ p->speed = gspeed;
+ p->duplex = duplex;
+ fc_spd = lan9645x_speed_fc_enc(p->speed);
+
+ if (phy_interface_num_ports(interface) == 4 ||
+ interface == PHY_INTERFACE_MODE_SGMII)
+ mode |= DEV_MAC_MODE_CFG_GIGA_MODE_ENA_SET(1);
+
+ 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(tx_ifg | rx_ifg1 | 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));
+
+ 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));
+
+ 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(DEV_MAC_HDX_CFG_SEED_LOAD_SET(0),
+ DEV_MAC_HDX_CFG_SEED_LOAD, lan9645x,
+ DEV_MAC_HDX_CFG(p->chip_port));
+
+ /* Set PFC link speed and enable map */
+ lan_rmw(ANA_PFC_CFG_FC_LINK_SPEED_SET(fc_spd) |
+ ANA_PFC_CFG_RX_PFC_ENA_SET(0),
+ ANA_PFC_CFG_FC_LINK_SPEED |
+ ANA_PFC_CFG_RX_PFC_ENA,
+ lan9645x, ANA_PFC_CFG(p->chip_port));
+
+ lan_rmw(DEV_PCS1G_CFG_PCS_ENA_SET(1),
+ DEV_PCS1G_CFG_PCS_ENA, lan9645x,
+ DEV_PCS1G_CFG(p->chip_port));
+
+ lan_rmw(DEV_PCS1G_SD_CFG_SD_ENA_SET(0),
+ DEV_PCS1G_SD_CFG_SD_ENA,
+ lan9645x, DEV_PCS1G_SD_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));
+
+ /* 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));
+
+ /* 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));
+
+ list_for_each_entry(path_delay, &p->path_delays, list) {
+ if (path_delay->speed == speed) {
+ lan_wr(path_delay->rx_delay + p->rx_delay,
+ lan9645x, SYS_PTP_RXDLY_CFG(p->chip_port));
+ lan_wr(path_delay->tx_delay,
+ lan9645x, SYS_PTP_TXDLY_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 == 0 ? 1 : 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));
+
+ lan_rmw(AFI_PORT_CFG_FC_SKIP_TTI_INJ_SET(0) |
+ AFI_PORT_CFG_FRM_OUT_MAX_SET(16),
+ AFI_PORT_CFG_FC_SKIP_TTI_INJ |
+ AFI_PORT_CFG_FRM_OUT_MAX,
+ lan9645x, AFI_PORT_CFG(p->chip_port));
+}
+
+void lan9645x_phylink_port_down(struct lan9645x *lan9645x, int port)
+{
+ struct lan9645x_port *p = lan9645x_to_port(lan9645x, port);
+ u32 val;
+
+ /* 0.5: Disable any AFI */
+ lan_rmw(AFI_PORT_CFG_FC_SKIP_TTI_INJ_SET(1) |
+ AFI_PORT_CFG_FRM_OUT_MAX_SET(0),
+ AFI_PORT_CFG_FC_SKIP_TTI_INJ |
+ AFI_PORT_CFG_FRM_OUT_MAX,
+ lan9645x, AFI_PORT_CFG(p->chip_port));
+
+ /* wait for reg afi_port_frm_out to become 0 for the port */
+ if (lan9645x_rd_poll_slow(lan9645x, AFI_PORT_FRM_OUT(p->chip_port),
+ val,
+ !AFI_PORT_FRM_OUT_FRM_OUT_CNT_GET(val)))
+ dev_err(lan9645x->dev, "AFI timeout chip port %u",
+ p->chip_port);
+
+ /* 2: 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));
+
+ /* 1: Reset the PCS Rx clock domain */
+ lan_rmw(DEV_CLOCK_CFG_PCS_RX_RST_SET(1),
+ DEV_CLOCK_CFG_PCS_RX_RST,
+ lan9645x, DEV_CLOCK_CFG(p->chip_port));
+
+ p->speed = LAN9645X_SPEED_DISABLED;
+ p->duplex = DUPLEX_UNKNOWN;
+
+ /* 3: 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));
+
+ /* 4: 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));
+
+ /* 5: Disable Flowcontrol */
+ lan_rmw(SYS_PAUSE_CFG_PAUSE_ENA_SET(0),
+ SYS_PAUSE_CFG_PAUSE_ENA,
+ lan9645x, SYS_PAUSE_CFG(p->chip_port));
+
+ /* 5.1: Disable PFC */
+ lan_rmw(QSYS_SW_PORT_MODE_TX_PFC_ENA_SET(0),
+ QSYS_SW_PORT_MODE_TX_PFC_ENA,
+ lan9645x, QSYS_SW_PORT_MODE(p->chip_port));
+
+ /* 6: Wait a worst case time 8ms (10K jumbo/10Mbit) */
+ usleep_range(8 * USEC_PER_MSEC, 9 * USEC_PER_MSEC);
+
+ /* 7: 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));
+
+ /* 8: 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));
+
+ /* 9: 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));
+
+ /* 10: 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", port);
+
+ /* 11: 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));
+
+ /* 12: 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));
+
+ /* 13: Clear flushing */
+ lan_rmw(QSYS_SW_PORT_MODE_AGING_MODE_SET(1),
+ QSYS_SW_PORT_MODE_AGING_MODE,
+ lan9645x, QSYS_SW_PORT_MODE(p->chip_port));
+}
+
+static void lan9645x_phylink_mac_link_down(struct lan9645x *lan9645x, int port,
+ unsigned int link_an_mode,
+ phy_interface_t interface)
+{
+ struct lan9645x_port *p = lan9645x_to_port(lan9645x, port);
+
+ lan9645x_phylink_port_down(lan9645x, port);
+
+ /* 14: Take PCS out of reset */
+ lan_rmw(DEV_CLOCK_CFG_PCS_RX_RST_SET(0) |
+ DEV_CLOCK_CFG_PCS_TX_RST_SET(0),
+ DEV_CLOCK_CFG_PCS_RX_RST |
+ DEV_CLOCK_CFG_PCS_TX_RST,
+ lan9645x, DEV_CLOCK_CFG(p->chip_port));
+}
+
+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 void
+lan9645x_port_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);
+
+ lan9645x_phylink_mac_config(p->lan9645x, p->chip_port, mode, state);
+}
+
+static void lan9645x_port_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);
+
+ lan9645x_phylink_mac_link_up(p->lan9645x, p->chip_port, link_an_mode,
+ interface, phydev, speed, duplex, tx_pause,
+ rx_pause);
+}
+
+static void lan9645x_port_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);
+
+ lan9645x_phylink_mac_link_down(p->lan9645x, p->chip_port, link_an_mode,
+ interface);
+}
+
+static int lan9645x_port_phylink_mac_prepare(struct phylink_config *config,
+ unsigned int mode,
+ phy_interface_t iface)
+{
+ struct lan9645x_port *p = lan9645x_phylink_config_to_port(config);
+
+ return lan9645x_phylink_mac_prepare(p->lan9645x, p->chip_port, mode,
+ iface);
+}
+
+const struct phylink_mac_ops lan9645x_phylink_mac_ops = {
+ .mac_config = lan9645x_port_phylink_mac_config,
+ .mac_link_up = lan9645x_port_phylink_mac_link_up,
+ .mac_link_down = lan9645x_port_phylink_mac_link_down,
+ .mac_prepare = lan9645x_port_phylink_mac_prepare,
+};
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..038868ae0a32
--- /dev/null
+++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_port.c
@@ -0,0 +1,289 @@
+// SPDX-License-Identifier: GPL-2.0+
+/* Copyright (C) 2026 Microchip Technology Inc.
+ */
+
+#include "lan9645x_main.h"
+
+int lan9645x_port_init(struct lan9645x *lan9645x, int port)
+{
+ struct lan9645x_port *p;
+
+ p = lan9645x_to_port(lan9645x, port);
+
+ /* 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);
+
+ lan9645x_phylink_port_down(lan9645x, port);
+
+ if (phy_interface_num_ports(p->phy_mode) == 4)
+ lan_rmw(DEV_CLOCK_CFG_PCS_RX_RST_SET(0) |
+ DEV_CLOCK_CFG_PCS_TX_RST_SET(0),
+ DEV_CLOCK_CFG_PCS_RX_RST |
+ DEV_CLOCK_CFG_PCS_TX_RST,
+ lan9645x, DEV_CLOCK_CFG(p->chip_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));
+
+ /* Enable receiving frames on the port, and activate auto-learning of
+ * MAC addresses.
+ */
+ 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;
+}
+
+void lan9645x_port_cpu_init(struct lan9645x *lan9645x)
+{
+ /* Map the 8 CPU extraction queues to CPU port 9 (datasheet is wrong) */
+ lan_wr(0, lan9645x, QSYS_CPU_GROUP_MAP);
+
+ /* Configure second cpu port (chip_port 10) for manual frame injection.
+ * The AFI can not inject frames via the NPI port, unless frame aging is
+ * disabled on frontports, so we use manual injection for AFI frames.
+ */
+
+ /* Set min-spacing of EOF to SOF on injected frames to 0, on cpu device
+ * 1. This is required when injecting with IFH.
+ * Default values emulates delay of std preamble/IFG setting on a front
+ * port.
+ */
+ lan_rmw(QS_INJ_CTRL_GAP_SIZE_SET(0),
+ QS_INJ_CTRL_GAP_SIZE,
+ lan9645x, QS_INJ_CTRL(1));
+
+ /* Injection: Mode: manual injection | Byte_swap */
+ lan_wr(QS_INJ_GRP_CFG_MODE_SET(1) |
+ QS_INJ_GRP_CFG_BYTE_SWAP_SET(1),
+ lan9645x, QS_INJ_GRP_CFG(1));
+
+ lan_rmw(QS_INJ_CTRL_GAP_SIZE_SET(0),
+ QS_INJ_CTRL_GAP_SIZE,
+ lan9645x, QS_INJ_CTRL(1));
+
+ lan_wr(SYS_PORT_MODE_INCL_INJ_HDR_SET(1),
+ lan9645x, SYS_PORT_MODE(CPU_PORT + 1));
+
+ /* 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(BIT(CPU_PORT)),
+ QSYS_EGR_NO_SHARING_EGR_NO_SHARING_SET(BIT(CPU_PORT)),
+ 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(BIT(CPU_PORT)),
+ QSYS_EGR_DROP_MODE_EGRESS_DROP_MODE_SET(BIT(CPU_PORT)),
+ lan9645x, QSYS_EGR_DROP_MODE);
+
+ lan_wr(BIT(CPU_PORT), lan9645x, ANA_PGID(PGID_CPU));
+
+ lan_rmw(ANA_PORT_CFG_PORTID_VAL_SET(CPU_PORT) |
+ ANA_PORT_CFG_RECV_ENA_SET(1),
+ ANA_PORT_CFG_PORTID_VAL |
+ ANA_PORT_CFG_RECV_ENA, lan9645x,
+ ANA_PORT_CFG(CPU_PORT));
+
+ /* 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(CPU_PORT));
+
+ /* Transmit cpu frames as received without any tagging, timing or other
+ * updates. This does not affect CPU-over-NPI, only manual extraction.
+ * On the NPI port we need NO_REWRITE=0 for HSR/PRP.
+ */
+ lan_wr(REW_PORT_CFG_NO_REWRITE_SET(1),
+ lan9645x, REW_PORT_CFG(CPU_PORT));
+}
+
+void lan9645x_port_set_tail_drop_wm(struct lan9645x *lan9645x)
+{
+ int shared_per_port;
+ int port;
+
+ /* Configure tail dropping watermark */
+ shared_per_port =
+ lan9645x->shared_queue_sz / (lan9645x->num_phys_ports + 1);
+
+ /* The total memory size is diveded by number of front ports plus CPU
+ * port.
+ */
+ lan9645x_for_each_chipport(lan9645x, port)
+ lan_wr(lan9645x_wm_enc(shared_per_port), lan9645x,
+ SYS_ATOP(port));
+
+ /* Tail dropping active based only on per port ATOP wm */
+ lan_wr(lan9645x_wm_enc(lan9645x->shared_queue_sz), lan9645x,
+ SYS_ATOP_TOT_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;
+ 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 int lan9645x_port_setup_leds(struct lan9645x *lan9645x,
+ struct fwnode_handle *portnp, int port)
+{
+ u32 drive_mode;
+ int err;
+
+ err = fwnode_property_read_u32(portnp, "microchip,led-drive-mode",
+ &drive_mode);
+ if (err)
+ return err;
+
+ lan_rmw(CHIP_TOP_CUPHY_LED_CFG_LED_DRIVE_MODE_SET(drive_mode),
+ CHIP_TOP_CUPHY_LED_CFG_LED_DRIVE_MODE, lan9645x,
+ CHIP_TOP_CUPHY_LED_CFG(port));
+
+ return 0;
+}
+
+static int lan9645x_port_parse_delays(struct lan9645x_port *port,
+ struct fwnode_handle *portnp)
+{
+ struct fwnode_handle *delay;
+ int err;
+
+ INIT_LIST_HEAD(&port->path_delays);
+
+ fwnode_for_each_available_child_node(portnp, delay) {
+ struct lan9645x_path_delay *path_delay;
+ s32 tx_delay;
+ s32 rx_delay;
+ u32 speed;
+
+ err = fwnode_property_read_u32(delay, "speed", &speed);
+ if (err)
+ return err;
+
+ err = fwnode_property_read_u32(delay, "rx_delay", &rx_delay);
+ if (err)
+ return err;
+
+ err = fwnode_property_read_u32(delay, "tx_delay", &tx_delay);
+ if (err)
+ return err;
+
+ path_delay = devm_kzalloc(port->lan9645x->dev,
+ sizeof(*path_delay), GFP_KERNEL);
+ if (!path_delay)
+ return -ENOMEM;
+
+ path_delay->rx_delay = rx_delay;
+ path_delay->tx_delay = tx_delay;
+ path_delay->speed = speed;
+ list_add_tail(&path_delay->list, &port->path_delays);
+ }
+
+ return 0;
+}
+
+int lan9645x_port_parse_ports_node(struct lan9645x *lan9645x)
+{
+ struct fwnode_handle *ports, *portnp;
+ struct device *dev = lan9645x->dev;
+ int max_ports, num_ports = 0;
+ int err = 0;
+
+ max_ports = NUM_PHYS_PORTS - lan9645x->num_port_dis;
+
+ ports = device_get_named_child_node(dev, "ethernet-ports");
+ if (!ports)
+ ports = device_get_named_child_node(dev, "ports");
+ if (!ports) {
+ dev_err(dev, "no ethernet-ports or ports child found\n");
+ return -ENODEV;
+ }
+
+ fwnode_for_each_available_child_node(ports, portnp) {
+ int phy_mode;
+ u32 p;
+
+ num_ports++;
+
+ if (num_ports > max_ports) {
+ dev_err(dev,
+ "Too many ports in device tree. Max ports supported by SKU: %d\n",
+ max_ports);
+ err = -ENODEV;
+ goto err_free_ports;
+ }
+
+ if (fwnode_property_read_u32(portnp, "reg", &p)) {
+ dev_err(dev, "Port number not defined in device tree (property \"reg\")\n");
+ err = -ENODEV;
+ fwnode_handle_put(portnp);
+ goto err_free_ports;
+ }
+
+ if (p >= lan9645x->num_phys_ports) {
+ dev_err(dev,
+ "Port number in device tree is invalid %u (property \"reg\")\n",
+ p);
+ err = -ENODEV;
+ fwnode_handle_put(portnp);
+ goto err_free_ports;
+ }
+
+ phy_mode = fwnode_get_phy_mode(portnp);
+ if (phy_mode < 0) {
+ dev_err(dev, "Failed to read phy-mode for port %u", p);
+ err = -ENODEV;
+ fwnode_handle_put(portnp);
+ goto err_free_ports;
+ }
+
+ lan9645x->ports[p]->phy_mode = phy_mode;
+ lan9645x_port_parse_delays(lan9645x->ports[p], portnp);
+ lan9645x_port_setup_leds(lan9645x, portnp, p);
+ }
+
+err_free_ports:
+ fwnode_handle_put(ports);
+ return err;
+}
diff --git a/drivers/net/ethernet/microchip/Kconfig b/drivers/net/ethernet/microchip/Kconfig
index ee046468652c..740f3c2e8199 100644
--- a/drivers/net/ethernet/microchip/Kconfig
+++ b/drivers/net/ethernet/microchip/Kconfig
@@ -62,5 +62,6 @@ source "drivers/net/ethernet/microchip/lan966x/Kconfig"
source "drivers/net/ethernet/microchip/sparx5/Kconfig"
source "drivers/net/ethernet/microchip/vcap/Kconfig"
source "drivers/net/ethernet/microchip/fdma/Kconfig"
+source "drivers/net/dsa/microchip/lan9645x/Kconfig"
endif # NET_VENDOR_MICROCHIP
--
2.52.0
On Tue, Mar 03, 2026 at 01:22:30PM +0100, Jens Emil Schulz Østergaard wrote:
> 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.
I'm reviewing the phylink implementation only.
> +static int lan9645x_rgmii_setup(struct lan9645x *lan9645x, int port, int speed,
> + phy_interface_t mode)
> +{
> + bool tx_delay = false, rx_delay = false;
> + u32 rx_idx, tx_idx;
> + u8 tx_clk;
> + int idx;
> +
> + /* Port 4 or 7 is RGMII_0 and port 8 is RGMII_1 */
> + idx = port == 8 ? 1 : 0;
> +
> + if (!phy_interface_mode_is_rgmii(mode))
> + return 0;
> +
> + 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),
What exactly is this clock configuration doing?
> + HSIO_RGMII_CFG_RGMII_RX_RST |
> + HSIO_RGMII_CFG_RGMII_TX_RST |
> + HSIO_RGMII_CFG_TX_CLK_CFG,
> + lan9645x, HSIO_RGMII_CFG(idx));
> +
> + /* We configure delays on the MAC side. When the PHY is not responsible
> + * for delays, the MAC is, which is why RGMII_TXID results in
> + * rx_delay=true.
> + */
> + if (mode == PHY_INTERFACE_MODE_RGMII ||
> + mode == PHY_INTERFACE_MODE_RGMII_TXID)
> + rx_delay = true;
> +
> + if (mode == PHY_INTERFACE_MODE_RGMII ||
> + mode == PHY_INTERFACE_MODE_RGMII_RXID)
> + tx_delay = true;
PHY_INTERFACE_MODE_RGMII means that the delays are provided by the
board layout, so this seems wrong to me. Please review the phylib
documentation concerning RGMII delays. I'd like Andrew to comment on
this too.
> +
> + /* Setup DLL configuration. Register layout:
> + * 0: RGMII_0_RX
> + * 1: RGMII_0_TX
> + * 2: RGMII_1_RX
> + * 3: RGMII_1_TX
> + * ...
> + * (N<<1) RGMII_N_RX,
> + * (N<<1)+1: RGMII_N_TX,
> + */
> + rx_idx = idx << 1;
> + tx_idx = rx_idx + 1;
Nit: I think the shift is too clever here. It's clearly:
rx_idx = idx * 2;
tx_idx = idx * 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(rx_delay) |
> + HSIO_DLL_CFG_DELAY_ENA_SET(rx_delay),
> + HSIO_DLL_CFG_DLL_CLK_ENA |
> + HSIO_DLL_CFG_DLL_RST |
> + HSIO_DLL_CFG_DLL_ENA |
> + HSIO_DLL_CFG_DELAY_ENA,
> + 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(tx_delay) |
> + HSIO_DLL_CFG_DELAY_ENA_SET(tx_delay),
> + HSIO_DLL_CFG_DLL_CLK_ENA |
> + HSIO_DLL_CFG_DLL_RST |
> + HSIO_DLL_CFG_DLL_ENA |
> + HSIO_DLL_CFG_DELAY_ENA,
> + lan9645x, HSIO_DLL_CFG(tx_idx));
> +
> + return 0;
> +}
> +
> +static void lan9645x_phylink_mac_config(struct lan9645x *lan9645x, int port,
> + unsigned int mode,
> + const struct phylink_link_state *state)
> +{
> + if (phy_interface_mode_is_rgmii(state->interface))
> + lan9645x_rgmii_setup(lan9645x, port, state->speed,
> + state->interface);
state->speed will always be SPEED_UNKNOWN here. Please don't use
state->speed here, it's even documented that drivers should not access
this member in their mac_config method().
> +}
> +
> +static int lan9645x_phylink_mac_prepare(struct lan9645x *lan9645x, int port,
> + unsigned int mode,
> + phy_interface_t iface)
> +{
> + switch (port) {
> + case 0 ... 3:
> + lan_rmw(HSIO_HW_CFG_GMII_ENA_SET(BIT(port)),
> + HSIO_HW_CFG_GMII_ENA_SET(BIT(port)), lan9645x,
> + HSIO_HW_CFG);
> + break;
> + case 4:
> + lan_rmw(HSIO_HW_CFG_GMII_ENA_SET(BIT(port)),
> + HSIO_HW_CFG_GMII_ENA_SET(BIT(port)), lan9645x,
> + HSIO_HW_CFG);
> +
> + if (phy_interface_mode_is_rgmii(iface))
> + lan_rmw(HSIO_HW_CFG_RGMII_0_CFG_SET(1),
> + HSIO_HW_CFG_RGMII_0_CFG,
> + lan9645x, HSIO_HW_CFG);
> +
> + break;
> + case 7 ... 8:
> + lan_rmw(HSIO_HW_CFG_GMII_ENA_SET(BIT(port)),
> + HSIO_HW_CFG_GMII_ENA_SET(BIT(port)), lan9645x,
> + HSIO_HW_CFG);
> + break;
> + default:
> + /* Ports 5-6 are SerDes-only and need PCS support (not yet
> + * implemented). They are excluded from phylink_get_caps.
... which means phylink_create() will fail, and this function will never
be called for those ports.
> + */
> + return -EINVAL;
> + }
As each of the above cases uses the exactly the same preamble, I'm
wondering whether something like:
u32 mask;
if (port == 5 || port == 6 || port > 8)
return -EINVAL;
mask = HSIO_HW_CFG_GMII_ENA_SET(BIT(port));
lan_rmw(mask, mask, lan9645x, HSIO_HW_CFG);
if (port == 4 && phy_interface_mode_is_rgmii(iface))
lan_rmw(HSIO_HW_CFG_RGMII_0_CFG_SET(1),
HSIO_HW_CFG_RGMII_0_CFG,
lan9645x, HSIO_HW_CFG);
would be neater.
> +
> + return 0;
> +}
> +
> +static int lan9645x_port_is_cuphy(struct lan9645x *lan9645x, int port,
> + phy_interface_t interface)
> +{
> + return port >= 0 && port <= 4 && interface == PHY_INTERFACE_MODE_GMII;
> +}
> +
> +static void lan9645x_phylink_mac_link_up(struct lan9645x *lan9645x, int port,
> + unsigned int link_an_mode,
> + phy_interface_t interface,
> + struct phy_device *phydev, int speed,
> + int duplex, bool tx_pause,
> + bool rx_pause)
> +{
> + struct lan9645x_port *p = lan9645x_to_port(lan9645x, port);
> + int rx_ifg1, rx_ifg2, tx_ifg, gtx_clk = 0;
> + struct lan9645x_path_delay *path_delay;
> + int gspeed = LAN9645X_SPEED_DISABLED;
> + int mode = 0;
> + int fc_spd;
> +
> + /* Configure speed for RGMII modules. */
> + if (phy_interface_mode_is_rgmii(interface))
> + lan9645x_rgmii_setup(lan9645x, port, speed, interface);
You shouldn't need to do a full setup of the RGMII state here - all
that should be done is to adjust the transmit clock speed. I would
prefer to see lan9645x_rgmii_setup() split into a "we're configuring
the bulk of RGMII state" function which is called from mac_config(),
and a "we have link up, do the minimal configuration that is dependent
on the resulting link speed" in mac_link_up().
> +
> + if (duplex == DUPLEX_FULL) {
> + mode |= DEV_MAC_MODE_CFG_FDX_ENA_SET(1);
> + rx_ifg2 = DEV_MAC_IFG_CFG_RX_IFG2_SET(0x2);
> + tx_ifg = DEV_MAC_IFG_CFG_TX_IFG_SET(0x5);
> +
> + } else {
> + rx_ifg2 = DEV_MAC_IFG_CFG_RX_IFG2_SET(0x2);
> + tx_ifg = DEV_MAC_IFG_CFG_TX_IFG_SET(0x6);
> + }
> +
> + switch (speed) {
> + case SPEED_10:
> + rx_ifg1 = DEV_MAC_IFG_CFG_RX_IFG1_SET(0x2);
> + gspeed = LAN9645X_SPEED_10;
> + break;
> + case SPEED_100:
> + rx_ifg1 = DEV_MAC_IFG_CFG_RX_IFG1_SET(0x1);
> + gspeed = LAN9645X_SPEED_100;
> + break;
> + case SPEED_1000:
> + gspeed = LAN9645X_SPEED_1000;
> + mode |= DEV_MAC_MODE_CFG_GIGA_MODE_ENA_SET(1);
> + mode |= DEV_MAC_MODE_CFG_FDX_ENA_SET(1);
> + rx_ifg1 = DEV_MAC_IFG_CFG_RX_IFG1_SET(0x1);
> + rx_ifg2 = DEV_MAC_IFG_CFG_RX_IFG2_SET(0x2);
> + tx_ifg = DEV_MAC_IFG_CFG_TX_IFG_SET(0x6);
> + gtx_clk = 1;
> + break;
> + case SPEED_2500:
> + gspeed = LAN9645X_SPEED_2500;
> + mode |= DEV_MAC_MODE_CFG_GIGA_MODE_ENA_SET(1);
> + mode |= DEV_MAC_MODE_CFG_FDX_ENA_SET(1);
> + rx_ifg1 = DEV_MAC_IFG_CFG_RX_IFG1_SET(0x1);
> + rx_ifg2 = DEV_MAC_IFG_CFG_RX_IFG2_SET(0x2);
> + tx_ifg = DEV_MAC_IFG_CFG_TX_IFG_SET(0x6);
> + break;
> + default:
> + dev_err(lan9645x->dev, "Unsupported speed on port %d: %d\n",
> + p->chip_port, speed);
> + return;
> + }
> +
> + p->speed = gspeed;
> + p->duplex = duplex;
> + fc_spd = lan9645x_speed_fc_enc(p->speed);
> +
> + if (phy_interface_num_ports(interface) == 4 ||
> + interface == PHY_INTERFACE_MODE_SGMII)
> + mode |= DEV_MAC_MODE_CFG_GIGA_MODE_ENA_SET(1);
PHY_INTERFACE_MODE_SGMII in the kernel means Cisco SGMII, and does not
include 1000BASE-X. As SGMII/BASE-X support is incomplete, I can't tell
whether the above is correct.
> +
> + 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(tx_ifg | rx_ifg1 | rx_ifg2,
It only makes sense to use seperate variables if you're going to be
placing them into the appropriate bitfield locations in a different
part of the code to that which is making decisions about their values.
However, I'm guessing that DEV_MAC_IFG_CFG_RX_IFG1_SET() et.al. are
preparing the field values.
So, I'd prefer to see a single variable called "mac_ifg_cfg" with
the values for each field accumulated into that:
switch ... {
case SPEED_1000:
mac_ifg_cfg = DEV_MAC_IFG_CFG_RX_IFG1_SET(0x1) |
DEV_MAC_IFG_CFG_RX_IFG2_SET(0x2) |
DEV_MAC_IFG_CFG_TX_IFG_SET(0x6);
}
...
lan_rmw(mac_ifg_cfg,
or separate variables with the creation of the register value at
the use site:
switch ... {
case SPEED_1000:
rx_ifg1 = 0x1;
rx_ifg2 = 0x2;
tx_ifg = 0x6;
}
...
lan_rmw(DEV_MAC_IFG_CFG_RX_IFG1_SET(rx_ifg1) |
DEV_MAC_IFG_CFG_RX_IFG2_SET(rx_ifg2) |
DEV_MAC_IFG_CFG_TX_IFG_SET(tx_ifg),
> + 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));
> +
> + 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));
> +
> + 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(DEV_MAC_HDX_CFG_SEED_LOAD_SET(0),
> + DEV_MAC_HDX_CFG_SEED_LOAD, lan9645x,
> + DEV_MAC_HDX_CFG(p->chip_port));
> +
> + /* Set PFC link speed and enable map */
> + lan_rmw(ANA_PFC_CFG_FC_LINK_SPEED_SET(fc_spd) |
> + ANA_PFC_CFG_RX_PFC_ENA_SET(0),
> + ANA_PFC_CFG_FC_LINK_SPEED |
> + ANA_PFC_CFG_RX_PFC_ENA,
> + lan9645x, ANA_PFC_CFG(p->chip_port));
> +
> + lan_rmw(DEV_PCS1G_CFG_PCS_ENA_SET(1),
> + DEV_PCS1G_CFG_PCS_ENA, lan9645x,
> + DEV_PCS1G_CFG(p->chip_port));
> +
> + lan_rmw(DEV_PCS1G_SD_CFG_SD_ENA_SET(0),
> + DEV_PCS1G_SD_CFG_SD_ENA,
> + lan9645x, DEV_PCS1G_SD_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));
> +
> + /* 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));
> +
> + /* 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));
> +
> + list_for_each_entry(path_delay, &p->path_delays, list) {
> + if (path_delay->speed == speed) {
> + lan_wr(path_delay->rx_delay + p->rx_delay,
> + lan9645x, SYS_PTP_RXDLY_CFG(p->chip_port));
> + lan_wr(path_delay->tx_delay,
> + lan9645x, SYS_PTP_TXDLY_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 == 0 ? 1 : 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));
> +
> + lan_rmw(AFI_PORT_CFG_FC_SKIP_TTI_INJ_SET(0) |
> + AFI_PORT_CFG_FRM_OUT_MAX_SET(16),
> + AFI_PORT_CFG_FC_SKIP_TTI_INJ |
> + AFI_PORT_CFG_FRM_OUT_MAX,
> + lan9645x, AFI_PORT_CFG(p->chip_port));
This looks overly complicated for link-up events. Please look to see
how much of this can be moved to the mac_prepare, mac_config and
mac_finish methods, which are supposed to be doing the bulk of
configuring the port for operating in the specified PHY interface mode.
mac_link_up is only supposed to configure for the negotiated parameters
such as speed, duplex, and pause and enable transmission/reception.
Conversely, mac_link_down should just stop the transmission/reception
of packets.
The PCS state fiddling concerns me greatly (although I do appreciate
you don't have that implemented fully yet) but given its presence in
mac_link_up, I think this will cause you future problems, especially
with inband support - the PCS link needs to remain active so it can
participate in negotiation, thereby allowing this method to be called.
You also don't want to be resetting the PCS on the mac_link_down()
method for the same reason.
> +}
> +
> +void lan9645x_phylink_port_down(struct lan9645x *lan9645x, int port)
> +{
> + struct lan9645x_port *p = lan9645x_to_port(lan9645x, port);
> + u32 val;
> +
> + /* 0.5: Disable any AFI */
> + lan_rmw(AFI_PORT_CFG_FC_SKIP_TTI_INJ_SET(1) |
> + AFI_PORT_CFG_FRM_OUT_MAX_SET(0),
> + AFI_PORT_CFG_FC_SKIP_TTI_INJ |
> + AFI_PORT_CFG_FRM_OUT_MAX,
> + lan9645x, AFI_PORT_CFG(p->chip_port));
> +
> + /* wait for reg afi_port_frm_out to become 0 for the port */
> + if (lan9645x_rd_poll_slow(lan9645x, AFI_PORT_FRM_OUT(p->chip_port),
> + val,
> + !AFI_PORT_FRM_OUT_FRM_OUT_CNT_GET(val)))
> + dev_err(lan9645x->dev, "AFI timeout chip port %u",
> + p->chip_port);
> +
> + /* 2: 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));
> +
> + /* 1: Reset the PCS Rx clock domain */
> + lan_rmw(DEV_CLOCK_CFG_PCS_RX_RST_SET(1),
> + DEV_CLOCK_CFG_PCS_RX_RST,
> + lan9645x, DEV_CLOCK_CFG(p->chip_port));
> +
> + p->speed = LAN9645X_SPEED_DISABLED;
> + p->duplex = DUPLEX_UNKNOWN;
> +
> + /* 3: 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));
> +
> + /* 4: 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));
> +
> + /* 5: Disable Flowcontrol */
> + lan_rmw(SYS_PAUSE_CFG_PAUSE_ENA_SET(0),
> + SYS_PAUSE_CFG_PAUSE_ENA,
> + lan9645x, SYS_PAUSE_CFG(p->chip_port));
> +
> + /* 5.1: Disable PFC */
> + lan_rmw(QSYS_SW_PORT_MODE_TX_PFC_ENA_SET(0),
> + QSYS_SW_PORT_MODE_TX_PFC_ENA,
> + lan9645x, QSYS_SW_PORT_MODE(p->chip_port));
> +
> + /* 6: Wait a worst case time 8ms (10K jumbo/10Mbit) */
> + usleep_range(8 * USEC_PER_MSEC, 9 * USEC_PER_MSEC);
> +
> + /* 7: 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));
> +
> + /* 8: 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));
> +
> + /* 9: 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));
> +
> + /* 10: 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", port);
> +
> + /* 11: 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));
> +
> + /* 12: 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));
> +
> + /* 13: Clear flushing */
> + lan_rmw(QSYS_SW_PORT_MODE_AGING_MODE_SET(1),
> + QSYS_SW_PORT_MODE_AGING_MODE,
> + lan9645x, QSYS_SW_PORT_MODE(p->chip_port));
> +}
> +
> +static void lan9645x_phylink_mac_link_down(struct lan9645x *lan9645x, int port,
> + unsigned int link_an_mode,
> + phy_interface_t interface)
> +{
> + struct lan9645x_port *p = lan9645x_to_port(lan9645x, port);
> +
> + lan9645x_phylink_port_down(lan9645x, port);
> +
> + /* 14: Take PCS out of reset */
> + lan_rmw(DEV_CLOCK_CFG_PCS_RX_RST_SET(0) |
> + DEV_CLOCK_CFG_PCS_TX_RST_SET(0),
> + DEV_CLOCK_CFG_PCS_RX_RST |
> + DEV_CLOCK_CFG_PCS_TX_RST,
> + lan9645x, DEV_CLOCK_CFG(p->chip_port));
> +}
> +
> +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 void
> +lan9645x_port_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);
> +
> + lan9645x_phylink_mac_config(p->lan9645x, p->chip_port, mode, state);
> +}
> +
> +static void lan9645x_port_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);
> +
> + lan9645x_phylink_mac_link_up(p->lan9645x, p->chip_port, link_an_mode,
> + interface, phydev, speed, duplex, tx_pause,
> + rx_pause);
> +}
> +
> +static void lan9645x_port_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);
> +
> + lan9645x_phylink_mac_link_down(p->lan9645x, p->chip_port, link_an_mode,
> + interface);
> +}
> +
> +static int lan9645x_port_phylink_mac_prepare(struct phylink_config *config,
> + unsigned int mode,
> + phy_interface_t iface)
> +{
> + struct lan9645x_port *p = lan9645x_phylink_config_to_port(config);
> +
> + return lan9645x_phylink_mac_prepare(p->lan9645x, p->chip_port, mode,
> + iface);
> +}
> +
> +const struct phylink_mac_ops lan9645x_phylink_mac_ops = {
> + .mac_config = lan9645x_port_phylink_mac_config,
> + .mac_link_up = lan9645x_port_phylink_mac_link_up,
> + .mac_link_down = lan9645x_port_phylink_mac_link_down,
> + .mac_prepare = lan9645x_port_phylink_mac_prepare,
Please order the functions, and the initialisers here in the same order
that they appear in struct phylink_mac_ops, which is:
mac_prepare (this will be called first)
mac_config (this will be called second)
mac_link_down
mac_link_up (this will be called last when bringing up the link)
The configuration functions (prepare, config, finish) are specifically
arranged in the order that they are called which helps make the code
more readable - and if you arrange the functions in the file in the
same order, then when one reads through the code, one reads the code in
the order it will be executed.
What you have is a bit like a book where the preface is oddly located
after the main body of the text!
> +};
> 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..038868ae0a32
> --- /dev/null
> +++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_port.c
> @@ -0,0 +1,289 @@
> +// SPDX-License-Identifier: GPL-2.0+
> +/* Copyright (C) 2026 Microchip Technology Inc.
> + */
> +
> +#include "lan9645x_main.h"
> +
> +int lan9645x_port_init(struct lan9645x *lan9645x, int port)
> +{
> + struct lan9645x_port *p;
> +
> + p = lan9645x_to_port(lan9645x, port);
> +
> + /* 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);
> +
> + lan9645x_phylink_port_down(lan9645x, port);
> +
> + if (phy_interface_num_ports(p->phy_mode) == 4)
> + lan_rmw(DEV_CLOCK_CFG_PCS_RX_RST_SET(0) |
> + DEV_CLOCK_CFG_PCS_TX_RST_SET(0),
> + DEV_CLOCK_CFG_PCS_RX_RST |
> + DEV_CLOCK_CFG_PCS_TX_RST,
> + lan9645x, DEV_CLOCK_CFG(p->chip_port));
Two things I don't like here:
1. the use of phy_mode rather than working out another way to handle
this.
2. I think the above is just a complex way of saying
if (p->phy_mode == PHY_INTERFACE_MODE_QSGMII)
since you don't support QUSGMII or 10G_QXGMII.
Thanks.
--
RMK's Patch system: https://www.armlinux.org.uk/developer/patches/
FTTP is here! 80Mbps down 10Mbps up. Decent connectivity at last!
On Wed, 2026-03-04 at 15:58 +0000, Russell King (Oracle) wrote:
> EXTERNAL EMAIL: Do not click links or open attachments unless you know the content is safe
>
> On Tue, Mar 03, 2026 at 01:22:30PM +0100, Jens Emil Schulz Østergaard wrote:
> > 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.
>
> I'm reviewing the phylink implementation only.
>
> > +static int lan9645x_rgmii_setup(struct lan9645x *lan9645x, int port, int speed,
> > + phy_interface_t mode)
> > +{
> > + bool tx_delay = false, rx_delay = false;
> > + u32 rx_idx, tx_idx;
> > + u8 tx_clk;
> > + int idx;
> > +
> > + /* Port 4 or 7 is RGMII_0 and port 8 is RGMII_1 */
> > + idx = port == 8 ? 1 : 0;
> > +
> > + if (!phy_interface_mode_is_rgmii(mode))
> > + return 0;
> > +
> > + 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),
>
> What exactly is this clock configuration doing?
>
This configures the outgoing txc clock. The encoding here is:
0: Disable TX clock generation
1: 125MHz for 1000Mbps operation
2: 25MHz for 100Mbps operation
3: 2.5MHz for 10Mbps operation
We take the RX and TX clock out of reset and configure this tx clock based on speed.
> > + HSIO_RGMII_CFG_RGMII_RX_RST |
> > + HSIO_RGMII_CFG_RGMII_TX_RST |
> > + HSIO_RGMII_CFG_TX_CLK_CFG,
> > + lan9645x, HSIO_RGMII_CFG(idx));
> > +
> > + /* We configure delays on the MAC side. When the PHY is not responsible
> > + * for delays, the MAC is, which is why RGMII_TXID results in
> > + * rx_delay=true.
> > + */
> > + if (mode == PHY_INTERFACE_MODE_RGMII ||
> > + mode == PHY_INTERFACE_MODE_RGMII_TXID)
> > + rx_delay = true;
> > +
> > + if (mode == PHY_INTERFACE_MODE_RGMII ||
> > + mode == PHY_INTERFACE_MODE_RGMII_RXID)
> > + tx_delay = true;
>
> PHY_INTERFACE_MODE_RGMII means that the delays are provided by the
> board layout, so this seems wrong to me. Please review the phylib
> documentation concerning RGMII delays. I'd like Andrew to comment on
> this too.
>
The idea with this was, if the PHY does not provide internal delay, then
we do it in the MAC. In Documentation/networking/phy.rst I saw this:
* PHY_INTERFACE_MODE_RGMII: the PHY is not responsible for inserting any
internal delay by itself, it assumes that either the Ethernet MAC (if capable)
or the PCB traces insert the correct 1.5-2ns delay
This code was intended for the Ethernet MAC case. When this delay configuration
is applied the ~2ns delay is added by the MAC. For instance, the tx_delay
case will add the delay to the txc clock configured above.
I understood the section below:
For cases where the PHY is not capable of providing this delay, but the
Ethernet MAC driver is capable of doing so, the correct phy_interface_t value
should be PHY_INTERFACE_MODE_RGMII, and the Ethernet MAC driver should be
configured correctly in order to provide the required transmit and/or receive
side delay from the perspective of the PHY device.
to mean that the mac driver is allowed to configure these delays based
on phy_interface_t. Because the mac delays can be configured assymmetrically
for rx and tx, the modes PHY_INTERFACE_MODE_RGMII_TXID and
PHY_INTERFACE_MODE_RGMII_RXID are also handled.
As Vladimir mentions, there are other drivers who configure this using
rx-internal-delay-ps
tx-internal-delay-ps
Which is also done by
drivers/net/ethernet/microchip/sparx5/lan969x/lan969x_rgmii.c
However, the DLL on lan969x is different. It can configure the delay
amount with CLK_SEL. On lan9645x it is simply ~2ns, on or off, in rx or tx
direction, so accepting a delay amount seemed out of place.
But I can change this to use {rx,tx}-internal-delay-ps, and enable the
delay base on these being 0 or not, if you think this is the better
way to do it.
> > +
> > + /* Setup DLL configuration. Register layout:
> > + * 0: RGMII_0_RX
> > + * 1: RGMII_0_TX
> > + * 2: RGMII_1_RX
> > + * 3: RGMII_1_TX
> > + * ...
> > + * (N<<1) RGMII_N_RX,
> > + * (N<<1)+1: RGMII_N_TX,
> > + */
> > + rx_idx = idx << 1;
> > + tx_idx = rx_idx + 1;
>
> Nit: I think the shift is too clever here. It's clearly:
>
> rx_idx = idx * 2;
> tx_idx = idx * 2 + 1;
>
I will change this.
> > +
> > + /* 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(rx_delay) |
> > + HSIO_DLL_CFG_DELAY_ENA_SET(rx_delay),
> > + HSIO_DLL_CFG_DLL_CLK_ENA |
> > + HSIO_DLL_CFG_DLL_RST |
> > + HSIO_DLL_CFG_DLL_ENA |
> > + HSIO_DLL_CFG_DELAY_ENA,
> > + 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(tx_delay) |
> > + HSIO_DLL_CFG_DELAY_ENA_SET(tx_delay),
> > + HSIO_DLL_CFG_DLL_CLK_ENA |
> > + HSIO_DLL_CFG_DLL_RST |
> > + HSIO_DLL_CFG_DLL_ENA |
> > + HSIO_DLL_CFG_DELAY_ENA,
> > + lan9645x, HSIO_DLL_CFG(tx_idx));
> > +
> > + return 0;
> > +}
> > +
> > +static void lan9645x_phylink_mac_config(struct lan9645x *lan9645x, int port,
> > + unsigned int mode,
> > + const struct phylink_link_state *state)
> > +{
> > + if (phy_interface_mode_is_rgmii(state->interface))
> > + lan9645x_rgmii_setup(lan9645x, port, state->speed,
> > + state->interface);
>
> state->speed will always be SPEED_UNKNOWN here. Please don't use
> state->speed here, it's even documented that drivers should not access
> this member in their mac_config method().
>
I will change this in the next version.
> > +}
> > +
> > +static int lan9645x_phylink_mac_prepare(struct lan9645x *lan9645x, int port,
> > + unsigned int mode,
> > + phy_interface_t iface)
> > +{
> > + switch (port) {
> > + case 0 ... 3:
> > + lan_rmw(HSIO_HW_CFG_GMII_ENA_SET(BIT(port)),
> > + HSIO_HW_CFG_GMII_ENA_SET(BIT(port)), lan9645x,
> > + HSIO_HW_CFG);
> > + break;
> > + case 4:
> > + lan_rmw(HSIO_HW_CFG_GMII_ENA_SET(BIT(port)),
> > + HSIO_HW_CFG_GMII_ENA_SET(BIT(port)), lan9645x,
> > + HSIO_HW_CFG);
> > +
> > + if (phy_interface_mode_is_rgmii(iface))
> > + lan_rmw(HSIO_HW_CFG_RGMII_0_CFG_SET(1),
> > + HSIO_HW_CFG_RGMII_0_CFG,
> > + lan9645x, HSIO_HW_CFG);
> > +
> > + break;
> > + case 7 ... 8:
> > + lan_rmw(HSIO_HW_CFG_GMII_ENA_SET(BIT(port)),
> > + HSIO_HW_CFG_GMII_ENA_SET(BIT(port)), lan9645x,
> > + HSIO_HW_CFG);
> > + break;
> > + default:
> > + /* Ports 5-6 are SerDes-only and need PCS support (not yet
> > + * implemented). They are excluded from phylink_get_caps.
>
> ... which means phylink_create() will fail, and this function will never
> be called for those ports.
>
> > + */
> > + return -EINVAL;
> > + }
>
> As each of the above cases uses the exactly the same preamble, I'm
> wondering whether something like:
>
> u32 mask;
>
> if (port == 5 || port == 6 || port > 8)
> return -EINVAL;
>
> mask = HSIO_HW_CFG_GMII_ENA_SET(BIT(port));
> lan_rmw(mask, mask, lan9645x, HSIO_HW_CFG);
>
>
> if (port == 4 && phy_interface_mode_is_rgmii(iface))
> lan_rmw(HSIO_HW_CFG_RGMII_0_CFG_SET(1),
> HSIO_HW_CFG_RGMII_0_CFG,
> lan9645x, HSIO_HW_CFG);
>
> would be neater.
>
Yes this is much simpler. With the serdes ports and qsgmii support,
the cases have a bit more variability, but I will use change this
for now.
> > +
> > + return 0;
> > +}
> > +
> > +static int lan9645x_port_is_cuphy(struct lan9645x *lan9645x, int port,
> > + phy_interface_t interface)
> > +{
> > + return port >= 0 && port <= 4 && interface == PHY_INTERFACE_MODE_GMII;
> > +}
> > +
> > +static void lan9645x_phylink_mac_link_up(struct lan9645x *lan9645x, int port,
> > + unsigned int link_an_mode,
> > + phy_interface_t interface,
> > + struct phy_device *phydev, int speed,
> > + int duplex, bool tx_pause,
> > + bool rx_pause)
> > +{
> > + struct lan9645x_port *p = lan9645x_to_port(lan9645x, port);
> > + int rx_ifg1, rx_ifg2, tx_ifg, gtx_clk = 0;
> > + struct lan9645x_path_delay *path_delay;
> > + int gspeed = LAN9645X_SPEED_DISABLED;
> > + int mode = 0;
> > + int fc_spd;
> > +
> > + /* Configure speed for RGMII modules. */
> > + if (phy_interface_mode_is_rgmii(interface))
> > + lan9645x_rgmii_setup(lan9645x, port, speed, interface);
>
> You shouldn't need to do a full setup of the RGMII state here - all
> that should be done is to adjust the transmit clock speed. I would
> prefer to see lan9645x_rgmii_setup() split into a "we're configuring
> the bulk of RGMII state" function which is called from mac_config(),
> and a "we have link up, do the minimal configuration that is dependent
> on the resulting link speed" in mac_link_up().
>
I will try to split up the RGMII configuration like this.
> > +
> > + if (duplex == DUPLEX_FULL) {
> > + mode |= DEV_MAC_MODE_CFG_FDX_ENA_SET(1);
> > + rx_ifg2 = DEV_MAC_IFG_CFG_RX_IFG2_SET(0x2);
> > + tx_ifg = DEV_MAC_IFG_CFG_TX_IFG_SET(0x5);
> > +
> > + } else {
> > + rx_ifg2 = DEV_MAC_IFG_CFG_RX_IFG2_SET(0x2);
> > + tx_ifg = DEV_MAC_IFG_CFG_TX_IFG_SET(0x6);
> > + }
> > +
> > + switch (speed) {
> > + case SPEED_10:
> > + rx_ifg1 = DEV_MAC_IFG_CFG_RX_IFG1_SET(0x2);
> > + gspeed = LAN9645X_SPEED_10;
> > + break;
> > + case SPEED_100:
> > + rx_ifg1 = DEV_MAC_IFG_CFG_RX_IFG1_SET(0x1);
> > + gspeed = LAN9645X_SPEED_100;
> > + break;
> > + case SPEED_1000:
> > + gspeed = LAN9645X_SPEED_1000;
> > + mode |= DEV_MAC_MODE_CFG_GIGA_MODE_ENA_SET(1);
> > + mode |= DEV_MAC_MODE_CFG_FDX_ENA_SET(1);
> > + rx_ifg1 = DEV_MAC_IFG_CFG_RX_IFG1_SET(0x1);
> > + rx_ifg2 = DEV_MAC_IFG_CFG_RX_IFG2_SET(0x2);
> > + tx_ifg = DEV_MAC_IFG_CFG_TX_IFG_SET(0x6);
> > + gtx_clk = 1;
> > + break;
> > + case SPEED_2500:
> > + gspeed = LAN9645X_SPEED_2500;
> > + mode |= DEV_MAC_MODE_CFG_GIGA_MODE_ENA_SET(1);
> > + mode |= DEV_MAC_MODE_CFG_FDX_ENA_SET(1);
> > + rx_ifg1 = DEV_MAC_IFG_CFG_RX_IFG1_SET(0x1);
> > + rx_ifg2 = DEV_MAC_IFG_CFG_RX_IFG2_SET(0x2);
> > + tx_ifg = DEV_MAC_IFG_CFG_TX_IFG_SET(0x6);
> > + break;
> > + default:
> > + dev_err(lan9645x->dev, "Unsupported speed on port %d: %d\n",
> > + p->chip_port, speed);
> > + return;
> > + }
> > +
> > + p->speed = gspeed;
> > + p->duplex = duplex;
> > + fc_spd = lan9645x_speed_fc_enc(p->speed);
> > +
> > + if (phy_interface_num_ports(interface) == 4 ||
> > + interface == PHY_INTERFACE_MODE_SGMII)
> > + mode |= DEV_MAC_MODE_CFG_GIGA_MODE_ENA_SET(1);
>
> PHY_INTERFACE_MODE_SGMII in the kernel means Cisco SGMII, and does not
> include 1000BASE-X. As SGMII/BASE-X support is incomplete, I can't tell
> whether the above is correct.
>
Yes this is for serdes ports, I will remove this.
> > +
> > + 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(tx_ifg | rx_ifg1 | rx_ifg2,
>
> It only makes sense to use seperate variables if you're going to be
> placing them into the appropriate bitfield locations in a different
> part of the code to that which is making decisions about their values.
> However, I'm guessing that DEV_MAC_IFG_CFG_RX_IFG1_SET() et.al. are
> preparing the field values.
>
> So, I'd prefer to see a single variable called "mac_ifg_cfg" with
> the values for each field accumulated into that:
>
> switch ... {
> case SPEED_1000:
> mac_ifg_cfg = DEV_MAC_IFG_CFG_RX_IFG1_SET(0x1) |
> DEV_MAC_IFG_CFG_RX_IFG2_SET(0x2) |
> DEV_MAC_IFG_CFG_TX_IFG_SET(0x6);
> }
> ...
> lan_rmw(mac_ifg_cfg,
>
> or separate variables with the creation of the register value at
> the use site:
>
> switch ... {
> case SPEED_1000:
> rx_ifg1 = 0x1;
> rx_ifg2 = 0x2;
> tx_ifg = 0x6;
> }
> ...
> lan_rmw(DEV_MAC_IFG_CFG_RX_IFG1_SET(rx_ifg1) |
> DEV_MAC_IFG_CFG_RX_IFG2_SET(rx_ifg2) |
> DEV_MAC_IFG_CFG_TX_IFG_SET(tx_ifg),
>
I will update this for the next version.
> > + 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));
> > +
> > + 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));
> > +
> > + 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(DEV_MAC_HDX_CFG_SEED_LOAD_SET(0),
> > + DEV_MAC_HDX_CFG_SEED_LOAD, lan9645x,
> > + DEV_MAC_HDX_CFG(p->chip_port));
> > +
> > + /* Set PFC link speed and enable map */
> > + lan_rmw(ANA_PFC_CFG_FC_LINK_SPEED_SET(fc_spd) |
> > + ANA_PFC_CFG_RX_PFC_ENA_SET(0),
> > + ANA_PFC_CFG_FC_LINK_SPEED |
> > + ANA_PFC_CFG_RX_PFC_ENA,
> > + lan9645x, ANA_PFC_CFG(p->chip_port));
> > +
> > + lan_rmw(DEV_PCS1G_CFG_PCS_ENA_SET(1),
> > + DEV_PCS1G_CFG_PCS_ENA, lan9645x,
> > + DEV_PCS1G_CFG(p->chip_port));
> > +
> > + lan_rmw(DEV_PCS1G_SD_CFG_SD_ENA_SET(0),
> > + DEV_PCS1G_SD_CFG_SD_ENA,
> > + lan9645x, DEV_PCS1G_SD_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));
> > +
> > + /* 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));
> > +
> > + /* 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));
> > +
> > + list_for_each_entry(path_delay, &p->path_delays, list) {
> > + if (path_delay->speed == speed) {
> > + lan_wr(path_delay->rx_delay + p->rx_delay,
> > + lan9645x, SYS_PTP_RXDLY_CFG(p->chip_port));
> > + lan_wr(path_delay->tx_delay,
> > + lan9645x, SYS_PTP_TXDLY_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 == 0 ? 1 : 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));
> > +
> > + lan_rmw(AFI_PORT_CFG_FC_SKIP_TTI_INJ_SET(0) |
> > + AFI_PORT_CFG_FRM_OUT_MAX_SET(16),
> > + AFI_PORT_CFG_FC_SKIP_TTI_INJ |
> > + AFI_PORT_CFG_FRM_OUT_MAX,
> > + lan9645x, AFI_PORT_CFG(p->chip_port));
>
> This looks overly complicated for link-up events. Please look to see
> how much of this can be moved to the mac_prepare, mac_config and
> mac_finish methods, which are supposed to be doing the bulk of
> configuring the port for operating in the specified PHY interface mode.
>
> mac_link_up is only supposed to configure for the negotiated parameters
> such as speed, duplex, and pause and enable transmission/reception.
>
> Conversely, mac_link_down should just stop the transmission/reception
> of packets.
>
> The PCS state fiddling concerns me greatly (although I do appreciate
> you don't have that implemented fully yet) but given its presence in
> mac_link_up, I think this will cause you future problems, especially
> with inband support - the PCS link needs to remain active so it can
> participate in negotiation, thereby allowing this method to be called.
>
> You also don't want to be resetting the PCS on the mac_link_down()
> method for the same reason.
>
I agree it is complicated. This is taken from the sequence in lan966x.
Back then it took some effort, and help from design, to get it right.
So I did not have the courage to change it, but I will try to
rearrange this to be more in the spirit of phylink.
> > +}
> > +
> > +void lan9645x_phylink_port_down(struct lan9645x *lan9645x, int port)
> > +{
> > + struct lan9645x_port *p = lan9645x_to_port(lan9645x, port);
> > + u32 val;
> > +
> > + /* 0.5: Disable any AFI */
> > + lan_rmw(AFI_PORT_CFG_FC_SKIP_TTI_INJ_SET(1) |
> > + AFI_PORT_CFG_FRM_OUT_MAX_SET(0),
> > + AFI_PORT_CFG_FC_SKIP_TTI_INJ |
> > + AFI_PORT_CFG_FRM_OUT_MAX,
> > + lan9645x, AFI_PORT_CFG(p->chip_port));
> > +
> > + /* wait for reg afi_port_frm_out to become 0 for the port */
> > + if (lan9645x_rd_poll_slow(lan9645x, AFI_PORT_FRM_OUT(p->chip_port),
> > + val,
> > + !AFI_PORT_FRM_OUT_FRM_OUT_CNT_GET(val)))
> > + dev_err(lan9645x->dev, "AFI timeout chip port %u",
> > + p->chip_port);
> > +
> > + /* 2: 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));
> > +
> > + /* 1: Reset the PCS Rx clock domain */
> > + lan_rmw(DEV_CLOCK_CFG_PCS_RX_RST_SET(1),
> > + DEV_CLOCK_CFG_PCS_RX_RST,
> > + lan9645x, DEV_CLOCK_CFG(p->chip_port));
> > +
> > + p->speed = LAN9645X_SPEED_DISABLED;
> > + p->duplex = DUPLEX_UNKNOWN;
> > +
> > + /* 3: 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));
> > +
> > + /* 4: 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));
> > +
> > + /* 5: Disable Flowcontrol */
> > + lan_rmw(SYS_PAUSE_CFG_PAUSE_ENA_SET(0),
> > + SYS_PAUSE_CFG_PAUSE_ENA,
> > + lan9645x, SYS_PAUSE_CFG(p->chip_port));
> > +
> > + /* 5.1: Disable PFC */
> > + lan_rmw(QSYS_SW_PORT_MODE_TX_PFC_ENA_SET(0),
> > + QSYS_SW_PORT_MODE_TX_PFC_ENA,
> > + lan9645x, QSYS_SW_PORT_MODE(p->chip_port));
> > +
> > + /* 6: Wait a worst case time 8ms (10K jumbo/10Mbit) */
> > + usleep_range(8 * USEC_PER_MSEC, 9 * USEC_PER_MSEC);
> > +
> > + /* 7: 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));
> > +
> > + /* 8: 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));
> > +
> > + /* 9: 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));
> > +
> > + /* 10: 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", port);
> > +
> > + /* 11: 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));
> > +
> > + /* 12: 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));
> > +
> > + /* 13: Clear flushing */
> > + lan_rmw(QSYS_SW_PORT_MODE_AGING_MODE_SET(1),
> > + QSYS_SW_PORT_MODE_AGING_MODE,
> > + lan9645x, QSYS_SW_PORT_MODE(p->chip_port));
> > +}
> > +
> > +static void lan9645x_phylink_mac_link_down(struct lan9645x *lan9645x, int port,
> > + unsigned int link_an_mode,
> > + phy_interface_t interface)
> > +{
> > + struct lan9645x_port *p = lan9645x_to_port(lan9645x, port);
> > +
> > + lan9645x_phylink_port_down(lan9645x, port);
> > +
> > + /* 14: Take PCS out of reset */
> > + lan_rmw(DEV_CLOCK_CFG_PCS_RX_RST_SET(0) |
> > + DEV_CLOCK_CFG_PCS_TX_RST_SET(0),
> > + DEV_CLOCK_CFG_PCS_RX_RST |
> > + DEV_CLOCK_CFG_PCS_TX_RST,
> > + lan9645x, DEV_CLOCK_CFG(p->chip_port));
> > +}
> > +
> > +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 void
> > +lan9645x_port_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);
> > +
> > + lan9645x_phylink_mac_config(p->lan9645x, p->chip_port, mode, state);
> > +}
> > +
> > +static void lan9645x_port_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);
> > +
> > + lan9645x_phylink_mac_link_up(p->lan9645x, p->chip_port, link_an_mode,
> > + interface, phydev, speed, duplex, tx_pause,
> > + rx_pause);
> > +}
> > +
> > +static void lan9645x_port_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);
> > +
> > + lan9645x_phylink_mac_link_down(p->lan9645x, p->chip_port, link_an_mode,
> > + interface);
> > +}
> > +
> > +static int lan9645x_port_phylink_mac_prepare(struct phylink_config *config,
> > + unsigned int mode,
> > + phy_interface_t iface)
> > +{
> > + struct lan9645x_port *p = lan9645x_phylink_config_to_port(config);
> > +
> > + return lan9645x_phylink_mac_prepare(p->lan9645x, p->chip_port, mode,
> > + iface);
> > +}
> > +
> > +const struct phylink_mac_ops lan9645x_phylink_mac_ops = {
> > + .mac_config = lan9645x_port_phylink_mac_config,
> > + .mac_link_up = lan9645x_port_phylink_mac_link_up,
> > + .mac_link_down = lan9645x_port_phylink_mac_link_down,
> > + .mac_prepare = lan9645x_port_phylink_mac_prepare,
>
> Please order the functions, and the initialisers here in the same order
> that they appear in struct phylink_mac_ops, which is:
>
> mac_prepare (this will be called first)
> mac_config (this will be called second)
>
> mac_link_down
> mac_link_up (this will be called last when bringing up the link)
>
> The configuration functions (prepare, config, finish) are specifically
> arranged in the order that they are called which helps make the code
> more readable - and if you arrange the functions in the file in the
> same order, then when one reads through the code, one reads the code in
> the order it will be executed.
>
> What you have is a bit like a book where the preface is oddly located
> after the main body of the text!
>
Thank you, I will shuffle this around.
> > +};
> > 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..038868ae0a32
> > --- /dev/null
> > +++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_port.c
> > @@ -0,0 +1,289 @@
> > +// SPDX-License-Identifier: GPL-2.0+
> > +/* Copyright (C) 2026 Microchip Technology Inc.
> > + */
> > +
> > +#include "lan9645x_main.h"
> > +
> > +int lan9645x_port_init(struct lan9645x *lan9645x, int port)
> > +{
> > + struct lan9645x_port *p;
> > +
> > + p = lan9645x_to_port(lan9645x, port);
> > +
> > + /* 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);
> > +
> > + lan9645x_phylink_port_down(lan9645x, port);
> > +
> > + if (phy_interface_num_ports(p->phy_mode) == 4)
> > + lan_rmw(DEV_CLOCK_CFG_PCS_RX_RST_SET(0) |
> > + DEV_CLOCK_CFG_PCS_TX_RST_SET(0),
> > + DEV_CLOCK_CFG_PCS_RX_RST |
> > + DEV_CLOCK_CFG_PCS_TX_RST,
> > + lan9645x, DEV_CLOCK_CFG(p->chip_port));
>
> Two things I don't like here:
>
> 1. the use of phy_mode rather than working out another way to handle
> this.
>
> 2. I think the above is just a complex way of saying
> if (p->phy_mode == PHY_INTERFACE_MODE_QSGMII)
>
> since you don't support QUSGMII or 10G_QXGMII.
>
> Thanks.
>
Indeed, that is for QSGMII. I will remove it now, and use
PHY_INTERFACE_MODE_QSGMII directly when the serdes ports are implemented.
> --
> RMK's Patch system: https://www.armlinux.org.uk/developer/patches/
> FTTP is here! 80Mbps down 10Mbps up. Decent connectivity at last!
Thank you for the comments,
Emil
> > > + /* We configure delays on the MAC side. When the PHY is not responsible
> > > + * for delays, the MAC is, which is why RGMII_TXID results in
> > > + * rx_delay=true.
> > > + */
> > > + if (mode == PHY_INTERFACE_MODE_RGMII ||
> > > + mode == PHY_INTERFACE_MODE_RGMII_TXID)
> > > + rx_delay = true;
> > > +
> > > + if (mode == PHY_INTERFACE_MODE_RGMII ||
> > > + mode == PHY_INTERFACE_MODE_RGMII_RXID)
> > > + tx_delay = true;
> >
> > PHY_INTERFACE_MODE_RGMII means that the delays are provided by the
> > board layout, so this seems wrong to me. Please review the phylib
> > documentation concerning RGMII delays. I'd like Andrew to comment on
> > this too.
> >
>
> The idea with this was, if the PHY does not provide internal delay, then
> we do it in the MAC.
That is not really what we want. The default in Linux is that the PHY
adds the delays. In general, MAC drivers read the phy-mode, and pass
it directly to the PHY. The MAC does not add delays. There are a small
number of boards which don't respect this. In the past, there was been
claims that an RDK used a PHY which was not capable of adding the
needed delays, so the MAC had to. In practice, all the PHY drivers in
linux which supporting RGMII support all four configurations of
delays.
So, the best thing to do is hard code the delays in the MAC to 0, and
pass phy-mode to the PHY.
Now, this is a switch, so there is one complexity. Some switches make
use of an RGMII port to connect to the host. You then have the host
RGMII MAC connected to the switch RGMII MAC. Delays have to be added
somewhere. Since the switch MAC is playing the role of the PHY, in the
eyes of the host, we allow such a port to add delays.
However, this is a multi-giga switch, with high speed SERDES ports. My
guess is, a SERDES port is used to connect to the host, not RGMII. So
i would not implement this, until somebody reports having a board with
RGMII connecting to the host. KISS.
Andrew
On Thu, Mar 05, 2026 at 03:58:16PM +0100, Andrew Lunn wrote:
> > > > + /* We configure delays on the MAC side. When the PHY is not responsible
> > > > + * for delays, the MAC is, which is why RGMII_TXID results in
> > > > + * rx_delay=true.
> > > > + */
> > > > + if (mode == PHY_INTERFACE_MODE_RGMII ||
> > > > + mode == PHY_INTERFACE_MODE_RGMII_TXID)
> > > > + rx_delay = true;
> > > > +
> > > > + if (mode == PHY_INTERFACE_MODE_RGMII ||
> > > > + mode == PHY_INTERFACE_MODE_RGMII_RXID)
> > > > + tx_delay = true;
> > >
> > > PHY_INTERFACE_MODE_RGMII means that the delays are provided by the
> > > board layout, so this seems wrong to me. Please review the phylib
> > > documentation concerning RGMII delays. I'd like Andrew to comment on
> > > this too.
> > >
> >
> > The idea with this was, if the PHY does not provide internal delay, then
> > we do it in the MAC.
>
> That is not really what we want. The default in Linux is that the PHY
> adds the delays. In general, MAC drivers read the phy-mode, and pass
> it directly to the PHY. The MAC does not add delays. There are a small
> number of boards which don't respect this. In the past, there was been
> claims that an RDK used a PHY which was not capable of adding the
> needed delays, so the MAC had to. In practice, all the PHY drivers in
> linux which supporting RGMII support all four configurations of
> delays.
Unfortunately, I've discovered another case - dwmac-qcom-ethqos::
ethqos_rgmii_macro_init():
/* Determine if the PHY adds a 2 ns TX delay or the MAC handles it */
if (ethqos->phy_mode == PHY_INTERFACE_MODE_RGMII_ID ||
ethqos->phy_mode == PHY_INTERFACE_MODE_RGMII_TXID)
phase_shift = 0;
else
phase_shift = RGMII_CONFIG2_TX_CLK_PHASE_SHIFT_EN;
This code was added by 164a9ebe9742 ("net: stmmac: dwmac-qcom-ethqos:
Respect phy-mode and TX delay") which doesn't have any acks from
phylib folk. However, before this commit, the driver always added the
transmit delay.
Sadly, too late to do anything about this now. :(
Should a regexp be added to MAINTAINERS phylib entry to detect patches
which contain PHY_INTERFACE_MODE_RGMII?
--
RMK's Patch system: https://www.armlinux.org.uk/developer/patches/
FTTP is here! 80Mbps down 10Mbps up. Decent connectivity at last!
On Fri, 6 Mar 2026 14:22:14 +0000 Russell King (Oracle) wrote: > Should a regexp be added to MAINTAINERS phylib entry to detect patches > which contain PHY_INTERFACE_MODE_RGMII? Yes, please!
On Thu, Mar 05, 2026 at 03:58:16PM +0100, Andrew Lunn wrote: > So, the best thing to do is hard code the delays in the MAC to 0, and > pass phy-mode to the PHY. To extend that line of reasoning one step further. If somebody does end up needing to configure delays in the MAC and observes that phy-mode is acted upon only by the PHY layer, and that irrespective of phy-mode the MAC delays are always 0, then they will turn to the rx-internal-delay-ps and tx-internal-delay-ps properties added to the MAC OF node. Using these has the benefit that it does not collide with the phy-mode meaning in any way, so is not a compatibility problem. > Now, this is a switch, so there is one complexity. Some switches make > use of an RGMII port to connect to the host. You then have the host > RGMII MAC connected to the switch RGMII MAC. Delays have to be added > somewhere. Since the switch MAC is playing the role of the PHY, in the > eyes of the host, we allow such a port to add delays. Well, since rx-internal-delay-ps and tx-internal-delay-ps in the MAC OF node would handle that case as well, there isn't any need really to complicate the implementation with any custom phy-mode interpretation there either. Introducing the concept of a "PHY role" for RGMII delays in CPU ports is rather unnecessary given the tools we have today.
Hi, Am Donnerstag, 5. März 2026, 15:58:16 CET schrieb Andrew Lunn: > > > > + /* We configure delays on the MAC side. When the PHY is not responsible > > > > + * for delays, the MAC is, which is why RGMII_TXID results in > > > > + * rx_delay=true. > > > > + */ > > > > + if (mode == PHY_INTERFACE_MODE_RGMII || > > > > + mode == PHY_INTERFACE_MODE_RGMII_TXID) > > > > + rx_delay = true; > > > > + > > > > + if (mode == PHY_INTERFACE_MODE_RGMII || > > > > + mode == PHY_INTERFACE_MODE_RGMII_RXID) > > > > + tx_delay = true; > > > > > > PHY_INTERFACE_MODE_RGMII means that the delays are provided by the > > > board layout, so this seems wrong to me. Please review the phylib > > > documentation concerning RGMII delays. I'd like Andrew to comment on > > > this too. > > > > > > > The idea with this was, if the PHY does not provide internal delay, then > > we do it in the MAC. > > That is not really what we want. The default in Linux is that the PHY > adds the delays. In general, MAC drivers read the phy-mode, and pass > it directly to the PHY. The MAC does not add delays. There are a small > number of boards which don't respect this. In the past, there was been > claims that an RDK used a PHY which was not capable of adding the > needed delays, so the MAC had to. In practice, all the PHY drivers in > linux which supporting RGMII support all four configurations of > delays. > > So, the best thing to do is hard code the delays in the MAC to 0, and > pass phy-mode to the PHY. > > Now, this is a switch, so there is one complexity. Some switches make > use of an RGMII port to connect to the host. You then have the host > RGMII MAC connected to the switch RGMII MAC. Delays have to be added > somewhere. Since the switch MAC is playing the role of the PHY, in the > eyes of the host, we allow such a port to add delays. > > However, this is a multi-giga switch, with high speed SERDES ports. My > guess is, a SERDES port is used to connect to the host, not RGMII. So > i would not implement this, until somebody reports having a board with > RGMII connecting to the host. KISS. We intend to do exactly that with this switch, just because the host doesn't support SGMII, just RMII/RGMII. Best regards, Alexander -- TQ-Systems GmbH | Mühlstraße 2, Gut Delling | 82229 Seefeld, Germany Amtsgericht München, HRB 105018 Geschäftsführer: Detlef Schneider, Rüdiger Stahl, Stefan Schneider http://www.tq-group.com/
> > However, this is a multi-giga switch, with high speed SERDES ports. My
> > guess is, a SERDES port is used to connect to the host, not RGMII. So
> > i would not implement this, until somebody reports having a board with
> > RGMII connecting to the host. KISS.
>
> We intend to do exactly that with this switch, just because the host
> doesn't support SGMII, just RMII/RGMII.
:-)
Then i suggest using {rx|tx}-internal-delay-ps as Vladimir suggested.
Andrew
On Thu, 2026-03-05 at 18:37 +0100, Andrew Lunn wrote:
> EXTERNAL EMAIL: Do not click links or open attachments unless you know the content is safe
>
> > > However, this is a multi-giga switch, with high speed SERDES ports. My
> > > guess is, a SERDES port is used to connect to the host, not RGMII. So
> > > i would not implement this, until somebody reports having a board with
> > > RGMII connecting to the host. KISS.
> >
> > We intend to do exactly that with this switch, just because the host
> > doesn't support SGMII, just RMII/RGMII.
>
> :-)
>
> Then i suggest using {rx|tx}-internal-delay-ps as Vladimir suggested.
>
> Andrew
OK, I will use {rx|tx}-internal-delay-ps instead. I can add that the
current EVB board for lan9645x also uses RGMI mac2mac to the host, so there
are already users who would appreciate this. This is the LAN96459 EDS2
Daughter Card with part number EV14H52A.
Thanks,
Emil
On Fri, Mar 06, 2026 at 04:03:36PM +0100, Jens Emil Schulz Ostergaard wrote:
> On Thu, 2026-03-05 at 18:37 +0100, Andrew Lunn wrote:
> > EXTERNAL EMAIL: Do not click links or open attachments unless you know the content is safe
> >
> > > > However, this is a multi-giga switch, with high speed SERDES ports. My
> > > > guess is, a SERDES port is used to connect to the host, not RGMII. So
> > > > i would not implement this, until somebody reports having a board with
> > > > RGMII connecting to the host. KISS.
> > >
> > > We intend to do exactly that with this switch, just because the host
> > > doesn't support SGMII, just RMII/RGMII.
> >
> > :-)
> >
> > Then i suggest using {rx|tx}-internal-delay-ps as Vladimir suggested.
> >
> > Andrew
>
> OK, I will use {rx|tx}-internal-delay-ps instead. I can add that the
> current EVB board for lan9645x also uses RGMI mac2mac to the host, so there
> are already users who would appreciate this. This is the LAN96459 EDS2
> Daughter Card with part number EV14H52A.
O.K. Do you plan to add a DT file for this? Since it is a daughter
card, is it a DT overlay? I see there is an I2C EEPROM, used for board
identification. So that tells you which overlay to load?
Andrew
On Fri, 2026-03-06 at 17:33 +0100, Andrew Lunn wrote:
> EXTERNAL EMAIL: Do not click links or open attachments unless you know the content is safe
>
> On Fri, Mar 06, 2026 at 04:03:36PM +0100, Jens Emil Schulz Ostergaard wrote:
> > On Thu, 2026-03-05 at 18:37 +0100, Andrew Lunn wrote:
> > > EXTERNAL EMAIL: Do not click links or open attachments unless you know the content is safe
> > >
> > > > > However, this is a multi-giga switch, with high speed SERDES ports. My
> > > > > guess is, a SERDES port is used to connect to the host, not RGMII. So
> > > > > i would not implement this, until somebody reports having a board with
> > > > > RGMII connecting to the host. KISS.
> > > >
> > > > We intend to do exactly that with this switch, just because the host
> > > > doesn't support SGMII, just RMII/RGMII.
> > >
> > > :-)
> > >
> > > Then i suggest using {rx|tx}-internal-delay-ps as Vladimir suggested.
> > >
> > > Andrew
> >
> > OK, I will use {rx|tx}-internal-delay-ps instead. I can add that the
> > current EVB board for lan9645x also uses RGMI mac2mac to the host, so there
> > are already users who would appreciate this. This is the LAN96459 EDS2
> > Daughter Card with part number EV14H52A.
>
> O.K. Do you plan to add a DT file for this? Since it is a daughter
> card, is it a DT overlay? I see there is an I2C EEPROM, used for board
> identification. So that tells you which overlay to load?
>
> Andrew
Yes, we would like to add a DT, and it would be an as an overlay. I believe
the EEPROM was intended for the firmware in unmanaged mode. We do not use
it for managed mode at the moment.
Thanks,
Emil
On Tue, Mar 03, 2026 at 01:22:30PM +0100, Jens Emil Schulz Østergaard wrote:
> 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.
>
> IPv6 is disabled on the conduit. When enabled, the RFC 4861 frames are
> dispatched directly on the conduit bypassing the tag driver. The switch
> parses these frames as if they have an IFH prepended, leading to a
> garbage in garbage out situation. Therefore, IPv6 on the conduit is not
> a sensible configuration.
>
> Reviewed-by: Steen Hegelund <Steen.Hegelund@microchip.com>
> Signed-off-by: Jens Emil Schulz Østergaard <jensemil.schulzostergaard@microchip.com>
> ---
> drivers/net/dsa/microchip/Makefile | 1 +
> drivers/net/dsa/microchip/lan9645x/Kconfig | 12 +
> drivers/net/dsa/microchip/lan9645x/Makefile | 7 +
> drivers/net/dsa/microchip/lan9645x/lan9645x_main.c | 435 +++++++++++++++++
> drivers/net/dsa/microchip/lan9645x/lan9645x_main.h | 396 +++++++++++++++
> drivers/net/dsa/microchip/lan9645x/lan9645x_npi.c | 99 ++++
> .../net/dsa/microchip/lan9645x/lan9645x_phylink.c | 537 +++++++++++++++++++++
> drivers/net/dsa/microchip/lan9645x/lan9645x_port.c | 289 +++++++++++
> drivers/net/ethernet/microchip/Kconfig | 1 +
> 9 files changed, 1777 insertions(+)
>
> 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..8cbac1f9875d
> --- /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
> + depends on NET_VENDOR_MICROCHIP
> + select NET_DSA_TAG_LAN9645X
> + 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..eea1edc5c0e3
> --- /dev/null
> +++ b/drivers/net/dsa/microchip/lan9645x/Makefile
> @@ -0,0 +1,7 @@
> +# 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_port.o \
> + lan9645x_phylink.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..739013f049d0
> --- /dev/null
> +++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_main.c
> @@ -0,0 +1,435 @@
> +// SPDX-License-Identifier: GPL-2.0+
> +/* Copyright (C) 2026 Microchip Technology Inc.
> + */
> +
> +#include <linux/debugfs.h>
> +#include <linux/platform_device.h>
> +#include <linux/phy/phy.h>
You neither need <linux/debugfs.h> nor <linux/phy/phy.h>
> +
> +#include "lan9645x_main.h"
> +
> +static const char *lan9645x_resource_names[NUM_TARGETS + 1] = {
> + [TARGET_GCB] = "gcb",
> + [TARGET_QS] = "qs",
> + [TARGET_CHIP_TOP] = "chip_top",
> + [TARGET_REW] = "rew",
> + [TARGET_SYS] = "sys",
> + [TARGET_HSIO] = "hsio",
> + [TARGET_DEV] = "dev",
> + [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_AFI] = "afi",
> + [TARGET_ANA] = "ana",
> + [NUM_TARGETS] = NULL,
> +};
> +
> +static int lan9645x_tag_npi_setup(struct dsa_switch *ds)
> +{
> + struct dsa_port *dp, *first_cpu_dp = NULL;
> + struct lan9645x *lan9645x = ds->priv;
> +
> + dsa_switch_for_each_user_port(dp, ds) {
> + 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_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)
> +{
> + struct lan9645x *lan9645x = ds->priv;
> +
> + return lan9645x->tag_proto;
Unnecessary indirection.
> +}
> +
> +static int lan9645x_connect_tag_protocol(struct dsa_switch *ds,
> + enum dsa_tag_protocol proto)
> +{
> + switch (proto) {
> + case DSA_TAG_PROTO_LAN9645X:
> + return 0;
> + default:
> + return -EPROTONOSUPPORT;
> + }
> +}
Completely unnecessary method implementation. See the notes on it in
Documentation/networking/dsa/dsa.rst.
> +
> +static void lan9645x_teardown(struct dsa_switch *ds)
> +{
> + struct lan9645x *lan9645x = ds->priv;
> +
> + debugfs_remove_recursive(lan9645x->debugfs_root);
> + 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)
> +{
> + struct lan9645x *lan9645x = ds->priv;
> + int max_mtu;
> +
> + /* 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.
> + */
> + max_mtu = 10000 - ETH_HLEN - ETH_FCS_LEN;
> +
> + if (port == lan9645x->npi) {
> + max_mtu -= LAN9645X_IFH_LEN;
> + max_mtu -= LAN9645X_LONG_PREFIX_LEN;
> + }
> +
> + return max_mtu;
> +}
> +
> +static int lan9645x_reset_switch(struct lan9645x *lan9645x)
> +{
> + int val = 0;
> + int err;
> +
> + 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, "Lan9645x setup: failed to init chip RAM.");
> + 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;
> + struct dsa_port *dp;
> + u32 all_phys_ports;
> + int err = 0;
> +
> + lan9645x->num_phys_ports = ds->num_ports;
> + all_phys_ports = GENMASK(lan9645x->num_phys_ports - 1, 0);
> +
> + err = lan9645x_reset_switch(lan9645x);
> + if (err)
> + return err;
> +
> + lan9645x->debugfs_root = debugfs_create_dir("lan9645x_sw", NULL);
DSA drivers do not use debugfs, but more targeted debugging APIs like
those offered by devlink resources, regions etc. We can give more
feedback about what debugging infrastructure you may use as a debugfs
replacement, but this is a simple stub.
> +
> + lan9645x->ports = devm_kcalloc(lan9645x->dev, lan9645x->num_phys_ports,
> + sizeof(struct lan9645x_port *),
> + GFP_KERNEL);
> + if (!lan9645x->ports)
> + return -ENOMEM;
> +
> + for (int port = 0; port < lan9645x->num_phys_ports; port++) {
> + struct lan9645x_port *p;
> +
> + p = devm_kzalloc(lan9645x->dev,
> + sizeof(struct lan9645x_port), GFP_KERNEL);
> + if (!p)
> + return -ENOMEM;
> +
> + p->lan9645x = lan9645x;
> + p->chip_port = port;
> + lan9645x->ports[port] = p;
> + }
> +
> + err = lan9645x_port_parse_ports_node(lan9645x);
> + if (err) {
> + dev_err(dev, "Lan9645x setup: failed to parse ports node.");
> + return err;
> + }
> +
> + err = lan9645x_tag_npi_setup(ds);
> + if (err) {
> + dev_err(dev, "Lan9645x setup: 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 */
> + mdelay(1);
> +
> + /* 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);
> +
> + /* There are 8 priorities */
> + for (int prio = 0; prio < 8; ++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));
> +
> + /* Set all the entries to obey VLAN. */
> + for (int i = 0; i < PGID_ENTRIES; ++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 copy them
> + * to CPU
> + */
> + lan_wr(ANA_CPU_FWD_BPDU_CFG_BPDU_REDIR_ENA,
> + lan9645x, ANA_CPU_FWD_BPDU_CFG(p));
> + }
> +
> + /* Set source buffer size for each priority and port to ~1700 bytes */
> + for (int i = 0; i <= QSYS_Q_RSRV; ++i) {
> + lan_wr(QS_SRC_BUF_RSV / 64, lan9645x, QSYS_RES_CFG(i));
> + lan_wr(QS_SRC_BUF_RSV / 64, lan9645x, QSYS_RES_CFG(512 + i));
> + }
> +
> + /* Configure and enable the CPU port */
> + lan9645x_port_cpu_init(lan9645x);
> +
> + /* Multicast to all front ports */
> + lan_wr(all_phys_ports, lan9645x, ANA_PGID(PGID_MC));
> +
> + /* IP multicast to all front ports */
> + lan_wr(all_phys_ports, lan9645x, ANA_PGID(PGID_MCIPV4));
> + lan_wr(all_phys_ports, lan9645x, ANA_PGID(PGID_MCIPV6));
> +
> + /* Unicast to all front ports */
> + lan_wr(all_phys_ports, lan9645x, ANA_PGID(PGID_UC));
> +
> + /* Broadcast to all ports */
> + lan_wr(BIT(CPU_PORT) | all_phys_ports, lan9645x, ANA_PGID(PGID_BC));
> +
> + dsa_switch_for_each_available_port(dp, ds)
> + lan9645x_port_init(lan9645x, dp->index);
> +
> + lan9645x_port_set_tail_drop_wm(lan9645x);
> +
> + ds->mtu_enforcement_ingress = true;
> + ds->assisted_learning_on_cpu_port = true;
> + ds->fdb_isolation = true;
> +
> + dev_info(lan9645x->dev,
> + "Setup complete. SKU features: tsn_dis=%d hsr_dis=%d max_ports=%d",
> + lan9645x->tsn_dis, lan9645x->dd_dis,
> + 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,
> + .connect_tag_protocol = lan9645x_connect_tag_protocol,
> +
> + .setup = lan9645x_setup,
> + .teardown = lan9645x_teardown,
> +
> + /* 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=%d", i);
> + 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);
> + lan9645x->dd_dis = GCB_FEAT_DISABLE_FEAT_DD_DIS_GET(feat_dis);
> + lan9645x->tsn_dis = GCB_FEAT_DISABLE_FEAT_TSN_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 dev_err_probe(dev, -ENOMEM,
> + "Failed to allocate LAN9645X");
> +
> + 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");
> +
> + ds = devm_kzalloc(dev, sizeof(*ds), GFP_KERNEL);
> + if (!ds)
> + return dev_err_probe(dev, -ENOMEM,
> + "Failed to allocate DSA switch");
> +
> + ds->dev = dev;
> + ds->num_ports = NUM_PHYS_PORTS;
> + ds->num_tx_queues = NUM_PRIO_QUEUES;
> + ds->dscp_prio_mapping_is_global = true;
> +
> + ds->ops = &lan9645x_switch_ops;
> + ds->phylink_mac_ops = &lan9645x_phylink_mac_ops;
> + ds->priv = lan9645x;
> +
> + lan9645x->ds = ds;
> + lan9645x->tag_proto = DSA_TAG_PROTO_LAN9645X;
> + lan9645x->shared_queue_sz = LAN9645X_BUFFER_MEMORY;
> +
> + lan9645x_set_feat_dis(lan9645x);
> +
> + err = dsa_register_switch(ds);
> + if (err)
> + return dev_err_probe(dev, err, "Failed to register DSA switch");
> +
> + 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..a51b637f28bf
> --- /dev/null
> +++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_main.h
> @@ -0,0 +1,396 @@
> +/* 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/regmap.h>
> +#include <net/dsa.h>
> +
> +#include "lan9645x_regs.h"
> +
> +#define lan9645x_for_each_chipport(_lan9645x, _i) \
> + for ((_i) = 0; (_i) < (_lan9645x)->num_phys_ports; (_i)++)
I would prefer you not introduce this iteration scheme. Eventually you
will need to get a "struct dsa_port *dp" pointer in the loop, and at that
point, you will need to call dsa_to_port(lan9645->ds, i). But guess
what, dsa_to_port() iterates again through the ports to get the "dp".
Please consider one of the iterators from the dsa_switch_for_each_available_port()
family as a replacement here, and for "i" do "dp->index". This gives you
"dp" as a pointer right away if you need it.
> +
> +/* Ports 0-8 are front ports
> + * Ports 9-10 are CPU ports
> + *
> + * CPU ports are logical ports in the chip intended for management. The frame
> + * delivery mechanism can vary: direct register injection/extraction or one can
> + * use a front port as CPU port, called a Node Processor Interface (NPI).
> + *
> + * This is the approach used by LAN9645X.
> + */
> +#define NUM_PHYS_PORTS 9
> +#define CPU_PORT 9
> +#define NUM_PRIO_QUEUES 8
> +#define LAN9645X_NUM_TC 8
> +
> +#define QS_SRC_BUF_RSV 1700
> +
> +/* Reserved amount for (SRC, PRIO) at index 8*SRC + PRIO
> + * See QSYS:RES_CTRL[*]:RES_CFG description
> + */
> +#define QSYS_Q_RSRV 95
> +
> +#define LAN9645X_ISDX_MAX 128
> +#define LAN9645X_ESDX_MAX 128
> +#define LAN9645X_SFID_MAX 128
> +
> +/* Reserved VLAN IDs. */
> +#define UNAWARE_PVID 0
> +#define HOST_PVID 4095
> +#define VLAN_HSR_PRP 4094
> +#define VLAN_MAX (VLAN_HSR_PRP - 1)
> +
> +/* VLAN flags for VLAN table defined in ANA_VLANTIDX */
> +#define VLAN_SRC_CHK 0x01
> +#define VLAN_MIR 0x02
> +#define VLAN_LRN_DIS 0x04
> +#define VLAN_PRV_VLAN 0x08
> +#define VLAN_FLD_DIS 0x10
> +#define VLAN_S_FWD_ENA 0x20
> +
> +/* 160KiB / 1.25Mbit */
> +#define LAN9645X_BUFFER_MEMORY (160 * 1024)
> +
> +/* Port Group Identifiers (PGID) are port-masks applied to all frames.
> + * 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: By default the first 9 port masks == BIT(port_num). Never change
> + * these except for aggregation. Remaining dst masks are for L2 MC and
> + * flooding. (See FLOODING and FLOODING_IPMC).
> + *
> + * Aggregation: Used to pick a port within an aggregation group. If no
> + * aggregation is configured, these are all-ones.
> + *
> + * Source: Control which ports a given source port can forward to. A frame that
> + * is received on port n, uses mask 80+n as a mask to filter out destination
> + * ports. The default values are that all bits are set except for the index
> + * number (no loopback).
> + *
> + * We reserve destination PGIDs at the end of the range.
> + */
> +
> +#define PGID_AGGR 64
> +#define PGID_SRC 80
> +#define PGID_ENTRIES 89
> +
> +#define PGID_AGGR_NUM (PGID_SRC - PGID_AGGR)
> +
> +/* General purpose PGIDs. */
> +#define PGID_GP_START CPU_PORT
> +#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 RD_SLEEP_US 3
> +#define RD_SLEEPTIMEOUT_US 100000
> +#define SLOW_RD_SLEEP_US 1000
> +#define SLOW_RD_SLEEPTIMEOUT_US 2000000
> +
> +#define lan9645x_rd_poll_timeout(_lan9645x, _reg_macro, _val, _cond) \
> + regmap_read_poll_timeout(lan_rmap((_lan9645x), _reg_macro), \
> + lan_rel_addr(_reg_macro), (_val), \
> + (_cond), RD_SLEEP_US, RD_SLEEPTIMEOUT_US)
> +
> +#define lan9645x_rd_poll_slow(_lan9645x, _reg_macro, _val, _cond) \
> + regmap_read_poll_timeout(lan_rmap((_lan9645x), _reg_macro), \
> + lan_rel_addr(_reg_macro), (_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;
> + enum dsa_tag_protocol tag_proto;
> + struct regmap *rmap[NUM_TARGETS];
> +
> + int shared_queue_sz;
> +
> + /* NPI chip_port */
> + int npi;
> +
> + u8 num_phys_ports;
> + struct lan9645x_port **ports;
> +
> + /* debugfs */
> + struct dentry *debugfs_root;
> +
> + int num_port_dis;
> + bool dd_dis;
> + bool tsn_dis;
> +};
> +
> +struct lan9645x_port {
> + struct lan9645x *lan9645x;
> + const char *name;
> +
> + u16 pvid;
> + u16 untagged_vid;
> + u8 chip_port;
> + u8 stp_state;
> + bool vlan_aware;
Unused (in this patch). Even later (I haven't yet looked), you should
probably use dsa_port_is_vlan_filtering() directly.
> + bool learn_ena;
Also unused, please introduce it on first use.
> +
> + phy_interface_t phy_mode;
> +
> + int speed; /* internal speed value LAN9645X_SPEED_* */
> + u8 duplex;
> + struct list_head path_delays;
> + u32 rx_delay;
I'm a bit lost among occurrences of RGMII delays and PTP path delays,
but I don't think this is used.
> +};
> +
> +struct lan9645x_path_delay {
> + struct list_head list;
> + u32 rx_delay;
> + u32 tx_delay;
> + u32 speed;
> +};
> +
> +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)
> +{
> +#define GWM_MULTIPLIER_BIT BIT(8)
> +#define LAN9645X_BUFFER_CELL_SZ 64
The placement of these definitions inside the function makes it
difficult to read.
> + 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)
> +{
> + if (WARN_ON(!(port >= 0 && port < lan9645x->num_phys_ports)))
> + return NULL;
> +
> + return lan9645x->ports[port];
> +}
> +
> +static inline struct net_device *lan9645x_port_to_ndev(struct lan9645x_port *p)
> +{
> + struct lan9645x *lan9645x = p->lan9645x;
> + struct dsa_port *dp;
> +
> + dp = dsa_to_port(lan9645x->ds, p->chip_port);
> + if (dp && dp->type == DSA_PORT_TYPE_USER)
> + return dp->user;
> +
> + return NULL;
> +}
> +
> +static inline struct net_device *
> +lan9645x_chipport_to_ndev(struct lan9645x *lan9645x, int port)
> +{
> + return lan9645x_port_to_ndev(lan9645x_to_port(lan9645x, port));
> +}
Unnecessary, please remove this along with lan9645x_port_to_ndev().
You really shouldn't need to do this. Ocelot had it because it was a
driver with two front-ends.
> +
> +static inline bool lan9645x_port_is_used(struct lan9645x *lan9645x, int port)
> +{
> + struct dsa_port *dp;
> +
> + dp = dsa_to_port(lan9645x->ds, port);
> + if (!dp)
> + return false;
> +
> + return dp->type != DSA_PORT_TYPE_UNUSED;
> +}
Unused, please remove.
> +
> +static inline struct regmap *lan_tgt2rmap(struct lan9645x *lan9645x,
> + enum lan9645x_target t, int tinst)
> +{
> + WARN_ON_ONCE(!lan9645x->rmap[t + tinst]);
Question: what purpose does the WARN_ON_ONCE() serve, if you dereference
the pointer without NULL checking afterwards?
Suppose the regmap could be NULL. Wouldn't you get a stack trace twice?
> + 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_init(struct lan9645x *lan9645x, int port);
> +void lan9645x_port_cpu_init(struct lan9645x *lan9645x);
> +void lan9645x_port_set_tail_drop_wm(struct lan9645x *lan9645x);
> +int lan9645x_port_set_maxlen(struct lan9645x *lan9645x, int port, size_t sdu);
> +int lan9645x_port_parse_ports_node(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..0ae8b9acb916
> --- /dev/null
> +++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_npi.c
> @@ -0,0 +1,99 @@
> +// SPDX-License-Identifier: GPL-2.0+
> +/* Copyright (C) 2026 Microchip Technology Inc.
> + */
> +#include <net/addrconf.h>
> +
> +#include "lan9645x_main.h"
> +
> +static void disable_conduit_ipv6(struct lan9645x *lan9645x,
> + struct net_device *conduit)
> +{
> + struct inet6_dev *dev_v6;
> +
> + if (!conduit)
> + return;
> +
> + /* IPv6 on the conduit will generate frames bypassing our tag driver, so
> + * they lack an IFH. This will be garbage in garbage out and we want to
> + * avoid this.
> + */
> + rtnl_lock();
> + dev_v6 = __in6_dev_get(conduit);
> + if (dev_v6) {
> + WRITE_ONCE(dev_v6->cnf.disable_ipv6, 1);
> + dev_warn(lan9645x->dev, "Disabled IPv6 on conduit device: %s\n",
> + netdev_name(conduit));
> + }
> + rtnl_unlock();
> +}
> +
> +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);
> +
> + /* Enabling IPv6 on the conduit will send frames directly on the
> + * interface, without being intercepted by our tag driver. This causes a
> + * GIGO situation.
> + */
> + disable_conduit_ipv6(lan9645x, cpu_port->conduit);
> +
> + /* 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. */
> + 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(port));
> +
> + /* Make sure frames with src_port=CPU_PORT 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 + CPU_PORT));
> +}
> +
> +void lan9645x_npi_port_deinit(struct lan9645x *lan9645x, int port)
> +{
> + struct lan9645x_port *p = lan9645x_to_port(lan9645x, port);
> +
> + lan9645x->npi = -1;
> +
> + lan_wr(QSYS_EXT_CPU_CFG_EXT_CPU_PORT_SET(0x1f) |
> + 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));
> +}
> 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..3acc48f12fae
> --- /dev/null
> +++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_phylink.c
> @@ -0,0 +1,537 @@
> +// SPDX-License-Identifier: GPL-2.0+
> +/* Copyright (C) 2026 Microchip Technology Inc.
> + */
> +
> +#include <linux/phy.h>
> +#include <linux/phy/phy.h>
> +
> +#include "lan9645x_main.h"
> +
> +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 | MAC_2500FD;
> +
> + switch (port) {
> + case 0 ... 3:
> + __set_bit(PHY_INTERFACE_MODE_GMII, c->supported_interfaces);
> + break;
> + case 4:
> + __set_bit(PHY_INTERFACE_MODE_GMII, c->supported_interfaces);
> + phy_interface_set_rgmii(c->supported_interfaces);
> + break;
> + case 5 ... 6:
> + /* SerDes ports: QSGMII/SGMII/1000BASEX/2500BASEX modes
> + * require PCS support which is not yet implemented.
> + */
> + 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 int lan9645x_rgmii_setup(struct lan9645x *lan9645x, int port, int speed,
> + phy_interface_t mode)
> +{
> + bool tx_delay = false, rx_delay = false;
> + u32 rx_idx, tx_idx;
> + u8 tx_clk;
> + int idx;
> +
> + /* Port 4 or 7 is RGMII_0 and port 8 is RGMII_1 */
> + idx = port == 8 ? 1 : 0;
> +
> + if (!phy_interface_mode_is_rgmii(mode))
> + return 0;
> +
> + 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(idx));
> +
> + /* We configure delays on the MAC side. When the PHY is not responsible
> + * for delays, the MAC is, which is why RGMII_TXID results in
> + * rx_delay=true.
> + */
> + if (mode == PHY_INTERFACE_MODE_RGMII ||
> + mode == PHY_INTERFACE_MODE_RGMII_TXID)
> + rx_delay = true;
> +
> + if (mode == PHY_INTERFACE_MODE_RGMII ||
> + mode == PHY_INTERFACE_MODE_RGMII_RXID)
> + tx_delay = true;
I'm not sure everyone would agree on this interpretation.
I would not let the MAC apply internal delays based upon phy-mode.
I would parse "rx-internal-delay-ps" and "tx-internal-delay-ps" instead,
and leave the phy-mode to signify a PHY-only description.
> +
> + /* Setup DLL configuration. Register layout:
> + * 0: RGMII_0_RX
> + * 1: RGMII_0_TX
> + * 2: RGMII_1_RX
> + * 3: RGMII_1_TX
> + * ...
> + * (N<<1) RGMII_N_RX,
> + * (N<<1)+1: RGMII_N_TX,
> + */
> + rx_idx = idx << 1;
> + tx_idx = rx_idx + 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(rx_delay) |
> + HSIO_DLL_CFG_DELAY_ENA_SET(rx_delay),
> + HSIO_DLL_CFG_DLL_CLK_ENA |
> + HSIO_DLL_CFG_DLL_RST |
> + HSIO_DLL_CFG_DLL_ENA |
> + HSIO_DLL_CFG_DELAY_ENA,
> + 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(tx_delay) |
> + HSIO_DLL_CFG_DELAY_ENA_SET(tx_delay),
> + HSIO_DLL_CFG_DLL_CLK_ENA |
> + HSIO_DLL_CFG_DLL_RST |
> + HSIO_DLL_CFG_DLL_ENA |
> + HSIO_DLL_CFG_DELAY_ENA,
> + lan9645x, HSIO_DLL_CFG(tx_idx));
> +
> + return 0;
> +}
> +
> +static void lan9645x_phylink_mac_config(struct lan9645x *lan9645x, int port,
> + unsigned int mode,
> + const struct phylink_link_state *state)
> +{
> + if (phy_interface_mode_is_rgmii(state->interface))
> + lan9645x_rgmii_setup(lan9645x, port, state->speed,
> + state->interface);
> +}
> +
> +static int lan9645x_phylink_mac_prepare(struct lan9645x *lan9645x, int port,
> + unsigned int mode,
> + phy_interface_t iface)
> +{
> + switch (port) {
> + case 0 ... 3:
> + lan_rmw(HSIO_HW_CFG_GMII_ENA_SET(BIT(port)),
> + HSIO_HW_CFG_GMII_ENA_SET(BIT(port)), lan9645x,
> + HSIO_HW_CFG);
> + break;
> + case 4:
> + lan_rmw(HSIO_HW_CFG_GMII_ENA_SET(BIT(port)),
> + HSIO_HW_CFG_GMII_ENA_SET(BIT(port)), lan9645x,
> + HSIO_HW_CFG);
> +
> + if (phy_interface_mode_is_rgmii(iface))
> + lan_rmw(HSIO_HW_CFG_RGMII_0_CFG_SET(1),
> + HSIO_HW_CFG_RGMII_0_CFG,
> + lan9645x, HSIO_HW_CFG);
> +
> + break;
> + case 7 ... 8:
> + lan_rmw(HSIO_HW_CFG_GMII_ENA_SET(BIT(port)),
> + HSIO_HW_CFG_GMII_ENA_SET(BIT(port)), lan9645x,
> + HSIO_HW_CFG);
> + break;
> + default:
> + /* Ports 5-6 are SerDes-only and need PCS support (not yet
> + * implemented). They are excluded from phylink_get_caps.
> + */
> + return -EINVAL;
> + }
> +
> + return 0;
> +}
> +
> +static int lan9645x_port_is_cuphy(struct lan9645x *lan9645x, int port,
> + phy_interface_t interface)
> +{
> + return port >= 0 && port <= 4 && interface == PHY_INTERFACE_MODE_GMII;
> +}
> +
> +static void lan9645x_phylink_mac_link_up(struct lan9645x *lan9645x, int port,
> + unsigned int link_an_mode,
> + phy_interface_t interface,
> + struct phy_device *phydev, int speed,
> + int duplex, bool tx_pause,
> + bool rx_pause)
> +{
> + struct lan9645x_port *p = lan9645x_to_port(lan9645x, port);
> + int rx_ifg1, rx_ifg2, tx_ifg, gtx_clk = 0;
> + struct lan9645x_path_delay *path_delay;
> + int gspeed = LAN9645X_SPEED_DISABLED;
> + int mode = 0;
> + int fc_spd;
> +
> + /* Configure speed for RGMII modules. */
> + if (phy_interface_mode_is_rgmii(interface))
> + lan9645x_rgmii_setup(lan9645x, port, speed, interface);
> +
> + if (duplex == DUPLEX_FULL) {
> + mode |= DEV_MAC_MODE_CFG_FDX_ENA_SET(1);
> + rx_ifg2 = DEV_MAC_IFG_CFG_RX_IFG2_SET(0x2);
> + tx_ifg = DEV_MAC_IFG_CFG_TX_IFG_SET(0x5);
> +
> + } else {
> + rx_ifg2 = DEV_MAC_IFG_CFG_RX_IFG2_SET(0x2);
> + tx_ifg = DEV_MAC_IFG_CFG_TX_IFG_SET(0x6);
> + }
> +
> + switch (speed) {
> + case SPEED_10:
> + rx_ifg1 = DEV_MAC_IFG_CFG_RX_IFG1_SET(0x2);
> + gspeed = LAN9645X_SPEED_10;
> + break;
> + case SPEED_100:
> + rx_ifg1 = DEV_MAC_IFG_CFG_RX_IFG1_SET(0x1);
> + gspeed = LAN9645X_SPEED_100;
> + break;
> + case SPEED_1000:
> + gspeed = LAN9645X_SPEED_1000;
> + mode |= DEV_MAC_MODE_CFG_GIGA_MODE_ENA_SET(1);
> + mode |= DEV_MAC_MODE_CFG_FDX_ENA_SET(1);
> + rx_ifg1 = DEV_MAC_IFG_CFG_RX_IFG1_SET(0x1);
> + rx_ifg2 = DEV_MAC_IFG_CFG_RX_IFG2_SET(0x2);
> + tx_ifg = DEV_MAC_IFG_CFG_TX_IFG_SET(0x6);
> + gtx_clk = 1;
> + break;
> + case SPEED_2500:
> + gspeed = LAN9645X_SPEED_2500;
> + mode |= DEV_MAC_MODE_CFG_GIGA_MODE_ENA_SET(1);
> + mode |= DEV_MAC_MODE_CFG_FDX_ENA_SET(1);
> + rx_ifg1 = DEV_MAC_IFG_CFG_RX_IFG1_SET(0x1);
> + rx_ifg2 = DEV_MAC_IFG_CFG_RX_IFG2_SET(0x2);
> + tx_ifg = DEV_MAC_IFG_CFG_TX_IFG_SET(0x6);
> + break;
> + default:
> + dev_err(lan9645x->dev, "Unsupported speed on port %d: %d\n",
> + p->chip_port, speed);
> + return;
> + }
> +
> + p->speed = gspeed;
> + p->duplex = duplex;
> + fc_spd = lan9645x_speed_fc_enc(p->speed);
> +
> + if (phy_interface_num_ports(interface) == 4 ||
> + interface == PHY_INTERFACE_MODE_SGMII)
> + mode |= DEV_MAC_MODE_CFG_GIGA_MODE_ENA_SET(1);
> +
> + 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(tx_ifg | rx_ifg1 | 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));
> +
> + 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));
> +
> + 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(DEV_MAC_HDX_CFG_SEED_LOAD_SET(0),
> + DEV_MAC_HDX_CFG_SEED_LOAD, lan9645x,
> + DEV_MAC_HDX_CFG(p->chip_port));
> +
> + /* Set PFC link speed and enable map */
> + lan_rmw(ANA_PFC_CFG_FC_LINK_SPEED_SET(fc_spd) |
> + ANA_PFC_CFG_RX_PFC_ENA_SET(0),
> + ANA_PFC_CFG_FC_LINK_SPEED |
> + ANA_PFC_CFG_RX_PFC_ENA,
> + lan9645x, ANA_PFC_CFG(p->chip_port));
> +
> + lan_rmw(DEV_PCS1G_CFG_PCS_ENA_SET(1),
> + DEV_PCS1G_CFG_PCS_ENA, lan9645x,
> + DEV_PCS1G_CFG(p->chip_port));
> +
> + lan_rmw(DEV_PCS1G_SD_CFG_SD_ENA_SET(0),
> + DEV_PCS1G_SD_CFG_SD_ENA,
> + lan9645x, DEV_PCS1G_SD_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));
> +
> + /* 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));
> +
> + /* 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));
> +
> + list_for_each_entry(path_delay, &p->path_delays, list) {
> + if (path_delay->speed == speed) {
> + lan_wr(path_delay->rx_delay + p->rx_delay,
> + lan9645x, SYS_PTP_RXDLY_CFG(p->chip_port));
> + lan_wr(path_delay->tx_delay,
> + lan9645x, SYS_PTP_TXDLY_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 == 0 ? 1 : 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));
> +
> + lan_rmw(AFI_PORT_CFG_FC_SKIP_TTI_INJ_SET(0) |
> + AFI_PORT_CFG_FRM_OUT_MAX_SET(16),
> + AFI_PORT_CFG_FC_SKIP_TTI_INJ |
> + AFI_PORT_CFG_FRM_OUT_MAX,
> + lan9645x, AFI_PORT_CFG(p->chip_port));
> +}
> +
> +void lan9645x_phylink_port_down(struct lan9645x *lan9645x, int port)
> +{
> + struct lan9645x_port *p = lan9645x_to_port(lan9645x, port);
> + u32 val;
> +
> + /* 0.5: Disable any AFI */
> + lan_rmw(AFI_PORT_CFG_FC_SKIP_TTI_INJ_SET(1) |
> + AFI_PORT_CFG_FRM_OUT_MAX_SET(0),
> + AFI_PORT_CFG_FC_SKIP_TTI_INJ |
> + AFI_PORT_CFG_FRM_OUT_MAX,
> + lan9645x, AFI_PORT_CFG(p->chip_port));
> +
> + /* wait for reg afi_port_frm_out to become 0 for the port */
> + if (lan9645x_rd_poll_slow(lan9645x, AFI_PORT_FRM_OUT(p->chip_port),
> + val,
> + !AFI_PORT_FRM_OUT_FRM_OUT_CNT_GET(val)))
> + dev_err(lan9645x->dev, "AFI timeout chip port %u",
> + p->chip_port);
> +
> + /* 2: 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));
> +
> + /* 1: Reset the PCS Rx clock domain */
> + lan_rmw(DEV_CLOCK_CFG_PCS_RX_RST_SET(1),
> + DEV_CLOCK_CFG_PCS_RX_RST,
> + lan9645x, DEV_CLOCK_CFG(p->chip_port));
> +
> + p->speed = LAN9645X_SPEED_DISABLED;
> + p->duplex = DUPLEX_UNKNOWN;
> +
> + /* 3: 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));
> +
> + /* 4: 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));
> +
> + /* 5: Disable Flowcontrol */
> + lan_rmw(SYS_PAUSE_CFG_PAUSE_ENA_SET(0),
> + SYS_PAUSE_CFG_PAUSE_ENA,
> + lan9645x, SYS_PAUSE_CFG(p->chip_port));
> +
> + /* 5.1: Disable PFC */
> + lan_rmw(QSYS_SW_PORT_MODE_TX_PFC_ENA_SET(0),
> + QSYS_SW_PORT_MODE_TX_PFC_ENA,
> + lan9645x, QSYS_SW_PORT_MODE(p->chip_port));
> +
> + /* 6: Wait a worst case time 8ms (10K jumbo/10Mbit) */
> + usleep_range(8 * USEC_PER_MSEC, 9 * USEC_PER_MSEC);
> +
> + /* 7: 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));
> +
> + /* 8: 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));
> +
> + /* 9: 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));
> +
> + /* 10: 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", port);
> +
> + /* 11: 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));
> +
> + /* 12: 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));
> +
> + /* 13: Clear flushing */
> + lan_rmw(QSYS_SW_PORT_MODE_AGING_MODE_SET(1),
> + QSYS_SW_PORT_MODE_AGING_MODE,
> + lan9645x, QSYS_SW_PORT_MODE(p->chip_port));
> +}
> +
> +static void lan9645x_phylink_mac_link_down(struct lan9645x *lan9645x, int port,
> + unsigned int link_an_mode,
> + phy_interface_t interface)
> +{
> + struct lan9645x_port *p = lan9645x_to_port(lan9645x, port);
> +
> + lan9645x_phylink_port_down(lan9645x, port);
> +
> + /* 14: Take PCS out of reset */
> + lan_rmw(DEV_CLOCK_CFG_PCS_RX_RST_SET(0) |
> + DEV_CLOCK_CFG_PCS_TX_RST_SET(0),
> + DEV_CLOCK_CFG_PCS_RX_RST |
> + DEV_CLOCK_CFG_PCS_TX_RST,
> + lan9645x, DEV_CLOCK_CFG(p->chip_port));
> +}
> +
> +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 void
> +lan9645x_port_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);
> +
> + lan9645x_phylink_mac_config(p->lan9645x, p->chip_port, mode, state);
Why the separate functions here and in all other phylink_mac_ops implementations?
> +}
> +
> +static void lan9645x_port_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);
> +
> + lan9645x_phylink_mac_link_up(p->lan9645x, p->chip_port, link_an_mode,
> + interface, phydev, speed, duplex, tx_pause,
> + rx_pause);
> +}
> +
> +static void lan9645x_port_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);
> +
> + lan9645x_phylink_mac_link_down(p->lan9645x, p->chip_port, link_an_mode,
> + interface);
> +}
> +
> +static int lan9645x_port_phylink_mac_prepare(struct phylink_config *config,
> + unsigned int mode,
> + phy_interface_t iface)
> +{
> + struct lan9645x_port *p = lan9645x_phylink_config_to_port(config);
> +
> + return lan9645x_phylink_mac_prepare(p->lan9645x, p->chip_port, mode,
> + iface);
> +}
> +
> +const struct phylink_mac_ops lan9645x_phylink_mac_ops = {
> + .mac_config = lan9645x_port_phylink_mac_config,
> + .mac_link_up = lan9645x_port_phylink_mac_link_up,
> + .mac_link_down = lan9645x_port_phylink_mac_link_down,
> + .mac_prepare = lan9645x_port_phylink_mac_prepare,
> +};
> 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..038868ae0a32
> --- /dev/null
> +++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_port.c
> @@ -0,0 +1,289 @@
> +// SPDX-License-Identifier: GPL-2.0+
> +/* Copyright (C) 2026 Microchip Technology Inc.
> + */
> +
> +#include "lan9645x_main.h"
> +
> +int lan9645x_port_init(struct lan9645x *lan9645x, int port)
> +{
> + struct lan9645x_port *p;
> +
> + p = lan9645x_to_port(lan9645x, port);
> +
> + /* 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);
> +
> + lan9645x_phylink_port_down(lan9645x, port);
> +
> + if (phy_interface_num_ports(p->phy_mode) == 4)
> + lan_rmw(DEV_CLOCK_CFG_PCS_RX_RST_SET(0) |
> + DEV_CLOCK_CFG_PCS_TX_RST_SET(0),
> + DEV_CLOCK_CFG_PCS_RX_RST |
> + DEV_CLOCK_CFG_PCS_TX_RST,
> + lan9645x, DEV_CLOCK_CFG(p->chip_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));
> +
> + /* Enable receiving frames on the port, and activate auto-learning of
> + * MAC addresses.
> + */
> + 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 |
How does ANA_PORT_CFG_LEARN_ENA (disabled above) interact with
ANA_PORT_CFG_LEARNAUTO (enabled here)?
Judging from the comments only, this configuration seems pretty
inconsistent. Non-bridged user ports should have address learning turned
off.
> + ANA_PORT_CFG_RECV_ENA |
> + ANA_PORT_CFG_PORTID_VAL,
> + lan9645x, ANA_PORT_CFG(p->chip_port));
> +
> + return 0;
> +}
> +
> +void lan9645x_port_cpu_init(struct lan9645x *lan9645x)
> +{
> + /* Map the 8 CPU extraction queues to CPU port 9 (datasheet is wrong) */
> + lan_wr(0, lan9645x, QSYS_CPU_GROUP_MAP);
> +
> + /* Configure second cpu port (chip_port 10) for manual frame injection.
> + * The AFI can not inject frames via the NPI port, unless frame aging is
> + * disabled on frontports, so we use manual injection for AFI frames.
> + */
AFI stands for? And what is it used for?
> +
> + /* Set min-spacing of EOF to SOF on injected frames to 0, on cpu device
> + * 1. This is required when injecting with IFH.
> + * Default values emulates delay of std preamble/IFG setting on a front
> + * port.
> + */
> + lan_rmw(QS_INJ_CTRL_GAP_SIZE_SET(0),
> + QS_INJ_CTRL_GAP_SIZE,
> + lan9645x, QS_INJ_CTRL(1));
> +
> + /* Injection: Mode: manual injection | Byte_swap */
> + lan_wr(QS_INJ_GRP_CFG_MODE_SET(1) |
> + QS_INJ_GRP_CFG_BYTE_SWAP_SET(1),
> + lan9645x, QS_INJ_GRP_CFG(1));
> +
> + lan_rmw(QS_INJ_CTRL_GAP_SIZE_SET(0),
> + QS_INJ_CTRL_GAP_SIZE,
> + lan9645x, QS_INJ_CTRL(1));
> +
> + lan_wr(SYS_PORT_MODE_INCL_INJ_HDR_SET(1),
> + lan9645x, SYS_PORT_MODE(CPU_PORT + 1));
> +
> + /* 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(BIT(CPU_PORT)),
> + QSYS_EGR_NO_SHARING_EGR_NO_SHARING_SET(BIT(CPU_PORT)),
> + 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(BIT(CPU_PORT)),
> + QSYS_EGR_DROP_MODE_EGRESS_DROP_MODE_SET(BIT(CPU_PORT)),
> + lan9645x, QSYS_EGR_DROP_MODE);
> +
> + lan_wr(BIT(CPU_PORT), lan9645x, ANA_PGID(PGID_CPU));
> +
> + lan_rmw(ANA_PORT_CFG_PORTID_VAL_SET(CPU_PORT) |
> + ANA_PORT_CFG_RECV_ENA_SET(1),
> + ANA_PORT_CFG_PORTID_VAL |
> + ANA_PORT_CFG_RECV_ENA, lan9645x,
> + ANA_PORT_CFG(CPU_PORT));
> +
> + /* 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(CPU_PORT));
> +
> + /* Transmit cpu frames as received without any tagging, timing or other
> + * updates. This does not affect CPU-over-NPI, only manual extraction.
> + * On the NPI port we need NO_REWRITE=0 for HSR/PRP.
> + */
> + lan_wr(REW_PORT_CFG_NO_REWRITE_SET(1),
> + lan9645x, REW_PORT_CFG(CPU_PORT));
> +}
> +
> +void lan9645x_port_set_tail_drop_wm(struct lan9645x *lan9645x)
> +{
> + int shared_per_port;
> + int port;
> +
> + /* Configure tail dropping watermark */
> + shared_per_port =
> + lan9645x->shared_queue_sz / (lan9645x->num_phys_ports + 1);
> +
> + /* The total memory size is diveded by number of front ports plus CPU
divided
> + * port.
> + */
> + lan9645x_for_each_chipport(lan9645x, port)
> + lan_wr(lan9645x_wm_enc(shared_per_port), lan9645x,
> + SYS_ATOP(port));
> +
> + /* Tail dropping active based only on per port ATOP wm */
> + lan_wr(lan9645x_wm_enc(lan9645x->shared_queue_sz), lan9645x,
> + SYS_ATOP_TOT_CFG);
> +}
> +
> +int lan9645x_port_set_maxlen(struct lan9645x *lan9645x, int port, size_t sdu)
> +{
> + struct lan9645x_port *p = lan9645x_to_port(lan9645x, port);
> +
No blank lines between local variable declarations.
> + int maxlen = sdu + ETH_HLEN + ETH_FCS_LEN;
> +
> + if (port == lan9645x->npi) {
> + maxlen += LAN9645X_IFH_LEN;
> + 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 int lan9645x_port_setup_leds(struct lan9645x *lan9645x,
> + struct fwnode_handle *portnp, int port)
> +{
> + u32 drive_mode;
> + int err;
> +
> + err = fwnode_property_read_u32(portnp, "microchip,led-drive-mode",
> + &drive_mode);
> + if (err)
> + return err;
This property is mandatory? 1. the schema doesn't put it in "required",
and 2. why would it be mandatory?
> +
> + lan_rmw(CHIP_TOP_CUPHY_LED_CFG_LED_DRIVE_MODE_SET(drive_mode),
> + CHIP_TOP_CUPHY_LED_CFG_LED_DRIVE_MODE, lan9645x,
> + CHIP_TOP_CUPHY_LED_CFG(port));
> +
> + return 0;
> +}
> +
> +static int lan9645x_port_parse_delays(struct lan9645x_port *port,
> + struct fwnode_handle *portnp)
> +{
> + struct fwnode_handle *delay;
> + int err;
> +
> + INIT_LIST_HEAD(&port->path_delays);
> +
> + fwnode_for_each_available_child_node(portnp, delay) {
> + struct lan9645x_path_delay *path_delay;
> + s32 tx_delay;
> + s32 rx_delay;
> + u32 speed;
> +
> + err = fwnode_property_read_u32(delay, "speed", &speed);
> + if (err)
> + return err;
> +
> + err = fwnode_property_read_u32(delay, "rx_delay", &rx_delay);
> + if (err)
> + return err;
> +
> + err = fwnode_property_read_u32(delay, "tx_delay", &tx_delay);
> + if (err)
> + return err;
> +
> + path_delay = devm_kzalloc(port->lan9645x->dev,
> + sizeof(*path_delay), GFP_KERNEL);
> + if (!path_delay)
> + return -ENOMEM;
> +
> + path_delay->rx_delay = rx_delay;
> + path_delay->tx_delay = tx_delay;
> + path_delay->speed = speed;
> + list_add_tail(&path_delay->list, &port->path_delays);
Show prior art about having such device tree properties, or defer this
feature to a separate series where the PTP maintainer is also on CC, and
properly explain your end goal and reasoning process towards this solution.
> + }
> +
> + return 0;
> +}
> +
> +int lan9645x_port_parse_ports_node(struct lan9645x *lan9645x)
> +{
> + struct fwnode_handle *ports, *portnp;
> + struct device *dev = lan9645x->dev;
> + int max_ports, num_ports = 0;
> + int err = 0;
> +
> + max_ports = NUM_PHYS_PORTS - lan9645x->num_port_dis;
> +
> + ports = device_get_named_child_node(dev, "ethernet-ports");
> + if (!ports)
> + ports = device_get_named_child_node(dev, "ports");
> + if (!ports) {
> + dev_err(dev, "no ethernet-ports or ports child found\n");
> + return -ENODEV;
> + }
> +
> + fwnode_for_each_available_child_node(ports, portnp) {
> + int phy_mode;
> + u32 p;
> +
> + num_ports++;
> +
> + if (num_ports > max_ports) {
> + dev_err(dev,
> + "Too many ports in device tree. Max ports supported by SKU: %d\n",
> + max_ports);
> + err = -ENODEV;
> + goto err_free_ports;
> + }
> +
> + if (fwnode_property_read_u32(portnp, "reg", &p)) {
> + dev_err(dev, "Port number not defined in device tree (property \"reg\")\n");
> + err = -ENODEV;
> + fwnode_handle_put(portnp);
> + goto err_free_ports;
> + }
> +
> + if (p >= lan9645x->num_phys_ports) {
> + dev_err(dev,
> + "Port number in device tree is invalid %u (property \"reg\")\n",
> + p);
> + err = -ENODEV;
> + fwnode_handle_put(portnp);
> + goto err_free_ports;
> + }
> +
> + phy_mode = fwnode_get_phy_mode(portnp);
> + if (phy_mode < 0) {
> + dev_err(dev, "Failed to read phy-mode for port %u", p);
> + err = -ENODEV;
> + fwnode_handle_put(portnp);
> + goto err_free_ports;
> + }
> +
> + lan9645x->ports[p]->phy_mode = phy_mode;
I have pending patches which remove this coding pattern from the ocelot
driver. Please work with the phy_mode that phylink gives you in mac_config.
Do not expect it to remain fixed throughout the lifetime of the device.
> + lan9645x_port_parse_delays(lan9645x->ports[p], portnp);
> + lan9645x_port_setup_leds(lan9645x, portnp, p);
FWIW you can also implement .port_setup(), get dp = dsa_to_port(ds, port),
and just call lan9645x_port_setup_leds(dp->dn). You don't need the
complicated iteration over ports starting from the root switch node.
> + }
> +
> +err_free_ports:
> + fwnode_handle_put(ports);
> + return err;
> +}
> diff --git a/drivers/net/ethernet/microchip/Kconfig b/drivers/net/ethernet/microchip/Kconfig
> index ee046468652c..740f3c2e8199 100644
> --- a/drivers/net/ethernet/microchip/Kconfig
> +++ b/drivers/net/ethernet/microchip/Kconfig
> @@ -62,5 +62,6 @@ source "drivers/net/ethernet/microchip/lan966x/Kconfig"
> source "drivers/net/ethernet/microchip/sparx5/Kconfig"
> source "drivers/net/ethernet/microchip/vcap/Kconfig"
> source "drivers/net/ethernet/microchip/fdma/Kconfig"
> +source "drivers/net/dsa/microchip/lan9645x/Kconfig"
The Kconfig of DSA drivers needs to be pulled in through drivers/net/dsa/Kconfig.
Put the proper "depends on" statements in place, if you're missing any.
>
> endif # NET_VENDOR_MICROCHIP
>
> --
> 2.52.0
>
On Tue, 2026-03-03 at 16:15 +0200, Vladimir Oltean wrote:
> EXTERNAL EMAIL: Do not click links or open attachments unless you know the content is safe
>
> On Tue, Mar 03, 2026 at 01:22:30PM +0100, Jens Emil Schulz Østergaard wrote:
> > 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.
> >
> > IPv6 is disabled on the conduit. When enabled, the RFC 4861 frames are
> > dispatched directly on the conduit bypassing the tag driver. The switch
> > parses these frames as if they have an IFH prepended, leading to a
> > garbage in garbage out situation. Therefore, IPv6 on the conduit is not
> > a sensible configuration.
> >
> > Reviewed-by: Steen Hegelund <Steen.Hegelund@microchip.com>
> > Signed-off-by: Jens Emil Schulz Østergaard <jensemil.schulzostergaard@microchip.com>
> > ---
> > drivers/net/dsa/microchip/Makefile | 1 +
> > drivers/net/dsa/microchip/lan9645x/Kconfig | 12 +
> > drivers/net/dsa/microchip/lan9645x/Makefile | 7 +
> > drivers/net/dsa/microchip/lan9645x/lan9645x_main.c | 435 +++++++++++++++++
> > drivers/net/dsa/microchip/lan9645x/lan9645x_main.h | 396 +++++++++++++++
> > drivers/net/dsa/microchip/lan9645x/lan9645x_npi.c | 99 ++++
> > .../net/dsa/microchip/lan9645x/lan9645x_phylink.c | 537 +++++++++++++++++++++
> > drivers/net/dsa/microchip/lan9645x/lan9645x_port.c | 289 +++++++++++
> > drivers/net/ethernet/microchip/Kconfig | 1 +
> > 9 files changed, 1777 insertions(+)
> >
> > 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..8cbac1f9875d
> > --- /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
> > + depends on NET_VENDOR_MICROCHIP
> > + select NET_DSA_TAG_LAN9645X
> > + 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..eea1edc5c0e3
> > --- /dev/null
> > +++ b/drivers/net/dsa/microchip/lan9645x/Makefile
> > @@ -0,0 +1,7 @@
> > +# 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_port.o \
> > + lan9645x_phylink.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..739013f049d0
> > --- /dev/null
> > +++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_main.c
> > @@ -0,0 +1,435 @@
> > +// SPDX-License-Identifier: GPL-2.0+
> > +/* Copyright (C) 2026 Microchip Technology Inc.
> > + */
> > +
> > +#include <linux/debugfs.h>
> > +#include <linux/platform_device.h>
> > +#include <linux/phy/phy.h>
>
> You neither need <linux/debugfs.h> nor <linux/phy/phy.h>
>
Ok, I will remove the debugfs.
> > +
> > +#include "lan9645x_main.h"
> > +
> > +static const char *lan9645x_resource_names[NUM_TARGETS + 1] = {
> > + [TARGET_GCB] = "gcb",
> > + [TARGET_QS] = "qs",
> > + [TARGET_CHIP_TOP] = "chip_top",
> > + [TARGET_REW] = "rew",
> > + [TARGET_SYS] = "sys",
> > + [TARGET_HSIO] = "hsio",
> > + [TARGET_DEV] = "dev",
> > + [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_AFI] = "afi",
> > + [TARGET_ANA] = "ana",
> > + [NUM_TARGETS] = NULL,
> > +};
> > +
> > +static int lan9645x_tag_npi_setup(struct dsa_switch *ds)
> > +{
> > + struct dsa_port *dp, *first_cpu_dp = NULL;
> > + struct lan9645x *lan9645x = ds->priv;
> > +
> > + dsa_switch_for_each_user_port(dp, ds) {
> > + 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_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)
> > +{
> > + struct lan9645x *lan9645x = ds->priv;
> > +
> > + return lan9645x->tag_proto;
>
> Unnecessary indirection.
>
I will return the tag_proto directly.
> > +}
> > +
> > +static int lan9645x_connect_tag_protocol(struct dsa_switch *ds,
> > + enum dsa_tag_protocol proto)
> > +{
> > + switch (proto) {
> > + case DSA_TAG_PROTO_LAN9645X:
> > + return 0;
> > + default:
> > + return -EPROTONOSUPPORT;
> > + }
> > +}
>
> Completely unnecessary method implementation. See the notes on it in
> Documentation/networking/dsa/dsa.rst.
>
I will remove it.
> > +
> > +static void lan9645x_teardown(struct dsa_switch *ds)
> > +{
> > + struct lan9645x *lan9645x = ds->priv;
> > +
> > + debugfs_remove_recursive(lan9645x->debugfs_root);
> > + 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)
> > +{
> > + struct lan9645x *lan9645x = ds->priv;
> > + int max_mtu;
> > +
> > + /* 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.
> > + */
> > + max_mtu = 10000 - ETH_HLEN - ETH_FCS_LEN;
> > +
> > + if (port == lan9645x->npi) {
> > + max_mtu -= LAN9645X_IFH_LEN;
> > + max_mtu -= LAN9645X_LONG_PREFIX_LEN;
> > + }
> > +
> > + return max_mtu;
> > +}
> > +
> > +static int lan9645x_reset_switch(struct lan9645x *lan9645x)
> > +{
> > + int val = 0;
> > + int err;
> > +
> > + 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, "Lan9645x setup: failed to init chip RAM.");
> > + 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;
> > + struct dsa_port *dp;
> > + u32 all_phys_ports;
> > + int err = 0;
> > +
> > + lan9645x->num_phys_ports = ds->num_ports;
> > + all_phys_ports = GENMASK(lan9645x->num_phys_ports - 1, 0);
> > +
> > + err = lan9645x_reset_switch(lan9645x);
> > + if (err)
> > + return err;
> > +
> > + lan9645x->debugfs_root = debugfs_create_dir("lan9645x_sw", NULL);
>
> DSA drivers do not use debugfs, but more targeted debugging APIs like
> those offered by devlink resources, regions etc. We can give more
> feedback about what debugging infrastructure you may use as a debugfs
> replacement, but this is a simple stub.
>
Removed. I am not too familiar with devlink, but I will look it.
> > +
> > + lan9645x->ports = devm_kcalloc(lan9645x->dev, lan9645x->num_phys_ports,
> > + sizeof(struct lan9645x_port *),
> > + GFP_KERNEL);
> > + if (!lan9645x->ports)
> > + return -ENOMEM;
> > +
> > + for (int port = 0; port < lan9645x->num_phys_ports; port++) {
> > + struct lan9645x_port *p;
> > +
> > + p = devm_kzalloc(lan9645x->dev,
> > + sizeof(struct lan9645x_port), GFP_KERNEL);
> > + if (!p)
> > + return -ENOMEM;
> > +
> > + p->lan9645x = lan9645x;
> > + p->chip_port = port;
> > + lan9645x->ports[port] = p;
> > + }
> > +
> > + err = lan9645x_port_parse_ports_node(lan9645x);
> > + if (err) {
> > + dev_err(dev, "Lan9645x setup: failed to parse ports node.");
> > + return err;
> > + }
> > +
> > + err = lan9645x_tag_npi_setup(ds);
> > + if (err) {
> > + dev_err(dev, "Lan9645x setup: 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 */
> > + mdelay(1);
> > +
> > + /* 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);
> > +
> > + /* There are 8 priorities */
> > + for (int prio = 0; prio < 8; ++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));
> > +
> > + /* Set all the entries to obey VLAN. */
> > + for (int i = 0; i < PGID_ENTRIES; ++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 copy them
> > + * to CPU
> > + */
> > + lan_wr(ANA_CPU_FWD_BPDU_CFG_BPDU_REDIR_ENA,
> > + lan9645x, ANA_CPU_FWD_BPDU_CFG(p));
> > + }
> > +
> > + /* Set source buffer size for each priority and port to ~1700 bytes */
> > + for (int i = 0; i <= QSYS_Q_RSRV; ++i) {
> > + lan_wr(QS_SRC_BUF_RSV / 64, lan9645x, QSYS_RES_CFG(i));
> > + lan_wr(QS_SRC_BUF_RSV / 64, lan9645x, QSYS_RES_CFG(512 + i));
> > + }
> > +
> > + /* Configure and enable the CPU port */
> > + lan9645x_port_cpu_init(lan9645x);
> > +
> > + /* Multicast to all front ports */
> > + lan_wr(all_phys_ports, lan9645x, ANA_PGID(PGID_MC));
> > +
> > + /* IP multicast to all front ports */
> > + lan_wr(all_phys_ports, lan9645x, ANA_PGID(PGID_MCIPV4));
> > + lan_wr(all_phys_ports, lan9645x, ANA_PGID(PGID_MCIPV6));
> > +
> > + /* Unicast to all front ports */
> > + lan_wr(all_phys_ports, lan9645x, ANA_PGID(PGID_UC));
> > +
> > + /* Broadcast to all ports */
> > + lan_wr(BIT(CPU_PORT) | all_phys_ports, lan9645x, ANA_PGID(PGID_BC));
> > +
> > + dsa_switch_for_each_available_port(dp, ds)
> > + lan9645x_port_init(lan9645x, dp->index);
> > +
> > + lan9645x_port_set_tail_drop_wm(lan9645x);
> > +
> > + ds->mtu_enforcement_ingress = true;
> > + ds->assisted_learning_on_cpu_port = true;
> > + ds->fdb_isolation = true;
> > +
> > + dev_info(lan9645x->dev,
> > + "Setup complete. SKU features: tsn_dis=%d hsr_dis=%d max_ports=%d",
> > + lan9645x->tsn_dis, lan9645x->dd_dis,
> > + 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,
> > + .connect_tag_protocol = lan9645x_connect_tag_protocol,
> > +
> > + .setup = lan9645x_setup,
> > + .teardown = lan9645x_teardown,
> > +
> > + /* 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=%d", i);
> > + 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);
> > + lan9645x->dd_dis = GCB_FEAT_DISABLE_FEAT_DD_DIS_GET(feat_dis);
> > + lan9645x->tsn_dis = GCB_FEAT_DISABLE_FEAT_TSN_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 dev_err_probe(dev, -ENOMEM,
> > + "Failed to allocate LAN9645X");
> > +
> > + 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");
> > +
> > + ds = devm_kzalloc(dev, sizeof(*ds), GFP_KERNEL);
> > + if (!ds)
> > + return dev_err_probe(dev, -ENOMEM,
> > + "Failed to allocate DSA switch");
> > +
> > + ds->dev = dev;
> > + ds->num_ports = NUM_PHYS_PORTS;
> > + ds->num_tx_queues = NUM_PRIO_QUEUES;
> > + ds->dscp_prio_mapping_is_global = true;
> > +
> > + ds->ops = &lan9645x_switch_ops;
> > + ds->phylink_mac_ops = &lan9645x_phylink_mac_ops;
> > + ds->priv = lan9645x;
> > +
> > + lan9645x->ds = ds;
> > + lan9645x->tag_proto = DSA_TAG_PROTO_LAN9645X;
> > + lan9645x->shared_queue_sz = LAN9645X_BUFFER_MEMORY;
> > +
> > + lan9645x_set_feat_dis(lan9645x);
> > +
> > + err = dsa_register_switch(ds);
> > + if (err)
> > + return dev_err_probe(dev, err, "Failed to register DSA switch");
> > +
> > + 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..a51b637f28bf
> > --- /dev/null
> > +++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_main.h
> > @@ -0,0 +1,396 @@
> > +/* 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/regmap.h>
> > +#include <net/dsa.h>
> > +
> > +#include "lan9645x_regs.h"
> > +
> > +#define lan9645x_for_each_chipport(_lan9645x, _i) \
> > + for ((_i) = 0; (_i) < (_lan9645x)->num_phys_ports; (_i)++)
>
> I would prefer you not introduce this iteration scheme. Eventually you
> will need to get a "struct dsa_port *dp" pointer in the loop, and at that
> point, you will need to call dsa_to_port(lan9645->ds, i). But guess
> what, dsa_to_port() iterates again through the ports to get the "dp".
> Please consider one of the iterators from the dsa_switch_for_each_available_port()
> family as a replacement here, and for "i" do "dp->index". This gives you
> "dp" as a pointer right away if you need it.
>
I will replace it with dsa iterators.
> > +
> > +/* Ports 0-8 are front ports
> > + * Ports 9-10 are CPU ports
> > + *
> > + * CPU ports are logical ports in the chip intended for management. The frame
> > + * delivery mechanism can vary: direct register injection/extraction or one can
> > + * use a front port as CPU port, called a Node Processor Interface (NPI).
> > + *
> > + * This is the approach used by LAN9645X.
> > + */
> > +#define NUM_PHYS_PORTS 9
> > +#define CPU_PORT 9
> > +#define NUM_PRIO_QUEUES 8
> > +#define LAN9645X_NUM_TC 8
> > +
> > +#define QS_SRC_BUF_RSV 1700
> > +
> > +/* Reserved amount for (SRC, PRIO) at index 8*SRC + PRIO
> > + * See QSYS:RES_CTRL[*]:RES_CFG description
> > + */
> > +#define QSYS_Q_RSRV 95
> > +
> > +#define LAN9645X_ISDX_MAX 128
> > +#define LAN9645X_ESDX_MAX 128
> > +#define LAN9645X_SFID_MAX 128
> > +
> > +/* Reserved VLAN IDs. */
> > +#define UNAWARE_PVID 0
> > +#define HOST_PVID 4095
> > +#define VLAN_HSR_PRP 4094
> > +#define VLAN_MAX (VLAN_HSR_PRP - 1)
> > +
> > +/* VLAN flags for VLAN table defined in ANA_VLANTIDX */
> > +#define VLAN_SRC_CHK 0x01
> > +#define VLAN_MIR 0x02
> > +#define VLAN_LRN_DIS 0x04
> > +#define VLAN_PRV_VLAN 0x08
> > +#define VLAN_FLD_DIS 0x10
> > +#define VLAN_S_FWD_ENA 0x20
> > +
> > +/* 160KiB / 1.25Mbit */
> > +#define LAN9645X_BUFFER_MEMORY (160 * 1024)
> > +
> > +/* Port Group Identifiers (PGID) are port-masks applied to all frames.
> > + * 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: By default the first 9 port masks == BIT(port_num). Never change
> > + * these except for aggregation. Remaining dst masks are for L2 MC and
> > + * flooding. (See FLOODING and FLOODING_IPMC).
> > + *
> > + * Aggregation: Used to pick a port within an aggregation group. If no
> > + * aggregation is configured, these are all-ones.
> > + *
> > + * Source: Control which ports a given source port can forward to. A frame that
> > + * is received on port n, uses mask 80+n as a mask to filter out destination
> > + * ports. The default values are that all bits are set except for the index
> > + * number (no loopback).
> > + *
> > + * We reserve destination PGIDs at the end of the range.
> > + */
> > +
> > +#define PGID_AGGR 64
> > +#define PGID_SRC 80
> > +#define PGID_ENTRIES 89
> > +
> > +#define PGID_AGGR_NUM (PGID_SRC - PGID_AGGR)
> > +
> > +/* General purpose PGIDs. */
> > +#define PGID_GP_START CPU_PORT
> > +#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 RD_SLEEP_US 3
> > +#define RD_SLEEPTIMEOUT_US 100000
> > +#define SLOW_RD_SLEEP_US 1000
> > +#define SLOW_RD_SLEEPTIMEOUT_US 2000000
> > +
> > +#define lan9645x_rd_poll_timeout(_lan9645x, _reg_macro, _val, _cond) \
> > + regmap_read_poll_timeout(lan_rmap((_lan9645x), _reg_macro), \
> > + lan_rel_addr(_reg_macro), (_val), \
> > + (_cond), RD_SLEEP_US, RD_SLEEPTIMEOUT_US)
> > +
> > +#define lan9645x_rd_poll_slow(_lan9645x, _reg_macro, _val, _cond) \
> > + regmap_read_poll_timeout(lan_rmap((_lan9645x), _reg_macro), \
> > + lan_rel_addr(_reg_macro), (_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;
> > + enum dsa_tag_protocol tag_proto;
> > + struct regmap *rmap[NUM_TARGETS];
> > +
> > + int shared_queue_sz;
> > +
> > + /* NPI chip_port */
> > + int npi;
> > +
> > + u8 num_phys_ports;
> > + struct lan9645x_port **ports;
> > +
> > + /* debugfs */
> > + struct dentry *debugfs_root;
> > +
> > + int num_port_dis;
> > + bool dd_dis;
> > + bool tsn_dis;
> > +};
> > +
> > +struct lan9645x_port {
> > + struct lan9645x *lan9645x;
> > + const char *name;
> > +
> > + u16 pvid;
> > + u16 untagged_vid;
> > + u8 chip_port;
> > + u8 stp_state;
> > + bool vlan_aware;
>
> Unused (in this patch). Even later (I haven't yet looked), you should
> probably use dsa_port_is_vlan_filtering() directly.
>
> > + bool learn_ena;
>
> Also unused, please introduce it on first use.
>
I will reorganize the patches to introduce types and fields on first use.
> > +
> > + phy_interface_t phy_mode;
> > +
> > + int speed; /* internal speed value LAN9645X_SPEED_* */
> > + u8 duplex;
> > + struct list_head path_delays;
> > + u32 rx_delay;
>
> I'm a bit lost among occurrences of RGMII delays and PTP path delays,
> but I don't think this is used.
>
I will remove all the port delay code.
> > +};
> > +
> > +struct lan9645x_path_delay {
> > + struct list_head list;
> > + u32 rx_delay;
> > + u32 tx_delay;
> > + u32 speed;
> > +};
> > +
> > +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)
> > +{
> > +#define GWM_MULTIPLIER_BIT BIT(8)
> > +#define LAN9645X_BUFFER_CELL_SZ 64
>
> The placement of these definitions inside the function makes it
> difficult to read.
>
Moving these to the top.
> > + 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)
> > +{
> > + if (WARN_ON(!(port >= 0 && port < lan9645x->num_phys_ports)))
> > + return NULL;
> > +
> > + return lan9645x->ports[port];
> > +}
> > +
> > +static inline struct net_device *lan9645x_port_to_ndev(struct lan9645x_port *p)
> > +{
> > + struct lan9645x *lan9645x = p->lan9645x;
> > + struct dsa_port *dp;
> > +
> > + dp = dsa_to_port(lan9645x->ds, p->chip_port);
> > + if (dp && dp->type == DSA_PORT_TYPE_USER)
> > + return dp->user;
> > +
> > + return NULL;
> > +}
> > +
> > +static inline struct net_device *
> > +lan9645x_chipport_to_ndev(struct lan9645x *lan9645x, int port)
> > +{
> > + return lan9645x_port_to_ndev(lan9645x_to_port(lan9645x, port));
> > +}
>
> Unnecessary, please remove this along with lan9645x_port_to_ndev().
> You really shouldn't need to do this. Ocelot had it because it was a
> driver with two front-ends.
>
I will remove it in the next version.
> > +
> > +static inline bool lan9645x_port_is_used(struct lan9645x *lan9645x, int port)
> > +{
> > + struct dsa_port *dp;
> > +
> > + dp = dsa_to_port(lan9645x->ds, port);
> > + if (!dp)
> > + return false;
> > +
> > + return dp->type != DSA_PORT_TYPE_UNUSED;
> > +}
>
> Unused, please remove.
>
> > +
> > +static inline struct regmap *lan_tgt2rmap(struct lan9645x *lan9645x,
> > + enum lan9645x_target t, int tinst)
> > +{
> > + WARN_ON_ONCE(!lan9645x->rmap[t + tinst]);
>
> Question: what purpose does the WARN_ON_ONCE() serve, if you dereference
> the pointer without NULL checking afterwards?
>
> Suppose the regmap could be NULL. Wouldn't you get a stack trace twice?
>
I think you are right. I wanted to be able to see if I messed up the array,
but I do not think I have ever had it happen. I can remove it in the next
version.
> > + 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_init(struct lan9645x *lan9645x, int port);
> > +void lan9645x_port_cpu_init(struct lan9645x *lan9645x);
> > +void lan9645x_port_set_tail_drop_wm(struct lan9645x *lan9645x);
> > +int lan9645x_port_set_maxlen(struct lan9645x *lan9645x, int port, size_t sdu);
> > +int lan9645x_port_parse_ports_node(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..0ae8b9acb916
> > --- /dev/null
> > +++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_npi.c
> > @@ -0,0 +1,99 @@
> > +// SPDX-License-Identifier: GPL-2.0+
> > +/* Copyright (C) 2026 Microchip Technology Inc.
> > + */
> > +#include <net/addrconf.h>
> > +
> > +#include "lan9645x_main.h"
> > +
> > +static void disable_conduit_ipv6(struct lan9645x *lan9645x,
> > + struct net_device *conduit)
> > +{
> > + struct inet6_dev *dev_v6;
> > +
> > + if (!conduit)
> > + return;
> > +
> > + /* IPv6 on the conduit will generate frames bypassing our tag driver, so
> > + * they lack an IFH. This will be garbage in garbage out and we want to
> > + * avoid this.
> > + */
> > + rtnl_lock();
> > + dev_v6 = __in6_dev_get(conduit);
> > + if (dev_v6) {
> > + WRITE_ONCE(dev_v6->cnf.disable_ipv6, 1);
> > + dev_warn(lan9645x->dev, "Disabled IPv6 on conduit device: %s\n",
> > + netdev_name(conduit));
> > + }
> > + rtnl_unlock();
> > +}
> > +
> > +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);
> > +
> > + /* Enabling IPv6 on the conduit will send frames directly on the
> > + * interface, without being intercepted by our tag driver. This causes a
> > + * GIGO situation.
> > + */
> > + disable_conduit_ipv6(lan9645x, cpu_port->conduit);
> > +
> > + /* 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. */
> > + 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(port));
> > +
> > + /* Make sure frames with src_port=CPU_PORT 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 + CPU_PORT));
> > +}
> > +
> > +void lan9645x_npi_port_deinit(struct lan9645x *lan9645x, int port)
> > +{
> > + struct lan9645x_port *p = lan9645x_to_port(lan9645x, port);
> > +
> > + lan9645x->npi = -1;
> > +
> > + lan_wr(QSYS_EXT_CPU_CFG_EXT_CPU_PORT_SET(0x1f) |
> > + 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));
> > +}
> > 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..3acc48f12fae
> > --- /dev/null
> > +++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_phylink.c
> > @@ -0,0 +1,537 @@
> > +// SPDX-License-Identifier: GPL-2.0+
> > +/* Copyright (C) 2026 Microchip Technology Inc.
> > + */
> > +
> > +#include <linux/phy.h>
> > +#include <linux/phy/phy.h>
> > +
> > +#include "lan9645x_main.h"
> > +
> > +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 | MAC_2500FD;
> > +
> > + switch (port) {
> > + case 0 ... 3:
> > + __set_bit(PHY_INTERFACE_MODE_GMII, c->supported_interfaces);
> > + break;
> > + case 4:
> > + __set_bit(PHY_INTERFACE_MODE_GMII, c->supported_interfaces);
> > + phy_interface_set_rgmii(c->supported_interfaces);
> > + break;
> > + case 5 ... 6:
> > + /* SerDes ports: QSGMII/SGMII/1000BASEX/2500BASEX modes
> > + * require PCS support which is not yet implemented.
> > + */
> > + 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 int lan9645x_rgmii_setup(struct lan9645x *lan9645x, int port, int speed,
> > + phy_interface_t mode)
> > +{
> > + bool tx_delay = false, rx_delay = false;
> > + u32 rx_idx, tx_idx;
> > + u8 tx_clk;
> > + int idx;
> > +
> > + /* Port 4 or 7 is RGMII_0 and port 8 is RGMII_1 */
> > + idx = port == 8 ? 1 : 0;
> > +
> > + if (!phy_interface_mode_is_rgmii(mode))
> > + return 0;
> > +
> > + 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(idx));
> > +
> > + /* We configure delays on the MAC side. When the PHY is not responsible
> > + * for delays, the MAC is, which is why RGMII_TXID results in
> > + * rx_delay=true.
> > + */
> > + if (mode == PHY_INTERFACE_MODE_RGMII ||
> > + mode == PHY_INTERFACE_MODE_RGMII_TXID)
> > + rx_delay = true;
> > +
> > + if (mode == PHY_INTERFACE_MODE_RGMII ||
> > + mode == PHY_INTERFACE_MODE_RGMII_RXID)
> > + tx_delay = true;
>
> I'm not sure everyone would agree on this interpretation.
>
> I would not let the MAC apply internal delays based upon phy-mode.
> I would parse "rx-internal-delay-ps" and "tx-internal-delay-ps" instead,
> and leave the phy-mode to signify a PHY-only description.
>
I looked at those, but I can only turn on/off the FSM, so I did not think it
would fit to accept the delay values. But I see other drivers just look for
{rx,tx}-internal-delay-ps > 0, I can do the same here.
> > +
> > + /* Setup DLL configuration. Register layout:
> > + * 0: RGMII_0_RX
> > + * 1: RGMII_0_TX
> > + * 2: RGMII_1_RX
> > + * 3: RGMII_1_TX
> > + * ...
> > + * (N<<1) RGMII_N_RX,
> > + * (N<<1)+1: RGMII_N_TX,
> > + */
> > + rx_idx = idx << 1;
> > + tx_idx = rx_idx + 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(rx_delay) |
> > + HSIO_DLL_CFG_DELAY_ENA_SET(rx_delay),
> > + HSIO_DLL_CFG_DLL_CLK_ENA |
> > + HSIO_DLL_CFG_DLL_RST |
> > + HSIO_DLL_CFG_DLL_ENA |
> > + HSIO_DLL_CFG_DELAY_ENA,
> > + 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(tx_delay) |
> > + HSIO_DLL_CFG_DELAY_ENA_SET(tx_delay),
> > + HSIO_DLL_CFG_DLL_CLK_ENA |
> > + HSIO_DLL_CFG_DLL_RST |
> > + HSIO_DLL_CFG_DLL_ENA |
> > + HSIO_DLL_CFG_DELAY_ENA,
> > + lan9645x, HSIO_DLL_CFG(tx_idx));
> > +
> > + return 0;
> > +}
> > +
> > +static void lan9645x_phylink_mac_config(struct lan9645x *lan9645x, int port,
> > + unsigned int mode,
> > + const struct phylink_link_state *state)
> > +{
> > + if (phy_interface_mode_is_rgmii(state->interface))
> > + lan9645x_rgmii_setup(lan9645x, port, state->speed,
> > + state->interface);
> > +}
> > +
> > +static int lan9645x_phylink_mac_prepare(struct lan9645x *lan9645x, int port,
> > + unsigned int mode,
> > + phy_interface_t iface)
> > +{
> > + switch (port) {
> > + case 0 ... 3:
> > + lan_rmw(HSIO_HW_CFG_GMII_ENA_SET(BIT(port)),
> > + HSIO_HW_CFG_GMII_ENA_SET(BIT(port)), lan9645x,
> > + HSIO_HW_CFG);
> > + break;
> > + case 4:
> > + lan_rmw(HSIO_HW_CFG_GMII_ENA_SET(BIT(port)),
> > + HSIO_HW_CFG_GMII_ENA_SET(BIT(port)), lan9645x,
> > + HSIO_HW_CFG);
> > +
> > + if (phy_interface_mode_is_rgmii(iface))
> > + lan_rmw(HSIO_HW_CFG_RGMII_0_CFG_SET(1),
> > + HSIO_HW_CFG_RGMII_0_CFG,
> > + lan9645x, HSIO_HW_CFG);
> > +
> > + break;
> > + case 7 ... 8:
> > + lan_rmw(HSIO_HW_CFG_GMII_ENA_SET(BIT(port)),
> > + HSIO_HW_CFG_GMII_ENA_SET(BIT(port)), lan9645x,
> > + HSIO_HW_CFG);
> > + break;
> > + default:
> > + /* Ports 5-6 are SerDes-only and need PCS support (not yet
> > + * implemented). They are excluded from phylink_get_caps.
> > + */
> > + return -EINVAL;
> > + }
> > +
> > + return 0;
> > +}
> > +
> > +static int lan9645x_port_is_cuphy(struct lan9645x *lan9645x, int port,
> > + phy_interface_t interface)
> > +{
> > + return port >= 0 && port <= 4 && interface == PHY_INTERFACE_MODE_GMII;
> > +}
> > +
> > +static void lan9645x_phylink_mac_link_up(struct lan9645x *lan9645x, int port,
> > + unsigned int link_an_mode,
> > + phy_interface_t interface,
> > + struct phy_device *phydev, int speed,
> > + int duplex, bool tx_pause,
> > + bool rx_pause)
> > +{
> > + struct lan9645x_port *p = lan9645x_to_port(lan9645x, port);
> > + int rx_ifg1, rx_ifg2, tx_ifg, gtx_clk = 0;
> > + struct lan9645x_path_delay *path_delay;
> > + int gspeed = LAN9645X_SPEED_DISABLED;
> > + int mode = 0;
> > + int fc_spd;
> > +
> > + /* Configure speed for RGMII modules. */
> > + if (phy_interface_mode_is_rgmii(interface))
> > + lan9645x_rgmii_setup(lan9645x, port, speed, interface);
> > +
> > + if (duplex == DUPLEX_FULL) {
> > + mode |= DEV_MAC_MODE_CFG_FDX_ENA_SET(1);
> > + rx_ifg2 = DEV_MAC_IFG_CFG_RX_IFG2_SET(0x2);
> > + tx_ifg = DEV_MAC_IFG_CFG_TX_IFG_SET(0x5);
> > +
> > + } else {
> > + rx_ifg2 = DEV_MAC_IFG_CFG_RX_IFG2_SET(0x2);
> > + tx_ifg = DEV_MAC_IFG_CFG_TX_IFG_SET(0x6);
> > + }
> > +
> > + switch (speed) {
> > + case SPEED_10:
> > + rx_ifg1 = DEV_MAC_IFG_CFG_RX_IFG1_SET(0x2);
> > + gspeed = LAN9645X_SPEED_10;
> > + break;
> > + case SPEED_100:
> > + rx_ifg1 = DEV_MAC_IFG_CFG_RX_IFG1_SET(0x1);
> > + gspeed = LAN9645X_SPEED_100;
> > + break;
> > + case SPEED_1000:
> > + gspeed = LAN9645X_SPEED_1000;
> > + mode |= DEV_MAC_MODE_CFG_GIGA_MODE_ENA_SET(1);
> > + mode |= DEV_MAC_MODE_CFG_FDX_ENA_SET(1);
> > + rx_ifg1 = DEV_MAC_IFG_CFG_RX_IFG1_SET(0x1);
> > + rx_ifg2 = DEV_MAC_IFG_CFG_RX_IFG2_SET(0x2);
> > + tx_ifg = DEV_MAC_IFG_CFG_TX_IFG_SET(0x6);
> > + gtx_clk = 1;
> > + break;
> > + case SPEED_2500:
> > + gspeed = LAN9645X_SPEED_2500;
> > + mode |= DEV_MAC_MODE_CFG_GIGA_MODE_ENA_SET(1);
> > + mode |= DEV_MAC_MODE_CFG_FDX_ENA_SET(1);
> > + rx_ifg1 = DEV_MAC_IFG_CFG_RX_IFG1_SET(0x1);
> > + rx_ifg2 = DEV_MAC_IFG_CFG_RX_IFG2_SET(0x2);
> > + tx_ifg = DEV_MAC_IFG_CFG_TX_IFG_SET(0x6);
> > + break;
> > + default:
> > + dev_err(lan9645x->dev, "Unsupported speed on port %d: %d\n",
> > + p->chip_port, speed);
> > + return;
> > + }
> > +
> > + p->speed = gspeed;
> > + p->duplex = duplex;
> > + fc_spd = lan9645x_speed_fc_enc(p->speed);
> > +
> > + if (phy_interface_num_ports(interface) == 4 ||
> > + interface == PHY_INTERFACE_MODE_SGMII)
> > + mode |= DEV_MAC_MODE_CFG_GIGA_MODE_ENA_SET(1);
> > +
> > + 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(tx_ifg | rx_ifg1 | 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));
> > +
> > + 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));
> > +
> > + 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(DEV_MAC_HDX_CFG_SEED_LOAD_SET(0),
> > + DEV_MAC_HDX_CFG_SEED_LOAD, lan9645x,
> > + DEV_MAC_HDX_CFG(p->chip_port));
> > +
> > + /* Set PFC link speed and enable map */
> > + lan_rmw(ANA_PFC_CFG_FC_LINK_SPEED_SET(fc_spd) |
> > + ANA_PFC_CFG_RX_PFC_ENA_SET(0),
> > + ANA_PFC_CFG_FC_LINK_SPEED |
> > + ANA_PFC_CFG_RX_PFC_ENA,
> > + lan9645x, ANA_PFC_CFG(p->chip_port));
> > +
> > + lan_rmw(DEV_PCS1G_CFG_PCS_ENA_SET(1),
> > + DEV_PCS1G_CFG_PCS_ENA, lan9645x,
> > + DEV_PCS1G_CFG(p->chip_port));
> > +
> > + lan_rmw(DEV_PCS1G_SD_CFG_SD_ENA_SET(0),
> > + DEV_PCS1G_SD_CFG_SD_ENA,
> > + lan9645x, DEV_PCS1G_SD_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));
> > +
> > + /* 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));
> > +
> > + /* 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));
> > +
> > + list_for_each_entry(path_delay, &p->path_delays, list) {
> > + if (path_delay->speed == speed) {
> > + lan_wr(path_delay->rx_delay + p->rx_delay,
> > + lan9645x, SYS_PTP_RXDLY_CFG(p->chip_port));
> > + lan_wr(path_delay->tx_delay,
> > + lan9645x, SYS_PTP_TXDLY_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 == 0 ? 1 : 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));
> > +
> > + lan_rmw(AFI_PORT_CFG_FC_SKIP_TTI_INJ_SET(0) |
> > + AFI_PORT_CFG_FRM_OUT_MAX_SET(16),
> > + AFI_PORT_CFG_FC_SKIP_TTI_INJ |
> > + AFI_PORT_CFG_FRM_OUT_MAX,
> > + lan9645x, AFI_PORT_CFG(p->chip_port));
> > +}
> > +
> > +void lan9645x_phylink_port_down(struct lan9645x *lan9645x, int port)
> > +{
> > + struct lan9645x_port *p = lan9645x_to_port(lan9645x, port);
> > + u32 val;
> > +
> > + /* 0.5: Disable any AFI */
> > + lan_rmw(AFI_PORT_CFG_FC_SKIP_TTI_INJ_SET(1) |
> > + AFI_PORT_CFG_FRM_OUT_MAX_SET(0),
> > + AFI_PORT_CFG_FC_SKIP_TTI_INJ |
> > + AFI_PORT_CFG_FRM_OUT_MAX,
> > + lan9645x, AFI_PORT_CFG(p->chip_port));
> > +
> > + /* wait for reg afi_port_frm_out to become 0 for the port */
> > + if (lan9645x_rd_poll_slow(lan9645x, AFI_PORT_FRM_OUT(p->chip_port),
> > + val,
> > + !AFI_PORT_FRM_OUT_FRM_OUT_CNT_GET(val)))
> > + dev_err(lan9645x->dev, "AFI timeout chip port %u",
> > + p->chip_port);
> > +
> > + /* 2: 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));
> > +
> > + /* 1: Reset the PCS Rx clock domain */
> > + lan_rmw(DEV_CLOCK_CFG_PCS_RX_RST_SET(1),
> > + DEV_CLOCK_CFG_PCS_RX_RST,
> > + lan9645x, DEV_CLOCK_CFG(p->chip_port));
> > +
> > + p->speed = LAN9645X_SPEED_DISABLED;
> > + p->duplex = DUPLEX_UNKNOWN;
> > +
> > + /* 3: 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));
> > +
> > + /* 4: 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));
> > +
> > + /* 5: Disable Flowcontrol */
> > + lan_rmw(SYS_PAUSE_CFG_PAUSE_ENA_SET(0),
> > + SYS_PAUSE_CFG_PAUSE_ENA,
> > + lan9645x, SYS_PAUSE_CFG(p->chip_port));
> > +
> > + /* 5.1: Disable PFC */
> > + lan_rmw(QSYS_SW_PORT_MODE_TX_PFC_ENA_SET(0),
> > + QSYS_SW_PORT_MODE_TX_PFC_ENA,
> > + lan9645x, QSYS_SW_PORT_MODE(p->chip_port));
> > +
> > + /* 6: Wait a worst case time 8ms (10K jumbo/10Mbit) */
> > + usleep_range(8 * USEC_PER_MSEC, 9 * USEC_PER_MSEC);
> > +
> > + /* 7: 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));
> > +
> > + /* 8: 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));
> > +
> > + /* 9: 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));
> > +
> > + /* 10: 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", port);
> > +
> > + /* 11: 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));
> > +
> > + /* 12: 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));
> > +
> > + /* 13: Clear flushing */
> > + lan_rmw(QSYS_SW_PORT_MODE_AGING_MODE_SET(1),
> > + QSYS_SW_PORT_MODE_AGING_MODE,
> > + lan9645x, QSYS_SW_PORT_MODE(p->chip_port));
> > +}
> > +
> > +static void lan9645x_phylink_mac_link_down(struct lan9645x *lan9645x, int port,
> > + unsigned int link_an_mode,
> > + phy_interface_t interface)
> > +{
> > + struct lan9645x_port *p = lan9645x_to_port(lan9645x, port);
> > +
> > + lan9645x_phylink_port_down(lan9645x, port);
> > +
> > + /* 14: Take PCS out of reset */
> > + lan_rmw(DEV_CLOCK_CFG_PCS_RX_RST_SET(0) |
> > + DEV_CLOCK_CFG_PCS_TX_RST_SET(0),
> > + DEV_CLOCK_CFG_PCS_RX_RST |
> > + DEV_CLOCK_CFG_PCS_TX_RST,
> > + lan9645x, DEV_CLOCK_CFG(p->chip_port));
> > +}
> > +
> > +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 void
> > +lan9645x_port_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);
> > +
> > + lan9645x_phylink_mac_config(p->lan9645x, p->chip_port, mode, state);
>
> Why the separate functions here and in all other phylink_mac_ops implementations?
>
It is not needed, I will skip the indirection.
> > +}
> > +
> > +static void lan9645x_port_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);
> > +
> > + lan9645x_phylink_mac_link_up(p->lan9645x, p->chip_port, link_an_mode,
> > + interface, phydev, speed, duplex, tx_pause,
> > + rx_pause);
> > +}
> > +
> > +static void lan9645x_port_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);
> > +
> > + lan9645x_phylink_mac_link_down(p->lan9645x, p->chip_port, link_an_mode,
> > + interface);
> > +}
> > +
> > +static int lan9645x_port_phylink_mac_prepare(struct phylink_config *config,
> > + unsigned int mode,
> > + phy_interface_t iface)
> > +{
> > + struct lan9645x_port *p = lan9645x_phylink_config_to_port(config);
> > +
> > + return lan9645x_phylink_mac_prepare(p->lan9645x, p->chip_port, mode,
> > + iface);
> > +}
> > +
> > +const struct phylink_mac_ops lan9645x_phylink_mac_ops = {
> > + .mac_config = lan9645x_port_phylink_mac_config,
> > + .mac_link_up = lan9645x_port_phylink_mac_link_up,
> > + .mac_link_down = lan9645x_port_phylink_mac_link_down,
> > + .mac_prepare = lan9645x_port_phylink_mac_prepare,
> > +};
> > 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..038868ae0a32
> > --- /dev/null
> > +++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_port.c
> > @@ -0,0 +1,289 @@
> > +// SPDX-License-Identifier: GPL-2.0+
> > +/* Copyright (C) 2026 Microchip Technology Inc.
> > + */
> > +
> > +#include "lan9645x_main.h"
> > +
> > +int lan9645x_port_init(struct lan9645x *lan9645x, int port)
> > +{
> > + struct lan9645x_port *p;
> > +
> > + p = lan9645x_to_port(lan9645x, port);
> > +
> > + /* 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);
> > +
> > + lan9645x_phylink_port_down(lan9645x, port);
> > +
> > + if (phy_interface_num_ports(p->phy_mode) == 4)
> > + lan_rmw(DEV_CLOCK_CFG_PCS_RX_RST_SET(0) |
> > + DEV_CLOCK_CFG_PCS_TX_RST_SET(0),
> > + DEV_CLOCK_CFG_PCS_RX_RST |
> > + DEV_CLOCK_CFG_PCS_TX_RST,
> > + lan9645x, DEV_CLOCK_CFG(p->chip_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));
> > +
> > + /* Enable receiving frames on the port, and activate auto-learning of
> > + * MAC addresses.
> > + */
> > + 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 |
>
> How does ANA_PORT_CFG_LEARN_ENA (disabled above) interact with
> ANA_PORT_CFG_LEARNAUTO (enabled here)?
>
> Judging from the comments only, this configuration seems pretty
> inconsistent. Non-bridged user ports should have address learning turned
> off.
>
If LEARN_ENA is cleared, LEARNAUTO is ignored. I will add a comment about this.
> > + ANA_PORT_CFG_RECV_ENA |
> > + ANA_PORT_CFG_PORTID_VAL,
> > + lan9645x, ANA_PORT_CFG(p->chip_port));
> > +
> > + return 0;
> > +}
> > +
> > +void lan9645x_port_cpu_init(struct lan9645x *lan9645x)
> > +{
> > + /* Map the 8 CPU extraction queues to CPU port 9 (datasheet is wrong) */
> > + lan_wr(0, lan9645x, QSYS_CPU_GROUP_MAP);
> > +
> > + /* Configure second cpu port (chip_port 10) for manual frame injection.
> > + * The AFI can not inject frames via the NPI port, unless frame aging is
> > + * disabled on frontports, so we use manual injection for AFI frames.
> > + */
>
> AFI stands for? And what is it used for?
>
It stands for Automatric Frame Injector. It can inject frames in either a timer
triggered manner or delay triggered, with possibility to modify it with the
rewriter. It it not used here, but would be used for MRP for instance.
I will remove this.
> > +
> > + /* Set min-spacing of EOF to SOF on injected frames to 0, on cpu device
> > + * 1. This is required when injecting with IFH.
> > + * Default values emulates delay of std preamble/IFG setting on a front
> > + * port.
> > + */
> > + lan_rmw(QS_INJ_CTRL_GAP_SIZE_SET(0),
> > + QS_INJ_CTRL_GAP_SIZE,
> > + lan9645x, QS_INJ_CTRL(1));
> > +
> > + /* Injection: Mode: manual injection | Byte_swap */
> > + lan_wr(QS_INJ_GRP_CFG_MODE_SET(1) |
> > + QS_INJ_GRP_CFG_BYTE_SWAP_SET(1),
> > + lan9645x, QS_INJ_GRP_CFG(1));
> > +
> > + lan_rmw(QS_INJ_CTRL_GAP_SIZE_SET(0),
> > + QS_INJ_CTRL_GAP_SIZE,
> > + lan9645x, QS_INJ_CTRL(1));
> > +
> > + lan_wr(SYS_PORT_MODE_INCL_INJ_HDR_SET(1),
> > + lan9645x, SYS_PORT_MODE(CPU_PORT + 1));
> > +
> > + /* 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(BIT(CPU_PORT)),
> > + QSYS_EGR_NO_SHARING_EGR_NO_SHARING_SET(BIT(CPU_PORT)),
> > + 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(BIT(CPU_PORT)),
> > + QSYS_EGR_DROP_MODE_EGRESS_DROP_MODE_SET(BIT(CPU_PORT)),
> > + lan9645x, QSYS_EGR_DROP_MODE);
> > +
> > + lan_wr(BIT(CPU_PORT), lan9645x, ANA_PGID(PGID_CPU));
> > +
> > + lan_rmw(ANA_PORT_CFG_PORTID_VAL_SET(CPU_PORT) |
> > + ANA_PORT_CFG_RECV_ENA_SET(1),
> > + ANA_PORT_CFG_PORTID_VAL |
> > + ANA_PORT_CFG_RECV_ENA, lan9645x,
> > + ANA_PORT_CFG(CPU_PORT));
> > +
> > + /* 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(CPU_PORT));
> > +
> > + /* Transmit cpu frames as received without any tagging, timing or other
> > + * updates. This does not affect CPU-over-NPI, only manual extraction.
> > + * On the NPI port we need NO_REWRITE=0 for HSR/PRP.
> > + */
> > + lan_wr(REW_PORT_CFG_NO_REWRITE_SET(1),
> > + lan9645x, REW_PORT_CFG(CPU_PORT));
> > +}
> > +
> > +void lan9645x_port_set_tail_drop_wm(struct lan9645x *lan9645x)
> > +{
> > + int shared_per_port;
> > + int port;
> > +
> > + /* Configure tail dropping watermark */
> > + shared_per_port =
> > + lan9645x->shared_queue_sz / (lan9645x->num_phys_ports + 1);
> > +
> > + /* The total memory size is diveded by number of front ports plus CPU
>
> divided
>
> > + * port.
> > + */
> > + lan9645x_for_each_chipport(lan9645x, port)
> > + lan_wr(lan9645x_wm_enc(shared_per_port), lan9645x,
> > + SYS_ATOP(port));
> > +
> > + /* Tail dropping active based only on per port ATOP wm */
> > + lan_wr(lan9645x_wm_enc(lan9645x->shared_queue_sz), lan9645x,
> > + SYS_ATOP_TOT_CFG);
> > +}
> > +
> > +int lan9645x_port_set_maxlen(struct lan9645x *lan9645x, int port, size_t sdu)
> > +{
> > + struct lan9645x_port *p = lan9645x_to_port(lan9645x, port);
> > +
>
> No blank lines between local variable declarations.
>
> > + int maxlen = sdu + ETH_HLEN + ETH_FCS_LEN;
> > +
> > + if (port == lan9645x->npi) {
> > + maxlen += LAN9645X_IFH_LEN;
> > + 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 int lan9645x_port_setup_leds(struct lan9645x *lan9645x,
> > + struct fwnode_handle *portnp, int port)
> > +{
> > + u32 drive_mode;
> > + int err;
> > +
> > + err = fwnode_property_read_u32(portnp, "microchip,led-drive-mode",
> > + &drive_mode);
> > + if (err)
> > + return err;
>
> This property is mandatory? 1. the schema doesn't put it in "required",
> and 2. why would it be mandatory?
>
No that is a mistake, the error is ignored now. Now this be reworked.
> > +
> > + lan_rmw(CHIP_TOP_CUPHY_LED_CFG_LED_DRIVE_MODE_SET(drive_mode),
> > + CHIP_TOP_CUPHY_LED_CFG_LED_DRIVE_MODE, lan9645x,
> > + CHIP_TOP_CUPHY_LED_CFG(port));
> > +
> > + return 0;
> > +}
> > +
> > +static int lan9645x_port_parse_delays(struct lan9645x_port *port,
> > + struct fwnode_handle *portnp)
> > +{
> > + struct fwnode_handle *delay;
> > + int err;
> > +
> > + INIT_LIST_HEAD(&port->path_delays);
> > +
> > + fwnode_for_each_available_child_node(portnp, delay) {
> > + struct lan9645x_path_delay *path_delay;
> > + s32 tx_delay;
> > + s32 rx_delay;
> > + u32 speed;
> > +
> > + err = fwnode_property_read_u32(delay, "speed", &speed);
> > + if (err)
> > + return err;
> > +
> > + err = fwnode_property_read_u32(delay, "rx_delay", &rx_delay);
> > + if (err)
> > + return err;
> > +
> > + err = fwnode_property_read_u32(delay, "tx_delay", &tx_delay);
> > + if (err)
> > + return err;
> > +
> > + path_delay = devm_kzalloc(port->lan9645x->dev,
> > + sizeof(*path_delay), GFP_KERNEL);
> > + if (!path_delay)
> > + return -ENOMEM;
> > +
> > + path_delay->rx_delay = rx_delay;
> > + path_delay->tx_delay = tx_delay;
> > + path_delay->speed = speed;
> > + list_add_tail(&path_delay->list, &port->path_delays);
>
> Show prior art about having such device tree properties, or defer this
> feature to a separate series where the PTP maintainer is also on CC, and
> properly explain your end goal and reasoning process towards this solution.
>
I will remove the path delay code.
> > + }
> > +
> > + return 0;
> > +}
> > +
> > +int lan9645x_port_parse_ports_node(struct lan9645x *lan9645x)
> > +{
> > + struct fwnode_handle *ports, *portnp;
> > + struct device *dev = lan9645x->dev;
> > + int max_ports, num_ports = 0;
> > + int err = 0;
> > +
> > + max_ports = NUM_PHYS_PORTS - lan9645x->num_port_dis;
> > +
> > + ports = device_get_named_child_node(dev, "ethernet-ports");
> > + if (!ports)
> > + ports = device_get_named_child_node(dev, "ports");
> > + if (!ports) {
> > + dev_err(dev, "no ethernet-ports or ports child found\n");
> > + return -ENODEV;
> > + }
> > +
> > + fwnode_for_each_available_child_node(ports, portnp) {
> > + int phy_mode;
> > + u32 p;
> > +
> > + num_ports++;
> > +
> > + if (num_ports > max_ports) {
> > + dev_err(dev,
> > + "Too many ports in device tree. Max ports supported by SKU: %d\n",
> > + max_ports);
> > + err = -ENODEV;
> > + goto err_free_ports;
> > + }
> > +
> > + if (fwnode_property_read_u32(portnp, "reg", &p)) {
> > + dev_err(dev, "Port number not defined in device tree (property \"reg\")\n");
> > + err = -ENODEV;
> > + fwnode_handle_put(portnp);
> > + goto err_free_ports;
> > + }
> > +
> > + if (p >= lan9645x->num_phys_ports) {
> > + dev_err(dev,
> > + "Port number in device tree is invalid %u (property \"reg\")\n",
> > + p);
> > + err = -ENODEV;
> > + fwnode_handle_put(portnp);
> > + goto err_free_ports;
> > + }
> > +
> > + phy_mode = fwnode_get_phy_mode(portnp);
> > + if (phy_mode < 0) {
> > + dev_err(dev, "Failed to read phy-mode for port %u", p);
> > + err = -ENODEV;
> > + fwnode_handle_put(portnp);
> > + goto err_free_ports;
> > + }
> > +
> > + lan9645x->ports[p]->phy_mode = phy_mode;
>
> I have pending patches which remove this coding pattern from the ocelot
> driver. Please work with the phy_mode that phylink gives you in mac_config.
> Do not expect it to remain fixed throughout the lifetime of the device.
>
Ok I will rework this.
> > + lan9645x_port_parse_delays(lan9645x->ports[p], portnp);
> > + lan9645x_port_setup_leds(lan9645x, portnp, p);
>
> FWIW you can also implement .port_setup(), get dp = dsa_to_port(ds, port),
> and just call lan9645x_port_setup_leds(dp->dn). You don't need the
> complicated iteration over ports starting from the root switch node.
>
Thank you I will look into that. I thought port_setup was only for devlink
related configuration.
> > + }
> > +
> > +err_free_ports:
> > + fwnode_handle_put(ports);
> > + return err;
> > +}
> > diff --git a/drivers/net/ethernet/microchip/Kconfig b/drivers/net/ethernet/microchip/Kconfig
> > index ee046468652c..740f3c2e8199 100644
> > --- a/drivers/net/ethernet/microchip/Kconfig
> > +++ b/drivers/net/ethernet/microchip/Kconfig
> > @@ -62,5 +62,6 @@ source "drivers/net/ethernet/microchip/lan966x/Kconfig"
> > source "drivers/net/ethernet/microchip/sparx5/Kconfig"
> > source "drivers/net/ethernet/microchip/vcap/Kconfig"
> > source "drivers/net/ethernet/microchip/fdma/Kconfig"
> > +source "drivers/net/dsa/microchip/lan9645x/Kconfig"
>
> The Kconfig of DSA drivers needs to be pulled in through drivers/net/dsa/Kconfig.
> Put the proper "depends on" statements in place, if you're missing any.
>
I will move this.
> >
> > endif # NET_VENDOR_MICROCHIP
> >
> > --
> > 2.52.0
> >
>
Thank you for the comments,
Emil
© 2016 - 2026 Red Hat, Inc.