diff mbox series

[net-next,06/12] net: dsa: rzn1-a5psw: add Renesas RZ/N1 advanced 5 port switch driver

Message ID 20220414122250.158113-7-clement.leger@bootlin.com (mailing list archive)
State Superseded
Delegated to: Geert Uytterhoeven
Headers show
Series add support for Renesas RZ/N1 ethernet subsystem devices | expand

Commit Message

Clément Léger April 14, 2022, 12:22 p.m. UTC
Add Renesas RZ/N1 advanced 5 port switch driver. This switch handles 5
ports including 1 CPU management port. A MDIO bus is also exposed by
this switch and allows to communicate with PHYs connected to the ports.
Each switch port (except for the CPU management ports) are connected to
the MII converter.

This driver include basic bridging support, more support will be added
later (vlan, etc).

Suggested-by: Laurent Gonzales <laurent.gonzales@non.se.com>
Suggested-by: Jean-Pierre Geslin <jean-pierre.geslin@non.se.com>
Suggested-by: Phil Edworthy <phil.edworthy@renesas.com>
Signed-off-by: Clément Léger <clement.leger@bootlin.com>
---
 drivers/net/dsa/Kconfig      |   9 +
 drivers/net/dsa/Makefile     |   2 +
 drivers/net/dsa/rzn1_a5psw.c | 676 +++++++++++++++++++++++++++++++++++
 drivers/net/dsa/rzn1_a5psw.h | 196 ++++++++++
 4 files changed, 883 insertions(+)
 create mode 100644 drivers/net/dsa/rzn1_a5psw.c
 create mode 100644 drivers/net/dsa/rzn1_a5psw.h

Comments

Russell King (Oracle) April 14, 2022, 1:02 p.m. UTC | #1
On Thu, Apr 14, 2022 at 02:22:44PM +0200, Clément Léger wrote:
> Add Renesas RZ/N1 advanced 5 port switch driver. This switch handles 5
> ports including 1 CPU management port. A MDIO bus is also exposed by
> this switch and allows to communicate with PHYs connected to the ports.
> Each switch port (except for the CPU management ports) are connected to
> the MII converter.
> 
> This driver include basic bridging support, more support will be added
> later (vlan, etc).

This patch looks to me like it needs to be updated...

> +static void a5psw_phylink_validate(struct dsa_switch *ds, int port,
> +				   unsigned long *supported,
> +				   struct phylink_link_state *state)
> +{
> +	__ETHTOOL_DECLARE_LINK_MODE_MASK(mask) = { 0 };
> +
> +	phylink_set_port_modes(mask);
> +
> +	phylink_set(mask, Autoneg);
> +	phylink_set(mask, Pause);
> +	phylink_set(mask, Asym_Pause);
> +
> +	phylink_set(mask, 1000baseT_Full);
> +	if (!dsa_is_cpu_port(ds, port)) {
> +		phylink_set(mask, 10baseT_Half);
> +		phylink_set(mask, 10baseT_Full);
> +		phylink_set(mask, 100baseT_Half);
> +		phylink_set(mask, 100baseT_Full);
> +	}

If the port supports e.g. RGMII (as it does via the media converter)
then it also supports 1000baseX modes as well - because a PHY attached
to the RGMII port can convert to 1000baseX.

> +
> +	linkmode_and(supported, supported, mask);
> +	linkmode_and(state->advertising, state->advertising, mask);
> +}

This basically means "I support every phy_interface_t mode that has ever
been implemented" which surely is not what you want. I doubt from the
above that you support 10GBASE-KR for example.

Please instead implement the .phylink_get_caps DSA switch interface, and
fill in the config->supported_interfaces for all interface modes that
the port supports (that including the media converter as well) and the
config->mac_capabilities members.
Vladimir Oltean April 14, 2022, 2:47 p.m. UTC | #2
On Thu, Apr 14, 2022 at 02:22:44PM +0200, Clément Léger wrote:
> Add Renesas RZ/N1 advanced 5 port switch driver. This switch handles 5
> ports including 1 CPU management port. A MDIO bus is also exposed by
> this switch and allows to communicate with PHYs connected to the ports.
> Each switch port (except for the CPU management ports) are connected to

s/are/is/

> the MII converter.
> 
> This driver include basic bridging support, more support will be added

s/include/includes/

> later (vlan, etc).
> 
> Suggested-by: Laurent Gonzales <laurent.gonzales@non.se.com>
> Suggested-by: Jean-Pierre Geslin <jean-pierre.geslin@non.se.com>
> Suggested-by: Phil Edworthy <phil.edworthy@renesas.com>

Suggested? What did they suggest? "You should write a driver"?
We have a Co-developed-by: tag, maybe it's more appropriate here?

> Signed-off-by: Clément Léger <clement.leger@bootlin.com>
> ---
>  drivers/net/dsa/Kconfig      |   9 +
>  drivers/net/dsa/Makefile     |   2 +
>  drivers/net/dsa/rzn1_a5psw.c | 676 +++++++++++++++++++++++++++++++++++
>  drivers/net/dsa/rzn1_a5psw.h | 196 ++++++++++
>  4 files changed, 883 insertions(+)
>  create mode 100644 drivers/net/dsa/rzn1_a5psw.c
>  create mode 100644 drivers/net/dsa/rzn1_a5psw.h
> 
> diff --git a/drivers/net/dsa/Kconfig b/drivers/net/dsa/Kconfig
> index 37a3dabdce31..0896c5fd9dec 100644
> --- a/drivers/net/dsa/Kconfig
> +++ b/drivers/net/dsa/Kconfig
> @@ -70,6 +70,15 @@ config NET_DSA_QCA8K
>  
>  source "drivers/net/dsa/realtek/Kconfig"
>  
> +config NET_DSA_RZN1_A5PSW
> +	tristate "Renesas RZ/N1 A5PSW Ethernet switch support"
> +	depends on NET_DSA
> +	select NET_DSA_TAG_RZN1_A5PSW
> +	select PCS_RZN1_MIIC
> +	help
> +	  This driver supports the A5PSW switch, which is embedded in Renesas
> +	  RZ/N1 SoC.
> +
>  config NET_DSA_SMSC_LAN9303
>  	tristate
>  	depends on VLAN_8021Q || VLAN_8021Q=n
> diff --git a/drivers/net/dsa/Makefile b/drivers/net/dsa/Makefile
> index e73838c12256..5daf3da4344e 100644
> --- a/drivers/net/dsa/Makefile
> +++ b/drivers/net/dsa/Makefile
> @@ -9,12 +9,14 @@ obj-$(CONFIG_NET_DSA_LANTIQ_GSWIP) += lantiq_gswip.o
>  obj-$(CONFIG_NET_DSA_MT7530)	+= mt7530.o
>  obj-$(CONFIG_NET_DSA_MV88E6060) += mv88e6060.o
>  obj-$(CONFIG_NET_DSA_QCA8K)	+= qca8k.o
> +obj-$(CONFIG_NET_DSA_RZN1_A5PSW) += rzn1_a5psw.o
>  obj-$(CONFIG_NET_DSA_SMSC_LAN9303) += lan9303-core.o
>  obj-$(CONFIG_NET_DSA_SMSC_LAN9303_I2C) += lan9303_i2c.o
>  obj-$(CONFIG_NET_DSA_SMSC_LAN9303_MDIO) += lan9303_mdio.o
>  obj-$(CONFIG_NET_DSA_VITESSE_VSC73XX) += vitesse-vsc73xx-core.o
>  obj-$(CONFIG_NET_DSA_VITESSE_VSC73XX_PLATFORM) += vitesse-vsc73xx-platform.o
>  obj-$(CONFIG_NET_DSA_VITESSE_VSC73XX_SPI) += vitesse-vsc73xx-spi.o
> +

Unrelated change.

>  obj-y				+= b53/
>  obj-y				+= hirschmann/
>  obj-y				+= microchip/
> diff --git a/drivers/net/dsa/rzn1_a5psw.c b/drivers/net/dsa/rzn1_a5psw.c
> new file mode 100644
> index 000000000000..5bee999f7050
> --- /dev/null
> +++ b/drivers/net/dsa/rzn1_a5psw.c
> @@ -0,0 +1,676 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +/*
> + * Copyright (C) 2022 Schneider-Electric
> + *
> + * Clément Léger <clement.leger@bootlin.com>
> + */
> +
> +#include <linux/clk.h>
> +#include <linux/etherdevice.h>
> +#include <linux/kernel.h>
> +#include <linux/module.h>
> +#include <linux/of.h>
> +#include <linux/of_mdio.h>
> +#include <net/dsa.h>
> +#include <uapi/linux/if_bridge.h>

Why do you need to include this header?

> +
> +#include "rzn1_a5psw.h"
> +
> +static void a5psw_reg_writel(struct a5psw *a5psw, int offset, u32 value)
> +{
> +	writel(value, a5psw->base + offset);
> +}
> +
> +static u32 a5psw_reg_readl(struct a5psw *a5psw, int offset)
> +{
> +	return readl(a5psw->base + offset);
> +}
> +
> +static void a5psw_reg_rmw(struct a5psw *a5psw, int offset, u32 mask, u32 val)
> +{
> +	u32 reg;
> +
> +	spin_lock(&a5psw->reg_lock);
> +
> +	reg = a5psw_reg_readl(a5psw, offset);
> +	reg &= ~mask;
> +	reg |= val;
> +	a5psw_reg_writel(a5psw, offset, reg);
> +
> +	spin_unlock(&a5psw->reg_lock);
> +}
> +
> +static enum dsa_tag_protocol a5psw_get_tag_protocol(struct dsa_switch *ds,
> +						    int port,
> +						    enum dsa_tag_protocol mp)
> +{
> +	return DSA_TAG_PROTO_RZN1_A5PSW;
> +}
> +
> +static void a5psw_port_pattern_set(struct a5psw *a5psw, int port, int pattern,
> +				   bool enable)
> +{
> +	u32 rx_match = 0;
> +
> +	if (enable)
> +		rx_match |= A5PSW_RXMATCH_CONFIG_PATTERN(pattern);
> +
> +	a5psw_reg_rmw(a5psw, A5PSW_RXMATCH_CONFIG(port),
> +		      A5PSW_RXMATCH_CONFIG_PATTERN(pattern), rx_match);
> +}
> +
> +static void a5psw_port_mgmtfwd_set(struct a5psw *a5psw, int port, bool enable)

Some explanation on what "management forward" means/does?

> +{
> +	a5psw_port_pattern_set(a5psw, port, A5PSW_PATTERN_MGMTFWD, enable);
> +}
> +
> +static void a5psw_port_enable_set(struct a5psw *a5psw, int port, bool enable)
> +{
> +	u32 port_ena = 0;
> +
> +	if (enable)
> +		port_ena |= A5PSW_PORT_ENA_TX_RX(port);
> +
> +	a5psw_reg_rmw(a5psw, A5PSW_PORT_ENA, A5PSW_PORT_ENA_TX_RX(port),
> +		      port_ena);
> +}
> +
> +static int a5psw_lk_execute_ctrl(struct a5psw *a5psw, u32 *ctrl)
> +{
> +	int ret;
> +
> +	a5psw_reg_writel(a5psw, A5PSW_LK_ADDR_CTRL, *ctrl);
> +
> +	ret = readl_poll_timeout(a5psw->base + A5PSW_LK_ADDR_CTRL,
> +				 *ctrl,
> +				 !(*ctrl & A5PSW_LK_ADDR_CTRL_BUSY),
> +				 A5PSW_LK_BUSY_USEC_POLL,
> +				 A5PSW_CTRL_TIMEOUT);
> +	if (ret)
> +		dev_err(a5psw->dev, "LK_CTRL timeout waiting for BUSY bit\n");
> +
> +	return ret;
> +}
> +
> +static void a5psw_port_fdb_flush(struct a5psw *a5psw, int port)
> +{
> +	u32 ctrl = A5PSW_LK_ADDR_CTRL_DELETE_PORT | BIT(port);
> +
> +	spin_lock(&a5psw->lk_lock);
> +	a5psw_lk_execute_ctrl(a5psw, &ctrl);
> +	spin_unlock(&a5psw->lk_lock);
> +}
> +
> +static void a5psw_port_authorize_set(struct a5psw *a5psw, int port,
> +				     bool authorize)
> +{
> +	u32 reg = a5psw_reg_readl(a5psw, A5PSW_AUTH_PORT(port));
> +
> +	if (authorize)
> +		reg |= A5PSW_AUTH_PORT_AUTHORIZED;
> +	else
> +		reg &= ~A5PSW_AUTH_PORT_AUTHORIZED;
> +
> +	a5psw_reg_writel(a5psw,  A5PSW_AUTH_PORT(port), reg);
> +}
> +
> +static void a5psw_port_disable(struct dsa_switch *ds, int port)
> +{
> +	struct a5psw *a5psw = ds->priv;
> +
> +	a5psw_port_authorize_set(a5psw, port, false);
> +	a5psw_port_enable_set(a5psw, port, false);
> +	a5psw_port_fdb_flush(a5psw, port);
> +}
> +
> +static int a5psw_port_enable(struct dsa_switch *ds, int port,
> +			     struct phy_device *phy)
> +{
> +	struct a5psw *a5psw = ds->priv;
> +
> +	a5psw_port_authorize_set(a5psw, port, true);
> +	a5psw_port_enable_set(a5psw, port, true);
> +
> +	return 0;
> +}
> +
> +static int a5psw_port_change_mtu(struct dsa_switch *ds, int port, int new_mtu)
> +{
> +	struct a5psw *a5psw = ds->priv;
> +
> +	new_mtu += ETH_HLEN + A5PSW_EXTRA_MTU_LEN + ETH_FCS_LEN;
> +	a5psw_reg_writel(a5psw, A5PSW_FRM_LENGTH(port), new_mtu);
> +
> +	return 0;
> +}
> +
> +static int a5psw_port_max_mtu(struct dsa_switch *ds, int port)
> +{
> +	return A5PSW_JUMBO_LEN - A5PSW_TAG_LEN;
> +}

> +static int a5psw_set_ageing_time(struct dsa_switch *ds, unsigned int msecs)
> +{
> +	struct a5psw *a5psw = ds->priv;
> +	unsigned long rate;
> +	u64 max, tmp;
> +	u32 agetime;
> +
> +	rate = clk_get_rate(a5psw->clk);
> +	max = div64_ul(((u64)A5PSW_LK_AGETIME_MASK * A5PSW_TABLE_ENTRIES * 1024),
> +		       rate) * 1000;
> +	if (msecs > max)
> +		return -EINVAL;
> +
> +	tmp = div_u64(rate, MSEC_PER_SEC);
> +	agetime = div_u64(msecs * tmp, 1024 * A5PSW_TABLE_ENTRIES);
> +
> +	a5psw_reg_writel(a5psw, A5PSW_LK_AGETIME, agetime);
> +
> +	return 0;
> +}
> +
> +static void a5psw_flooding_set_resolution(struct a5psw *a5psw, int port,
> +					  bool set)
> +{
> +	u8 offsets[] = {A5PSW_UCAST_DEF_MASK, A5PSW_BCAST_DEF_MASK,
> +		       A5PSW_MCAST_DEF_MASK};
> +	int i;
> +
> +	if (set)
> +		a5psw->flooding_ports |= BIT(port);
> +	else
> +		a5psw->flooding_ports &= ~BIT(port);
> +
> +	for (i = 0; i < ARRAY_SIZE(offsets); i++)
> +		a5psw_reg_writel(a5psw, offsets[i], a5psw->flooding_ports);
> +}
> +
> +static int a5psw_port_bridge_join(struct dsa_switch *ds, int port,
> +				  struct dsa_bridge bridge,
> +				  bool *tx_fwd_offload,
> +				  struct netlink_ext_ack *extack)
> +{
> +	struct a5psw *a5psw = ds->priv;
> +
> +	a5psw_flooding_set_resolution(a5psw, port, true);
> +	a5psw_port_mgmtfwd_set(a5psw, port, false);
> +
> +	return 0;
> +}
> +
> +static void a5psw_port_bridge_leave(struct dsa_switch *ds, int port,
> +				    struct dsa_bridge bridge)
> +{
> +	struct a5psw *a5psw = ds->priv;
> +
> +	a5psw_flooding_set_resolution(a5psw, port, false);
> +	a5psw_port_mgmtfwd_set(a5psw, port, true);
> +}
> +
> +static void a5psw_port_stp_state_set(struct dsa_switch *ds, int port,
> +				     u8 state)
> +{
> +	struct a5psw *a5psw = ds->priv;
> +	u32 reg = 0;
> +	u32 mask = A5PSW_INPUT_LEARN_DIS(port) | A5PSW_INPUT_LEARN_BLOCK(port);
> +
> +	switch (state) {
> +	case BR_STATE_DISABLED:
> +	case BR_STATE_BLOCKING:
> +		reg |= A5PSW_INPUT_LEARN_DIS(port);
> +		reg |= A5PSW_INPUT_LEARN_BLOCK(port);
> +		break;
> +	case BR_STATE_LISTENING:
> +		reg |= A5PSW_INPUT_LEARN_DIS(port);
> +		break;
> +	case BR_STATE_LEARNING:
> +		reg |= A5PSW_INPUT_LEARN_BLOCK(port);
> +		break;
> +	case BR_STATE_FORWARDING:
> +	default:
> +		break;
> +	}
> +
> +	a5psw_reg_rmw(a5psw, A5PSW_INPUT_LEARN, mask, reg);
> +}
> +
> +static void a5psw_port_fast_age(struct dsa_switch *ds, int port)
> +{
> +	struct a5psw *a5psw = ds->priv;
> +
> +	a5psw_port_fdb_flush(a5psw, port);
> +}
> +
> +static int a5psw_setup(struct dsa_switch *ds)
> +{
> +	struct a5psw *a5psw = ds->priv;
> +	int port, vlan, ret;
> +	u32 reg;
> +
> +	/* Configure management port */
> +	reg = A5PSW_CPU_PORT | A5PSW_MGMT_CFG_DISCARD;
> +	a5psw_reg_writel(a5psw, A5PSW_MGMT_CFG, reg);

Perhaps you should validate the DT blob that the CPU port is the one
that you think it is?

> +
> +	/* Set pattern 0 to forward all frame to mgmt port */
> +	a5psw_reg_writel(a5psw, A5PSW_PATTERN_CTRL(0),
> +			 A5PSW_PATTERN_CTRL_MGMTFWD);
> +
> +	/* Enable port tagging */
> +	reg = FIELD_PREP(A5PSW_MGMT_TAG_CFG_TAGFIELD, A5PSW_MGMT_TAG_VALUE);
> +	reg |= A5PSW_MGMT_TAG_CFG_ENABLE | A5PSW_MGMT_TAG_CFG_ALL_FRAMES;
> +	a5psw_reg_writel(a5psw, A5PSW_MGMT_TAG_CFG, reg);
> +
> +	/* Enable normal switch operation */
> +	reg = A5PSW_LK_ADDR_CTRL_BLOCKING | A5PSW_LK_ADDR_CTRL_LEARNING |
> +	      A5PSW_LK_ADDR_CTRL_AGEING | A5PSW_LK_ADDR_CTRL_ALLOW_MIGR |
> +	      A5PSW_LK_ADDR_CTRL_CLEAR_TABLE;
> +	a5psw_reg_writel(a5psw, A5PSW_LK_CTRL, reg);
> +
> +	ret = readl_poll_timeout(a5psw->base + A5PSW_LK_CTRL,
> +				 reg,
> +				 !(reg & A5PSW_LK_ADDR_CTRL_CLEAR_TABLE),
> +				 A5PSW_LK_BUSY_USEC_POLL,
> +				 A5PSW_CTRL_TIMEOUT);
> +	if (ret) {
> +		dev_err(a5psw->dev, "Failed to clear lookup table\n");
> +		return ret;
> +	}
> +
> +	/* Reset learn count to 0 */
> +	reg = A5PSW_LK_LEARNCOUNT_MODE_SET;
> +	a5psw_reg_writel(a5psw, A5PSW_LK_LEARNCOUNT, reg);
> +
> +	/* Clear VLAN resource table */
> +	reg = A5PSW_VLAN_RES_WR_PORTMASK | A5PSW_VLAN_RES_WR_TAGMASK;
> +	for (vlan = 0; vlan < A5PSW_VLAN_COUNT; vlan++)
> +		a5psw_reg_writel(a5psw, A5PSW_VLAN_RES(vlan), reg);
> +
> +	/* Reset all ports */
> +	for (port = 0; port < ds->num_ports; port++) {

Because dsa_is_cpu_port() internally calls dsa_to_port() which iterates
through a list, we tend to avoid the pattern where we call a list
iterating function from a loop over essentially the same data.
Instead, we have:

	dsa_switch_for_each_port(dp, ds) {
		if (dsa_port_is_unused(dp))
			do stuff with dp->index
		if (dsa_port_is_cpu(dp))
			...
		if (dsa_port_is_user(dp))
			...
	}

> +		/* Reset the port */
> +		a5psw_reg_writel(a5psw, A5PSW_CMD_CFG(port),
> +				 A5PSW_CMD_CFG_SW_RESET);
> +
> +		/* Enable only CPU port */
> +		a5psw_port_enable_set(a5psw, port, dsa_is_cpu_port(ds, port));
> +
> +		if (dsa_is_unused_port(ds, port))
> +			continue;
> +
> +		/* Enable egress flooding for CPU port */
> +		if (dsa_is_cpu_port(ds, port))
> +			a5psw_flooding_set_resolution(a5psw, port, true);
> +
> +		/* Enable management forward only for user ports */
> +		if (dsa_is_user_port(ds, port))
> +			a5psw_port_mgmtfwd_set(a5psw, port, true);
> +	}
> +
> +	return 0;
> +}
> +
> +const struct dsa_switch_ops a5psw_switch_ops = {
> +	.get_tag_protocol = a5psw_get_tag_protocol,
> +	.setup = a5psw_setup,
> +	.port_disable = a5psw_port_disable,
> +	.port_enable = a5psw_port_enable,
> +	.phylink_validate = a5psw_phylink_validate,
> +	.phylink_mac_select_pcs = a5psw_phylink_mac_select_pcs,
> +	.phylink_mac_link_down = a5psw_phylink_mac_link_down,
> +	.phylink_mac_link_up = a5psw_phylink_mac_link_up,
> +	.port_change_mtu = a5psw_port_change_mtu,
> +	.port_max_mtu = a5psw_port_max_mtu,
> +	.set_ageing_time = a5psw_set_ageing_time,
> +	.port_bridge_join = a5psw_port_bridge_join,
> +	.port_bridge_leave = a5psw_port_bridge_leave,
> +	.port_stp_state_set = a5psw_port_stp_state_set,
> +	.port_fast_age = a5psw_port_fast_age,
> +

Stray empty line.

> +};
> +
> +static int a5psw_mdio_wait_busy(struct a5psw *a5psw)
> +{
> +	u32 status;
> +	int err;
> +
> +	err = readl_poll_timeout(a5psw->base + A5PSW_MDIO_CFG_STATUS,
> +				 status,
> +				 !(status & A5PSW_MDIO_CFG_STATUS_BUSY),
> +				 10,
> +				 1000 * USEC_PER_MSEC);
> +	if (err)
> +		dev_err(a5psw->dev, "MDIO command timeout\n");
> +
> +	return err;
> +}
> +
> +static int a5psw_mdio_read(struct mii_bus *bus, int phy_id, int phy_reg)
> +{
> +	struct a5psw *a5psw = bus->priv;
> +	u32 cmd, status;
> +	int ret;
> +
> +	cmd = A5PSW_MDIO_COMMAND_READ;
> +	cmd |= FIELD_PREP(A5PSW_MDIO_COMMAND_REG_ADDR, phy_reg);
> +	cmd |= FIELD_PREP(A5PSW_MDIO_COMMAND_PHY_ADDR, phy_id);
> +
> +	a5psw_reg_writel(a5psw, A5PSW_MDIO_COMMAND, cmd);
> +
> +	ret = a5psw_mdio_wait_busy(a5psw);
> +	if (ret)
> +		return ret;
> +
> +	ret = a5psw_reg_readl(a5psw, A5PSW_MDIO_DATA) & A5PSW_MDIO_DATA_MASK;
> +
> +	status = a5psw_reg_readl(a5psw, A5PSW_MDIO_CFG_STATUS);
> +	if (status & A5PSW_MDIO_CFG_STATUS_READERR)
> +		return -EIO;
> +
> +	return ret;
> +}
> +
> +static int a5psw_mdio_write(struct mii_bus *bus, int phy_id, int phy_reg,
> +			    u16 phy_data)
> +{
> +	struct a5psw *a5psw = bus->priv;
> +	u32 cmd;
> +
> +	cmd = FIELD_PREP(A5PSW_MDIO_COMMAND_REG_ADDR, phy_reg);
> +	cmd |= FIELD_PREP(A5PSW_MDIO_COMMAND_PHY_ADDR, phy_id);
> +
> +	a5psw_reg_writel(a5psw, A5PSW_MDIO_COMMAND, cmd);
> +	a5psw_reg_writel(a5psw, A5PSW_MDIO_DATA, phy_data);
> +
> +	return a5psw_mdio_wait_busy(a5psw);
> +}
> +
> +static int a5psw_mdio_reset(struct mii_bus *bus)
> +{
> +	struct a5psw *a5psw = bus->priv;
> +	unsigned long rate;
> +	unsigned long div;
> +	u32 cfgstatus;
> +
> +	rate = clk_get_rate(a5psw->hclk);
> +	div = ((rate / a5psw->mdio_freq) / 2);
> +	if (div >= 511 || div <= 5) {
> +		dev_err(a5psw->dev, "MDIO clock div %ld out of range\n", div);
> +		return -ERANGE;
> +	}
> +
> +	cfgstatus = FIELD_PREP(A5PSW_MDIO_CFG_STATUS_CLKDIV, div);
> +
> +	a5psw_reg_writel(a5psw, A5PSW_MDIO_CFG_STATUS, cfgstatus);
> +
> +	return 0;
> +}
> +
> +static int a5psw_probe_mdio(struct a5psw *a5psw)
> +{
> +	struct device *dev = a5psw->dev;
> +	struct device_node *mdio_node;
> +	struct mii_bus *bus;
> +	int err;
> +
> +	if (of_property_read_u32(dev->of_node, "clock-frequency",
> +				 &a5psw->mdio_freq))
> +		a5psw->mdio_freq = A5PSW_MDIO_DEF_FREQ;

Shouldn't the clock-frequency be a property of the "mdio" node?
At least I see it in Documentation/devicetree/bindings/net/mdio.yaml.

> +
> +	bus = devm_mdiobus_alloc(dev);
> +	if (!bus)
> +		return -ENOMEM;
> +
> +	bus->name = "a5psw_mdio";
> +	bus->read = a5psw_mdio_read;
> +	bus->write = a5psw_mdio_write;
> +	bus->reset = a5psw_mdio_reset;
> +	bus->priv = a5psw;
> +	bus->parent = dev;
> +	snprintf(bus->id, MII_BUS_ID_SIZE, "%s", dev_name(dev));
> +
> +	a5psw->mii_bus = bus;
> +
> +	mdio_node = of_get_child_by_name(dev->of_node, "mdio");
> +	err = devm_of_mdiobus_register(dev, bus, mdio_node);
> +	of_node_put(mdio_node);
> +	if (err)
> +		return err;
> +
> +	return 0;
> +}
> +

> +static int a5psw_probe(struct platform_device *pdev)
> +{
> +	struct device *dev = &pdev->dev;
> +	struct dsa_switch *ds;
> +	struct a5psw *a5psw;
> +	int ret;
> +
> +	a5psw = devm_kzalloc(dev, sizeof(*a5psw), GFP_KERNEL);
> +	if (!a5psw)
> +		return -ENOMEM;
> +
> +	a5psw->dev = dev;
> +	spin_lock_init(&a5psw->lk_lock);
> +	spin_lock_init(&a5psw->reg_lock);
> +	a5psw->base = devm_platform_ioremap_resource(pdev, 0);
> +	if (!a5psw->base)
> +		return -EINVAL;
> +
> +	/* Probe PCS */
> +	ret = a5psw_pcs_get(a5psw);
> +	if (ret)
> +		return ret;
> +
> +	a5psw->hclk = devm_clk_get(dev, "hclk");
> +	if (IS_ERR(a5psw->hclk)) {
> +		dev_err(dev, "failed get hclk clock\n");
> +		ret = PTR_ERR(a5psw->hclk);
> +		goto free_pcs;
> +	}
> +
> +	a5psw->clk = devm_clk_get(dev, "clk");
> +	if (IS_ERR(a5psw->clk)) {
> +		dev_err(dev, "failed get clk_switch clock\n");
> +		ret = PTR_ERR(a5psw->clk);
> +		goto free_pcs;
> +	}
> +
> +	ret = clk_prepare_enable(a5psw->clk);
> +	if (ret)
> +		goto free_pcs;
> +
> +	ret = clk_prepare_enable(a5psw->hclk);
> +	if (ret)
> +		goto clk_disable;
> +
> +	ret = a5psw_probe_mdio(a5psw);
> +	if (ret) {
> +		dev_err(&pdev->dev, "Failed to register MDIO: %d\n", ret);
> +		goto hclk_disable;
> +	}
> +
> +	ds = &a5psw->ds;
> +	ds->dev = &pdev->dev;
> +	ds->num_ports = A5PSW_PORTS_NUM;
> +	ds->ops = &a5psw_switch_ops;
> +	ds->priv = a5psw;
> +
> +	ret = dsa_register_switch(ds);
> +	if (ret) {
> +		dev_err(&pdev->dev, "Failed to register DSA switch: %d\n", ret);
> +		goto hclk_disable;
> +	}
> +
> +	return 0;
> +
> +hclk_disable:
> +	clk_disable_unprepare(a5psw->hclk);
> +clk_disable:
> +	clk_disable_unprepare(a5psw->clk);
> +free_pcs:
> +	a5psw_pcs_free(a5psw);
> +
> +	return ret;
> +}
> +
> +static int a5psw_remove(struct platform_device *pdev)
> +{
> +	struct a5psw *a5psw = platform_get_drvdata(pdev);
> +
> +	dsa_unregister_switch(&a5psw->ds);
> +	a5psw_pcs_free(a5psw);
> +	clk_disable_unprepare(a5psw->hclk);
> +	clk_disable_unprepare(a5psw->clk);
> +
> +	return 0;
> +}
> +
> +static const struct of_device_id a5psw_of_mtable[] = {
> +	{ .compatible = "renesas,rzn1-a5psw", },
> +	{ /* sentinel */ },
> +};
> +MODULE_DEVICE_TABLE(of, a5psw_of_mtable);
> +
> +static struct platform_driver a5psw_driver = {
> +	.driver = {
> +		.name	 = "rzn1_a5psw",
> +		.of_match_table = of_match_ptr(a5psw_of_mtable),
> +	},
> +	.probe = a5psw_probe,
> +	.remove = a5psw_remove,

Please implement .shutdown too, it's non-optional.

> +/**
> + * struct a5psw - switch struct
> + * @base: Base address of the switch
> + * @hclk_rate: hclk_switch clock rate
> + * @clk_rate: clk_switch clock rate
> + * @dev: Device associated to the switch
> + * @mii_bus: MDIO bus struct
> + * @mdio_freq: MDIO bus frequency requested
> + * @pcs: Array of PCS connected to the switch ports (not for the CPU)
> + * @ds: DSA switch struct
> + * @lk_lock: Lock for the lookup table
> + * @reg_lock: Lock for register read-modify-write operation

Interesting concept. Generally we see higher-level locking schemes
(i.e. a rmw lock won't really ensure much in terms of consistency of
settings if that's the only thing that serializes concurrent thread
accesses to some register).

Anyway, probably doesn't hurt to have it.

> + * @flooding_ports: List of ports that should be flooded
> + */
> +struct a5psw {
> +	void __iomem *base;
> +	struct clk* hclk;
> +	struct clk* clk;
> +	struct device *dev;
> +	struct mii_bus	*mii_bus;
> +	u32 mdio_freq;
> +	struct phylink_pcs *pcs[A5PSW_PORTS_NUM - 1];
> +	struct dsa_switch ds;
> +	spinlock_t lk_lock;
> +	spinlock_t reg_lock;
> +	u32 flooding_ports;
> +};
> -- 
> 2.34.1
> 

We have some selftests in tools/testing/selftests/net/forwarding/, like
for example bridge_vlan_unaware.sh. They create veth pairs by default,
but if you edit the NETIF_CREATE configuration you should be able to
pass your DSA interfaces.
The selftests don't cover nearly enough, but just to make sure that they
pass for your switch, when you use 2 switch ports as h1 and h2 (hosts),
and 2 ports as swp1 and swp2? There's surprisingly little that you do on
.port_bridge_join, I need to study the code more.
Andrew Lunn April 14, 2022, 5:51 p.m. UTC | #3
> > +static int a5psw_probe_mdio(struct a5psw *a5psw)
> > +{
> > +	struct device *dev = a5psw->dev;
> > +	struct device_node *mdio_node;
> > +	struct mii_bus *bus;
> > +	int err;
> > +
> > +	if (of_property_read_u32(dev->of_node, "clock-frequency",
> > +				 &a5psw->mdio_freq))
> > +		a5psw->mdio_freq = A5PSW_MDIO_DEF_FREQ;
> 
> Shouldn't the clock-frequency be a property of the "mdio" node?
> At least I see it in Documentation/devicetree/bindings/net/mdio.yaml.

Yes. And the example in the binding document for this driver also
places it in the mdio node.

       Andrew
Andrew Lunn April 14, 2022, 5:55 p.m. UTC | #4
+static int a5psw_mdio_reset(struct mii_bus *bus)
> +{
> +	struct a5psw *a5psw = bus->priv;
> +	unsigned long rate;
> +	unsigned long div;
> +	u32 cfgstatus;
> +
> +	rate = clk_get_rate(a5psw->hclk);
> +	div = ((rate / a5psw->mdio_freq) / 2);
> +	if (div >= 511 || div <= 5) {
> +		dev_err(a5psw->dev, "MDIO clock div %ld out of range\n", div);
> +		return -ERANGE;
> +	}
> +
> +	cfgstatus = FIELD_PREP(A5PSW_MDIO_CFG_STATUS_CLKDIV, div);
> +
> +	a5psw_reg_writel(a5psw, A5PSW_MDIO_CFG_STATUS, cfgstatus);

I don't see anything here which does an actual reset. So i think this
function has the wrong name. Please also pass the frequency as a
parameter, because at a quick glance it was not easy to see where it
was used. There does not seem to be any need to store it in a5psw.

> +static int a5psw_probe_mdio(struct a5psw *a5psw)
> +{
> +	struct device *dev = a5psw->dev;
> +	struct device_node *mdio_node;
> +	struct mii_bus *bus;
> +	int err;
> +
> +	if (of_property_read_u32(dev->of_node, "clock-frequency",
> +				 &a5psw->mdio_freq))
> +		a5psw->mdio_freq = A5PSW_MDIO_DEF_FREQ;
> +
> +	bus = devm_mdiobus_alloc(dev);
> +	if (!bus)
> +		return -ENOMEM;
> +
> +	bus->name = "a5psw_mdio";
> +	bus->read = a5psw_mdio_read;
> +	bus->write = a5psw_mdio_write;
> +	bus->reset = a5psw_mdio_reset;

As far as i can see, the read and write functions don't support
C45. Please return -EOPNOTSUPP if they are passed C45 addresses.

     Andrew
Clément Léger April 15, 2022, 8:40 a.m. UTC | #5
Le Thu, 14 Apr 2022 14:02:10 +0100,
"Russell King (Oracle)" <linux@armlinux.org.uk> a écrit :

> On Thu, Apr 14, 2022 at 02:22:44PM +0200, Clément Léger wrote:
> > Add Renesas RZ/N1 advanced 5 port switch driver. This switch handles 5
> > ports including 1 CPU management port. A MDIO bus is also exposed by
> > this switch and allows to communicate with PHYs connected to the ports.
> > Each switch port (except for the CPU management ports) are connected to
> > the MII converter.
> > 
> > This driver include basic bridging support, more support will be added
> > later (vlan, etc).  
> 
> This patch looks to me like it needs to be updated...

Hi Russell,

When you say so, do you expect the VLAN support to be included ?

> 
> > +static void a5psw_phylink_validate(struct dsa_switch *ds, int port,
> > +				   unsigned long *supported,
> > +				   struct phylink_link_state *state)
> > +{
> > +	__ETHTOOL_DECLARE_LINK_MODE_MASK(mask) = { 0 };
> > +
> > +	phylink_set_port_modes(mask);
> > +
> > +	phylink_set(mask, Autoneg);
> > +	phylink_set(mask, Pause);
> > +	phylink_set(mask, Asym_Pause);
> > +
> > +	phylink_set(mask, 1000baseT_Full);
> > +	if (!dsa_is_cpu_port(ds, port)) {
> > +		phylink_set(mask, 10baseT_Half);
> > +		phylink_set(mask, 10baseT_Full);
> > +		phylink_set(mask, 100baseT_Half);
> > +		phylink_set(mask, 100baseT_Full);
> > +	}  
> 
> If the port supports e.g. RGMII (as it does via the media converter)
> then it also supports 1000baseX modes as well - because a PHY attached
> to the RGMII port can convert to 1000baseX.
> 
> > +
> > +	linkmode_and(supported, supported, mask);
> > +	linkmode_and(state->advertising, state->advertising, mask);
> > +}  
> 
> This basically means "I support every phy_interface_t mode that has ever
> been implemented" which surely is not what you want. I doubt from the
> above that you support 10GBASE-KR for example.

Hmmm yes indeed, that's not what I meant *at all*.

> 
> Please instead implement the .phylink_get_caps DSA switch interface, and
> fill in the config->supported_interfaces for all interface modes that
> the port supports (that including the media converter as well) and the
> config->mac_capabilities members.
> 

Ok, looks indeed more fitted and easier to understand.
Russell King (Oracle) April 15, 2022, 8:52 a.m. UTC | #6
On Fri, Apr 15, 2022 at 10:40:29AM +0200, Clément Léger wrote:
> Le Thu, 14 Apr 2022 14:02:10 +0100,
> "Russell King (Oracle)" <linux@armlinux.org.uk> a écrit :
> 
> > On Thu, Apr 14, 2022 at 02:22:44PM +0200, Clément Léger wrote:
> > > Add Renesas RZ/N1 advanced 5 port switch driver. This switch handles 5
> > > ports including 1 CPU management port. A MDIO bus is also exposed by
> > > this switch and allows to communicate with PHYs connected to the ports.
> > > Each switch port (except for the CPU management ports) are connected to
> > > the MII converter.
> > > 
> > > This driver include basic bridging support, more support will be added
> > > later (vlan, etc).  
> > 
> > This patch looks to me like it needs to be updated...
> 
> Hi Russell,
> 
> When you say so, do you expect the VLAN support to be included ?

I was referring to the use of .phylink_validate rather than
.phylink_get_caps - all but one DSA driver have been recently updated
to use the latter, and the former should now only be used in
exceptional circumstances.

Thanks.
Clément Léger April 15, 2022, 9:34 a.m. UTC | #7
Le Thu, 14 Apr 2022 17:47:09 +0300,
Vladimir Oltean <olteanv@gmail.com> a écrit :

> 
> > later (vlan, etc).
> > 
> > Suggested-by: Laurent Gonzales <laurent.gonzales@non.se.com>
> > Suggested-by: Jean-Pierre Geslin <jean-pierre.geslin@non.se.com>
> > Suggested-by: Phil Edworthy <phil.edworthy@renesas.com>  
> 
> Suggested? What did they suggest? "You should write a driver"?
> We have a Co-developed-by: tag, maybe it's more appropriate here?

This driver was written from scratch but some ideas (port isolation
using pattern matcher) was inspired from a previous driver. I thought it
would be nice to give them credit for that.

[...]

> >  obj-y				+= hirschmann/
> >  obj-y				+= microchip/
> > diff --git a/drivers/net/dsa/rzn1_a5psw.c b/drivers/net/dsa/rzn1_a5psw.c
> > new file mode 100644
> > index 000000000000..5bee999f7050
> > --- /dev/null
> > +++ b/drivers/net/dsa/rzn1_a5psw.c
> > @@ -0,0 +1,676 @@
> > +// SPDX-License-Identifier: GPL-2.0-only
> > +/*
> > + * Copyright (C) 2022 Schneider-Electric
> > + *
> > + * Clément Léger <clement.leger@bootlin.com>
> > + */
> > +
> > +#include <linux/clk.h>
> > +#include <linux/etherdevice.h>
> > +#include <linux/kernel.h>
> > +#include <linux/module.h>
> > +#include <linux/of.h>
> > +#include <linux/of_mdio.h>
> > +#include <net/dsa.h>
> > +#include <uapi/linux/if_bridge.h>  
> 
> Why do you need to include this header?

It defines BR_STATE_* but I guess linux/if_bridge.h does include it.

> > +
> > +static void a5psw_port_pattern_set(struct a5psw *a5psw, int port, int pattern,
> > +				   bool enable)
> > +{
> > +	u32 rx_match = 0;
> > +
> > +	if (enable)
> > +		rx_match |= A5PSW_RXMATCH_CONFIG_PATTERN(pattern);
> > +
> > +	a5psw_reg_rmw(a5psw, A5PSW_RXMATCH_CONFIG(port),
> > +		      A5PSW_RXMATCH_CONFIG_PATTERN(pattern), rx_match);
> > +}
> > +
> > +static void a5psw_port_mgmtfwd_set(struct a5psw *a5psw, int port, bool enable)  
> 
> Some explanation on what "management forward" means/does?

I'll probably rename that cpu_port_forward to match the dsa naming.
It'll actually isolate the port from other ports by only forwarding the
packets to the CPU port.

> > +
> > +static int a5psw_setup(struct dsa_switch *ds)
> > +{
> > +	struct a5psw *a5psw = ds->priv;
> > +	int port, vlan, ret;
> > +	u32 reg;
> > +
> > +	/* Configure management port */
> > +	reg = A5PSW_CPU_PORT | A5PSW_MGMT_CFG_DISCARD;
> > +	a5psw_reg_writel(a5psw, A5PSW_MGMT_CFG, reg);  
> 
> Perhaps you should validate the DT blob that the CPU port is the one
> that you think it is?

You are right, the datasheet says that the management port should
actually always be the CPU port so I guess a check would be nice.

> > +
> > +	/* Reset learn count to 0 */
> > +	reg = A5PSW_LK_LEARNCOUNT_MODE_SET;
> > +	a5psw_reg_writel(a5psw, A5PSW_LK_LEARNCOUNT, reg);
> > +
> > +	/* Clear VLAN resource table */
> > +	reg = A5PSW_VLAN_RES_WR_PORTMASK | A5PSW_VLAN_RES_WR_TAGMASK;
> > +	for (vlan = 0; vlan < A5PSW_VLAN_COUNT; vlan++)
> > +		a5psw_reg_writel(a5psw, A5PSW_VLAN_RES(vlan), reg);
> > +
> > +	/* Reset all ports */
> > +	for (port = 0; port < ds->num_ports; port++) {  
> 
> Because dsa_is_cpu_port() internally calls dsa_to_port() which iterates
> through a list, we tend to avoid the pattern where we call a list
> iterating function from a loop over essentially the same data.
> Instead, we have:
> 
> 	dsa_switch_for_each_port(dp, ds) {
> 		if (dsa_port_is_unused(dp))
> 			do stuff with dp->index
> 		if (dsa_port_is_cpu(dp))
> 			...
> 		if (dsa_port_is_user(dp))
> 			...
> 	}

Nice catch indeed, I'll convert that.

> > +
> > +static int a5psw_probe_mdio(struct a5psw *a5psw)
> > +{
> > +	struct device *dev = a5psw->dev;
> > +	struct device_node *mdio_node;
> > +	struct mii_bus *bus;
> > +	int err;
> > +
> > +	if (of_property_read_u32(dev->of_node, "clock-frequency",
> > +				 &a5psw->mdio_freq))
> > +		a5psw->mdio_freq = A5PSW_MDIO_DEF_FREQ;  
> 
> Shouldn't the clock-frequency be a property of the "mdio" node?
> At least I see it in Documentation/devicetree/bindings/net/mdio.yaml.

Yes, totally.

> > +static const struct of_device_id a5psw_of_mtable[] = {
> > +	{ .compatible = "renesas,rzn1-a5psw", },
> > +	{ /* sentinel */ },
> > +};
> > +MODULE_DEVICE_TABLE(of, a5psw_of_mtable);
> > +
> > +static struct platform_driver a5psw_driver = {
> > +	.driver = {
> > +		.name	 = "rzn1_a5psw",
> > +		.of_match_table = of_match_ptr(a5psw_of_mtable),
> > +	},
> > +	.probe = a5psw_probe,
> > +	.remove = a5psw_remove,  
> 
> Please implement .shutdown too, it's non-optional.

Hum, platform_shutdown does seems to check for the .shutdown callback:

static void platform_shutdown(struct device *_dev)
{
	struct platform_device *dev = to_platform_device(_dev);
	struct platform_driver *drv;

	if (!_dev->driver)
		return;

	drv = to_platform_driver(_dev->driver);
	if (drv->shutdown)
		drv->shutdown(dev);
}

Is there some documentation specifying that this is mandatory ?
If so, should I just set it to point to an empty shutdown function then
?

> 
> > +/**
> > + * struct a5psw - switch struct
> > + * @base: Base address of the switch
> > + * @hclk_rate: hclk_switch clock rate
> > + * @clk_rate: clk_switch clock rate
> > + * @dev: Device associated to the switch
> > + * @mii_bus: MDIO bus struct
> > + * @mdio_freq: MDIO bus frequency requested
> > + * @pcs: Array of PCS connected to the switch ports (not for the CPU)
> > + * @ds: DSA switch struct
> > + * @lk_lock: Lock for the lookup table
> > + * @reg_lock: Lock for register read-modify-write operation  
> 
> Interesting concept. Generally we see higher-level locking schemes
> (i.e. a rmw lock won't really ensure much in terms of consistency of
> settings if that's the only thing that serializes concurrent thread
> accesses to some register).

Agreed, this does not guarantee consistency of settings but guarantees
that rmw modifications are atomic between devices. I wasn't sure about
the locking guarantee that I could have. After looking at other
drivers, I guess I will switch to something more common such as using
a global mutex for register accesses.

> 
> Anyway, probably doesn't hurt to have it.
> 
> > + * @flooding_ports: List of ports that should be flooded
> > + */
> > +struct a5psw {
> > +	void __iomem *base;
> > +	struct clk* hclk;
> > +	struct clk* clk;
> > +	struct device *dev;
> > +	struct mii_bus	*mii_bus;
> > +	u32 mdio_freq;
> > +	struct phylink_pcs *pcs[A5PSW_PORTS_NUM - 1];
> > +	struct dsa_switch ds;
> > +	spinlock_t lk_lock;
> > +	spinlock_t reg_lock;
> > +	u32 flooding_ports;
> > +};
> > -- 
> > 2.34.1
> >   
> 
> We have some selftests in tools/testing/selftests/net/forwarding/, like
> for example bridge_vlan_unaware.sh. They create veth pairs by default,
> but if you edit the NETIF_CREATE configuration you should be able to
> pass your DSA interfaces.

Ok, great to know that there are some tests that can be used.

> The selftests don't cover nearly enough, but just to make sure that they
> pass for your switch, when you use 2 switch ports as h1 and h2 (hosts),
> and 2 ports as swp1 and swp2? There's surprisingly little that you do on
> .port_bridge_join, I need to study the code more.

Port isolation is handled by using a pattern matcher which is enabled
for each port at setup. If set, the port packet will only be forwarded
to the CPU port. When bridging is needed, the pattern matching is
disabled and thus, the packets are forwarded between all the ports that
are enabled in the bridge.

Thanks,
Vladimir Oltean April 15, 2022, 10:55 a.m. UTC | #8
On Fri, Apr 15, 2022 at 11:34:53AM +0200, Clément Léger wrote:
> Le Thu, 14 Apr 2022 17:47:09 +0300,
> Vladimir Oltean <olteanv@gmail.com> a écrit :
> > > later (vlan, etc).
> > > 
> > > Suggested-by: Laurent Gonzales <laurent.gonzales@non.se.com>
> > > Suggested-by: Jean-Pierre Geslin <jean-pierre.geslin@non.se.com>
> > > Suggested-by: Phil Edworthy <phil.edworthy@renesas.com>  
> > 
> > Suggested? What did they suggest? "You should write a driver"?
> > We have a Co-developed-by: tag, maybe it's more appropriate here?
> 
> This driver was written from scratch but some ideas (port isolation
> using pattern matcher) was inspired from a previous driver. I thought it
> would be nice to give them credit for that.
> 
> [...]

Ok, in that case I don't really know how to mark sources of inspiration
in the commit message, maybe your approach is fine.

> > >  obj-y				+= hirschmann/
> > >  obj-y				+= microchip/
> > > diff --git a/drivers/net/dsa/rzn1_a5psw.c b/drivers/net/dsa/rzn1_a5psw.c
> > > new file mode 100644
> > > index 000000000000..5bee999f7050
> > > --- /dev/null
> > > +++ b/drivers/net/dsa/rzn1_a5psw.c
> > > @@ -0,0 +1,676 @@
> > > +// SPDX-License-Identifier: GPL-2.0-only
> > > +/*
> > > + * Copyright (C) 2022 Schneider-Electric
> > > + *
> > > + * Clément Léger <clement.leger@bootlin.com>
> > > + */
> > > +
> > > +#include <linux/clk.h>
> > > +#include <linux/etherdevice.h>
> > > +#include <linux/kernel.h>
> > > +#include <linux/module.h>
> > > +#include <linux/of.h>
> > > +#include <linux/of_mdio.h>
> > > +#include <net/dsa.h>
> > > +#include <uapi/linux/if_bridge.h>  
> > 
> > Why do you need to include this header?
> 
> It defines BR_STATE_* but I guess linux/if_bridge.h does include it.

Yes.

> > > +static void a5psw_port_pattern_set(struct a5psw *a5psw, int port, int pattern,
> > > +				   bool enable)
> > > +{
> > > +	u32 rx_match = 0;
> > > +
> > > +	if (enable)
> > > +		rx_match |= A5PSW_RXMATCH_CONFIG_PATTERN(pattern);
> > > +
> > > +	a5psw_reg_rmw(a5psw, A5PSW_RXMATCH_CONFIG(port),
> > > +		      A5PSW_RXMATCH_CONFIG_PATTERN(pattern), rx_match);
> > > +}
> > > +
> > > +static void a5psw_port_mgmtfwd_set(struct a5psw *a5psw, int port, bool enable)  
> > 
> > Some explanation on what "management forward" means/does?
> 
> I'll probably rename that cpu_port_forward to match the dsa naming.
> It'll actually isolate the port from other ports by only forwarding the
> packets to the CPU port.

You could probably do without a rename by just adding a comment that
says that it enables forwarding only towards the management port.

> > Please implement .shutdown too, it's non-optional.
> 
> Hum, platform_shutdown does seems to check for the .shutdown callback:
> 
> static void platform_shutdown(struct device *_dev)
> {
> 	struct platform_device *dev = to_platform_device(_dev);
> 	struct platform_driver *drv;
> 
> 	if (!_dev->driver)
> 		return;
> 
> 	drv = to_platform_driver(_dev->driver);
> 	if (drv->shutdown)
> 		drv->shutdown(dev);
> }
> 
> Is there some documentation specifying that this is mandatory ?
> If so, should I just set it to point to an empty shutdown function then
> ?

I meant that for a DSA switch driver is mandatory to call dsa_switch_shutdown()
from your ->shutdown method, otherwise subtle things break, sorry for being unclear.

Please blindly copy-paste the odd pattern that all other DSA drivers use
in ->shutdown and ->remove (with the platform_set_drvdata(dev, NULL) calls),
like a normal person :)

> > > + * @reg_lock: Lock for register read-modify-write operation  
> > 
> > Interesting concept. Generally we see higher-level locking schemes
> > (i.e. a rmw lock won't really ensure much in terms of consistency of
> > settings if that's the only thing that serializes concurrent thread
> > accesses to some register).
> 
> Agreed, this does not guarantee consistency of settings but guarantees
> that rmw modifications are atomic between devices. I wasn't sure about
> the locking guarantee that I could have. After looking at other
> drivers, I guess I will switch to something more common such as using
> a global mutex for register accesses.

LOL, that isn't better...

Ideally locking would be done per functionality that the hardware can
perform independently (like lookup table access, VLAN table access,
forwarding domain control, PTP block, link state control, etc).
You don't want to artificially serialize unrelated stuff.
A "read-modify-write" lock would similarly artificially serialize
unrelated stuff for you, even if you intend it to only serialize
something entirely different.

Most things as seen by a DSA switch driver are implicitly serialized by
the rtnl_mutex anyway. Some things aren't (->port_fdb_add, ->port_fdb_del).
There is a point to be made about adding locks for stuff that is
implicitly serialized by the rtnl_mutex, since you can't really test
their effectiveness. This makes it more difficult for the driver writer
to make the right decision about locking, since in some cases, the
serialization given by the rtnl_mutex isn't something fundamental and
may be removed, to reduce contention on that lock. In that case, it is
always a nice surprise to find a backup locking scheme in converted
drivers. With the mention that said backup locking scheme was never
really tested, so it may be that it needs further work anyway.

> > The selftests don't cover nearly enough, but just to make sure that they
> > pass for your switch, when you use 2 switch ports as h1 and h2 (hosts),
> > and 2 ports as swp1 and swp2? There's surprisingly little that you do on
> > .port_bridge_join, I need to study the code more.
> 
> Port isolation is handled by using a pattern matcher which is enabled
> for each port at setup. If set, the port packet will only be forwarded
> to the CPU port. When bridging is needed, the pattern matching is
> disabled and thus, the packets are forwarded between all the ports that
> are enabled in the bridge.

Is there some public documentation for this pattern matcher?
Russell King (Oracle) April 15, 2022, 11:02 a.m. UTC | #9
On Fri, Apr 15, 2022 at 01:55:03PM +0300, Vladimir Oltean wrote:
> I meant that for a DSA switch driver is mandatory to call dsa_switch_shutdown()
> from your ->shutdown method, otherwise subtle things break, sorry for being unclear.
> 
> Please blindly copy-paste the odd pattern that all other DSA drivers use
> in ->shutdown and ->remove (with the platform_set_drvdata(dev, NULL) calls),
> like a normal person :)

Those platform_set_drvdata(, NULL) calls should be killed - the
driver model will set the driver data to NULL after ->remove has
been called - so having drivers also setting the driver data to
NULL is mere duplication.

The only case it would matter is if someone is looking up the device
and then accessing the driver data - and one would hope that's done
with appropriate locking or other guarantees (e.g. driver can never
be unbound once the driver data has been set.)
Vladimir Oltean April 15, 2022, 11:05 a.m. UTC | #10
On Fri, Apr 15, 2022 at 01:55:03PM +0300, Vladimir Oltean wrote:
> > > The selftests don't cover nearly enough, but just to make sure that they
> > > pass for your switch, when you use 2 switch ports as h1 and h2 (hosts),
> > > and 2 ports as swp1 and swp2? There's surprisingly little that you do on
> > > .port_bridge_join, I need to study the code more.
> > 
> > Port isolation is handled by using a pattern matcher which is enabled
> > for each port at setup. If set, the port packet will only be forwarded
> > to the CPU port. When bridging is needed, the pattern matching is
> > disabled and thus, the packets are forwarded between all the ports that
> > are enabled in the bridge.
> 
> Is there some public documentation for this pattern matcher?

Again, I realize I haven't made it clear what concerns me here.
On ->port_bridge_join() and ->port_bridge_leave(), the "bridge" is given
to you as argument. 2 ports may join br0, and 2 ports may join br1.
You disregard the "bridge" argument. So you enable forwarding between
br0 and br1. What I'd like to see is what the hardware can do in terms
of this "pattern matching", to improve on this situation.
Vladimir Oltean April 15, 2022, 11:14 a.m. UTC | #11
On Fri, Apr 15, 2022 at 12:02:14PM +0100, Russell King (Oracle) wrote:
> On Fri, Apr 15, 2022 at 01:55:03PM +0300, Vladimir Oltean wrote:
> > I meant that for a DSA switch driver is mandatory to call dsa_switch_shutdown()
> > from your ->shutdown method, otherwise subtle things break, sorry for being unclear.
> > 
> > Please blindly copy-paste the odd pattern that all other DSA drivers use
> > in ->shutdown and ->remove (with the platform_set_drvdata(dev, NULL) calls),
> > like a normal person :)
> 
> Those platform_set_drvdata(, NULL) calls should be killed - the
> driver model will set the driver data to NULL after ->remove has
> been called - so having drivers also setting the driver data to
> NULL is mere duplication.

I can see why you say that, but the reverse is not true.
A driver can be removed from a device after said device has been shut
down, and DSA does things in dsa_unregister_switch() and in
dsa_switch_shutdown() that are incompatible with each other, so either
one or the other should be called, but not both.
The platform_set_drvdata(dev, NULL) from the ->remove path may be
redundant for the reason you mentioned, but it doesn't really hurt
anything, really (it's a pointer assignment), and perhaps would lead to
even more confusion (why are we setting the drvdata to NULL from
->shutdown but not also from ->remove?).

> The only case it would matter is if someone is looking up the device
> and then accessing the driver data - and one would hope that's done
> with appropriate locking or other guarantees (e.g. driver can never
> be unbound once the driver data has been set.)

No, this isn't what is happening.
Russell King (Oracle) April 15, 2022, 11:23 a.m. UTC | #12
On Fri, Apr 15, 2022 at 02:14:19PM +0300, Vladimir Oltean wrote:
> On Fri, Apr 15, 2022 at 12:02:14PM +0100, Russell King (Oracle) wrote:
> > On Fri, Apr 15, 2022 at 01:55:03PM +0300, Vladimir Oltean wrote:
> > > I meant that for a DSA switch driver is mandatory to call dsa_switch_shutdown()
> > > from your ->shutdown method, otherwise subtle things break, sorry for being unclear.
> > > 
> > > Please blindly copy-paste the odd pattern that all other DSA drivers use
> > > in ->shutdown and ->remove (with the platform_set_drvdata(dev, NULL) calls),
> > > like a normal person :)
> > 
> > Those platform_set_drvdata(, NULL) calls should be killed - the
> > driver model will set the driver data to NULL after ->remove has
> > been called - so having drivers also setting the driver data to
> > NULL is mere duplication.
> 
> I can see why you say that, but the reverse is not true.
> A driver can be removed from a device after said device has been shut
> down, and DSA does things in dsa_unregister_switch() and in
> dsa_switch_shutdown() that are incompatible with each other, so either
> one or the other should be called, but not both.

How would ->remove be called after ->shutdown has been called? Aren't
the two calls already exclusive - if ->remove has been called, the
device is no longer bound to the driver, so ->shutdown can't be called
for the device.  If ->shutdown has been called, the system is going
down, and userspace is probably already dead.
Vladimir Oltean April 15, 2022, 12:01 p.m. UTC | #13
On Fri, Apr 15, 2022 at 12:23:30PM +0100, Russell King (Oracle) wrote:
> If ->shutdown has been called, the system is going down, and userspace
> is probably already dead.

There isn't anything preventing ->remove from being called after ->shutdown
has been called.

It all starts with the pattern that some driver authors prefer, which is
to redirect their ->shutdown method to ->remove. They argue that it
provides for a well-tested common path, so in turn, this pattern is
quite widespread and I'm not one to argue for removing it.

When such driver (redirecting ->shutdown to ->remove) is a bus driver
(SPI, I2C, lately even the fsl-mc bus), the implication is that the
controller will be unregistered on shutdown. To unregister a bus, you
need to unregister all devices on the bus too.

Due to implicit device ordering on the dpm_list, the ->shutdown() method
of children on said bus has already executed, now we're in the context
of the ->shutdown() procedure of the bus driver itself.

You can argue "hey, that's SPI/I2C and this is a platform driver, there
isn't any bus that unregisters on shutdown here", and you may have a
point there. But platform devices aren't just memory-mapped devices,
they can also be children of mfd devices on SPI/I2C buses. So in theory
you can see this pattern happen on platform devices as well.

This is the reason why I insist for uniformity in the DSA layer in the
way that shutdown is handled. People copy and paste code a lot, and by
leaving them with less variance in the code that they copy, subtle
differences that are not understood but do matter are less likely to
creep in.
Clément Léger April 15, 2022, 12:28 p.m. UTC | #14
Le Fri, 15 Apr 2022 13:55:03 +0300,
Vladimir Oltean <olteanv@gmail.com> a écrit :

> On Fri, Apr 15, 2022 at 11:34:53AM +0200, Clément Léger wrote:
> > Le Thu, 14 Apr 2022 17:47:09 +0300,
> > Vladimir Oltean <olteanv@gmail.com> a écrit :  
> > > > later (vlan, etc).
> > > > 
> > > > Suggested-by: Laurent Gonzales <laurent.gonzales@non.se.com>
> > > > Suggested-by: Jean-Pierre Geslin <jean-pierre.geslin@non.se.com>
> > > > Suggested-by: Phil Edworthy <phil.edworthy@renesas.com>    
> > > 
> > > Suggested? What did they suggest? "You should write a driver"?
> > > We have a Co-developed-by: tag, maybe it's more appropriate here?  
> > 
> > This driver was written from scratch but some ideas (port isolation
> > using pattern matcher) was inspired from a previous driver. I thought it
> > would be nice to give them credit for that.
> > 
> > [...]  
> 
> Ok, in that case I don't really know how to mark sources of inspiration
> in the commit message, maybe your approach is fine.
> 
> > > >  obj-y				+= hirschmann/
> > > >  obj-y				+= microchip/
> > > > diff --git a/drivers/net/dsa/rzn1_a5psw.c b/drivers/net/dsa/rzn1_a5psw.c
> > > > new file mode 100644
> > > > index 000000000000..5bee999f7050
> > > > --- /dev/null
> > > > +++ b/drivers/net/dsa/rzn1_a5psw.c
> > > > @@ -0,0 +1,676 @@
> > > > +// SPDX-License-Identifier: GPL-2.0-only
> > > > +/*
> > > > + * Copyright (C) 2022 Schneider-Electric
> > > > + *
> > > > + * Clément Léger <clement.leger@bootlin.com>
> > > > + */
> > > > +
> > > > +#include <linux/clk.h>
> > > > +#include <linux/etherdevice.h>
> > > > +#include <linux/kernel.h>
> > > > +#include <linux/module.h>
> > > > +#include <linux/of.h>
> > > > +#include <linux/of_mdio.h>
> > > > +#include <net/dsa.h>
> > > > +#include <uapi/linux/if_bridge.h>    
> > > 
> > > Why do you need to include this header?  
> > 
> > It defines BR_STATE_* but I guess linux/if_bridge.h does include it.  
> 
> Yes.
> 
> > > > +static void a5psw_port_pattern_set(struct a5psw *a5psw, int port, int pattern,
> > > > +				   bool enable)
> > > > +{
> > > > +	u32 rx_match = 0;
> > > > +
> > > > +	if (enable)
> > > > +		rx_match |= A5PSW_RXMATCH_CONFIG_PATTERN(pattern);
> > > > +
> > > > +	a5psw_reg_rmw(a5psw, A5PSW_RXMATCH_CONFIG(port),
> > > > +		      A5PSW_RXMATCH_CONFIG_PATTERN(pattern), rx_match);
> > > > +}
> > > > +
> > > > +static void a5psw_port_mgmtfwd_set(struct a5psw *a5psw, int port, bool enable)    
> > > 
> > > Some explanation on what "management forward" means/does?  
> > 
> > I'll probably rename that cpu_port_forward to match the dsa naming.
> > It'll actually isolate the port from other ports by only forwarding the
> > packets to the CPU port.  
> 
> You could probably do without a rename by just adding a comment that
> says that it enables forwarding only towards the management port.
> 
> > > Please implement .shutdown too, it's non-optional.  
> > 
> > Hum, platform_shutdown does seems to check for the .shutdown callback:
> > 
> > static void platform_shutdown(struct device *_dev)
> > {
> > 	struct platform_device *dev = to_platform_device(_dev);
> > 	struct platform_driver *drv;
> > 
> > 	if (!_dev->driver)
> > 		return;
> > 
> > 	drv = to_platform_driver(_dev->driver);
> > 	if (drv->shutdown)
> > 		drv->shutdown(dev);
> > }
> > 
> > Is there some documentation specifying that this is mandatory ?
> > If so, should I just set it to point to an empty shutdown function then
> > ?  
> 
> I meant that for a DSA switch driver is mandatory to call dsa_switch_shutdown()
> from your ->shutdown method, otherwise subtle things break, sorry for being unclear.
> 
> Please blindly copy-paste the odd pattern that all other DSA drivers use
> in ->shutdown and ->remove (with the platform_set_drvdata(dev, NULL) calls),
> like a normal person :)
> 
> > > > + * @reg_lock: Lock for register read-modify-write operation    
> > > 
> > > Interesting concept. Generally we see higher-level locking schemes
> > > (i.e. a rmw lock won't really ensure much in terms of consistency of
> > > settings if that's the only thing that serializes concurrent thread
> > > accesses to some register).  
> > 
> > Agreed, this does not guarantee consistency of settings but guarantees
> > that rmw modifications are atomic between devices. I wasn't sure about
> > the locking guarantee that I could have. After looking at other
> > drivers, I guess I will switch to something more common such as using
> > a global mutex for register accesses.  
> 
> LOL, that isn't better...
> 
> Ideally locking would be done per functionality that the hardware can
> perform independently (like lookup table access, VLAN table access,
> forwarding domain control, PTP block, link state control, etc).
> You don't want to artificially serialize unrelated stuff.
> A "read-modify-write" lock would similarly artificially serialize
> unrelated stuff for you, even if you intend it to only serialize
> something entirely different.
> 
> Most things as seen by a DSA switch driver are implicitly serialized by
> the rtnl_mutex anyway. 

Is there a list of the functions that are protected by the RTNL lock
without having to deep dive in the whole stacks ? That would be really
useful to remove useless locking from my driver. But I guess I'll have
to look at other drivers to see that.

> Some things aren't (->port_fdb_add, ->port_fdb_del).

Ok, looks like for them a mutex is often used which also seems more
appropriate in my case since the operation on the learn table can take
some times.

> There is a point to be made about adding locks for stuff that is
> implicitly serialized by the rtnl_mutex, since you can't really test
> their effectiveness. This makes it more difficult for the driver writer
> to make the right decision about locking, since in some cases, the
> serialization given by the rtnl_mutex isn't something fundamental and
> may be removed, to reduce contention on that lock. In that case, it is
> always a nice surprise to find a backup locking scheme in converted
> drivers. With the mention that said backup locking scheme was never
> really tested, so it may be that it needs further work anyway.

Ok understood.

> 
> > > The selftests don't cover nearly enough, but just to make sure that they
> > > pass for your switch, when you use 2 switch ports as h1 and h2 (hosts),
> > > and 2 ports as swp1 and swp2? There's surprisingly little that you do on
> > > .port_bridge_join, I need to study the code more.  
> > 
> > Port isolation is handled by using a pattern matcher which is enabled
> > for each port at setup. If set, the port packet will only be forwarded
> > to the CPU port. When bridging is needed, the pattern matching is
> > disabled and thus, the packets are forwarded between all the ports that
> > are enabled in the bridge.  
> 
> Is there some public documentation for this pattern matcher?

Yes, the manual is public [1]. See the Advanced 5 Ports Switch section.

[1]
https://www.renesas.com/us/en/document/mah/rzn1d-group-rzn1s-group-rzn1l-group-users-manual-r-engine-and-ethernet-peripherals
Clément Léger April 15, 2022, 12:31 p.m. UTC | #15
Le Fri, 15 Apr 2022 14:05:24 +0300,
Vladimir Oltean <olteanv@gmail.com> a écrit :

> On Fri, Apr 15, 2022 at 01:55:03PM +0300, Vladimir Oltean wrote:
> > > > The selftests don't cover nearly enough, but just to make sure that they
> > > > pass for your switch, when you use 2 switch ports as h1 and h2 (hosts),
> > > > and 2 ports as swp1 and swp2? There's surprisingly little that you do on
> > > > .port_bridge_join, I need to study the code more.  
> > > 
> > > Port isolation is handled by using a pattern matcher which is enabled
> > > for each port at setup. If set, the port packet will only be forwarded
> > > to the CPU port. When bridging is needed, the pattern matching is
> > > disabled and thus, the packets are forwarded between all the ports that
> > > are enabled in the bridge.  
> > 
> > Is there some public documentation for this pattern matcher?  
> 
> Again, I realize I haven't made it clear what concerns me here.
> On ->port_bridge_join() and ->port_bridge_leave(), the "bridge" is given
> to you as argument. 2 ports may join br0, and 2 ports may join br1.
> You disregard the "bridge" argument. So you enable forwarding between
> br0 and br1. What I'd like to see is what the hardware can do in terms
> of this "pattern matching", to improve on this situation.

Yes, you are right, the driver currently won't support 2 differents
bridges. Either I add checks to support explicitely only one, or I add
support for multiple bridges. This would probably requires to use VLAN
internally to separate trafic.
Clément Léger April 15, 2022, 12:33 p.m. UTC | #16
Le Thu, 14 Apr 2022 19:55:55 +0200,
Andrew Lunn <andrew@lunn.ch> a écrit :

>  +static int a5psw_mdio_reset(struct mii_bus *bus)
> > +{
> > +	struct a5psw *a5psw = bus->priv;
> > +	unsigned long rate;
> > +	unsigned long div;
> > +	u32 cfgstatus;
> > +
> > +	rate = clk_get_rate(a5psw->hclk);
> > +	div = ((rate / a5psw->mdio_freq) / 2);
> > +	if (div >= 511 || div <= 5) {
> > +		dev_err(a5psw->dev, "MDIO clock div %ld out of range\n", div);
> > +		return -ERANGE;
> > +	}
> > +
> > +	cfgstatus = FIELD_PREP(A5PSW_MDIO_CFG_STATUS_CLKDIV, div);
> > +
> > +	a5psw_reg_writel(a5psw, A5PSW_MDIO_CFG_STATUS, cfgstatus);  
> 
> I don't see anything here which does an actual reset. So i think this
> function has the wrong name. Please also pass the frequency as a
> parameter, because at a quick glance it was not easy to see where it
> was used. There does not seem to be any need to store it in a5psw.

Indeed, the reset callback can be removed entirely and the mdio bus
could be setup directly from a5psw_probe_mdio().

> 
> > +static int a5psw_probe_mdio(struct a5psw *a5psw)
> > +{
> > +	struct device *dev = a5psw->dev;
> > +	struct device_node *mdio_node;
> > +	struct mii_bus *bus;
> > +	int err;
> > +
> > +	if (of_property_read_u32(dev->of_node, "clock-frequency",
> > +				 &a5psw->mdio_freq))
> > +		a5psw->mdio_freq = A5PSW_MDIO_DEF_FREQ;
> > +
> > +	bus = devm_mdiobus_alloc(dev);
> > +	if (!bus)
> > +		return -ENOMEM;
> > +
> > +	bus->name = "a5psw_mdio";
> > +	bus->read = a5psw_mdio_read;
> > +	bus->write = a5psw_mdio_write;
> > +	bus->reset = a5psw_mdio_reset;  
> 
> As far as i can see, the read and write functions don't support
> C45. Please return -EOPNOTSUPP if they are passed C45 addresses.

Ok.

> 
>      Andrew
Vladimir Oltean April 15, 2022, 12:41 p.m. UTC | #17
On Fri, Apr 15, 2022 at 02:28:57PM +0200, Clément Léger wrote:
> > Most things as seen by a DSA switch driver are implicitly serialized by
> > the rtnl_mutex anyway. 
> 
> Is there a list of the functions that are protected by the RTNL lock
> without having to deep dive in the whole stacks ? That would be really
> useful to remove useless locking from my driver. But I guess I'll have
> to look at other drivers to see that.

No, there isn't, but in Documentation/networking/dsa/dsa.rst we do have
a list of dsa_switch_ops functions which used to be comprehensive
(but now needs to be updated again due to development that happened in
the meantime). I suppose that if you do a thorough job of documenting
the synchronization rules, you could add that information to this file.
This would be similar to how we have the "struct net_device synchronization
rules" section in Documentation/networking/netdevices.rst.
diff mbox series

Patch

diff --git a/drivers/net/dsa/Kconfig b/drivers/net/dsa/Kconfig
index 37a3dabdce31..0896c5fd9dec 100644
--- a/drivers/net/dsa/Kconfig
+++ b/drivers/net/dsa/Kconfig
@@ -70,6 +70,15 @@  config NET_DSA_QCA8K
 
 source "drivers/net/dsa/realtek/Kconfig"
 
+config NET_DSA_RZN1_A5PSW
+	tristate "Renesas RZ/N1 A5PSW Ethernet switch support"
+	depends on NET_DSA
+	select NET_DSA_TAG_RZN1_A5PSW
+	select PCS_RZN1_MIIC
+	help
+	  This driver supports the A5PSW switch, which is embedded in Renesas
+	  RZ/N1 SoC.
+
 config NET_DSA_SMSC_LAN9303
 	tristate
 	depends on VLAN_8021Q || VLAN_8021Q=n
diff --git a/drivers/net/dsa/Makefile b/drivers/net/dsa/Makefile
index e73838c12256..5daf3da4344e 100644
--- a/drivers/net/dsa/Makefile
+++ b/drivers/net/dsa/Makefile
@@ -9,12 +9,14 @@  obj-$(CONFIG_NET_DSA_LANTIQ_GSWIP) += lantiq_gswip.o
 obj-$(CONFIG_NET_DSA_MT7530)	+= mt7530.o
 obj-$(CONFIG_NET_DSA_MV88E6060) += mv88e6060.o
 obj-$(CONFIG_NET_DSA_QCA8K)	+= qca8k.o
+obj-$(CONFIG_NET_DSA_RZN1_A5PSW) += rzn1_a5psw.o
 obj-$(CONFIG_NET_DSA_SMSC_LAN9303) += lan9303-core.o
 obj-$(CONFIG_NET_DSA_SMSC_LAN9303_I2C) += lan9303_i2c.o
 obj-$(CONFIG_NET_DSA_SMSC_LAN9303_MDIO) += lan9303_mdio.o
 obj-$(CONFIG_NET_DSA_VITESSE_VSC73XX) += vitesse-vsc73xx-core.o
 obj-$(CONFIG_NET_DSA_VITESSE_VSC73XX_PLATFORM) += vitesse-vsc73xx-platform.o
 obj-$(CONFIG_NET_DSA_VITESSE_VSC73XX_SPI) += vitesse-vsc73xx-spi.o
+
 obj-y				+= b53/
 obj-y				+= hirschmann/
 obj-y				+= microchip/
diff --git a/drivers/net/dsa/rzn1_a5psw.c b/drivers/net/dsa/rzn1_a5psw.c
new file mode 100644
index 000000000000..5bee999f7050
--- /dev/null
+++ b/drivers/net/dsa/rzn1_a5psw.c
@@ -0,0 +1,676 @@ 
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (C) 2022 Schneider-Electric
+ *
+ * Clément Léger <clement.leger@bootlin.com>
+ */
+
+#include <linux/clk.h>
+#include <linux/etherdevice.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/of_mdio.h>
+#include <net/dsa.h>
+#include <uapi/linux/if_bridge.h>
+
+#include "rzn1_a5psw.h"
+
+static void a5psw_reg_writel(struct a5psw *a5psw, int offset, u32 value)
+{
+	writel(value, a5psw->base + offset);
+}
+
+static u32 a5psw_reg_readl(struct a5psw *a5psw, int offset)
+{
+	return readl(a5psw->base + offset);
+}
+
+static void a5psw_reg_rmw(struct a5psw *a5psw, int offset, u32 mask, u32 val)
+{
+	u32 reg;
+
+	spin_lock(&a5psw->reg_lock);
+
+	reg = a5psw_reg_readl(a5psw, offset);
+	reg &= ~mask;
+	reg |= val;
+	a5psw_reg_writel(a5psw, offset, reg);
+
+	spin_unlock(&a5psw->reg_lock);
+}
+
+static enum dsa_tag_protocol a5psw_get_tag_protocol(struct dsa_switch *ds,
+						    int port,
+						    enum dsa_tag_protocol mp)
+{
+	return DSA_TAG_PROTO_RZN1_A5PSW;
+}
+
+static void a5psw_port_pattern_set(struct a5psw *a5psw, int port, int pattern,
+				   bool enable)
+{
+	u32 rx_match = 0;
+
+	if (enable)
+		rx_match |= A5PSW_RXMATCH_CONFIG_PATTERN(pattern);
+
+	a5psw_reg_rmw(a5psw, A5PSW_RXMATCH_CONFIG(port),
+		      A5PSW_RXMATCH_CONFIG_PATTERN(pattern), rx_match);
+}
+
+static void a5psw_port_mgmtfwd_set(struct a5psw *a5psw, int port, bool enable)
+{
+	a5psw_port_pattern_set(a5psw, port, A5PSW_PATTERN_MGMTFWD, enable);
+}
+
+static void a5psw_port_enable_set(struct a5psw *a5psw, int port, bool enable)
+{
+	u32 port_ena = 0;
+
+	if (enable)
+		port_ena |= A5PSW_PORT_ENA_TX_RX(port);
+
+	a5psw_reg_rmw(a5psw, A5PSW_PORT_ENA, A5PSW_PORT_ENA_TX_RX(port),
+		      port_ena);
+}
+
+static int a5psw_lk_execute_ctrl(struct a5psw *a5psw, u32 *ctrl)
+{
+	int ret;
+
+	a5psw_reg_writel(a5psw, A5PSW_LK_ADDR_CTRL, *ctrl);
+
+	ret = readl_poll_timeout(a5psw->base + A5PSW_LK_ADDR_CTRL,
+				 *ctrl,
+				 !(*ctrl & A5PSW_LK_ADDR_CTRL_BUSY),
+				 A5PSW_LK_BUSY_USEC_POLL,
+				 A5PSW_CTRL_TIMEOUT);
+	if (ret)
+		dev_err(a5psw->dev, "LK_CTRL timeout waiting for BUSY bit\n");
+
+	return ret;
+}
+
+static void a5psw_port_fdb_flush(struct a5psw *a5psw, int port)
+{
+	u32 ctrl = A5PSW_LK_ADDR_CTRL_DELETE_PORT | BIT(port);
+
+	spin_lock(&a5psw->lk_lock);
+	a5psw_lk_execute_ctrl(a5psw, &ctrl);
+	spin_unlock(&a5psw->lk_lock);
+}
+
+static void a5psw_port_authorize_set(struct a5psw *a5psw, int port,
+				     bool authorize)
+{
+	u32 reg = a5psw_reg_readl(a5psw, A5PSW_AUTH_PORT(port));
+
+	if (authorize)
+		reg |= A5PSW_AUTH_PORT_AUTHORIZED;
+	else
+		reg &= ~A5PSW_AUTH_PORT_AUTHORIZED;
+
+	a5psw_reg_writel(a5psw,  A5PSW_AUTH_PORT(port), reg);
+}
+
+static void a5psw_port_disable(struct dsa_switch *ds, int port)
+{
+	struct a5psw *a5psw = ds->priv;
+
+	a5psw_port_authorize_set(a5psw, port, false);
+	a5psw_port_enable_set(a5psw, port, false);
+	a5psw_port_fdb_flush(a5psw, port);
+}
+
+static int a5psw_port_enable(struct dsa_switch *ds, int port,
+			     struct phy_device *phy)
+{
+	struct a5psw *a5psw = ds->priv;
+
+	a5psw_port_authorize_set(a5psw, port, true);
+	a5psw_port_enable_set(a5psw, port, true);
+
+	return 0;
+}
+
+static int a5psw_port_change_mtu(struct dsa_switch *ds, int port, int new_mtu)
+{
+	struct a5psw *a5psw = ds->priv;
+
+	new_mtu += ETH_HLEN + A5PSW_EXTRA_MTU_LEN + ETH_FCS_LEN;
+	a5psw_reg_writel(a5psw, A5PSW_FRM_LENGTH(port), new_mtu);
+
+	return 0;
+}
+
+static int a5psw_port_max_mtu(struct dsa_switch *ds, int port)
+{
+	return A5PSW_JUMBO_LEN - A5PSW_TAG_LEN;
+}
+
+static void a5psw_phylink_validate(struct dsa_switch *ds, int port,
+				   unsigned long *supported,
+				   struct phylink_link_state *state)
+{
+	__ETHTOOL_DECLARE_LINK_MODE_MASK(mask) = { 0 };
+
+	phylink_set_port_modes(mask);
+
+	phylink_set(mask, Autoneg);
+	phylink_set(mask, Pause);
+	phylink_set(mask, Asym_Pause);
+
+	phylink_set(mask, 1000baseT_Full);
+	if (!dsa_is_cpu_port(ds, port)) {
+		phylink_set(mask, 10baseT_Half);
+		phylink_set(mask, 10baseT_Full);
+		phylink_set(mask, 100baseT_Half);
+		phylink_set(mask, 100baseT_Full);
+	}
+
+	linkmode_and(supported, supported, mask);
+	linkmode_and(state->advertising, state->advertising, mask);
+}
+
+static struct phylink_pcs *
+a5psw_phylink_mac_select_pcs(struct dsa_switch *ds, int port,
+			     phy_interface_t interface)
+{
+	struct dsa_port *dp = dsa_to_port(ds, port);
+	struct a5psw *a5psw = ds->priv;
+
+	if (!dsa_port_is_cpu(dp) && a5psw->pcs[port])
+		return a5psw->pcs[port];
+
+	return NULL;
+}
+
+static void a5psw_phylink_mac_link_down(struct dsa_switch *ds, int port,
+					unsigned int mode,
+					phy_interface_t interface)
+{
+	struct a5psw *a5psw = ds->priv;
+	u32 cmd_cfg;
+
+	cmd_cfg = a5psw_reg_readl(a5psw, A5PSW_CMD_CFG(port));
+	cmd_cfg &= ~(A5PSW_CMD_CFG_RX_ENA | A5PSW_CMD_CFG_TX_ENA);
+	a5psw_reg_writel(a5psw, A5PSW_CMD_CFG(port), cmd_cfg);
+}
+
+static void a5psw_phylink_mac_link_up(struct dsa_switch *ds, int port,
+				      unsigned int mode,
+				      phy_interface_t interface,
+				      struct phy_device *phydev, int speed,
+				      int duplex, bool tx_pause,
+				      bool rx_pause)
+{
+	u32 cmd_cfg = A5PSW_CMD_CFG_RX_ENA | A5PSW_CMD_CFG_TX_ENA |
+		      A5PSW_CMD_CFG_TX_CRC_APPEND;
+	struct a5psw *a5psw = ds->priv;
+
+	if (speed == SPEED_1000)
+		cmd_cfg |= A5PSW_CMD_CFG_ETH_SPEED;
+
+	if (duplex == DUPLEX_HALF)
+		cmd_cfg |= A5PSW_CMD_CFG_HD_ENA;
+
+	cmd_cfg |= A5PSW_CMD_CFG_CNTL_FRM_ENA;
+
+	if (!rx_pause)
+		cmd_cfg &= ~A5PSW_CMD_CFG_PAUSE_IGNORE;
+
+	a5psw_reg_writel(a5psw, A5PSW_CMD_CFG(port), cmd_cfg);
+}
+
+static int a5psw_set_ageing_time(struct dsa_switch *ds, unsigned int msecs)
+{
+	struct a5psw *a5psw = ds->priv;
+	unsigned long rate;
+	u64 max, tmp;
+	u32 agetime;
+
+	rate = clk_get_rate(a5psw->clk);
+	max = div64_ul(((u64)A5PSW_LK_AGETIME_MASK * A5PSW_TABLE_ENTRIES * 1024),
+		       rate) * 1000;
+	if (msecs > max)
+		return -EINVAL;
+
+	tmp = div_u64(rate, MSEC_PER_SEC);
+	agetime = div_u64(msecs * tmp, 1024 * A5PSW_TABLE_ENTRIES);
+
+	a5psw_reg_writel(a5psw, A5PSW_LK_AGETIME, agetime);
+
+	return 0;
+}
+
+static void a5psw_flooding_set_resolution(struct a5psw *a5psw, int port,
+					  bool set)
+{
+	u8 offsets[] = {A5PSW_UCAST_DEF_MASK, A5PSW_BCAST_DEF_MASK,
+		       A5PSW_MCAST_DEF_MASK};
+	int i;
+
+	if (set)
+		a5psw->flooding_ports |= BIT(port);
+	else
+		a5psw->flooding_ports &= ~BIT(port);
+
+	for (i = 0; i < ARRAY_SIZE(offsets); i++)
+		a5psw_reg_writel(a5psw, offsets[i], a5psw->flooding_ports);
+}
+
+static int a5psw_port_bridge_join(struct dsa_switch *ds, int port,
+				  struct dsa_bridge bridge,
+				  bool *tx_fwd_offload,
+				  struct netlink_ext_ack *extack)
+{
+	struct a5psw *a5psw = ds->priv;
+
+	a5psw_flooding_set_resolution(a5psw, port, true);
+	a5psw_port_mgmtfwd_set(a5psw, port, false);
+
+	return 0;
+}
+
+static void a5psw_port_bridge_leave(struct dsa_switch *ds, int port,
+				    struct dsa_bridge bridge)
+{
+	struct a5psw *a5psw = ds->priv;
+
+	a5psw_flooding_set_resolution(a5psw, port, false);
+	a5psw_port_mgmtfwd_set(a5psw, port, true);
+}
+
+static void a5psw_port_stp_state_set(struct dsa_switch *ds, int port,
+				     u8 state)
+{
+	struct a5psw *a5psw = ds->priv;
+	u32 reg = 0;
+	u32 mask = A5PSW_INPUT_LEARN_DIS(port) | A5PSW_INPUT_LEARN_BLOCK(port);
+
+	switch (state) {
+	case BR_STATE_DISABLED:
+	case BR_STATE_BLOCKING:
+		reg |= A5PSW_INPUT_LEARN_DIS(port);
+		reg |= A5PSW_INPUT_LEARN_BLOCK(port);
+		break;
+	case BR_STATE_LISTENING:
+		reg |= A5PSW_INPUT_LEARN_DIS(port);
+		break;
+	case BR_STATE_LEARNING:
+		reg |= A5PSW_INPUT_LEARN_BLOCK(port);
+		break;
+	case BR_STATE_FORWARDING:
+	default:
+		break;
+	}
+
+	a5psw_reg_rmw(a5psw, A5PSW_INPUT_LEARN, mask, reg);
+}
+
+static void a5psw_port_fast_age(struct dsa_switch *ds, int port)
+{
+	struct a5psw *a5psw = ds->priv;
+
+	a5psw_port_fdb_flush(a5psw, port);
+}
+
+static int a5psw_setup(struct dsa_switch *ds)
+{
+	struct a5psw *a5psw = ds->priv;
+	int port, vlan, ret;
+	u32 reg;
+
+	/* Configure management port */
+	reg = A5PSW_CPU_PORT | A5PSW_MGMT_CFG_DISCARD;
+	a5psw_reg_writel(a5psw, A5PSW_MGMT_CFG, reg);
+
+	/* Set pattern 0 to forward all frame to mgmt port */
+	a5psw_reg_writel(a5psw, A5PSW_PATTERN_CTRL(0),
+			 A5PSW_PATTERN_CTRL_MGMTFWD);
+
+	/* Enable port tagging */
+	reg = FIELD_PREP(A5PSW_MGMT_TAG_CFG_TAGFIELD, A5PSW_MGMT_TAG_VALUE);
+	reg |= A5PSW_MGMT_TAG_CFG_ENABLE | A5PSW_MGMT_TAG_CFG_ALL_FRAMES;
+	a5psw_reg_writel(a5psw, A5PSW_MGMT_TAG_CFG, reg);
+
+	/* Enable normal switch operation */
+	reg = A5PSW_LK_ADDR_CTRL_BLOCKING | A5PSW_LK_ADDR_CTRL_LEARNING |
+	      A5PSW_LK_ADDR_CTRL_AGEING | A5PSW_LK_ADDR_CTRL_ALLOW_MIGR |
+	      A5PSW_LK_ADDR_CTRL_CLEAR_TABLE;
+	a5psw_reg_writel(a5psw, A5PSW_LK_CTRL, reg);
+
+	ret = readl_poll_timeout(a5psw->base + A5PSW_LK_CTRL,
+				 reg,
+				 !(reg & A5PSW_LK_ADDR_CTRL_CLEAR_TABLE),
+				 A5PSW_LK_BUSY_USEC_POLL,
+				 A5PSW_CTRL_TIMEOUT);
+	if (ret) {
+		dev_err(a5psw->dev, "Failed to clear lookup table\n");
+		return ret;
+	}
+
+	/* Reset learn count to 0 */
+	reg = A5PSW_LK_LEARNCOUNT_MODE_SET;
+	a5psw_reg_writel(a5psw, A5PSW_LK_LEARNCOUNT, reg);
+
+	/* Clear VLAN resource table */
+	reg = A5PSW_VLAN_RES_WR_PORTMASK | A5PSW_VLAN_RES_WR_TAGMASK;
+	for (vlan = 0; vlan < A5PSW_VLAN_COUNT; vlan++)
+		a5psw_reg_writel(a5psw, A5PSW_VLAN_RES(vlan), reg);
+
+	/* Reset all ports */
+	for (port = 0; port < ds->num_ports; port++) {
+		/* Reset the port */
+		a5psw_reg_writel(a5psw, A5PSW_CMD_CFG(port),
+				 A5PSW_CMD_CFG_SW_RESET);
+
+		/* Enable only CPU port */
+		a5psw_port_enable_set(a5psw, port, dsa_is_cpu_port(ds, port));
+
+		if (dsa_is_unused_port(ds, port))
+			continue;
+
+		/* Enable egress flooding for CPU port */
+		if (dsa_is_cpu_port(ds, port))
+			a5psw_flooding_set_resolution(a5psw, port, true);
+
+		/* Enable management forward only for user ports */
+		if (dsa_is_user_port(ds, port))
+			a5psw_port_mgmtfwd_set(a5psw, port, true);
+	}
+
+	return 0;
+}
+
+const struct dsa_switch_ops a5psw_switch_ops = {
+	.get_tag_protocol = a5psw_get_tag_protocol,
+	.setup = a5psw_setup,
+	.port_disable = a5psw_port_disable,
+	.port_enable = a5psw_port_enable,
+	.phylink_validate = a5psw_phylink_validate,
+	.phylink_mac_select_pcs = a5psw_phylink_mac_select_pcs,
+	.phylink_mac_link_down = a5psw_phylink_mac_link_down,
+	.phylink_mac_link_up = a5psw_phylink_mac_link_up,
+	.port_change_mtu = a5psw_port_change_mtu,
+	.port_max_mtu = a5psw_port_max_mtu,
+	.set_ageing_time = a5psw_set_ageing_time,
+	.port_bridge_join = a5psw_port_bridge_join,
+	.port_bridge_leave = a5psw_port_bridge_leave,
+	.port_stp_state_set = a5psw_port_stp_state_set,
+	.port_fast_age = a5psw_port_fast_age,
+
+};
+
+static int a5psw_mdio_wait_busy(struct a5psw *a5psw)
+{
+	u32 status;
+	int err;
+
+	err = readl_poll_timeout(a5psw->base + A5PSW_MDIO_CFG_STATUS,
+				 status,
+				 !(status & A5PSW_MDIO_CFG_STATUS_BUSY),
+				 10,
+				 1000 * USEC_PER_MSEC);
+	if (err)
+		dev_err(a5psw->dev, "MDIO command timeout\n");
+
+	return err;
+}
+
+static int a5psw_mdio_read(struct mii_bus *bus, int phy_id, int phy_reg)
+{
+	struct a5psw *a5psw = bus->priv;
+	u32 cmd, status;
+	int ret;
+
+	cmd = A5PSW_MDIO_COMMAND_READ;
+	cmd |= FIELD_PREP(A5PSW_MDIO_COMMAND_REG_ADDR, phy_reg);
+	cmd |= FIELD_PREP(A5PSW_MDIO_COMMAND_PHY_ADDR, phy_id);
+
+	a5psw_reg_writel(a5psw, A5PSW_MDIO_COMMAND, cmd);
+
+	ret = a5psw_mdio_wait_busy(a5psw);
+	if (ret)
+		return ret;
+
+	ret = a5psw_reg_readl(a5psw, A5PSW_MDIO_DATA) & A5PSW_MDIO_DATA_MASK;
+
+	status = a5psw_reg_readl(a5psw, A5PSW_MDIO_CFG_STATUS);
+	if (status & A5PSW_MDIO_CFG_STATUS_READERR)
+		return -EIO;
+
+	return ret;
+}
+
+static int a5psw_mdio_write(struct mii_bus *bus, int phy_id, int phy_reg,
+			    u16 phy_data)
+{
+	struct a5psw *a5psw = bus->priv;
+	u32 cmd;
+
+	cmd = FIELD_PREP(A5PSW_MDIO_COMMAND_REG_ADDR, phy_reg);
+	cmd |= FIELD_PREP(A5PSW_MDIO_COMMAND_PHY_ADDR, phy_id);
+
+	a5psw_reg_writel(a5psw, A5PSW_MDIO_COMMAND, cmd);
+	a5psw_reg_writel(a5psw, A5PSW_MDIO_DATA, phy_data);
+
+	return a5psw_mdio_wait_busy(a5psw);
+}
+
+static int a5psw_mdio_reset(struct mii_bus *bus)
+{
+	struct a5psw *a5psw = bus->priv;
+	unsigned long rate;
+	unsigned long div;
+	u32 cfgstatus;
+
+	rate = clk_get_rate(a5psw->hclk);
+	div = ((rate / a5psw->mdio_freq) / 2);
+	if (div >= 511 || div <= 5) {
+		dev_err(a5psw->dev, "MDIO clock div %ld out of range\n", div);
+		return -ERANGE;
+	}
+
+	cfgstatus = FIELD_PREP(A5PSW_MDIO_CFG_STATUS_CLKDIV, div);
+
+	a5psw_reg_writel(a5psw, A5PSW_MDIO_CFG_STATUS, cfgstatus);
+
+	return 0;
+}
+
+static int a5psw_probe_mdio(struct a5psw *a5psw)
+{
+	struct device *dev = a5psw->dev;
+	struct device_node *mdio_node;
+	struct mii_bus *bus;
+	int err;
+
+	if (of_property_read_u32(dev->of_node, "clock-frequency",
+				 &a5psw->mdio_freq))
+		a5psw->mdio_freq = A5PSW_MDIO_DEF_FREQ;
+
+	bus = devm_mdiobus_alloc(dev);
+	if (!bus)
+		return -ENOMEM;
+
+	bus->name = "a5psw_mdio";
+	bus->read = a5psw_mdio_read;
+	bus->write = a5psw_mdio_write;
+	bus->reset = a5psw_mdio_reset;
+	bus->priv = a5psw;
+	bus->parent = dev;
+	snprintf(bus->id, MII_BUS_ID_SIZE, "%s", dev_name(dev));
+
+	a5psw->mii_bus = bus;
+
+	mdio_node = of_get_child_by_name(dev->of_node, "mdio");
+	err = devm_of_mdiobus_register(dev, bus, mdio_node);
+	of_node_put(mdio_node);
+	if (err)
+		return err;
+
+	return 0;
+}
+
+static void a5psw_pcs_free(struct a5psw *a5psw)
+{
+	int i;
+
+	for (i = 0; i < ARRAY_SIZE(a5psw->pcs); i++) {
+		if (a5psw->pcs[i])
+			miic_destroy(a5psw->pcs[i]);
+	}
+}
+
+static int a5psw_pcs_get(struct a5psw *a5psw)
+{
+	struct device_node *ports, *port, *pcs_node;
+	struct phylink_pcs *pcs;
+	int ret;
+	u32 reg;
+
+	ports = of_get_child_by_name(a5psw->dev->of_node, "ports");
+	if (!ports)
+		return -EINVAL;
+
+	for_each_available_child_of_node(ports, port) {
+		pcs_node = of_parse_phandle(port, "pcs-handle", 0);
+		if (!pcs_node)
+			continue;
+
+		if (of_property_read_u32(port, "reg", &reg)) {
+			ret = -EINVAL;
+			goto free_pcs;
+		}
+
+		if (reg >= ARRAY_SIZE(a5psw->pcs)) {
+			ret = -ENODEV;
+			goto free_pcs;
+		}
+
+		pcs = miic_create(pcs_node);
+		if (IS_ERR(pcs)) {
+			ret = PTR_ERR(pcs);
+			goto free_pcs;
+		}
+
+		a5psw->pcs[reg] = pcs;
+	}
+	of_node_put(ports);
+
+	return 0;
+
+free_pcs:
+	a5psw_pcs_free(a5psw);
+
+	return ret;
+}
+
+static int a5psw_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct dsa_switch *ds;
+	struct a5psw *a5psw;
+	int ret;
+
+	a5psw = devm_kzalloc(dev, sizeof(*a5psw), GFP_KERNEL);
+	if (!a5psw)
+		return -ENOMEM;
+
+	a5psw->dev = dev;
+	spin_lock_init(&a5psw->lk_lock);
+	spin_lock_init(&a5psw->reg_lock);
+	a5psw->base = devm_platform_ioremap_resource(pdev, 0);
+	if (!a5psw->base)
+		return -EINVAL;
+
+	/* Probe PCS */
+	ret = a5psw_pcs_get(a5psw);
+	if (ret)
+		return ret;
+
+	a5psw->hclk = devm_clk_get(dev, "hclk");
+	if (IS_ERR(a5psw->hclk)) {
+		dev_err(dev, "failed get hclk clock\n");
+		ret = PTR_ERR(a5psw->hclk);
+		goto free_pcs;
+	}
+
+	a5psw->clk = devm_clk_get(dev, "clk");
+	if (IS_ERR(a5psw->clk)) {
+		dev_err(dev, "failed get clk_switch clock\n");
+		ret = PTR_ERR(a5psw->clk);
+		goto free_pcs;
+	}
+
+	ret = clk_prepare_enable(a5psw->clk);
+	if (ret)
+		goto free_pcs;
+
+	ret = clk_prepare_enable(a5psw->hclk);
+	if (ret)
+		goto clk_disable;
+
+	ret = a5psw_probe_mdio(a5psw);
+	if (ret) {
+		dev_err(&pdev->dev, "Failed to register MDIO: %d\n", ret);
+		goto hclk_disable;
+	}
+
+	ds = &a5psw->ds;
+	ds->dev = &pdev->dev;
+	ds->num_ports = A5PSW_PORTS_NUM;
+	ds->ops = &a5psw_switch_ops;
+	ds->priv = a5psw;
+
+	ret = dsa_register_switch(ds);
+	if (ret) {
+		dev_err(&pdev->dev, "Failed to register DSA switch: %d\n", ret);
+		goto hclk_disable;
+	}
+
+	return 0;
+
+hclk_disable:
+	clk_disable_unprepare(a5psw->hclk);
+clk_disable:
+	clk_disable_unprepare(a5psw->clk);
+free_pcs:
+	a5psw_pcs_free(a5psw);
+
+	return ret;
+}
+
+static int a5psw_remove(struct platform_device *pdev)
+{
+	struct a5psw *a5psw = platform_get_drvdata(pdev);
+
+	dsa_unregister_switch(&a5psw->ds);
+	a5psw_pcs_free(a5psw);
+	clk_disable_unprepare(a5psw->hclk);
+	clk_disable_unprepare(a5psw->clk);
+
+	return 0;
+}
+
+static const struct of_device_id a5psw_of_mtable[] = {
+	{ .compatible = "renesas,rzn1-a5psw", },
+	{ /* sentinel */ },
+};
+MODULE_DEVICE_TABLE(of, a5psw_of_mtable);
+
+static struct platform_driver a5psw_driver = {
+	.driver = {
+		.name	 = "rzn1_a5psw",
+		.of_match_table = of_match_ptr(a5psw_of_mtable),
+	},
+	.probe = a5psw_probe,
+	.remove = a5psw_remove,
+};
+module_platform_driver(a5psw_driver);
+
+MODULE_LICENSE("GPL");
+MODULE_DESCRIPTION("Renesas RZ/N1 Advanced 5-port Switch driver");
+MODULE_AUTHOR("Clément Léger <clement.leger@bootlin.com>");
diff --git a/drivers/net/dsa/rzn1_a5psw.h b/drivers/net/dsa/rzn1_a5psw.h
new file mode 100644
index 000000000000..2d96a2afbc3a
--- /dev/null
+++ b/drivers/net/dsa/rzn1_a5psw.h
@@ -0,0 +1,196 @@ 
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Copyright (C) 2022 Schneider Electric
+ *
+ * Clément Léger <clement.leger@bootlin.com>
+ */
+
+#include <linux/clk.h>
+#include <linux/debugfs.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/of_mdio.h>
+#include <linux/platform_device.h>
+#include <linux/pcs-rzn1-miic.h>
+#include <net/dsa.h>
+
+#define A5PSW_REVISION			0x0
+#define A5PSW_PORT_OFFSET(port)		(0x400 * (port))
+
+#define A5PSW_PORT_ENA			0x8
+#define A5PSW_PORT_ENA_RX_SHIFT		16
+#define A5PSW_PORT_ENA_TX_RX(port)	(BIT((port) + A5PSW_PORT_ENA_RX_SHIFT) | \
+					 BIT(port))
+#define A5PSW_UCAST_DEF_MASK		0xC
+
+#define A5PSW_VLAN_VERIFY		0x10
+#define A5PSW_VLAN_VERI_SHIFT		0
+#define A5PSW_VLAN_DISC_SHIFT		16
+
+#define A5PSW_BCAST_DEF_MASK		0x14
+#define A5PSW_MCAST_DEF_MASK		0x18
+
+#define A5PSW_INPUT_LEARN		0x1C
+#define A5PSW_INPUT_LEARN_DIS(p)	BIT((p) + 16)
+#define A5PSW_INPUT_LEARN_BLOCK(p)	BIT(p)
+
+#define A5PSW_MGMT_CFG			0x20
+#define A5PSW_MGMT_CFG_DISCARD		BIT(7)
+
+#define A5PSW_MODE_CFG			0x24
+#define A5PSW_MODE_STATS_RESET		BIT(31)
+
+#define A5PSW_VLAN_IN_MODE		0x28
+#define A5PSW_VLAN_IN_MODE_PORT_SHIFT(port)	((port) * 2)
+#define A5PSW_VLAN_IN_MODE_PORT(port)		(GENMASK(1, 0) << \
+					A5PSW_VLAN_IN_MODE_PORT_SHIFT(port))
+#define A5PSW_VLAN_IN_MODE_SINGLE_PASSTHROUGH	0x0
+#define A5PSW_VLAN_IN_MODE_SINGLE_REPLACE	0x1
+#define A5PSW_VLAN_IN_MODE_TAG_ALWAYS		0x2
+
+#define A5PSW_VLAN_OUT_MODE		0x2C
+#define A5PSW_VLAN_OUT_MODE_PORT(port)	(GENMASK(1, 0) << ((port) * 2))
+#define A5PSW_VLAN_OUT_MODE_DIS		0x0
+#define A5PSW_VLAN_OUT_MODE_STRIP	0x1
+#define A5PSW_VLAN_OUT_MODE_TAG_THROUGH	0x2
+#define A5PSW_VLAN_OUT_MODE_TRANSPARENT	0x3
+
+#define A5PSW_VLAN_IN_MODE_ENA		0x30
+#define A5PSW_VLAN_TAG_ID		0x34
+
+#define A5PSW_SYSTEM_TAGINFO(port)	(0x200 + A5PSW_PORT_OFFSET(port))
+
+#define A5PSW_AUTH_PORT(port)		(0x240 + 4 * (port))
+#define A5PSW_AUTH_PORT_AUTHORIZED	BIT(0)
+
+#define A5PSW_VLAN_RES(entry)		(0x280 + 4 * (entry))
+#define A5PSW_VLAN_RES_WR_PORTMASK	BIT(30)
+#define A5PSW_VLAN_RES_WR_TAGMASK	BIT(29)
+#define A5PSW_VLAN_RES_RD_TAGMASK	BIT(28)
+#define A5PSW_VLAN_RES_ID		GENMASK(16, 5)
+#define A5PSW_VLAN_RES_PORTMASK		GENMASK(4, 0)
+
+#define A5PSW_RXMATCH_CONFIG(port)	(0x3e80 + 4 * (port))
+#define A5PSW_RXMATCH_CONFIG_PATTERN(p)	BIT(p)
+
+#define A5PSW_PATTERN_CTRL(p)		(0x3eb0 + 4  * (p))
+#define A5PSW_PATTERN_CTRL_MGMTFWD	BIT(1)
+
+#define A5PSW_LK_CTRL		0x400
+#define A5PSW_LK_ADDR_CTRL_BLOCKING	BIT(0)
+#define A5PSW_LK_ADDR_CTRL_LEARNING	BIT(1)
+#define A5PSW_LK_ADDR_CTRL_AGEING	BIT(2)
+#define A5PSW_LK_ADDR_CTRL_ALLOW_MIGR	BIT(3)
+#define A5PSW_LK_ADDR_CTRL_CLEAR_TABLE	BIT(6)
+
+#define A5PSW_LK_ADDR_CTRL		0x408
+#define A5PSW_LK_ADDR_CTRL_BUSY		BIT(31)
+#define A5PSW_LK_ADDR_CTRL_DELETE_PORT	BIT(30)
+#define A5PSW_LK_ADDR_CTRL_CLEAR	BIT(29)
+#define A5PSW_LK_ADDR_CTRL_LOOKUP	BIT(28)
+#define A5PSW_LK_ADDR_CTRL_WAIT		BIT(27)
+#define A5PSW_LK_ADDR_CTRL_READ		BIT(26)
+#define A5PSW_LK_ADDR_CTRL_WRITE	BIT(25)
+#define A5PSW_LK_ADDR_CTRL_ADDRESS	GENMASK(12, 0)
+
+#define A5PSW_LK_DATA_LO		0x40C
+#define A5PSW_LK_DATA_HI		0x410
+#define A5PSW_LK_DATA_HI_VALID		BIT(16)
+#define A5PSW_LK_DATA_HI_PORT		BIT(16)
+
+#define A5PSW_LK_LEARNCOUNT		0x418
+#define A5PSW_LK_LEARNCOUNT_COUNT	GENMASK(13, 0)
+#define A5PSW_LK_LEARNCOUNT_MODE	GENMASK(31, 30)
+#define A5PSW_LK_LEARNCOUNT_MODE_SET	0x0
+#define A5PSW_LK_LEARNCOUNT_MODE_INC	0x1
+#define A5PSW_LK_LEARNCOUNT_MODE_DEC	0x2
+
+#define A5PSW_MGMT_TAG_CFG		0x480
+#define A5PSW_MGMT_TAG_CFG_TAGFIELD	GENMASK(31, 16)
+#define A5PSW_MGMT_TAG_CFG_ALL_FRAMES	BIT(1)
+#define A5PSW_MGMT_TAG_CFG_ENABLE	BIT(0)
+
+#define A5PSW_LK_AGETIME		0x41C
+#define A5PSW_LK_AGETIME_MASK		GENMASK(23, 0)
+
+#define A5PSW_MDIO_CFG_STATUS		0x700
+#define A5PSW_MDIO_CFG_STATUS_CLKDIV	GENMASK(15, 7)
+#define A5PSW_MDIO_CFG_STATUS_READERR	BIT(1)
+#define A5PSW_MDIO_CFG_STATUS_BUSY	BIT(0)
+
+#define A5PSW_MDIO_COMMAND		0x704
+/* Register is named TRAININIT in datasheet and should be set when reading */
+#define A5PSW_MDIO_COMMAND_READ		BIT(15)
+#define A5PSW_MDIO_COMMAND_PHY_ADDR	GENMASK(9, 5)
+#define A5PSW_MDIO_COMMAND_REG_ADDR	GENMASK(4, 0)
+
+#define A5PSW_MDIO_DATA			0x708
+#define A5PSW_MDIO_DATA_MASK		GENMASK(15, 0)
+
+#define A5PSW_CMD_CFG(port)		(0x808 + A5PSW_PORT_OFFSET(port))
+#define A5PSW_CMD_CFG_CNTL_FRM_ENA	BIT(23)
+#define A5PSW_CMD_CFG_SW_RESET		BIT(13)
+#define A5PSW_CMD_CFG_TX_CRC_APPEND	BIT(11)
+#define A5PSW_CMD_CFG_HD_ENA		BIT(10)
+#define A5PSW_CMD_CFG_PAUSE_IGNORE	BIT(8)
+#define A5PSW_CMD_CFG_CRC_FWD		BIT(6)
+#define A5PSW_CMD_CFG_ETH_SPEED		BIT(3)
+#define A5PSW_CMD_CFG_RX_ENA		BIT(1)
+#define A5PSW_CMD_CFG_TX_ENA		BIT(0)
+
+#define A5PSW_FRM_LENGTH(port)		(0x814 + A5PSW_PORT_OFFSET(port))
+#define A5PSW_FRM_LENGTH_MASK		GENMASK(13, 0)
+
+#define A5PSW_STATUS(port)		(0x840 + A5PSW_PORT_OFFSET(port))
+
+#define A5PSW_STATS_HIWORD		0x900
+
+#define A5PSW_DUMMY_WORKAROUND		0x5000
+
+#define A5PSW_VLAN_TAG(prio, id)	(((prio) << 12) | (id))
+#define A5PSW_PORTS_NUM			5
+#define A5PSW_CPU_PORT			(A5PSW_PORTS_NUM - 1)
+#define A5PSW_MDIO_DEF_FREQ		2500000
+#define A5PSW_MDIO_TIMEOUT		100
+#define A5PSW_JUMBO_LEN			(10 * SZ_1K)
+#define A5PSW_TAG_LEN			8
+#define A5PSW_VLAN_COUNT		32
+
+/* Ensure enough space for 2 VLAN tags */
+#define A5PSW_EXTRA_MTU_LEN		(A5PSW_TAG_LEN + 8)
+#define A5PSW_MGMT_TAG_VALUE		0xE001
+
+#define A5PSW_PATTERN_MGMTFWD		0
+
+#define A5PSW_LK_BUSY_USEC_POLL		10
+#define A5PSW_CTRL_TIMEOUT		1000
+#define A5PSW_TABLE_ENTRIES		8192
+
+/**
+ * struct a5psw - switch struct
+ * @base: Base address of the switch
+ * @hclk_rate: hclk_switch clock rate
+ * @clk_rate: clk_switch clock rate
+ * @dev: Device associated to the switch
+ * @mii_bus: MDIO bus struct
+ * @mdio_freq: MDIO bus frequency requested
+ * @pcs: Array of PCS connected to the switch ports (not for the CPU)
+ * @ds: DSA switch struct
+ * @lk_lock: Lock for the lookup table
+ * @reg_lock: Lock for register read-modify-write operation
+ * @flooding_ports: List of ports that should be flooded
+ */
+struct a5psw {
+	void __iomem *base;
+	struct clk* hclk;
+	struct clk* clk;
+	struct device *dev;
+	struct mii_bus	*mii_bus;
+	u32 mdio_freq;
+	struct phylink_pcs *pcs[A5PSW_PORTS_NUM - 1];
+	struct dsa_switch ds;
+	spinlock_t lk_lock;
+	spinlock_t reg_lock;
+	u32 flooding_ports;
+};