[PATCH 05/17] net: add r9a06g032 A5PSW switch driver

Ralph Siemsen <[email protected]> Fri, 31 Jul 2026 12:33:34 -0400
Newsgroups gmane.comp.boot-loaders.u-boot
Message-ID <[email protected]>
Driver for the Advanced 5-port Ethernet switch (A5PSW) on RZ/N1 SoC.

The A5PSW switch is modeled using DSA framework, but without actually
tagging packets. The goal is just to support network booting over a
single Ethernet port.

This driver is loosely based on the Linux driver by Clément Léger
888cdb892b61 ("net: dsa: rzn1-a5psw: add Renesas RZ/N1 advanced 5 port
switch driver") but without any of the VLAN tagging or other features.

Signed-off-by: Ralph Siemsen <[email protected]>
---
 drivers/net/Kconfig      |   6 +
 drivers/net/Makefile     |   1 +
 drivers/net/rzn1_a5psw.c | 377 +++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 384 insertions(+)

diff --git a/drivers/net/Kconfig b/drivers/net/Kconfig
index faa08e9ebba..edc4cb29598 100644
--- a/drivers/net/Kconfig
+++ b/drivers/net/Kconfig
@@ -880,6 +880,12 @@ config RENESAS_RAVB
 	  This driver implements support for the Ethernet AVB block in
 	  several Renesas R-Car and RZ SoCs.
 
+config RZN1_A5PSW
+	bool "Renesas RZ/N1 A5PSW ethernet switch"
+	help
+	  Support the Advanced 5-Port ethernet switch (A5PSW) found in
+	  the Renesas RZ/N1 SoC.
+
 config MPC8XX_FEC
 	bool "Fast Ethernet Controller on MPC8XX"
 	depends on MPC8xx
diff --git a/drivers/net/Makefile b/drivers/net/Makefile
index 4c1f4094ab2..b8f270ae233 100644
--- a/drivers/net/Makefile
+++ b/drivers/net/Makefile
@@ -94,6 +94,7 @@ obj-$(CONFIG_PCNET) += pcnet.o
 obj-$(CONFIG_PIC32_ETH) += pic32_mdio.o pic32_eth.o
 obj-$(CONFIG_RENESAS_ETHER_SWITCH) += rswitch.o
 obj-$(CONFIG_RENESAS_RAVB) += ravb.o
+obj-$(CONFIG_RZN1_A5PSW) += rzn1_a5psw.o
 obj-$(CONFIG_RTL8139) += rtl8139.o
 obj-$(CONFIG_RTL8169) += rtl8169.o
 obj-$(CONFIG_SH_ETHER) += sh_eth.o
diff --git a/drivers/net/rzn1_a5psw.c b/drivers/net/rzn1_a5psw.c
new file mode 100644
index 00000000000..2733ebcdfbb
--- /dev/null
+++ b/drivers/net/rzn1_a5psw.c
@@ -0,0 +1,377 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Adapted from Linux drivers/net/dsa/rzn1_a5psw.c
+ * but without DSA tag support.
+ */
+
+#include <clk.h>
+#include <dm/device.h>
+#include <dm/device_compat.h>
+#include <dm/device-internal.h>
+#include <dm/lists.h>
+#include <dm/of_extra.h>
+#include <linux/bitfield.h>
+#include <linux/delay.h>
+#include <miiphy.h>
+#include <net/dsa.h>
+#include <syscon.h>
+#include <regmap.h>
+#include <reset.h>
+
+struct rzn1_a5psw_plat {
+	struct clk_bulk bulk;
+	struct regmap *regmap;
+	struct udevice *miic;
+	struct regmap *syscon;
+};
+
+/* MoreThanIP 5pt Switch regs */
+#define MT5PT_REVISION		0x0
+#define MT5PT_SCRATCH		0x4
+#define MT5PT_PORT_ENA		0x8
+#define  MT5PT_PORT_ENA_TX(x)		BIT(16 + (x))
+#define  MT5PT_PORT_ENA_RX(x)		BIT(x)
+#define  MT5PT_PORT_ENA_TXRX(x)		((MT5PT_PORT_ENA_TX(0) | MT5PT_PORT_ENA_RX(0)) << (x))
+#define MT5PT_AUTH_PORT(x)	(0x240 + (x) * 4)
+#define  MT5PT_AUTH_PORT_AUTHORIZED	BIT(0)
+#define  MT5PT_AUTH_PORT_CONTROLLED	BIT(1)
+#define  MT5PT_AUTH_PORT_EAPOL_EN	BIT(2)
+#define  MT5PT_AUTH_PORT_GUEST		BIT(3)
+#define  MT5PT_AUTH_PORT_EAPOL_PORT(x)	((x) << 12)
+#define MT5PT_MDIO_CFG_STATUS	0x700
+#define MT5PT_MDIO_COMMAND	0x704
+#define MT5PT_MDIO_DATA		0x708
+#define MT5PT_MAC_CMD_CFGn(x)	(0x808 + (x) * 0x400)
+#define  MT5PT_TX_ENA			BIT(0)
+#define  MT5PT_RX_ENA			BIT(1)
+#define  MT5PT_MBPS_1000		BIT(3)
+#define  MT5PT_HALF_DUPLEX		BIT(10)
+#define MT5PT_MAC_FRM_LENGTHn(x)	(0x814 + (x) * 0x400)
+#define MT5PT_DUMMY_NOOP	0x5000
+
+static void rzn1_switch_write(struct regmap *regmap, uint offset, uint val)
+{
+	/*
+	 * MTIP switch errata:
+	 * There is a clock-crossing issue within the switch,
+	 * which can cause writes to be corrupted.
+	 * To avoid the problem, perform a dummy write with the
+	 * same data value, prior to the actual write.
+	 */
+	regmap_write(regmap, MT5PT_DUMMY_NOOP, val);
+	regmap_write(regmap, offset, val);
+}
+
+static void rzn1_switch_port_speed_duplex(struct udevice *dev, int port, int speed, int duplex)
+{
+	struct rzn1_a5psw_plat *plat = dev_get_plat(dev);
+	struct regmap *regmap = plat->regmap;
+	u32 val;
+
+	regmap_read(regmap, MT5PT_MAC_CMD_CFGn(port), &val);
+	val &= ~(MT5PT_HALF_DUPLEX | MT5PT_MBPS_1000);
+	if (speed == SPEED_1000)
+		val |= MT5PT_MBPS_1000;
+	if (duplex == DUPLEX_HALF)
+		val |= MT5PT_HALF_DUPLEX;
+
+	rzn1_switch_write(regmap, MT5PT_MAC_CMD_CFGn(port), val);
+}
+
+static void rzn1_switch_port_enable(struct udevice *dev, int port)
+{
+	struct rzn1_a5psw_plat *plat = dev_get_plat(dev);
+	struct regmap *regmap = plat->regmap;
+	u32 val;
+
+	regmap_read(regmap, MT5PT_AUTH_PORT(port), &val);
+	val |= MT5PT_AUTH_PORT_AUTHORIZED;
+	rzn1_switch_write(regmap, MT5PT_AUTH_PORT(port), val);
+
+	regmap_read(regmap, MT5PT_PORT_ENA, &val);
+	val |= MT5PT_PORT_ENA_TXRX(port);
+	rzn1_switch_write(regmap, MT5PT_PORT_ENA, val);
+
+	/* Max frame size */
+	rzn1_switch_write(regmap, MT5PT_MAC_FRM_LENGTHn(port), 9224);
+}
+
+static void rzn1_switch_port_disable(struct udevice *dev, int port)
+{
+	struct rzn1_a5psw_plat *plat = dev_get_plat(dev);
+	struct regmap *regmap = plat->regmap;
+	u32 val;
+
+	regmap_read(regmap, MT5PT_AUTH_PORT(port), &val);
+	val &= ~MT5PT_AUTH_PORT_AUTHORIZED;
+	rzn1_switch_write(regmap, MT5PT_AUTH_PORT(port), val);
+
+	regmap_read(regmap, MT5PT_PORT_ENA, &val);
+	val &= ~MT5PT_PORT_ENA_TXRX(port);
+	rzn1_switch_write(regmap, MT5PT_PORT_ENA, val);
+}
+
+static int rzn1_phy_wait(struct udevice *dev)
+{
+	struct rzn1_a5psw_plat *plat = dev_get_plat(dev->parent);
+	struct regmap *regmap = plat->regmap;
+	u32 timeout = 100;
+	int ret;
+	u32 val;
+
+	do {
+		ret = regmap_read(regmap, MT5PT_MDIO_CFG_STATUS, &val);
+		if (ret == 0 && (val & BIT(0)) == 0)
+			return 0;
+
+		mdelay(1);
+	} while (--timeout);
+
+	return -ETIMEDOUT;
+}
+
+static int rzn1_a5psw_mdio_read(struct udevice *dev, int addr, int devad, int reg)
+{
+	struct rzn1_a5psw_plat *plat = dev_get_plat(dev->parent);
+	struct regmap *regmap = plat->regmap;
+	int ret;
+	u32 val = BIT(15) | ((addr & 0x1f) << 5) | (reg & 0x1f);
+
+	/* Clause 22 only */
+	rzn1_switch_write(regmap, MT5PT_MDIO_COMMAND, val);
+
+	ret = rzn1_phy_wait(dev);
+	if (ret) {
+		dev_warn(dev, "PHY read timeout\n");
+		return ret;
+	}
+
+	ret = regmap_read(regmap, MT5PT_MDIO_DATA, &val);
+	if (ret) {
+		dev_warn(dev, "PHY read error\n");
+		return ret;
+	}
+	val &= 0xffff;
+
+	// TODO: check CFG_STATUS for READERR
+
+	dev_dbg(dev, "addr=0x%x reg=0x%x read 0x%04x\n", addr, reg, val);
+
+	return val;
+}
+
+static int rzn1_a5psw_mdio_write(struct udevice *dev, int addr, int devad,
+				 int reg, u16 val)
+{
+	struct rzn1_a5psw_plat *plat = dev_get_plat(dev->parent);
+	struct regmap *regmap = plat->regmap;
+	int ret;
+	u32 cmd = ((addr & 0x1f) << 5) | (reg & 0x1f);
+
+	dev_dbg(dev, "addr=0x%x reg=0x%x write 0x%04x\n", addr, reg, val);
+
+	/* Clause 22 only */
+	rzn1_switch_write(regmap, MT5PT_MDIO_COMMAND, cmd);
+	rzn1_switch_write(regmap, MT5PT_MDIO_DATA, val);
+
+	ret = rzn1_phy_wait(dev);
+	if (ret) {
+		dev_warn(dev, "PHY write timeout\n");
+		return ret;
+	}
+
+	return 0;
+}
+
+static const struct mdio_ops rzn1_a5psw_mdio_ops = {
+	.read = rzn1_a5psw_mdio_read,
+	.write = rzn1_a5psw_mdio_write,
+};
+
+static int rzn1_a5psw_mdio_bind(struct udevice *dev)
+{
+	char name[32];
+	static int num_devices;
+
+	sprintf(name, "rzn1-a5psw-mdio-%d", num_devices++);
+	device_set_name(dev, name);
+
+	return 0;
+}
+
+U_BOOT_DRIVER(rzn1_a5psw_mdio) = {
+	.name		= "rzn1_a5psw_mdio",
+	.id		= UCLASS_MDIO,
+	.ops		= &rzn1_a5psw_mdio_ops,
+	.bind		= rzn1_a5psw_mdio_bind,
+	.plat_auto	= sizeof(struct mdio_perdev_priv),
+};
+
+static int rzn1_a5psw_port_probe(struct udevice *dev, int port, struct phy_device *phy)
+{
+	/* Called once when switch is probed */
+	return phy_config(phy);
+}
+
+void rzn1_rgmii_rmii_conv_speed(struct udevice *dev, int phy,
+				int full_duplex, int speed);
+
+static int rzn1_a5psw_port_enable(struct udevice *dev, int port, struct phy_device *phy)
+{
+	struct rzn1_a5psw_plat *plat = dev_get_plat(dev);
+
+	/* CPU port is already enabled */
+	if (port == 4)
+		return 0;
+
+	/* Get speed/duplex from PHY */
+	genphy_update_link(phy);
+	if (!phy->link) {
+		dev_err(dev, "PHY %s no ethernet link\n", phy->dev->name);
+		return 0;
+	}
+	genphy_parse_link(phy);
+
+	/* Program the converter accoringly */
+	dev_info(dev, "MIIC = %p\n", plat->miic);
+	rzn1_rgmii_rmii_conv_speed(plat->miic, 4 - port, phy->duplex, phy->speed);
+
+	/* Program the switch port accordingly */
+	dev_info(dev, "%s port=%d using speed=%d\n", __func__, port, phy->speed);
+	rzn1_switch_port_speed_duplex(dev, port, phy->speed, phy->duplex);
+	rzn1_switch_port_enable(dev, port);
+	return 0;
+}
+
+static void rzn1_a5psw_port_disable(struct udevice *dev, int port, struct phy_device *phy)
+{
+	/* CPU port is already enabled */
+	if (port == 4)
+		return;
+
+	rzn1_switch_port_disable(dev, port);
+}
+
+static const struct dsa_ops rzn1_a5psw_dsa_ops = {
+	.port_probe = rzn1_a5psw_port_probe,
+	.port_enable = rzn1_a5psw_port_enable,
+	.port_disable = rzn1_a5psw_port_disable,
+};
+
+static int rzn1_a5psw_probe_mdio(struct udevice *dev)
+{
+	struct udevice *mdev;
+	const char *name;
+	ofnode node;
+	int ret;
+
+	/* bind phy ports of mdio child node to rzn1_a5psw_mdio device */
+	node = dev_read_subnode(dev, "mdio");
+	if (!ofnode_valid(node))
+		return 0;
+
+	name = ofnode_get_name(node);
+	ret = device_bind_driver_to_node(dev,
+					 "rzn1_a5psw_mdio",
+					 name, node, NULL);
+	if (ret) {
+		dev_err(dev, "failed to bind %s: %d\n", name, ret);
+	} else {
+		/* need to probe it as there is no compatible to do so */
+		ret = uclass_get_device_by_ofnode(UCLASS_MDIO, node, &mdev);
+		if (ret)
+			dev_err(dev, "failed to probe %s: %d\n", name, ret);
+	}
+
+	return ret;
+}
+
+static int rzn1_a5psw_probe(struct udevice *dev)
+{
+	struct rzn1_a5psw_plat *plat = dev_get_plat(dev);
+	int ret;
+
+	dev_dbg(dev, "%s:%d\n", __func__, __LINE__);
+
+	/* Enable clocks */
+	ret = clk_get_bulk(dev, &plat->bulk);
+	if (ret) {
+		dev_err(dev, "Failed to get clocks\n");
+		return ret;
+	}
+	ret = clk_enable_bulk(&plat->bulk);
+	if (ret) {
+		dev_err(dev, "Failed to enable clocks\n");
+		return ret;
+	}
+
+	/* Access the registers */
+	ret = regmap_init_mem(dev_ofnode(dev), &plat->regmap);
+	if (ret) {
+		dev_err(dev, "Failed to get regmap\n");
+		return ret;
+	}
+
+	/* Find the MIIC driver */
+	ret = uclass_get_device_by_name(UCLASS_MISC, "eth-miic@44030000", &plat->miic);
+	if (ret) {
+		dev_err(dev, "Failed to get MIIC\n");
+		return ret;
+	}
+	dev_dbg(dev, "got MIIC %p <%s>\n", plat->miic, plat->miic->name);
+
+	/* Upstream port is always 1Gbps */
+	rzn1_switch_port_speed_duplex(dev, 4, SPEED_1000, DUPLEX_FULL);
+	rzn1_switch_port_enable(dev, 4);
+
+	/* Some extra resets are located in system controller */
+	plat->syscon = syscon_regmap_lookup_by_phandle(dev, "syscon");
+	if (IS_ERR(plat->syscon)) {
+		dev_err(dev, "No syscon node found\n");
+		return PTR_ERR(plat->syscon);
+	}
+
+	/* Clear ETH and CLK25 resets */
+#define RZN1_SYSCTRL_PWRCTRL_SWITCH 0x188
+	regmap_update_bits(plat->syscon, RZN1_SYSCTRL_PWRCTRL_SWITCH,
+			   BIT(4) | BIT(3), BIT(4) | BIT(3));
+
+	/* Reset all PHYs */
+	struct gpio_desc phy_reset_gpio;
+	u32 phy_reset_delay;
+
+	ret = gpio_request_by_name(dev, "phy-reset-gpios", 0,
+				   &phy_reset_gpio, GPIOD_IS_OUT | GPIOD_IS_OUT_ACTIVE);
+	if (ret) {
+		dev_err(dev, "failed to get phy-reset-gpios: %d\n", ret);
+		return ret;
+	}
+
+	phy_reset_delay = dev_read_u32_default(dev, "phy-reset-duration", 15);
+
+	dm_gpio_set_value(&phy_reset_gpio, 1);
+	mdelay(phy_reset_delay);
+	dm_gpio_set_value(&phy_reset_gpio, 0);
+
+	ret = rzn1_a5psw_probe_mdio(dev);
+	if (ret) {
+		dev_warn(dev, "mdio probe failed\n");
+		return ret;
+	}
+
+	return 0;
+}
+
+static const struct udevice_id rzn1_a5psw_ids[] = {
+	{ .compatible = "renesas,rzn1-a5psw" },
+	{ }
+};
+
+U_BOOT_DRIVER(rzn1_a5psw) = {
+	.name		= "rzn1_a5psw",
+	.id		= UCLASS_DSA,
+	.of_match	= rzn1_a5psw_ids,
+	.probe		= rzn1_a5psw_probe,
+	.ops		= &rzn1_a5psw_dsa_ops,
+	.plat_auto	= sizeof(struct rzn1_a5psw_plat),
+};

-- 
2.55.0