diff mbox series

[v3,1/6] net: introduce DSA class for Ethernet switches

Message ID 20191203145645.13361-2-alexandru.marginean@nxp.com
State Deferred
Delegated to: Tom Rini
Headers show
Series Introduce DSA Ethernet switch class and Felix driver | expand

Commit Message

Alexandru Marginean Dec. 3, 2019, 2:56 p.m. UTC
DSA stands for Distributed Switch Architecture and it covers switches that
are connected to the CPU through an Ethernet link and generally use frame
tags to pass information about the source/destination ports to/from CPU.
Front panel ports are presented as regular ethernet devices in U-Boot and
they are expected to support the typical networking commands.
DSA switches may be cascaded, DSA class code does not currently support
this.

Signed-off-by: Alex Marginean <alexandru.marginean@nxp.com>
---
 drivers/net/Kconfig    |  13 ++
 include/dm/uclass-id.h |   1 +
 include/net/dsa.h      | 141 ++++++++++++++++
 net/Makefile           |   1 +
 net/dsa-uclass.c       | 369 +++++++++++++++++++++++++++++++++++++++++
 5 files changed, 525 insertions(+)
 create mode 100644 include/net/dsa.h
 create mode 100644 net/dsa-uclass.c

Comments

Vladimir Oltean Dec. 15, 2019, 12:44 p.m. UTC | #1
Hi Alex,

On Tue, 3 Dec 2019 at 17:32, Alex Marginean <alexandru.marginean@nxp.com> wrote:
>
> DSA stands for Distributed Switch Architecture and it covers switches that
> are connected to the CPU through an Ethernet link and generally use frame
> tags to pass information about the source/destination ports to/from CPU.
> Front panel ports are presented as regular ethernet devices in U-Boot and
> they are expected to support the typical networking commands.
> DSA switches may be cascaded, DSA class code does not currently support
> this.
>
> Signed-off-by: Alex Marginean <alexandru.marginean@nxp.com>
> ---
>  drivers/net/Kconfig    |  13 ++
>  include/dm/uclass-id.h |   1 +
>  include/net/dsa.h      | 141 ++++++++++++++++
>  net/Makefile           |   1 +
>  net/dsa-uclass.c       | 369 +++++++++++++++++++++++++++++++++++++++++
>  5 files changed, 525 insertions(+)
>  create mode 100644 include/net/dsa.h
>  create mode 100644 net/dsa-uclass.c
>
> diff --git a/drivers/net/Kconfig b/drivers/net/Kconfig
> index 4182897d89..a4157cb122 100644
> --- a/drivers/net/Kconfig
> +++ b/drivers/net/Kconfig
> @@ -37,6 +37,19 @@ config DM_MDIO_MUX
>           This is currently implemented in net/mdio-mux-uclass.c
>           Look in include/miiphy.h for details.
>
> +config DM_DSA
> +       bool "Enable Driver Model for DSA switches"
> +       depends on DM_ETH && DM_MDIO
> +       help
> +         Enable Driver Model for DSA switches
> +
> +         Adds UCLASS_DSA class supporting switches that follow the Distributed
> +         Switch Architecture (DSA).  These switches rely on the presence of a
> +         management switch port connected to an Ethernet controller capable of
> +         receiving frames from the switch.  This host Ethernet controller is
> +         called "master" and "cpu" in DSA terminology.
> +         This is currently implemented in net/dsa-uclass.c
> +
>  config MDIO_SANDBOX
>         depends on DM_MDIO && SANDBOX
>         default y
> diff --git a/include/dm/uclass-id.h b/include/dm/uclass-id.h
> index 0c563d898b..8f37a91488 100644
> --- a/include/dm/uclass-id.h
> +++ b/include/dm/uclass-id.h
> @@ -42,6 +42,7 @@ enum uclass_id {
>         UCLASS_DISPLAY,         /* Display (e.g. DisplayPort, HDMI) */
>         UCLASS_DSI_HOST,        /* Display Serial Interface host */
>         UCLASS_DMA,             /* Direct Memory Access */
> +       UCLASS_DSA,             /* Distributed (Ethernet) Switch Architecture */
>         UCLASS_EFI,             /* EFI managed devices */
>         UCLASS_ETH,             /* Ethernet device */
>         UCLASS_FIRMWARE,        /* Firmware */
> diff --git a/include/net/dsa.h b/include/net/dsa.h
> new file mode 100644
> index 0000000000..2387419b9d
> --- /dev/null
> +++ b/include/net/dsa.h
> @@ -0,0 +1,141 @@
> +/* SPDX-License-Identifier: GPL-2.0+ */
> +/*
> + * Copyright (c) 2019 NXP
> + */
> +
> +#ifndef __DSA_H__
> +#define __DSA_H__
> +
> +#include <common.h>
> +#include <dm.h>
> +#include <phy.h>
> +
> +/**
> + * DSA stands for Distributed Switch Architecture and it is infrastructure
> + * intended to support drivers for Switches that rely on an intermediary
> + * Ethernet device for I/O.  These switches may support cascading allowing
> + * them to be arranged as a tree.
> + * DSA is documented in detail in the Linux kernel documentation under
> + * Documentation/networking/dsa/dsa.txt
> + * The network layout of such a switch is shown below:
> + *
> + *         |---------------------------
> + *         | CPU network device (eth0)|
> + *         ----------------------------
> + *         | <tag added by switch     |
> + *         |                          |
> + *         |                          |
> + *         |        tag added by CPU> |
> + *     |--------------------------------------------|
> + *     | Switch driver                              |
> + *     |--------------------------------------------|
> + *         ||        ||         ||
> + *     |-------|  |-------|  |-------|
> + *     | sw0p0 |  | sw0p1 |  | sw0p2 |
> + *     |-------|  |-------|  |-------|
> + *
> + * In U-Boot the intent is to allow access to front panel ports (shown at the
> + * bottom of the picture) though the master Ethernet port (eth0 in the picture).
> + * Front panel ports are presented as regular Ethernet devices in U-Boot and
> + * they are expected to support the typical networking commands.
> + * In general DSA switches require the use of tags, extra headers added both by
> + * software on Tx and by the switch on Rx.  These tags carry at a minimum port
> + * information and switch information for cascaded set-ups.
> + * In U-Boot these tags are inserted and parsed by the DSA switch driver, the
> + * class code helps with headroom/tailroom for the extra headers.
> + *
> + * TODO:
> + * - handle switch cascading, for now U-Boot only supports stand-alone switches.
> + * - propagate the master Eth MAC address to switch ports, this is used in
> + * Linux to avoid using additional MAC addresses on master Eth.

Any idea how this would be done?
The DSA master port needs to have the MAC address of the switch eth
device in its RX filter (eth_get_ops(dev)->write_hwaddr), or otherwise
be in promiscuous mode to receive packets destined to its MAC address.
Either that, or the switch eth devices need to inherit the MAC address
of the DSA master eth device, but that implies a strict probing order
between switch ports and the DSA master, which is not enforced
currently, and custom logic for DSA switches in eth_post_probe().
So it's not so much of a TODO as it is something to sort out from the
beginning. On the LS1028A, this is not a problem because you use a
tagging prefix that circumvents the need to put the DSA master in
promiscuous mode (6 bytes of ff:ff:ff:ff:ff:ff in the headroom).
However a more generic solution is needed.
For the LS1021A-TSN board, I need to hardcode promiscuous mode in
startup_tsec in order to get any other traffic than broadcast from the
sja1105 driver.
And by the way, if CONFIG_NET_RANDOM_ETHADDR is not set and the DSA
master port does not have a MAC address, U-Boot crashes pretty badly
for me.

> + * - Add support to probe DSA switches connected to a MDIO bus, this is needed
> + * to convert switch drivers that are now under drivers/net/phy.
> + */
> +
> +#define DSA_PORT_NAME_LENGTH   16
> +
> +/* Maximum number of ports each DSA device can have */
> +#define DSA_MAX_PORTS          12
> +/* Used to size internal buffers, no support for jumbo yet */
> +#define DSA_MAX_FRAME_SIZE     2048
> +
> +/**
> + * struct dsa_ops - DSA operations
> + *
> + * @port_enable:  Initialize a switch port for I/O
> + * @port_disable: Disable a port
> + * @xmit:         Insert the DSA tag for transmission
> + *                DSA drivers receive a copy of the packet with headroom and
> + *                tailroom reserved and set to 0.
> + *                Packet points to headroom and length is updated to include
> + *                both headroom and tailroom
> + * @rcv:          Process the DSA tag on reception
> + *                Packet and length describe the frame as received from master
> + *                including any additional headers
> + */
> +struct dsa_ops {
> +       int (*port_enable)(struct udevice *dev, int port,
> +                          struct phy_device *phy);
> +       void (*port_disable)(struct udevice *dev, int port,
> +                            struct phy_device *phy);
> +       int (*xmit)(struct udevice *dev, int port, void *packet, int length);
> +       int (*rcv)(struct udevice *dev, int *port, void *packet, int length);
> +};
> +
> +#define dsa_get_ops(dev) ((struct dsa_ops *)(dev)->driver->ops)
> +
> +/**
> + * struct dsa_port_platdata - DSA port platform data
> + *
> + * @dev :  Port u-device
> + *         Uclass code sets this field for all ports
> + * @phy:   PHY device associated with this port
> + *         Uclass code sets this field for all ports except CPU port, based on
> + *         DT information.  It may be NULL.
> + * @node:  Port DT node, if any.  Uclass code sets this field.
> + * @index: Port index in the DSA switch, set by class code.
> + * @name:  Name of the port Eth device.  If a label property is present in the
> + *         port DT node, it is used as name.  Drivers can use custom names by
> + *         populating this field, otherwise class code generates a default.
> + */
> +struct dsa_port_platdata {
> +       struct udevice *dev;
> +       struct phy_device *phy;
> +       ofnode node;
> +       int index;
> +       char name[DSA_PORT_NAME_LENGTH];
> +};
> +
> +/**
> + * struct dsa_perdev_platdata - Per-device platform data for DSA DM
> + *
> + * @num_ports:   Number of ports the device has, must be <= DSA_MAX_PORTS
> + *               All DSA drivers must set this at _bind
> + * @headroom:    Size, in bytes, of headroom needed for the DSA tag
> + *               All DSA drivers must set this at _bind or _probe
> + * @tailroom:    Size, in bytes, of tailroom needed for the DSA tag
> + *               DSA class code allocates headroom and tailroom on Tx before
> + *               calling DSA driver xmit function
> + *               All DSA drivers must set this at _bind or _probe
> + * @master_node: DT node of the master Ethernet.  DT is optional so this may be
> + *               null.
> + * @master_dev:  Ethernet device to be used as master.  Uclass code sets this
> + *               based on DT information if present, otherwise drivers must set
> + *               this field in _probe.
> + * @cpu_port:    Index of switch port linked to master Ethernet.
> + *               Uclass code sets this based on DT information if present,
> + *               otherwise drivers must set this field in _bind.
> + * @port:        per-port data
> + */
> +struct dsa_perdev_platdata {
> +       int num_ports;
> +       int headroom;
> +       int tailroom;
> +
> +       ofnode master_node;
> +       struct udevice *master_dev;
> +       int cpu_port;
> +       struct dsa_port_platdata port[DSA_MAX_PORTS];
> +};
> +
> +#endif /* __DSA_H__ */
> diff --git a/net/Makefile b/net/Makefile
> index 2a700c8401..fac8c8beb9 100644
> --- a/net/Makefile
> +++ b/net/Makefile
> @@ -28,6 +28,7 @@ obj-$(CONFIG_CMD_SNTP) += sntp.o
>  obj-$(CONFIG_CMD_TFTPBOOT) += tftp.o
>  obj-$(CONFIG_UDP_FUNCTION_FASTBOOT)  += fastboot.o
>  obj-$(CONFIG_CMD_WOL)  += wol.o
> +obj-$(CONFIG_DM_DSA)   += dsa-uclass.o
>
>  # Disable this warning as it is triggered by:
>  # sprintf(buf, index ? "foo%d" : "foo", index)
> diff --git a/net/dsa-uclass.c b/net/dsa-uclass.c
> new file mode 100644
> index 0000000000..3790a72841
> --- /dev/null
> +++ b/net/dsa-uclass.c
> @@ -0,0 +1,369 @@
> +// SPDX-License-Identifier: GPL-2.0+
> +/*
> + * (C) Copyright 2019, NXP
> + */
> +
> +#include <net/dsa.h>
> +#include <dm/lists.h>
> +#include <dm/device-internal.h>
> +#include <dm/uclass-internal.h>
> +#include <miiphy.h>
> +
> +#define DSA_PORT_CHILD_DRV_NAME "dsa-port"
> +
> +/* helper that returns the DSA master Ethernet device. */
> +static struct udevice *dsa_port_get_master(struct udevice *pdev, bool probe)
> +{
> +       struct udevice *dev = dev_get_parent(pdev);
> +       struct dsa_perdev_platdata *platdata = dev_get_platdata(dev);
> +
> +       if (probe)
> +               device_probe(platdata->master_dev);
> +
> +       return platdata->master_dev;
> +}
> +
> +/*
> + * Start the desired port, the CPU port and the master Eth interface.
> + * TODO: if cascaded we may need to _start ports in other switches too
> + */
> +static int dsa_port_start(struct udevice *pdev)
> +{
> +       struct udevice *dev = dev_get_parent(pdev);
> +       struct dsa_perdev_platdata *platdata = dev_get_platdata(dev);
> +       struct udevice *master = dsa_port_get_master(pdev, true);
> +       struct dsa_port_platdata *ppriv = dev_get_priv(pdev);
> +       struct dsa_ops *ops = dsa_get_ops(dev);
> +       int err;
> +
> +       if (!ppriv || !platdata)
> +               return -EINVAL;
> +
> +       if (!master) {
> +               dev_err(pdev, "DSA master Ethernet device not found!\n");
> +               return -EINVAL;
> +       }
> +
> +       if (ops->port_enable) {
> +               err = ops->port_enable(dev, ppriv->index, ppriv->phy);
> +               if (err)
> +                       return err;
> +               err = ops->port_enable(dev, platdata->cpu_port,
> +                                      platdata->port[platdata->cpu_port].phy);
> +               if (err)
> +                       return err;
> +       }
> +
> +       return eth_get_ops(master)->start(master);
> +}
> +
> +/* Stop the desired port, the CPU port and the master Eth interface */
> +static void dsa_port_stop(struct udevice *pdev)
> +{
> +       struct udevice *dev = dev_get_parent(pdev);
> +       struct dsa_perdev_platdata *platdata = dev_get_platdata(dev);
> +       struct udevice *master = dsa_port_get_master(pdev, false);
> +       struct dsa_port_platdata *ppriv = dev_get_priv(pdev);
> +       struct dsa_ops *ops = dsa_get_ops(dev);
> +
> +       if (!ppriv || !platdata)
> +               return;
> +
> +       if (ops->port_disable) {
> +               ops->port_disable(dev, ppriv->index, ppriv->phy);
> +               ops->port_disable(dev, platdata->cpu_port,
> +                                 platdata->port[platdata->cpu_port].phy);
> +       }
> +
> +       /*
> +        * stop master only if it's active, don't probe it otherwise.
> +        * Under normal usage it would be active because we're using it, but
> +        * during tear-down it may have been removed ahead of us.
> +        */
> +       if (master && device_active(master))
> +               eth_get_ops(master)->stop(master);
> +}
> +
> +/*
> + * Insert a DSA tag and call master Ethernet send on the resulting packet
> + * We copy the frame to a stack buffer where we have reserved headroom and
> + * tailroom space.  Headroom and tailroom are set to 0.
> + */
> +static int dsa_port_send(struct udevice *pdev, void *packet, int length)
> +{
> +       struct udevice *dev = dev_get_parent(pdev);
> +       struct dsa_perdev_platdata *platdata = dev_get_platdata(dev);
> +       struct udevice *master = dsa_port_get_master(pdev, true);
> +       struct dsa_port_platdata *ppriv = dev_get_priv(pdev);
> +       struct dsa_ops *ops = dsa_get_ops(dev);
> +       uchar dsa_packet[DSA_MAX_FRAME_SIZE];
> +       int head = platdata->headroom, tail = platdata->tailroom;
> +       int err;
> +
> +       if (!master)
> +               return -EINVAL;
> +
> +       if (length + head + tail > DSA_MAX_FRAME_SIZE)
> +               return -EINVAL;
> +
> +       memset(dsa_packet, 0, head);
> +       memset(dsa_packet + head + length, 0, tail);
> +       memcpy(dsa_packet + head, packet, length);
> +       length += head + tail;
> +
> +       err = ops->xmit(dev, ppriv->index, dsa_packet, length);
> +       if (err)
> +               return err;
> +
> +       return eth_get_ops(master)->send(master, dsa_packet, length);
> +}
> +
> +/* Receive a frame from master Ethernet, process it and pass it on */
> +static int dsa_port_recv(struct udevice *pdev, int flags, uchar **packetp)
> +{
> +       struct udevice *dev = dev_get_parent(pdev);
> +       struct dsa_perdev_platdata *platdata = dev_get_platdata(dev);
> +       struct udevice *master = dsa_port_get_master(pdev, true);
> +       struct dsa_port_platdata *ppriv = dev_get_priv(pdev);
> +       struct dsa_ops *ops = dsa_get_ops(dev);
> +       int head = platdata->headroom, tail = platdata->tailroom;
> +       int length, port_index, err;
> +
> +       if (!master)
> +               return -EINVAL;
> +
> +       length = eth_get_ops(master)->recv(master, flags, packetp);
> +       if (length <= 0)
> +               return length;
> +
> +       /*
> +        * if we receive frames from a different port or frames that DSA driver
> +        * doesn't like we discard them here.
> +        * In case of discard we return with no frame and expect to be called
> +        * again instead of looping here, so upper layer can deal with timeouts
> +        * and ctrl-c
> +        */
> +       err = ops->rcv(dev, &port_index, *packetp, length);
> +       if (err || port_index != ppriv->index || (length <= head + tail)) {
> +               if (eth_get_ops(master)->free_pkt)
> +                       eth_get_ops(master)->free_pkt(master, *packetp, length);
> +               return -EAGAIN;
> +       }
> +
> +       /*
> +        * We move the pointer over headroom here to avoid a copy.  If free_pkt
> +        * gets called we move the pointer back before calling master free_pkt.
> +        */
> +       *packetp += head;
> +
> +       return length - head - tail;
> +}
> +
> +static int dsa_port_free_pkt(struct udevice *pdev, uchar *packet, int length)
> +{
> +       struct udevice *dev = dev_get_parent(pdev);
> +       struct dsa_perdev_platdata *platdata = dev_get_platdata(dev);
> +       struct udevice *master = dsa_port_get_master(pdev, true);
> +
> +       if (!master)
> +               return -EINVAL;
> +
> +       if (eth_get_ops(master)->free_pkt) {
> +               /* return the original pointer and length to master Eth */
> +               packet -= platdata->headroom;
> +               length += platdata->headroom - platdata->tailroom;
> +
> +               return eth_get_ops(master)->free_pkt(master, packet, length);
> +       }
> +
> +       return 0;
> +}
> +
> +static const struct eth_ops dsa_port_ops = {
> +       .start          = dsa_port_start,
> +       .send           = dsa_port_send,
> +       .recv           = dsa_port_recv,
> +       .stop           = dsa_port_stop,
> +       .free_pkt       = dsa_port_free_pkt,
> +};
> +
> +U_BOOT_DRIVER(dsa_port) = {
> +       .name   = DSA_PORT_CHILD_DRV_NAME,
> +       .id     = UCLASS_ETH,
> +       .ops    = &dsa_port_ops,
> +       .platdata_auto_alloc_size = sizeof(struct eth_pdata),
> +};
> +
> +/*
> + * reads the DT properties of the given DSA port.
> + * If the return value is != 0 then the port is skipped
> + */
> +static int dsa_port_parse_dt(struct udevice *dev, int port_index,
> +                            ofnode ports_node, bool *is_cpu)
> +{
> +       struct dsa_perdev_platdata *platdata = dev_get_platdata(dev);
> +       struct dsa_port_platdata *port = &platdata->port[port_index];
> +       ofnode temp_node;
> +       u32 ethernet;
> +
> +       /*
> +        * if we don't have a DT we don't do anything here but the port is
> +        * registered normally
> +        */
> +       if (!ofnode_valid(ports_node))
> +               return 0;
> +
> +       ofnode_for_each_subnode(temp_node, ports_node) {
> +               const char *port_label;
> +               u32 reg;
> +
> +               if (ofnode_read_u32(temp_node, "reg", &reg) ||
> +                   reg != port_index)
> +                       continue;
> +
> +               /* if the port is explicitly disabled in DT skip it */
> +               if (!ofnode_is_available(temp_node))
> +                       return -ENODEV;
> +
> +               port->node = temp_node;
> +
> +               dev_dbg(dev, "port %d node %s\n", port->index,
> +                       ofnode_get_name(port->node));
> +
> +               /* Use 'label' if present in DT */
> +               port_label = ofnode_read_string(port->node, "label");
> +               if (port_label)
> +                       strncpy(port->name, port_label, DSA_PORT_NAME_LENGTH);
> +
> +               *is_cpu = !ofnode_read_u32(port->node, "ethernet",
> +                                          &ethernet);
> +
> +               if (*is_cpu) {
> +                       platdata->master_node =
> +                               ofnode_get_by_phandle(ethernet);
> +                       platdata->cpu_port = port_index;
> +
> +                       dev_dbg(dev, "master node %s on port %d\n",
> +                               ofnode_get_name(platdata->master_node),
> +                               port_index);
> +               }
> +               break;
> +       }
> +
> +       return 0;
> +}
> +
> +/**
> + * This function mostly deals with pulling information out of the device tree
> + * into the platdata structure.
> + * It goes through the list of switch ports, registers an Eth device for each
> + * front panel port and identifies the cpu port connected to master Eth device.
> + * TODO: support cascaded switches
> + */
> +static int dm_dsa_post_bind(struct udevice *dev)
> +{
> +       struct dsa_perdev_platdata *platdata = dev_get_platdata(dev);
> +       ofnode ports_node = ofnode_null();
> +       int first_err = 0, err = 0, i;
> +
> +       if (!platdata) {
> +               dev_err(dev, "missing plaform data\n");
> +               return -EINVAL;
> +       }
> +
> +       if (platdata->num_ports <= 0 || platdata->num_ports > DSA_MAX_PORTS) {
> +               dev_err(dev, "unexpected num_ports value (%d)\n",
> +                       platdata->num_ports);
> +               return -EINVAL;
> +       }
> +
> +       platdata->master_node = ofnode_null();
> +
> +       if (!ofnode_valid(dev->node)) {
> +               dev_dbg(dev, "Device doesn't have a valid DT node!\n");
> +       } else {
> +               ports_node = ofnode_find_subnode(dev->node, "ports");
> +               if (!ofnode_valid(ports_node))
> +                       dev_dbg(dev,
> +                               "ports node is missing under DSA device!\n");
> +       }
> +
> +       for (i = 0; i < platdata->num_ports; i++) {
> +               struct dsa_port_platdata *port = &platdata->port[i];
> +               bool skip_port, is_cpu = false;
> +
> +               port->index = i;
> +
> +               /*
> +                * If the driver set up port names in _bind use those, otherwise
> +                * use default ones.
> +                * If present, DT label is used as name and overrides anything
> +                * we may have here.
> +                */
> +               if (!strlen(port->name))
> +                       snprintf(port->name, DSA_PORT_NAME_LENGTH, "%s@%d",
> +                                dev->name, i);
> +
> +               skip_port = !!dsa_port_parse_dt(dev, i, ports_node, &is_cpu);
> +
> +               /*
> +                * if this is the CPU port don't register it as an ETH device,
> +                * we skip it on purpose since I/O to/from it from the CPU
> +                * isn't useful
> +                * TODO: cpu port may have a PHY and we don't handle that yet.
> +                */
> +               if (is_cpu || skip_port)
> +                       continue;
> +
> +               err = device_bind_driver_to_node(dev, DSA_PORT_CHILD_DRV_NAME,
> +                                                port->name, port->node,
> +                                                &port->dev);
> +
> +               /* try to bind all ports but keep 1st error */
> +               if (err && !first_err)
> +                       first_err = err;
> +       }
> +
> +       if (!ofnode_valid(platdata->master_node))
> +               dev_dbg(dev, "DSA master Eth device is missing!\n");
> +
> +       return first_err;
> +}
> +
> +/**
> + * This function deals with additional devices around the switch as these should
> + * have been bound to drivers by now.
> + * TODO: pick up references to other switch devices here, if we're cascaded.
> + */
> +static int dm_dsa_pre_probe(struct udevice *dev)
> +{
> +       struct dsa_perdev_platdata *platdata = dev_get_platdata(dev);
> +       int i;
> +
> +       if (!platdata)
> +               return -EINVAL;
> +
> +       if (ofnode_valid(platdata->master_node))
> +               uclass_find_device_by_ofnode(UCLASS_ETH, platdata->master_node,
> +                                            &platdata->master_dev);
> +
> +       for (i = 0; i < platdata->num_ports; i++) {
> +               struct dsa_port_platdata *port = &platdata->port[i];
> +
> +               if (port->dev) {
> +                       port->dev->priv = port;
> +                       port->phy = dm_eth_phy_connect(port->dev);
> +               }
> +       }
> +
> +       return 0;
> +}
> +
> +UCLASS_DRIVER(dsa) = {
> +       .id = UCLASS_DSA,
> +       .name = "dsa",
> +       .post_bind  = dm_dsa_post_bind,
> +       .pre_probe = dm_dsa_pre_probe,
> +       .per_device_platdata_auto_alloc_size =
> +                       sizeof(struct dsa_perdev_platdata),
> +};
> --
> 2.17.1
>

Thanks,
-Vladimir
Vladimir Oltean Dec. 15, 2019, 1:08 p.m. UTC | #2
On Tue, 3 Dec 2019 at 17:32, Alex Marginean <alexandru.marginean@nxp.com> wrote:
> +/**
> + * This function deals with additional devices around the switch as these should
> + * have been bound to drivers by now.
> + * TODO: pick up references to other switch devices here, if we're cascaded.
> + */
> +static int dm_dsa_pre_probe(struct udevice *dev)
> +{
> +       struct dsa_perdev_platdata *platdata = dev_get_platdata(dev);
> +       int i;
> +
> +       if (!platdata)
> +               return -EINVAL;
> +
> +       if (ofnode_valid(platdata->master_node))
> +               uclass_find_device_by_ofnode(UCLASS_ETH, platdata->master_node,
> +                                            &platdata->master_dev);
> +
> +       for (i = 0; i < platdata->num_ports; i++) {
> +               struct dsa_port_platdata *port = &platdata->port[i];
> +
> +               if (port->dev) {
> +                       port->dev->priv = port;
> +                       port->phy = dm_eth_phy_connect(port->dev);

Fixed-link interfaces don't work with DM_MDIO. That is somewhat
natural as there is no MDIO bus for a fixed-link. However the legacy
phy_connect function can be made rather easily to work with
fixed-link, since it has the necessary code for dealing with it
already. I am not, however, sure how it ever worked in the absence of
an MDIO bus.

commit 1b7e23cc7e6d0dc3fe7ae9c55390b40717ff3c3a
Author: Vladimir Oltean <olteanv@gmail.com>
Date:   Sat Dec 14 23:25:40 2019 +0200

    phy: make phy_connect_fixed work with a null mdio bus

    It is utterly pointless to require an MDIO bus pointer for a fixed PHY
    device. The fixed.c implementation does not require it, only
    phy_device_create. Fix that.

    Signed-off-by: Vladimir Oltean <olteanv@gmail.com>

diff --git a/drivers/net/phy/phy.c b/drivers/net/phy/phy.c
index 80a7664e4978..8ea5c9005291 100644
--- a/drivers/net/phy/phy.c
+++ b/drivers/net/phy/phy.c
@@ -658,7 +658,7 @@ static struct phy_device *phy_device_create(struct
mii_dev *bus, int addr,
        dev = malloc(sizeof(*dev));
        if (!dev) {
                printf("Failed to allocate PHY device for %s:%d\n",
-                      bus->name, addr);
+                      bus ? bus->name : "(null bus)", addr);
                return NULL;
        }

@@ -686,7 +686,7 @@ static struct phy_device *phy_device_create(struct
mii_dev *bus, int addr,
                return NULL;
        }

-       if (addr >= 0 && addr < PHY_MAX_ADDR)
+       if (addr >= 0 && addr < PHY_MAX_ADDR && phy_id != PHY_FIXED_ID)
                bus->phymap[addr] = dev;

        return dev;

With the patch above in place, fixed-link can also be made to work
with some logic similar to what can be seen below:

    if (ofnode_valid(ofnode_find_subnode(port->dev->node, "fixed-link")))
        port->phy = phy_connect(NULL, 0, port->dev, phy_mode); //
phy_mode needs to be pre-parsed somewhere else as well
    else
        port->phy = dm_eth_phy_connect(port->dev);

How would you see fixed-link interfaces being treated? My question so
far is in the context of front-panel ports but I am interested in your
view of the CPU port situation as well.

> +               }
> +       }
> +
> +       return 0;
> +}

Thanks,
-Vladimir
Alexandru Marginean Dec. 17, 2019, 7:11 a.m. UTC | #3
Hi Vladimir,

On 12/15/2019 1:44 PM, Vladimir Oltean wrote:
> Hi Alex,
> 
> On Tue, 3 Dec 2019 at 17:32, Alex Marginean <alexandru.marginean@nxp.com> wrote:
>>
>> DSA stands for Distributed Switch Architecture and it covers switches that
>> are connected to the CPU through an Ethernet link and generally use frame
>> tags to pass information about the source/destination ports to/from CPU.
>> Front panel ports are presented as regular ethernet devices in U-Boot and
>> they are expected to support the typical networking commands.
>> DSA switches may be cascaded, DSA class code does not currently support
>> this.
>>
>> Signed-off-by: Alex Marginean <alexandru.marginean@nxp.com>
>> ---
>>   drivers/net/Kconfig    |  13 ++
>>   include/dm/uclass-id.h |   1 +
>>   include/net/dsa.h      | 141 ++++++++++++++++
>>   net/Makefile           |   1 +
>>   net/dsa-uclass.c       | 369 +++++++++++++++++++++++++++++++++++++++++
>>   5 files changed, 525 insertions(+)
>>   create mode 100644 include/net/dsa.h
>>   create mode 100644 net/dsa-uclass.c
>>
>> diff --git a/drivers/net/Kconfig b/drivers/net/Kconfig
>> index 4182897d89..a4157cb122 100644
>> --- a/drivers/net/Kconfig
>> +++ b/drivers/net/Kconfig
>> @@ -37,6 +37,19 @@ config DM_MDIO_MUX
>>            This is currently implemented in net/mdio-mux-uclass.c
>>            Look in include/miiphy.h for details.
>>
>> +config DM_DSA
>> +       bool "Enable Driver Model for DSA switches"
>> +       depends on DM_ETH && DM_MDIO
>> +       help
>> +         Enable Driver Model for DSA switches
>> +
>> +         Adds UCLASS_DSA class supporting switches that follow the Distributed
>> +         Switch Architecture (DSA).  These switches rely on the presence of a
>> +         management switch port connected to an Ethernet controller capable of
>> +         receiving frames from the switch.  This host Ethernet controller is
>> +         called "master" and "cpu" in DSA terminology.
>> +         This is currently implemented in net/dsa-uclass.c
>> +
>>   config MDIO_SANDBOX
>>          depends on DM_MDIO && SANDBOX
>>          default y
>> diff --git a/include/dm/uclass-id.h b/include/dm/uclass-id.h
>> index 0c563d898b..8f37a91488 100644
>> --- a/include/dm/uclass-id.h
>> +++ b/include/dm/uclass-id.h
>> @@ -42,6 +42,7 @@ enum uclass_id {
>>          UCLASS_DISPLAY,         /* Display (e.g. DisplayPort, HDMI) */
>>          UCLASS_DSI_HOST,        /* Display Serial Interface host */
>>          UCLASS_DMA,             /* Direct Memory Access */
>> +       UCLASS_DSA,             /* Distributed (Ethernet) Switch Architecture */
>>          UCLASS_EFI,             /* EFI managed devices */
>>          UCLASS_ETH,             /* Ethernet device */
>>          UCLASS_FIRMWARE,        /* Firmware */
>> diff --git a/include/net/dsa.h b/include/net/dsa.h
>> new file mode 100644
>> index 0000000000..2387419b9d
>> --- /dev/null
>> +++ b/include/net/dsa.h
>> @@ -0,0 +1,141 @@
>> +/* SPDX-License-Identifier: GPL-2.0+ */
>> +/*
>> + * Copyright (c) 2019 NXP
>> + */
>> +
>> +#ifndef __DSA_H__
>> +#define __DSA_H__
>> +
>> +#include <common.h>
>> +#include <dm.h>
>> +#include <phy.h>
>> +
>> +/**
>> + * DSA stands for Distributed Switch Architecture and it is infrastructure
>> + * intended to support drivers for Switches that rely on an intermediary
>> + * Ethernet device for I/O.  These switches may support cascading allowing
>> + * them to be arranged as a tree.
>> + * DSA is documented in detail in the Linux kernel documentation under
>> + * Documentation/networking/dsa/dsa.txt
>> + * The network layout of such a switch is shown below:
>> + *
>> + *         |---------------------------
>> + *         | CPU network device (eth0)|
>> + *         ----------------------------
>> + *         | <tag added by switch     |
>> + *         |                          |
>> + *         |                          |
>> + *         |        tag added by CPU> |
>> + *     |--------------------------------------------|
>> + *     | Switch driver                              |
>> + *     |--------------------------------------------|
>> + *         ||        ||         ||
>> + *     |-------|  |-------|  |-------|
>> + *     | sw0p0 |  | sw0p1 |  | sw0p2 |
>> + *     |-------|  |-------|  |-------|
>> + *
>> + * In U-Boot the intent is to allow access to front panel ports (shown at the
>> + * bottom of the picture) though the master Ethernet port (eth0 in the picture).
>> + * Front panel ports are presented as regular Ethernet devices in U-Boot and
>> + * they are expected to support the typical networking commands.
>> + * In general DSA switches require the use of tags, extra headers added both by
>> + * software on Tx and by the switch on Rx.  These tags carry at a minimum port
>> + * information and switch information for cascaded set-ups.
>> + * In U-Boot these tags are inserted and parsed by the DSA switch driver, the
>> + * class code helps with headroom/tailroom for the extra headers.
>> + *
>> + * TODO:
>> + * - handle switch cascading, for now U-Boot only supports stand-alone switches.
>> + * - propagate the master Eth MAC address to switch ports, this is used in
>> + * Linux to avoid using additional MAC addresses on master Eth.
> 
> Any idea how this would be done?
> The DSA master port needs to have the MAC address of the switch eth
> device in its RX filter (eth_get_ops(dev)->write_hwaddr), or otherwise
> be in promiscuous mode to receive packets destined to its MAC address.
> Either that, or the switch eth devices need to inherit the MAC address
> of the DSA master eth device, but that implies a strict probing order
> between switch ports and the DSA master, which is not enforced
> currently, and custom logic for DSA switches in eth_post_probe().
> So it's not so much of a TODO as it is something to sort out from the
> beginning. On the LS1028A, this is not a problem because you use a
> tagging prefix that circumvents the need to put the DSA master in
> promiscuous mode (6 bytes of ff:ff:ff:ff:ff:ff in the headroom).
> However a more generic solution is needed.
> For the LS1021A-TSN board, I need to hardcode promiscuous mode in
> startup_tsec in order to get any other traffic than broadcast from the
> sja1105 driver.
> And by the way, if CONFIG_NET_RANDOM_ETHADDR is not set and the DSA
> master port does not have a MAC address, U-Boot crashes pretty badly
> for me.

For DSA ports in particular U-Boot shouldn't need to pass MAC addresses 
to Linux.
I'm thinking to add some sort of flag to ETH devices to skip the MAC 
address set-up part in eth_initialize, this would be used for the switch 
ports as they don't need their own private addresses.  The MAC address 
can be resolved for them later in dsa uclass code when the ports are 
used and by that time master Eth should also be probed.

Alex

>> + * - Add support to probe DSA switches connected to a MDIO bus, this is needed
>> + * to convert switch drivers that are now under drivers/net/phy.
>> + */
>> +
>> +#define DSA_PORT_NAME_LENGTH   16
>> +
>> +/* Maximum number of ports each DSA device can have */
>> +#define DSA_MAX_PORTS          12
>> +/* Used to size internal buffers, no support for jumbo yet */
>> +#define DSA_MAX_FRAME_SIZE     2048
>> +
>> +/**
>> + * struct dsa_ops - DSA operations
>> + *
>> + * @port_enable:  Initialize a switch port for I/O
>> + * @port_disable: Disable a port
>> + * @xmit:         Insert the DSA tag for transmission
>> + *                DSA drivers receive a copy of the packet with headroom and
>> + *                tailroom reserved and set to 0.
>> + *                Packet points to headroom and length is updated to include
>> + *                both headroom and tailroom
>> + * @rcv:          Process the DSA tag on reception
>> + *                Packet and length describe the frame as received from master
>> + *                including any additional headers
>> + */
>> +struct dsa_ops {
>> +       int (*port_enable)(struct udevice *dev, int port,
>> +                          struct phy_device *phy);
>> +       void (*port_disable)(struct udevice *dev, int port,
>> +                            struct phy_device *phy);
>> +       int (*xmit)(struct udevice *dev, int port, void *packet, int length);
>> +       int (*rcv)(struct udevice *dev, int *port, void *packet, int length);
>> +};
>> +
>> +#define dsa_get_ops(dev) ((struct dsa_ops *)(dev)->driver->ops)
>> +
>> +/**
>> + * struct dsa_port_platdata - DSA port platform data
>> + *
>> + * @dev :  Port u-device
>> + *         Uclass code sets this field for all ports
>> + * @phy:   PHY device associated with this port
>> + *         Uclass code sets this field for all ports except CPU port, based on
>> + *         DT information.  It may be NULL.
>> + * @node:  Port DT node, if any.  Uclass code sets this field.
>> + * @index: Port index in the DSA switch, set by class code.
>> + * @name:  Name of the port Eth device.  If a label property is present in the
>> + *         port DT node, it is used as name.  Drivers can use custom names by
>> + *         populating this field, otherwise class code generates a default.
>> + */
>> +struct dsa_port_platdata {
>> +       struct udevice *dev;
>> +       struct phy_device *phy;
>> +       ofnode node;
>> +       int index;
>> +       char name[DSA_PORT_NAME_LENGTH];
>> +};
>> +
>> +/**
>> + * struct dsa_perdev_platdata - Per-device platform data for DSA DM
>> + *
>> + * @num_ports:   Number of ports the device has, must be <= DSA_MAX_PORTS
>> + *               All DSA drivers must set this at _bind
>> + * @headroom:    Size, in bytes, of headroom needed for the DSA tag
>> + *               All DSA drivers must set this at _bind or _probe
>> + * @tailroom:    Size, in bytes, of tailroom needed for the DSA tag
>> + *               DSA class code allocates headroom and tailroom on Tx before
>> + *               calling DSA driver xmit function
>> + *               All DSA drivers must set this at _bind or _probe
>> + * @master_node: DT node of the master Ethernet.  DT is optional so this may be
>> + *               null.
>> + * @master_dev:  Ethernet device to be used as master.  Uclass code sets this
>> + *               based on DT information if present, otherwise drivers must set
>> + *               this field in _probe.
>> + * @cpu_port:    Index of switch port linked to master Ethernet.
>> + *               Uclass code sets this based on DT information if present,
>> + *               otherwise drivers must set this field in _bind.
>> + * @port:        per-port data
>> + */
>> +struct dsa_perdev_platdata {
>> +       int num_ports;
>> +       int headroom;
>> +       int tailroom;
>> +
>> +       ofnode master_node;
>> +       struct udevice *master_dev;
>> +       int cpu_port;
>> +       struct dsa_port_platdata port[DSA_MAX_PORTS];
>> +};
>> +
>> +#endif /* __DSA_H__ */
>> diff --git a/net/Makefile b/net/Makefile
>> index 2a700c8401..fac8c8beb9 100644
>> --- a/net/Makefile
>> +++ b/net/Makefile
>> @@ -28,6 +28,7 @@ obj-$(CONFIG_CMD_SNTP) += sntp.o
>>   obj-$(CONFIG_CMD_TFTPBOOT) += tftp.o
>>   obj-$(CONFIG_UDP_FUNCTION_FASTBOOT)  += fastboot.o
>>   obj-$(CONFIG_CMD_WOL)  += wol.o
>> +obj-$(CONFIG_DM_DSA)   += dsa-uclass.o
>>
>>   # Disable this warning as it is triggered by:
>>   # sprintf(buf, index ? "foo%d" : "foo", index)
>> diff --git a/net/dsa-uclass.c b/net/dsa-uclass.c
>> new file mode 100644
>> index 0000000000..3790a72841
>> --- /dev/null
>> +++ b/net/dsa-uclass.c
>> @@ -0,0 +1,369 @@
>> +// SPDX-License-Identifier: GPL-2.0+
>> +/*
>> + * (C) Copyright 2019, NXP
>> + */
>> +
>> +#include <net/dsa.h>
>> +#include <dm/lists.h>
>> +#include <dm/device-internal.h>
>> +#include <dm/uclass-internal.h>
>> +#include <miiphy.h>
>> +
>> +#define DSA_PORT_CHILD_DRV_NAME "dsa-port"
>> +
>> +/* helper that returns the DSA master Ethernet device. */
>> +static struct udevice *dsa_port_get_master(struct udevice *pdev, bool probe)
>> +{
>> +       struct udevice *dev = dev_get_parent(pdev);
>> +       struct dsa_perdev_platdata *platdata = dev_get_platdata(dev);
>> +
>> +       if (probe)
>> +               device_probe(platdata->master_dev);
>> +
>> +       return platdata->master_dev;
>> +}
>> +
>> +/*
>> + * Start the desired port, the CPU port and the master Eth interface.
>> + * TODO: if cascaded we may need to _start ports in other switches too
>> + */
>> +static int dsa_port_start(struct udevice *pdev)
>> +{
>> +       struct udevice *dev = dev_get_parent(pdev);
>> +       struct dsa_perdev_platdata *platdata = dev_get_platdata(dev);
>> +       struct udevice *master = dsa_port_get_master(pdev, true);
>> +       struct dsa_port_platdata *ppriv = dev_get_priv(pdev);
>> +       struct dsa_ops *ops = dsa_get_ops(dev);
>> +       int err;
>> +
>> +       if (!ppriv || !platdata)
>> +               return -EINVAL;
>> +
>> +       if (!master) {
>> +               dev_err(pdev, "DSA master Ethernet device not found!\n");
>> +               return -EINVAL;
>> +       }
>> +
>> +       if (ops->port_enable) {
>> +               err = ops->port_enable(dev, ppriv->index, ppriv->phy);
>> +               if (err)
>> +                       return err;
>> +               err = ops->port_enable(dev, platdata->cpu_port,
>> +                                      platdata->port[platdata->cpu_port].phy);
>> +               if (err)
>> +                       return err;
>> +       }
>> +
>> +       return eth_get_ops(master)->start(master);
>> +}
>> +
>> +/* Stop the desired port, the CPU port and the master Eth interface */
>> +static void dsa_port_stop(struct udevice *pdev)
>> +{
>> +       struct udevice *dev = dev_get_parent(pdev);
>> +       struct dsa_perdev_platdata *platdata = dev_get_platdata(dev);
>> +       struct udevice *master = dsa_port_get_master(pdev, false);
>> +       struct dsa_port_platdata *ppriv = dev_get_priv(pdev);
>> +       struct dsa_ops *ops = dsa_get_ops(dev);
>> +
>> +       if (!ppriv || !platdata)
>> +               return;
>> +
>> +       if (ops->port_disable) {
>> +               ops->port_disable(dev, ppriv->index, ppriv->phy);
>> +               ops->port_disable(dev, platdata->cpu_port,
>> +                                 platdata->port[platdata->cpu_port].phy);
>> +       }
>> +
>> +       /*
>> +        * stop master only if it's active, don't probe it otherwise.
>> +        * Under normal usage it would be active because we're using it, but
>> +        * during tear-down it may have been removed ahead of us.
>> +        */
>> +       if (master && device_active(master))
>> +               eth_get_ops(master)->stop(master);
>> +}
>> +
>> +/*
>> + * Insert a DSA tag and call master Ethernet send on the resulting packet
>> + * We copy the frame to a stack buffer where we have reserved headroom and
>> + * tailroom space.  Headroom and tailroom are set to 0.
>> + */
>> +static int dsa_port_send(struct udevice *pdev, void *packet, int length)
>> +{
>> +       struct udevice *dev = dev_get_parent(pdev);
>> +       struct dsa_perdev_platdata *platdata = dev_get_platdata(dev);
>> +       struct udevice *master = dsa_port_get_master(pdev, true);
>> +       struct dsa_port_platdata *ppriv = dev_get_priv(pdev);
>> +       struct dsa_ops *ops = dsa_get_ops(dev);
>> +       uchar dsa_packet[DSA_MAX_FRAME_SIZE];
>> +       int head = platdata->headroom, tail = platdata->tailroom;
>> +       int err;
>> +
>> +       if (!master)
>> +               return -EINVAL;
>> +
>> +       if (length + head + tail > DSA_MAX_FRAME_SIZE)
>> +               return -EINVAL;
>> +
>> +       memset(dsa_packet, 0, head);
>> +       memset(dsa_packet + head + length, 0, tail);
>> +       memcpy(dsa_packet + head, packet, length);
>> +       length += head + tail;
>> +
>> +       err = ops->xmit(dev, ppriv->index, dsa_packet, length);
>> +       if (err)
>> +               return err;
>> +
>> +       return eth_get_ops(master)->send(master, dsa_packet, length);
>> +}
>> +
>> +/* Receive a frame from master Ethernet, process it and pass it on */
>> +static int dsa_port_recv(struct udevice *pdev, int flags, uchar **packetp)
>> +{
>> +       struct udevice *dev = dev_get_parent(pdev);
>> +       struct dsa_perdev_platdata *platdata = dev_get_platdata(dev);
>> +       struct udevice *master = dsa_port_get_master(pdev, true);
>> +       struct dsa_port_platdata *ppriv = dev_get_priv(pdev);
>> +       struct dsa_ops *ops = dsa_get_ops(dev);
>> +       int head = platdata->headroom, tail = platdata->tailroom;
>> +       int length, port_index, err;
>> +
>> +       if (!master)
>> +               return -EINVAL;
>> +
>> +       length = eth_get_ops(master)->recv(master, flags, packetp);
>> +       if (length <= 0)
>> +               return length;
>> +
>> +       /*
>> +        * if we receive frames from a different port or frames that DSA driver
>> +        * doesn't like we discard them here.
>> +        * In case of discard we return with no frame and expect to be called
>> +        * again instead of looping here, so upper layer can deal with timeouts
>> +        * and ctrl-c
>> +        */
>> +       err = ops->rcv(dev, &port_index, *packetp, length);
>> +       if (err || port_index != ppriv->index || (length <= head + tail)) {
>> +               if (eth_get_ops(master)->free_pkt)
>> +                       eth_get_ops(master)->free_pkt(master, *packetp, length);
>> +               return -EAGAIN;
>> +       }
>> +
>> +       /*
>> +        * We move the pointer over headroom here to avoid a copy.  If free_pkt
>> +        * gets called we move the pointer back before calling master free_pkt.
>> +        */
>> +       *packetp += head;
>> +
>> +       return length - head - tail;
>> +}
>> +
>> +static int dsa_port_free_pkt(struct udevice *pdev, uchar *packet, int length)
>> +{
>> +       struct udevice *dev = dev_get_parent(pdev);
>> +       struct dsa_perdev_platdata *platdata = dev_get_platdata(dev);
>> +       struct udevice *master = dsa_port_get_master(pdev, true);
>> +
>> +       if (!master)
>> +               return -EINVAL;
>> +
>> +       if (eth_get_ops(master)->free_pkt) {
>> +               /* return the original pointer and length to master Eth */
>> +               packet -= platdata->headroom;
>> +               length += platdata->headroom - platdata->tailroom;
>> +
>> +               return eth_get_ops(master)->free_pkt(master, packet, length);
>> +       }
>> +
>> +       return 0;
>> +}
>> +
>> +static const struct eth_ops dsa_port_ops = {
>> +       .start          = dsa_port_start,
>> +       .send           = dsa_port_send,
>> +       .recv           = dsa_port_recv,
>> +       .stop           = dsa_port_stop,
>> +       .free_pkt       = dsa_port_free_pkt,
>> +};
>> +
>> +U_BOOT_DRIVER(dsa_port) = {
>> +       .name   = DSA_PORT_CHILD_DRV_NAME,
>> +       .id     = UCLASS_ETH,
>> +       .ops    = &dsa_port_ops,
>> +       .platdata_auto_alloc_size = sizeof(struct eth_pdata),
>> +};
>> +
>> +/*
>> + * reads the DT properties of the given DSA port.
>> + * If the return value is != 0 then the port is skipped
>> + */
>> +static int dsa_port_parse_dt(struct udevice *dev, int port_index,
>> +                            ofnode ports_node, bool *is_cpu)
>> +{
>> +       struct dsa_perdev_platdata *platdata = dev_get_platdata(dev);
>> +       struct dsa_port_platdata *port = &platdata->port[port_index];
>> +       ofnode temp_node;
>> +       u32 ethernet;
>> +
>> +       /*
>> +        * if we don't have a DT we don't do anything here but the port is
>> +        * registered normally
>> +        */
>> +       if (!ofnode_valid(ports_node))
>> +               return 0;
>> +
>> +       ofnode_for_each_subnode(temp_node, ports_node) {
>> +               const char *port_label;
>> +               u32 reg;
>> +
>> +               if (ofnode_read_u32(temp_node, "reg", &reg) ||
>> +                   reg != port_index)
>> +                       continue;
>> +
>> +               /* if the port is explicitly disabled in DT skip it */
>> +               if (!ofnode_is_available(temp_node))
>> +                       return -ENODEV;
>> +
>> +               port->node = temp_node;
>> +
>> +               dev_dbg(dev, "port %d node %s\n", port->index,
>> +                       ofnode_get_name(port->node));
>> +
>> +               /* Use 'label' if present in DT */
>> +               port_label = ofnode_read_string(port->node, "label");
>> +               if (port_label)
>> +                       strncpy(port->name, port_label, DSA_PORT_NAME_LENGTH);
>> +
>> +               *is_cpu = !ofnode_read_u32(port->node, "ethernet",
>> +                                          &ethernet);
>> +
>> +               if (*is_cpu) {
>> +                       platdata->master_node =
>> +                               ofnode_get_by_phandle(ethernet);
>> +                       platdata->cpu_port = port_index;
>> +
>> +                       dev_dbg(dev, "master node %s on port %d\n",
>> +                               ofnode_get_name(platdata->master_node),
>> +                               port_index);
>> +               }
>> +               break;
>> +       }
>> +
>> +       return 0;
>> +}
>> +
>> +/**
>> + * This function mostly deals with pulling information out of the device tree
>> + * into the platdata structure.
>> + * It goes through the list of switch ports, registers an Eth device for each
>> + * front panel port and identifies the cpu port connected to master Eth device.
>> + * TODO: support cascaded switches
>> + */
>> +static int dm_dsa_post_bind(struct udevice *dev)
>> +{
>> +       struct dsa_perdev_platdata *platdata = dev_get_platdata(dev);
>> +       ofnode ports_node = ofnode_null();
>> +       int first_err = 0, err = 0, i;
>> +
>> +       if (!platdata) {
>> +               dev_err(dev, "missing plaform data\n");
>> +               return -EINVAL;
>> +       }
>> +
>> +       if (platdata->num_ports <= 0 || platdata->num_ports > DSA_MAX_PORTS) {
>> +               dev_err(dev, "unexpected num_ports value (%d)\n",
>> +                       platdata->num_ports);
>> +               return -EINVAL;
>> +       }
>> +
>> +       platdata->master_node = ofnode_null();
>> +
>> +       if (!ofnode_valid(dev->node)) {
>> +               dev_dbg(dev, "Device doesn't have a valid DT node!\n");
>> +       } else {
>> +               ports_node = ofnode_find_subnode(dev->node, "ports");
>> +               if (!ofnode_valid(ports_node))
>> +                       dev_dbg(dev,
>> +                               "ports node is missing under DSA device!\n");
>> +       }
>> +
>> +       for (i = 0; i < platdata->num_ports; i++) {
>> +               struct dsa_port_platdata *port = &platdata->port[i];
>> +               bool skip_port, is_cpu = false;
>> +
>> +               port->index = i;
>> +
>> +               /*
>> +                * If the driver set up port names in _bind use those, otherwise
>> +                * use default ones.
>> +                * If present, DT label is used as name and overrides anything
>> +                * we may have here.
>> +                */
>> +               if (!strlen(port->name))
>> +                       snprintf(port->name, DSA_PORT_NAME_LENGTH, "%s@%d",
>> +                                dev->name, i);
>> +
>> +               skip_port = !!dsa_port_parse_dt(dev, i, ports_node, &is_cpu);
>> +
>> +               /*
>> +                * if this is the CPU port don't register it as an ETH device,
>> +                * we skip it on purpose since I/O to/from it from the CPU
>> +                * isn't useful
>> +                * TODO: cpu port may have a PHY and we don't handle that yet.
>> +                */
>> +               if (is_cpu || skip_port)
>> +                       continue;
>> +
>> +               err = device_bind_driver_to_node(dev, DSA_PORT_CHILD_DRV_NAME,
>> +                                                port->name, port->node,
>> +                                                &port->dev);
>> +
>> +               /* try to bind all ports but keep 1st error */
>> +               if (err && !first_err)
>> +                       first_err = err;
>> +       }
>> +
>> +       if (!ofnode_valid(platdata->master_node))
>> +               dev_dbg(dev, "DSA master Eth device is missing!\n");
>> +
>> +       return first_err;
>> +}
>> +
>> +/**
>> + * This function deals with additional devices around the switch as these should
>> + * have been bound to drivers by now.
>> + * TODO: pick up references to other switch devices here, if we're cascaded.
>> + */
>> +static int dm_dsa_pre_probe(struct udevice *dev)
>> +{
>> +       struct dsa_perdev_platdata *platdata = dev_get_platdata(dev);
>> +       int i;
>> +
>> +       if (!platdata)
>> +               return -EINVAL;
>> +
>> +       if (ofnode_valid(platdata->master_node))
>> +               uclass_find_device_by_ofnode(UCLASS_ETH, platdata->master_node,
>> +                                            &platdata->master_dev);
>> +
>> +       for (i = 0; i < platdata->num_ports; i++) {
>> +               struct dsa_port_platdata *port = &platdata->port[i];
>> +
>> +               if (port->dev) {
>> +                       port->dev->priv = port;
>> +                       port->phy = dm_eth_phy_connect(port->dev);
>> +               }
>> +       }
>> +
>> +       return 0;
>> +}
>> +
>> +UCLASS_DRIVER(dsa) = {
>> +       .id = UCLASS_DSA,
>> +       .name = "dsa",
>> +       .post_bind  = dm_dsa_post_bind,
>> +       .pre_probe = dm_dsa_pre_probe,
>> +       .per_device_platdata_auto_alloc_size =
>> +                       sizeof(struct dsa_perdev_platdata),
>> +};
>> --
>> 2.17.1
>>
> 
> Thanks,
> -Vladimir
>
Alexandru Marginean Dec. 17, 2019, 7:18 a.m. UTC | #4
On 12/15/2019 2:08 PM, Vladimir Oltean wrote:
> On Tue, 3 Dec 2019 at 17:32, Alex Marginean <alexandru.marginean@nxp.com> wrote:
>> +/**
>> + * This function deals with additional devices around the switch as these should
>> + * have been bound to drivers by now.
>> + * TODO: pick up references to other switch devices here, if we're cascaded.
>> + */
>> +static int dm_dsa_pre_probe(struct udevice *dev)
>> +{
>> +       struct dsa_perdev_platdata *platdata = dev_get_platdata(dev);
>> +       int i;
>> +
>> +       if (!platdata)
>> +               return -EINVAL;
>> +
>> +       if (ofnode_valid(platdata->master_node))
>> +               uclass_find_device_by_ofnode(UCLASS_ETH, platdata->master_node,
>> +                                            &platdata->master_dev);
>> +
>> +       for (i = 0; i < platdata->num_ports; i++) {
>> +               struct dsa_port_platdata *port = &platdata->port[i];
>> +
>> +               if (port->dev) {
>> +                       port->dev->priv = port;
>> +                       port->phy = dm_eth_phy_connect(port->dev);
> 
> Fixed-link interfaces don't work with DM_MDIO. That is somewhat
> natural as there is no MDIO bus for a fixed-link. However the legacy
> phy_connect function can be made rather easily to work with
> fixed-link, since it has the necessary code for dealing with it
> already. I am not, however, sure how it ever worked in the absence of
> an MDIO bus.
> 
> commit 1b7e23cc7e6d0dc3fe7ae9c55390b40717ff3c3a
> Author: Vladimir Oltean <olteanv@gmail.com>
> Date:   Sat Dec 14 23:25:40 2019 +0200
> 
>      phy: make phy_connect_fixed work with a null mdio bus
> 
>      It is utterly pointless to require an MDIO bus pointer for a fixed PHY
>      device. The fixed.c implementation does not require it, only
>      phy_device_create. Fix that.
> 
>      Signed-off-by: Vladimir Oltean <olteanv@gmail.com>
> 
> diff --git a/drivers/net/phy/phy.c b/drivers/net/phy/phy.c
> index 80a7664e4978..8ea5c9005291 100644
> --- a/drivers/net/phy/phy.c
> +++ b/drivers/net/phy/phy.c
> @@ -658,7 +658,7 @@ static struct phy_device *phy_device_create(struct
> mii_dev *bus, int addr,
>          dev = malloc(sizeof(*dev));
>          if (!dev) {
>                  printf("Failed to allocate PHY device for %s:%d\n",
> -                      bus->name, addr);
> +                      bus ? bus->name : "(null bus)", addr);
>                  return NULL;
>          }
> 
> @@ -686,7 +686,7 @@ static struct phy_device *phy_device_create(struct
> mii_dev *bus, int addr,
>                  return NULL;
>          }
> 
> -       if (addr >= 0 && addr < PHY_MAX_ADDR)
> +       if (addr >= 0 && addr < PHY_MAX_ADDR && phy_id != PHY_FIXED_ID)
>                  bus->phymap[addr] = dev;
> 
>          return dev;
> 
> With the patch above in place, fixed-link can also be made to work
> with some logic similar to what can be seen below:
> 
>      if (ofnode_valid(ofnode_find_subnode(port->dev->node, "fixed-link")))
>          port->phy = phy_connect(NULL, 0, port->dev, phy_mode); //
> phy_mode needs to be pre-parsed somewhere else as well
>      else
>          port->phy = dm_eth_phy_connect(port->dev);
> 
> How would you see fixed-link interfaces being treated? My question so
> far is in the context of front-panel ports but I am interested in your
> view of the CPU port situation as well.

I was thinking turning dm_eth_phy_connect into a more generic helper 
that also deals with fixed links, which it does not yet.  That would 
move the "fixed-link" if out of the driver code.  Ideally the driver 
should be able to call a single helper and, if the device has a DT node, 
it would get back a PHY handle to either a proper PHY or to a fixed link 
(from phy_connect_fixed).

Alex

> 
>> +               }
>> +       }
>> +
>> +       return 0;
>> +}
> 
> Thanks,
> -Vladimir
>
Vladimir Oltean Dec. 17, 2019, 9:41 a.m. UTC | #5
On Tue, 17 Dec 2019 at 09:18, Alexandru Marginean
<alexm.osslist@gmail.com> wrote:
>
> On 12/15/2019 2:08 PM, Vladimir Oltean wrote:
> > On Tue, 3 Dec 2019 at 17:32, Alex Marginean <alexandru.marginean@nxp.com> wrote:
> >> +/**
> >> + * This function deals with additional devices around the switch as these should
> >> + * have been bound to drivers by now.
> >> + * TODO: pick up references to other switch devices here, if we're cascaded.
> >> + */
> >> +static int dm_dsa_pre_probe(struct udevice *dev)
> >> +{
> >> +       struct dsa_perdev_platdata *platdata = dev_get_platdata(dev);
> >> +       int i;
> >> +
> >> +       if (!platdata)
> >> +               return -EINVAL;
> >> +
> >> +       if (ofnode_valid(platdata->master_node))
> >> +               uclass_find_device_by_ofnode(UCLASS_ETH, platdata->master_node,
> >> +                                            &platdata->master_dev);
> >> +
> >> +       for (i = 0; i < platdata->num_ports; i++) {
> >> +               struct dsa_port_platdata *port = &platdata->port[i];
> >> +
> >> +               if (port->dev) {
> >> +                       port->dev->priv = port;
> >> +                       port->phy = dm_eth_phy_connect(port->dev);
> >
> > Fixed-link interfaces don't work with DM_MDIO. That is somewhat
> > natural as there is no MDIO bus for a fixed-link. However the legacy
> > phy_connect function can be made rather easily to work with
> > fixed-link, since it has the necessary code for dealing with it
> > already. I am not, however, sure how it ever worked in the absence of
> > an MDIO bus.
> >
> > commit 1b7e23cc7e6d0dc3fe7ae9c55390b40717ff3c3a
> > Author: Vladimir Oltean <olteanv@gmail.com>
> > Date:   Sat Dec 14 23:25:40 2019 +0200
> >
> >      phy: make phy_connect_fixed work with a null mdio bus
> >
> >      It is utterly pointless to require an MDIO bus pointer for a fixed PHY
> >      device. The fixed.c implementation does not require it, only
> >      phy_device_create. Fix that.
> >
> >      Signed-off-by: Vladimir Oltean <olteanv@gmail.com>
> >
> > diff --git a/drivers/net/phy/phy.c b/drivers/net/phy/phy.c
> > index 80a7664e4978..8ea5c9005291 100644
> > --- a/drivers/net/phy/phy.c
> > +++ b/drivers/net/phy/phy.c
> > @@ -658,7 +658,7 @@ static struct phy_device *phy_device_create(struct
> > mii_dev *bus, int addr,
> >          dev = malloc(sizeof(*dev));
> >          if (!dev) {
> >                  printf("Failed to allocate PHY device for %s:%d\n",
> > -                      bus->name, addr);
> > +                      bus ? bus->name : "(null bus)", addr);
> >                  return NULL;
> >          }
> >
> > @@ -686,7 +686,7 @@ static struct phy_device *phy_device_create(struct
> > mii_dev *bus, int addr,
> >                  return NULL;
> >          }
> >
> > -       if (addr >= 0 && addr < PHY_MAX_ADDR)
> > +       if (addr >= 0 && addr < PHY_MAX_ADDR && phy_id != PHY_FIXED_ID)
> >                  bus->phymap[addr] = dev;
> >
> >          return dev;
> >
> > With the patch above in place, fixed-link can also be made to work
> > with some logic similar to what can be seen below:
> >
> >      if (ofnode_valid(ofnode_find_subnode(port->dev->node, "fixed-link")))
> >          port->phy = phy_connect(NULL, 0, port->dev, phy_mode); //
> > phy_mode needs to be pre-parsed somewhere else as well
> >      else
> >          port->phy = dm_eth_phy_connect(port->dev);
> >
> > How would you see fixed-link interfaces being treated? My question so
> > far is in the context of front-panel ports but I am interested in your
> > view of the CPU port situation as well.
>
> I was thinking turning dm_eth_phy_connect into a more generic helper
> that also deals with fixed links, which it does not yet.  That would

How would you do that? Just put my snippet above in a higher-level
wrapper over dm_eth_phy_connect and phy_connect? Otherwise I think
it's pretty difficult and hacky to pass a null mdiodev pointer through
dm_mdio_phy_connect.

> move the "fixed-link" if out of the driver code.  Ideally the driver
> should be able to call a single helper and, if the device has a DT node,
> it would get back a PHY handle to either a proper PHY or to a fixed link
> (from phy_connect_fixed).
>
> Alex
>
> >
> >> +               }
> >> +       }
> >> +
> >> +       return 0;
> >> +}
> >
> > Thanks,
> > -Vladimir
> >
Alexandru Marginean Dec. 17, 2019, 10:36 a.m. UTC | #6
On 12/17/2019 10:41 AM, Vladimir Oltean wrote:
> On Tue, 17 Dec 2019 at 09:18, Alexandru Marginean
> <alexm.osslist@gmail.com> wrote:
>>
>> On 12/15/2019 2:08 PM, Vladimir Oltean wrote:
>>> On Tue, 3 Dec 2019 at 17:32, Alex Marginean <alexandru.marginean@nxp.com> wrote:
>>>> +/**
>>>> + * This function deals with additional devices around the switch as these should
>>>> + * have been bound to drivers by now.
>>>> + * TODO: pick up references to other switch devices here, if we're cascaded.
>>>> + */
>>>> +static int dm_dsa_pre_probe(struct udevice *dev)
>>>> +{
>>>> +       struct dsa_perdev_platdata *platdata = dev_get_platdata(dev);
>>>> +       int i;
>>>> +
>>>> +       if (!platdata)
>>>> +               return -EINVAL;
>>>> +
>>>> +       if (ofnode_valid(platdata->master_node))
>>>> +               uclass_find_device_by_ofnode(UCLASS_ETH, platdata->master_node,
>>>> +                                            &platdata->master_dev);
>>>> +
>>>> +       for (i = 0; i < platdata->num_ports; i++) {
>>>> +               struct dsa_port_platdata *port = &platdata->port[i];
>>>> +
>>>> +               if (port->dev) {
>>>> +                       port->dev->priv = port;
>>>> +                       port->phy = dm_eth_phy_connect(port->dev);
>>>
>>> Fixed-link interfaces don't work with DM_MDIO. That is somewhat
>>> natural as there is no MDIO bus for a fixed-link. However the legacy
>>> phy_connect function can be made rather easily to work with
>>> fixed-link, since it has the necessary code for dealing with it
>>> already. I am not, however, sure how it ever worked in the absence of
>>> an MDIO bus.
>>>
>>> commit 1b7e23cc7e6d0dc3fe7ae9c55390b40717ff3c3a
>>> Author: Vladimir Oltean <olteanv@gmail.com>
>>> Date:   Sat Dec 14 23:25:40 2019 +0200
>>>
>>>       phy: make phy_connect_fixed work with a null mdio bus
>>>
>>>       It is utterly pointless to require an MDIO bus pointer for a fixed PHY
>>>       device. The fixed.c implementation does not require it, only
>>>       phy_device_create. Fix that.
>>>
>>>       Signed-off-by: Vladimir Oltean <olteanv@gmail.com>
>>>
>>> diff --git a/drivers/net/phy/phy.c b/drivers/net/phy/phy.c
>>> index 80a7664e4978..8ea5c9005291 100644
>>> --- a/drivers/net/phy/phy.c
>>> +++ b/drivers/net/phy/phy.c
>>> @@ -658,7 +658,7 @@ static struct phy_device *phy_device_create(struct
>>> mii_dev *bus, int addr,
>>>           dev = malloc(sizeof(*dev));
>>>           if (!dev) {
>>>                   printf("Failed to allocate PHY device for %s:%d\n",
>>> -                      bus->name, addr);
>>> +                      bus ? bus->name : "(null bus)", addr);
>>>                   return NULL;
>>>           }
>>>
>>> @@ -686,7 +686,7 @@ static struct phy_device *phy_device_create(struct
>>> mii_dev *bus, int addr,
>>>                   return NULL;
>>>           }
>>>
>>> -       if (addr >= 0 && addr < PHY_MAX_ADDR)
>>> +       if (addr >= 0 && addr < PHY_MAX_ADDR && phy_id != PHY_FIXED_ID)
>>>                   bus->phymap[addr] = dev;
>>>
>>>           return dev;
>>>
>>> With the patch above in place, fixed-link can also be made to work
>>> with some logic similar to what can be seen below:
>>>
>>>       if (ofnode_valid(ofnode_find_subnode(port->dev->node, "fixed-link")))
>>>           port->phy = phy_connect(NULL, 0, port->dev, phy_mode); //
>>> phy_mode needs to be pre-parsed somewhere else as well
>>>       else
>>>           port->phy = dm_eth_phy_connect(port->dev);
>>>
>>> How would you see fixed-link interfaces being treated? My question so
>>> far is in the context of front-panel ports but I am interested in your
>>> view of the CPU port situation as well.
>>
>> I was thinking turning dm_eth_phy_connect into a more generic helper
>> that also deals with fixed links, which it does not yet.  That would
> 
> How would you do that? Just put my snippet above in a higher-level
> wrapper over dm_eth_phy_connect and phy_connect? Otherwise I think
> it's pretty difficult and hacky to pass a null mdiodev pointer through
> dm_mdio_phy_connect.

It wouldn't go through mdio_phy_connect, dm_eth_phy_connect would need 
to figure out if it's a fixed link or PHY and then call the proper 
function, like in your snippet.
The MDIO NULL topic is a bit more complicated I think.  Some of the Eth 
drivers just deal with fixed link internally and don't create a phy 
device at all.  If there is a PHY device I think we need to have a MDIO 
bus for it, and we shouldn't piggy-back on real MDIO buses.  I'm 
thinking we could have a dummy MDIO bus created automatically by 
ETH/MDIO uclass code for fixed link PHY devices.

Thoughts?
Alex

> 
>> move the "fixed-link" if out of the driver code.  Ideally the driver
>> should be able to call a single helper and, if the device has a DT node,
>> it would get back a PHY handle to either a proper PHY or to a fixed link
>> (from phy_connect_fixed).
>>
>> Alex
>>
>>>
>>>> +               }
>>>> +       }
>>>> +
>>>> +       return 0;
>>>> +}
>>>
>>> Thanks,
>>> -Vladimir
>>>
Vladimir Oltean Dec. 17, 2019, 10:47 a.m. UTC | #7
On Tue, 17 Dec 2019 at 12:36, Alexandru Marginean
<alexm.osslist@gmail.com> wrote:
> It wouldn't go through mdio_phy_connect, dm_eth_phy_connect would need
> to figure out if it's a fixed link or PHY and then call the proper
> function, like in your snippet.
> The MDIO NULL topic is a bit more complicated I think.  Some of the Eth
> drivers just deal with fixed link internally and don't create a phy
> device at all.  If there is a PHY device I think we need to have a MDIO
> bus for it, and we shouldn't piggy-back on real MDIO buses.  I'm
> thinking we could have a dummy MDIO bus created automatically by
> ETH/MDIO uclass code for fixed link PHY devices.

Well, either one or the other, really. The dummy MDIO bus is not
really needed, but on the other hand, if that's what it takes to make
DM_MDIO support fixed-link with the least amount of special cases,
then ok, fine with me.

>
> Thoughts?
> Alex
>

Regards,
-Vladimir
Vladimir Oltean Feb. 9, 2020, 1:10 p.m. UTC | #8
Hi Alex,

On Tue, 3 Dec 2019 at 17:32, Alex Marginean <alexandru.marginean@nxp.com> wrote:
>
> DSA stands for Distributed Switch Architecture and it covers switches that
> are connected to the CPU through an Ethernet link and generally use frame
> tags to pass information about the source/destination ports to/from CPU.
> Front panel ports are presented as regular ethernet devices in U-Boot and
> they are expected to support the typical networking commands.
> DSA switches may be cascaded, DSA class code does not currently support
> this.
>
> Signed-off-by: Alex Marginean <alexandru.marginean@nxp.com>
> ---
>  drivers/net/Kconfig    |  13 ++
>  include/dm/uclass-id.h |   1 +
>  include/net/dsa.h      | 141 ++++++++++++++++
>  net/Makefile           |   1 +
>  net/dsa-uclass.c       | 369 +++++++++++++++++++++++++++++++++++++++++
>  5 files changed, 525 insertions(+)
>  create mode 100644 include/net/dsa.h
>  create mode 100644 net/dsa-uclass.c
>
> diff --git a/drivers/net/Kconfig b/drivers/net/Kconfig
> index 4182897d89..a4157cb122 100644
> --- a/drivers/net/Kconfig
> +++ b/drivers/net/Kconfig
> @@ -37,6 +37,19 @@ config DM_MDIO_MUX
>           This is currently implemented in net/mdio-mux-uclass.c
>           Look in include/miiphy.h for details.
>
> +config DM_DSA
> +       bool "Enable Driver Model for DSA switches"
> +       depends on DM_ETH && DM_MDIO
> +       help
> +         Enable Driver Model for DSA switches
> +
> +         Adds UCLASS_DSA class supporting switches that follow the Distributed
> +         Switch Architecture (DSA).  These switches rely on the presence of a
> +         management switch port connected to an Ethernet controller capable of
> +         receiving frames from the switch.  This host Ethernet controller is
> +         called "master" and "cpu" in DSA terminology.
> +         This is currently implemented in net/dsa-uclass.c
> +
>  config MDIO_SANDBOX
>         depends on DM_MDIO && SANDBOX
>         default y
> diff --git a/include/dm/uclass-id.h b/include/dm/uclass-id.h
> index 0c563d898b..8f37a91488 100644
> --- a/include/dm/uclass-id.h
> +++ b/include/dm/uclass-id.h
> @@ -42,6 +42,7 @@ enum uclass_id {
>         UCLASS_DISPLAY,         /* Display (e.g. DisplayPort, HDMI) */
>         UCLASS_DSI_HOST,        /* Display Serial Interface host */
>         UCLASS_DMA,             /* Direct Memory Access */
> +       UCLASS_DSA,             /* Distributed (Ethernet) Switch Architecture */
>         UCLASS_EFI,             /* EFI managed devices */
>         UCLASS_ETH,             /* Ethernet device */
>         UCLASS_FIRMWARE,        /* Firmware */
> diff --git a/include/net/dsa.h b/include/net/dsa.h
> new file mode 100644
> index 0000000000..2387419b9d
> --- /dev/null
> +++ b/include/net/dsa.h
> @@ -0,0 +1,141 @@
> +/* SPDX-License-Identifier: GPL-2.0+ */
> +/*
> + * Copyright (c) 2019 NXP
> + */
> +
> +#ifndef __DSA_H__
> +#define __DSA_H__
> +
> +#include <common.h>
> +#include <dm.h>
> +#include <phy.h>
> +
> +/**
> + * DSA stands for Distributed Switch Architecture and it is infrastructure
> + * intended to support drivers for Switches that rely on an intermediary
> + * Ethernet device for I/O.  These switches may support cascading allowing
> + * them to be arranged as a tree.
> + * DSA is documented in detail in the Linux kernel documentation under
> + * Documentation/networking/dsa/dsa.txt
> + * The network layout of such a switch is shown below:
> + *
> + *         |---------------------------
> + *         | CPU network device (eth0)|
> + *         ----------------------------
> + *         | <tag added by switch     |
> + *         |                          |
> + *         |                          |
> + *         |        tag added by CPU> |
> + *     |--------------------------------------------|
> + *     | Switch driver                              |
> + *     |--------------------------------------------|
> + *         ||        ||         ||
> + *     |-------|  |-------|  |-------|
> + *     | sw0p0 |  | sw0p1 |  | sw0p2 |
> + *     |-------|  |-------|  |-------|
> + *
> + * In U-Boot the intent is to allow access to front panel ports (shown at the
> + * bottom of the picture) though the master Ethernet port (eth0 in the picture).
> + * Front panel ports are presented as regular Ethernet devices in U-Boot and
> + * they are expected to support the typical networking commands.
> + * In general DSA switches require the use of tags, extra headers added both by
> + * software on Tx and by the switch on Rx.  These tags carry at a minimum port
> + * information and switch information for cascaded set-ups.
> + * In U-Boot these tags are inserted and parsed by the DSA switch driver, the
> + * class code helps with headroom/tailroom for the extra headers.
> + *
> + * TODO:
> + * - handle switch cascading, for now U-Boot only supports stand-alone switches.
> + * - propagate the master Eth MAC address to switch ports, this is used in
> + * Linux to avoid using additional MAC addresses on master Eth.
> + * - Add support to probe DSA switches connected to a MDIO bus, this is needed
> + * to convert switch drivers that are now under drivers/net/phy.
> + */
> +
> +#define DSA_PORT_NAME_LENGTH   16
> +
> +/* Maximum number of ports each DSA device can have */
> +#define DSA_MAX_PORTS          12
> +/* Used to size internal buffers, no support for jumbo yet */
> +#define DSA_MAX_FRAME_SIZE     2048
> +
> +/**
> + * struct dsa_ops - DSA operations
> + *
> + * @port_enable:  Initialize a switch port for I/O
> + * @port_disable: Disable a port
> + * @xmit:         Insert the DSA tag for transmission
> + *                DSA drivers receive a copy of the packet with headroom and
> + *                tailroom reserved and set to 0.
> + *                Packet points to headroom and length is updated to include
> + *                both headroom and tailroom
> + * @rcv:          Process the DSA tag on reception
> + *                Packet and length describe the frame as received from master
> + *                including any additional headers
> + */
> +struct dsa_ops {
> +       int (*port_enable)(struct udevice *dev, int port,
> +                          struct phy_device *phy);
> +       void (*port_disable)(struct udevice *dev, int port,
> +                            struct phy_device *phy);
> +       int (*xmit)(struct udevice *dev, int port, void *packet, int length);
> +       int (*rcv)(struct udevice *dev, int *port, void *packet, int length);
> +};
> +
> +#define dsa_get_ops(dev) ((struct dsa_ops *)(dev)->driver->ops)
> +
> +/**
> + * struct dsa_port_platdata - DSA port platform data
> + *
> + * @dev :  Port u-device
> + *         Uclass code sets this field for all ports
> + * @phy:   PHY device associated with this port
> + *         Uclass code sets this field for all ports except CPU port, based on
> + *         DT information.  It may be NULL.
> + * @node:  Port DT node, if any.  Uclass code sets this field.
> + * @index: Port index in the DSA switch, set by class code.
> + * @name:  Name of the port Eth device.  If a label property is present in the
> + *         port DT node, it is used as name.  Drivers can use custom names by
> + *         populating this field, otherwise class code generates a default.
> + */
> +struct dsa_port_platdata {
> +       struct udevice *dev;
> +       struct phy_device *phy;
> +       ofnode node;
> +       int index;
> +       char name[DSA_PORT_NAME_LENGTH];
> +};
> +
> +/**
> + * struct dsa_perdev_platdata - Per-device platform data for DSA DM
> + *
> + * @num_ports:   Number of ports the device has, must be <= DSA_MAX_PORTS
> + *               All DSA drivers must set this at _bind
> + * @headroom:    Size, in bytes, of headroom needed for the DSA tag
> + *               All DSA drivers must set this at _bind or _probe
> + * @tailroom:    Size, in bytes, of tailroom needed for the DSA tag
> + *               DSA class code allocates headroom and tailroom on Tx before
> + *               calling DSA driver xmit function
> + *               All DSA drivers must set this at _bind or _probe
> + * @master_node: DT node of the master Ethernet.  DT is optional so this may be
> + *               null.
> + * @master_dev:  Ethernet device to be used as master.  Uclass code sets this
> + *               based on DT information if present, otherwise drivers must set
> + *               this field in _probe.
> + * @cpu_port:    Index of switch port linked to master Ethernet.
> + *               Uclass code sets this based on DT information if present,
> + *               otherwise drivers must set this field in _bind.
> + * @port:        per-port data
> + */
> +struct dsa_perdev_platdata {
> +       int num_ports;
> +       int headroom;
> +       int tailroom;
> +
> +       ofnode master_node;
> +       struct udevice *master_dev;
> +       int cpu_port;
> +       struct dsa_port_platdata port[DSA_MAX_PORTS];
> +};
> +
> +#endif /* __DSA_H__ */
> diff --git a/net/Makefile b/net/Makefile
> index 2a700c8401..fac8c8beb9 100644
> --- a/net/Makefile
> +++ b/net/Makefile
> @@ -28,6 +28,7 @@ obj-$(CONFIG_CMD_SNTP) += sntp.o
>  obj-$(CONFIG_CMD_TFTPBOOT) += tftp.o
>  obj-$(CONFIG_UDP_FUNCTION_FASTBOOT)  += fastboot.o
>  obj-$(CONFIG_CMD_WOL)  += wol.o
> +obj-$(CONFIG_DM_DSA)   += dsa-uclass.o
>
>  # Disable this warning as it is triggered by:
>  # sprintf(buf, index ? "foo%d" : "foo", index)
> diff --git a/net/dsa-uclass.c b/net/dsa-uclass.c
> new file mode 100644
> index 0000000000..3790a72841
> --- /dev/null
> +++ b/net/dsa-uclass.c
> @@ -0,0 +1,369 @@
> +// SPDX-License-Identifier: GPL-2.0+
> +/*
> + * (C) Copyright 2019, NXP
> + */
> +
> +#include <net/dsa.h>
> +#include <dm/lists.h>
> +#include <dm/device-internal.h>
> +#include <dm/uclass-internal.h>
> +#include <miiphy.h>
> +
> +#define DSA_PORT_CHILD_DRV_NAME "dsa-port"
> +
> +/* helper that returns the DSA master Ethernet device. */
> +static struct udevice *dsa_port_get_master(struct udevice *pdev, bool probe)
> +{
> +       struct udevice *dev = dev_get_parent(pdev);
> +       struct dsa_perdev_platdata *platdata = dev_get_platdata(dev);
> +
> +       if (probe)
> +               device_probe(platdata->master_dev);
> +
> +       return platdata->master_dev;
> +}
> +
> +/*
> + * Start the desired port, the CPU port and the master Eth interface.
> + * TODO: if cascaded we may need to _start ports in other switches too
> + */
> +static int dsa_port_start(struct udevice *pdev)
> +{
> +       struct udevice *dev = dev_get_parent(pdev);
> +       struct dsa_perdev_platdata *platdata = dev_get_platdata(dev);
> +       struct udevice *master = dsa_port_get_master(pdev, true);
> +       struct dsa_port_platdata *ppriv = dev_get_priv(pdev);
> +       struct dsa_ops *ops = dsa_get_ops(dev);
> +       int err;
> +
> +       if (!ppriv || !platdata)
> +               return -EINVAL;
> +
> +       if (!master) {
> +               dev_err(pdev, "DSA master Ethernet device not found!\n");
> +               return -EINVAL;
> +       }
> +
> +       if (ops->port_enable) {
> +               err = ops->port_enable(dev, ppriv->index, ppriv->phy);
> +               if (err)
> +                       return err;
> +               err = ops->port_enable(dev, platdata->cpu_port,
> +                                      platdata->port[platdata->cpu_port].phy);
> +               if (err)
> +                       return err;
> +       }
> +
> +       return eth_get_ops(master)->start(master);
> +}
> +
> +/* Stop the desired port, the CPU port and the master Eth interface */
> +static void dsa_port_stop(struct udevice *pdev)
> +{
> +       struct udevice *dev = dev_get_parent(pdev);
> +       struct dsa_perdev_platdata *platdata = dev_get_platdata(dev);
> +       struct udevice *master = dsa_port_get_master(pdev, false);
> +       struct dsa_port_platdata *ppriv = dev_get_priv(pdev);
> +       struct dsa_ops *ops = dsa_get_ops(dev);
> +
> +       if (!ppriv || !platdata)
> +               return;
> +
> +       if (ops->port_disable) {
> +               ops->port_disable(dev, ppriv->index, ppriv->phy);
> +               ops->port_disable(dev, platdata->cpu_port,
> +                                 platdata->port[platdata->cpu_port].phy);
> +       }
> +
> +       /*
> +        * stop master only if it's active, don't probe it otherwise.
> +        * Under normal usage it would be active because we're using it, but
> +        * during tear-down it may have been removed ahead of us.
> +        */
> +       if (master && device_active(master))
> +               eth_get_ops(master)->stop(master);
> +}
> +
> +/*
> + * Insert a DSA tag and call master Ethernet send on the resulting packet
> + * We copy the frame to a stack buffer where we have reserved headroom and
> + * tailroom space.  Headroom and tailroom are set to 0.
> + */
> +static int dsa_port_send(struct udevice *pdev, void *packet, int length)
> +{
> +       struct udevice *dev = dev_get_parent(pdev);
> +       struct dsa_perdev_platdata *platdata = dev_get_platdata(dev);
> +       struct udevice *master = dsa_port_get_master(pdev, true);
> +       struct dsa_port_platdata *ppriv = dev_get_priv(pdev);
> +       struct dsa_ops *ops = dsa_get_ops(dev);
> +       uchar dsa_packet[DSA_MAX_FRAME_SIZE];
> +       int head = platdata->headroom, tail = platdata->tailroom;
> +       int err;
> +
> +       if (!master)
> +               return -EINVAL;
> +
> +       if (length + head + tail > DSA_MAX_FRAME_SIZE)
> +               return -EINVAL;
> +
> +       memset(dsa_packet, 0, head);
> +       memset(dsa_packet + head + length, 0, tail);
> +       memcpy(dsa_packet + head, packet, length);
> +       length += head + tail;
> +
> +       err = ops->xmit(dev, ppriv->index, dsa_packet, length);
> +       if (err)
> +               return err;
> +
> +       return eth_get_ops(master)->send(master, dsa_packet, length);
> +}
> +
> +/* Receive a frame from master Ethernet, process it and pass it on */
> +static int dsa_port_recv(struct udevice *pdev, int flags, uchar **packetp)
> +{
> +       struct udevice *dev = dev_get_parent(pdev);
> +       struct dsa_perdev_platdata *platdata = dev_get_platdata(dev);
> +       struct udevice *master = dsa_port_get_master(pdev, true);
> +       struct dsa_port_platdata *ppriv = dev_get_priv(pdev);
> +       struct dsa_ops *ops = dsa_get_ops(dev);
> +       int head = platdata->headroom, tail = platdata->tailroom;
> +       int length, port_index, err;
> +
> +       if (!master)
> +               return -EINVAL;
> +
> +       length = eth_get_ops(master)->recv(master, flags, packetp);
> +       if (length <= 0)
> +               return length;
> +
> +       /*
> +        * if we receive frames from a different port or frames that DSA driver
> +        * doesn't like we discard them here.
> +        * In case of discard we return with no frame and expect to be called
> +        * again instead of looping here, so upper layer can deal with timeouts
> +        * and ctrl-c
> +        */
> +       err = ops->rcv(dev, &port_index, *packetp, length);
> +       if (err || port_index != ppriv->index || (length <= head + tail)) {
> +               if (eth_get_ops(master)->free_pkt)
> +                       eth_get_ops(master)->free_pkt(master, *packetp, length);
> +               return -EAGAIN;
> +       }
> +
> +       /*
> +        * We move the pointer over headroom here to avoid a copy.  If free_pkt
> +        * gets called we move the pointer back before calling master free_pkt.
> +        */
> +       *packetp += head;
> +
> +       return length - head - tail;
> +}
> +
> +static int dsa_port_free_pkt(struct udevice *pdev, uchar *packet, int length)
> +{
> +       struct udevice *dev = dev_get_parent(pdev);
> +       struct dsa_perdev_platdata *platdata = dev_get_platdata(dev);
> +       struct udevice *master = dsa_port_get_master(pdev, true);
> +
> +       if (!master)
> +               return -EINVAL;
> +
> +       if (eth_get_ops(master)->free_pkt) {
> +               /* return the original pointer and length to master Eth */
> +               packet -= platdata->headroom;
> +               length += platdata->headroom - platdata->tailroom;
> +
> +               return eth_get_ops(master)->free_pkt(master, packet, length);
> +       }
> +
> +       return 0;
> +}
> +
> +static const struct eth_ops dsa_port_ops = {
> +       .start          = dsa_port_start,
> +       .send           = dsa_port_send,
> +       .recv           = dsa_port_recv,
> +       .stop           = dsa_port_stop,
> +       .free_pkt       = dsa_port_free_pkt,
> +};
> +
> +U_BOOT_DRIVER(dsa_port) = {
> +       .name   = DSA_PORT_CHILD_DRV_NAME,
> +       .id     = UCLASS_ETH,
> +       .ops    = &dsa_port_ops,
> +       .platdata_auto_alloc_size = sizeof(struct eth_pdata),
> +};
> +
> +/*
> + * reads the DT properties of the given DSA port.
> + * If the return value is != 0 then the port is skipped
> + */
> +static int dsa_port_parse_dt(struct udevice *dev, int port_index,
> +                            ofnode ports_node, bool *is_cpu)
> +{
> +       struct dsa_perdev_platdata *platdata = dev_get_platdata(dev);
> +       struct dsa_port_platdata *port = &platdata->port[port_index];
> +       ofnode temp_node;
> +       u32 ethernet;
> +
> +       /*
> +        * if we don't have a DT we don't do anything here but the port is
> +        * registered normally
> +        */
> +       if (!ofnode_valid(ports_node))
> +               return 0;
> +
> +       ofnode_for_each_subnode(temp_node, ports_node) {
> +               const char *port_label;
> +               u32 reg;
> +
> +               if (ofnode_read_u32(temp_node, "reg", &reg) ||
> +                   reg != port_index)
> +                       continue;
> +
> +               /* if the port is explicitly disabled in DT skip it */
> +               if (!ofnode_is_available(temp_node))
> +                       return -ENODEV;
> +
> +               port->node = temp_node;
> +

I know I said that this is in principle good to go, but actually!
you should swap these 2, and set port->node = temp_node before you
actually skip ports with status = "disabled". The reason is that you
want the driver to be able to check for
ofnode_is_available(platdata->port[port].node) too, for whatever
reason.

> +               dev_dbg(dev, "port %d node %s\n", port->index,
> +                       ofnode_get_name(port->node));
> +
> +               /* Use 'label' if present in DT */
> +               port_label = ofnode_read_string(port->node, "label");
> +               if (port_label)
> +                       strncpy(port->name, port_label, DSA_PORT_NAME_LENGTH);
> +
> +               *is_cpu = !ofnode_read_u32(port->node, "ethernet",
> +                                          &ethernet);
> +
> +               if (*is_cpu) {
> +                       platdata->master_node =
> +                               ofnode_get_by_phandle(ethernet);
> +                       platdata->cpu_port = port_index;
> +
> +                       dev_dbg(dev, "master node %s on port %d\n",
> +                               ofnode_get_name(platdata->master_node),
> +                               port_index);
> +               }
> +               break;
> +       }
> +
> +       return 0;
> +}
> +
> +/**
> + * This function mostly deals with pulling information out of the device tree
> + * into the platdata structure.
> + * It goes through the list of switch ports, registers an Eth device for each
> + * front panel port and identifies the cpu port connected to master Eth device.
> + * TODO: support cascaded switches
> + */
> +static int dm_dsa_post_bind(struct udevice *dev)
> +{
> +       struct dsa_perdev_platdata *platdata = dev_get_platdata(dev);
> +       ofnode ports_node = ofnode_null();
> +       int first_err = 0, err = 0, i;
> +
> +       if (!platdata) {
> +               dev_err(dev, "missing plaform data\n");
> +               return -EINVAL;
> +       }
> +
> +       if (platdata->num_ports <= 0 || platdata->num_ports > DSA_MAX_PORTS) {
> +               dev_err(dev, "unexpected num_ports value (%d)\n",
> +                       platdata->num_ports);
> +               return -EINVAL;
> +       }
> +
> +       platdata->master_node = ofnode_null();
> +
> +       if (!ofnode_valid(dev->node)) {
> +               dev_dbg(dev, "Device doesn't have a valid DT node!\n");
> +       } else {
> +               ports_node = ofnode_find_subnode(dev->node, "ports");
> +               if (!ofnode_valid(ports_node))
> +                       dev_dbg(dev,
> +                               "ports node is missing under DSA device!\n");
> +       }
> +
> +       for (i = 0; i < platdata->num_ports; i++) {
> +               struct dsa_port_platdata *port = &platdata->port[i];
> +               bool skip_port, is_cpu = false;
> +
> +               port->index = i;
> +
> +               /*
> +                * If the driver set up port names in _bind use those, otherwise
> +                * use default ones.
> +                * If present, DT label is used as name and overrides anything
> +                * we may have here.
> +                */
> +               if (!strlen(port->name))
> +                       snprintf(port->name, DSA_PORT_NAME_LENGTH, "%s@%d",
> +                                dev->name, i);
> +
> +               skip_port = !!dsa_port_parse_dt(dev, i, ports_node, &is_cpu);
> +
> +               /*
> +                * if this is the CPU port don't register it as an ETH device,
> +                * we skip it on purpose since I/O to/from it from the CPU
> +                * isn't useful
> +                * TODO: cpu port may have a PHY and we don't handle that yet.
> +                */
> +               if (is_cpu || skip_port)
> +                       continue;
> +
> +               err = device_bind_driver_to_node(dev, DSA_PORT_CHILD_DRV_NAME,
> +                                                port->name, port->node,
> +                                                &port->dev);
> +
> +               /* try to bind all ports but keep 1st error */
> +               if (err && !first_err)
> +                       first_err = err;
> +       }
> +
> +       if (!ofnode_valid(platdata->master_node))
> +               dev_dbg(dev, "DSA master Eth device is missing!\n");
> +
> +       return first_err;
> +}
> +
> +/**
> + * This function deals with additional devices around the switch as these should
> + * have been bound to drivers by now.
> + * TODO: pick up references to other switch devices here, if we're cascaded.
> + */
> +static int dm_dsa_pre_probe(struct udevice *dev)
> +{
> +       struct dsa_perdev_platdata *platdata = dev_get_platdata(dev);
> +       int i;
> +
> +       if (!platdata)
> +               return -EINVAL;
> +
> +       if (ofnode_valid(platdata->master_node))
> +               uclass_find_device_by_ofnode(UCLASS_ETH, platdata->master_node,
> +                                            &platdata->master_dev);
> +
> +       for (i = 0; i < platdata->num_ports; i++) {
> +               struct dsa_port_platdata *port = &platdata->port[i];
> +
> +               if (port->dev) {
> +                       port->dev->priv = port;
> +                       port->phy = dm_eth_phy_connect(port->dev);
> +               }
> +       }
> +
> +       return 0;
> +}
> +
> +UCLASS_DRIVER(dsa) = {
> +       .id = UCLASS_DSA,
> +       .name = "dsa",
> +       .post_bind  = dm_dsa_post_bind,
> +       .pre_probe = dm_dsa_pre_probe,
> +       .per_device_platdata_auto_alloc_size =
> +                       sizeof(struct dsa_perdev_platdata),
> +};
> --
> 2.17.1
>

Regards,
-Vladimir
diff mbox series

Patch

diff --git a/drivers/net/Kconfig b/drivers/net/Kconfig
index 4182897d89..a4157cb122 100644
--- a/drivers/net/Kconfig
+++ b/drivers/net/Kconfig
@@ -37,6 +37,19 @@  config DM_MDIO_MUX
 	  This is currently implemented in net/mdio-mux-uclass.c
 	  Look in include/miiphy.h for details.
 
+config DM_DSA
+	bool "Enable Driver Model for DSA switches"
+	depends on DM_ETH && DM_MDIO
+	help
+	  Enable Driver Model for DSA switches
+
+	  Adds UCLASS_DSA class supporting switches that follow the Distributed
+	  Switch Architecture (DSA).  These switches rely on the presence of a
+	  management switch port connected to an Ethernet controller capable of
+	  receiving frames from the switch.  This host Ethernet controller is
+	  called "master" and "cpu" in DSA terminology.
+	  This is currently implemented in net/dsa-uclass.c
+
 config MDIO_SANDBOX
 	depends on DM_MDIO && SANDBOX
 	default y
diff --git a/include/dm/uclass-id.h b/include/dm/uclass-id.h
index 0c563d898b..8f37a91488 100644
--- a/include/dm/uclass-id.h
+++ b/include/dm/uclass-id.h
@@ -42,6 +42,7 @@  enum uclass_id {
 	UCLASS_DISPLAY,		/* Display (e.g. DisplayPort, HDMI) */
 	UCLASS_DSI_HOST,	/* Display Serial Interface host */
 	UCLASS_DMA,		/* Direct Memory Access */
+	UCLASS_DSA,		/* Distributed (Ethernet) Switch Architecture */
 	UCLASS_EFI,		/* EFI managed devices */
 	UCLASS_ETH,		/* Ethernet device */
 	UCLASS_FIRMWARE,	/* Firmware */
diff --git a/include/net/dsa.h b/include/net/dsa.h
new file mode 100644
index 0000000000..2387419b9d
--- /dev/null
+++ b/include/net/dsa.h
@@ -0,0 +1,141 @@ 
+/* SPDX-License-Identifier: GPL-2.0+ */
+/*
+ * Copyright (c) 2019 NXP
+ */
+
+#ifndef __DSA_H__
+#define __DSA_H__
+
+#include <common.h>
+#include <dm.h>
+#include <phy.h>
+
+/**
+ * DSA stands for Distributed Switch Architecture and it is infrastructure
+ * intended to support drivers for Switches that rely on an intermediary
+ * Ethernet device for I/O.  These switches may support cascading allowing
+ * them to be arranged as a tree.
+ * DSA is documented in detail in the Linux kernel documentation under
+ * Documentation/networking/dsa/dsa.txt
+ * The network layout of such a switch is shown below:
+ *
+ *	    |---------------------------
+ *	    | CPU network device (eth0)|
+ *	    ----------------------------
+ *	    | <tag added by switch     |
+ *	    |                          |
+ *	    |                          |
+ *	    |        tag added by CPU> |
+ *	|--------------------------------------------|
+ *	| Switch driver				     |
+ *	|--------------------------------------------|
+ *	    ||        ||         ||
+ *	|-------|  |-------|  |-------|
+ *	| sw0p0 |  | sw0p1 |  | sw0p2 |
+ *	|-------|  |-------|  |-------|
+ *
+ * In U-Boot the intent is to allow access to front panel ports (shown at the
+ * bottom of the picture) though the master Ethernet port (eth0 in the picture).
+ * Front panel ports are presented as regular Ethernet devices in U-Boot and
+ * they are expected to support the typical networking commands.
+ * In general DSA switches require the use of tags, extra headers added both by
+ * software on Tx and by the switch on Rx.  These tags carry at a minimum port
+ * information and switch information for cascaded set-ups.
+ * In U-Boot these tags are inserted and parsed by the DSA switch driver, the
+ * class code helps with headroom/tailroom for the extra headers.
+ *
+ * TODO:
+ * - handle switch cascading, for now U-Boot only supports stand-alone switches.
+ * - propagate the master Eth MAC address to switch ports, this is used in
+ * Linux to avoid using additional MAC addresses on master Eth.
+ * - Add support to probe DSA switches connected to a MDIO bus, this is needed
+ * to convert switch drivers that are now under drivers/net/phy.
+ */
+
+#define DSA_PORT_NAME_LENGTH	16
+
+/* Maximum number of ports each DSA device can have */
+#define DSA_MAX_PORTS		12
+/* Used to size internal buffers, no support for jumbo yet */
+#define DSA_MAX_FRAME_SIZE	2048
+
+/**
+ * struct dsa_ops - DSA operations
+ *
+ * @port_enable:  Initialize a switch port for I/O
+ * @port_disable: Disable a port
+ * @xmit:         Insert the DSA tag for transmission
+ *                DSA drivers receive a copy of the packet with headroom and
+ *                tailroom reserved and set to 0.
+ *                Packet points to headroom and length is updated to include
+ *                both headroom and tailroom
+ * @rcv:          Process the DSA tag on reception
+ *                Packet and length describe the frame as received from master
+ *                including any additional headers
+ */
+struct dsa_ops {
+	int (*port_enable)(struct udevice *dev, int port,
+			   struct phy_device *phy);
+	void (*port_disable)(struct udevice *dev, int port,
+			     struct phy_device *phy);
+	int (*xmit)(struct udevice *dev, int port, void *packet, int length);
+	int (*rcv)(struct udevice *dev, int *port, void *packet, int length);
+};
+
+#define dsa_get_ops(dev) ((struct dsa_ops *)(dev)->driver->ops)
+
+/**
+ * struct dsa_port_platdata - DSA port platform data
+ *
+ * @dev :  Port u-device
+ *         Uclass code sets this field for all ports
+ * @phy:   PHY device associated with this port
+ *         Uclass code sets this field for all ports except CPU port, based on
+ *         DT information.  It may be NULL.
+ * @node:  Port DT node, if any.  Uclass code sets this field.
+ * @index: Port index in the DSA switch, set by class code.
+ * @name:  Name of the port Eth device.  If a label property is present in the
+ *         port DT node, it is used as name.  Drivers can use custom names by
+ *         populating this field, otherwise class code generates a default.
+ */
+struct dsa_port_platdata {
+	struct udevice *dev;
+	struct phy_device *phy;
+	ofnode node;
+	int index;
+	char name[DSA_PORT_NAME_LENGTH];
+};
+
+/**
+ * struct dsa_perdev_platdata - Per-device platform data for DSA DM
+ *
+ * @num_ports:   Number of ports the device has, must be <= DSA_MAX_PORTS
+ *               All DSA drivers must set this at _bind
+ * @headroom:    Size, in bytes, of headroom needed for the DSA tag
+ *               All DSA drivers must set this at _bind or _probe
+ * @tailroom:    Size, in bytes, of tailroom needed for the DSA tag
+ *               DSA class code allocates headroom and tailroom on Tx before
+ *               calling DSA driver xmit function
+ *               All DSA drivers must set this at _bind or _probe
+ * @master_node: DT node of the master Ethernet.  DT is optional so this may be
+ *               null.
+ * @master_dev:  Ethernet device to be used as master.  Uclass code sets this
+ *               based on DT information if present, otherwise drivers must set
+ *               this field in _probe.
+ * @cpu_port:    Index of switch port linked to master Ethernet.
+ *               Uclass code sets this based on DT information if present,
+ *               otherwise drivers must set this field in _bind.
+ * @port:        per-port data
+ */
+struct dsa_perdev_platdata {
+	int num_ports;
+	int headroom;
+	int tailroom;
+
+	ofnode master_node;
+	struct udevice *master_dev;
+	int cpu_port;
+	struct dsa_port_platdata port[DSA_MAX_PORTS];
+};
+
+#endif /* __DSA_H__ */
diff --git a/net/Makefile b/net/Makefile
index 2a700c8401..fac8c8beb9 100644
--- a/net/Makefile
+++ b/net/Makefile
@@ -28,6 +28,7 @@  obj-$(CONFIG_CMD_SNTP) += sntp.o
 obj-$(CONFIG_CMD_TFTPBOOT) += tftp.o
 obj-$(CONFIG_UDP_FUNCTION_FASTBOOT)  += fastboot.o
 obj-$(CONFIG_CMD_WOL)  += wol.o
+obj-$(CONFIG_DM_DSA)   += dsa-uclass.o
 
 # Disable this warning as it is triggered by:
 # sprintf(buf, index ? "foo%d" : "foo", index)
diff --git a/net/dsa-uclass.c b/net/dsa-uclass.c
new file mode 100644
index 0000000000..3790a72841
--- /dev/null
+++ b/net/dsa-uclass.c
@@ -0,0 +1,369 @@ 
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * (C) Copyright 2019, NXP
+ */
+
+#include <net/dsa.h>
+#include <dm/lists.h>
+#include <dm/device-internal.h>
+#include <dm/uclass-internal.h>
+#include <miiphy.h>
+
+#define DSA_PORT_CHILD_DRV_NAME "dsa-port"
+
+/* helper that returns the DSA master Ethernet device. */
+static struct udevice *dsa_port_get_master(struct udevice *pdev, bool probe)
+{
+	struct udevice *dev = dev_get_parent(pdev);
+	struct dsa_perdev_platdata *platdata = dev_get_platdata(dev);
+
+	if (probe)
+		device_probe(platdata->master_dev);
+
+	return platdata->master_dev;
+}
+
+/*
+ * Start the desired port, the CPU port and the master Eth interface.
+ * TODO: if cascaded we may need to _start ports in other switches too
+ */
+static int dsa_port_start(struct udevice *pdev)
+{
+	struct udevice *dev = dev_get_parent(pdev);
+	struct dsa_perdev_platdata *platdata = dev_get_platdata(dev);
+	struct udevice *master = dsa_port_get_master(pdev, true);
+	struct dsa_port_platdata *ppriv = dev_get_priv(pdev);
+	struct dsa_ops *ops = dsa_get_ops(dev);
+	int err;
+
+	if (!ppriv || !platdata)
+		return -EINVAL;
+
+	if (!master) {
+		dev_err(pdev, "DSA master Ethernet device not found!\n");
+		return -EINVAL;
+	}
+
+	if (ops->port_enable) {
+		err = ops->port_enable(dev, ppriv->index, ppriv->phy);
+		if (err)
+			return err;
+		err = ops->port_enable(dev, platdata->cpu_port,
+				       platdata->port[platdata->cpu_port].phy);
+		if (err)
+			return err;
+	}
+
+	return eth_get_ops(master)->start(master);
+}
+
+/* Stop the desired port, the CPU port and the master Eth interface */
+static void dsa_port_stop(struct udevice *pdev)
+{
+	struct udevice *dev = dev_get_parent(pdev);
+	struct dsa_perdev_platdata *platdata = dev_get_platdata(dev);
+	struct udevice *master = dsa_port_get_master(pdev, false);
+	struct dsa_port_platdata *ppriv = dev_get_priv(pdev);
+	struct dsa_ops *ops = dsa_get_ops(dev);
+
+	if (!ppriv || !platdata)
+		return;
+
+	if (ops->port_disable) {
+		ops->port_disable(dev, ppriv->index, ppriv->phy);
+		ops->port_disable(dev, platdata->cpu_port,
+				  platdata->port[platdata->cpu_port].phy);
+	}
+
+	/*
+	 * stop master only if it's active, don't probe it otherwise.
+	 * Under normal usage it would be active because we're using it, but
+	 * during tear-down it may have been removed ahead of us.
+	 */
+	if (master && device_active(master))
+		eth_get_ops(master)->stop(master);
+}
+
+/*
+ * Insert a DSA tag and call master Ethernet send on the resulting packet
+ * We copy the frame to a stack buffer where we have reserved headroom and
+ * tailroom space.  Headroom and tailroom are set to 0.
+ */
+static int dsa_port_send(struct udevice *pdev, void *packet, int length)
+{
+	struct udevice *dev = dev_get_parent(pdev);
+	struct dsa_perdev_platdata *platdata = dev_get_platdata(dev);
+	struct udevice *master = dsa_port_get_master(pdev, true);
+	struct dsa_port_platdata *ppriv = dev_get_priv(pdev);
+	struct dsa_ops *ops = dsa_get_ops(dev);
+	uchar dsa_packet[DSA_MAX_FRAME_SIZE];
+	int head = platdata->headroom, tail = platdata->tailroom;
+	int err;
+
+	if (!master)
+		return -EINVAL;
+
+	if (length + head + tail > DSA_MAX_FRAME_SIZE)
+		return -EINVAL;
+
+	memset(dsa_packet, 0, head);
+	memset(dsa_packet + head + length, 0, tail);
+	memcpy(dsa_packet + head, packet, length);
+	length += head + tail;
+
+	err = ops->xmit(dev, ppriv->index, dsa_packet, length);
+	if (err)
+		return err;
+
+	return eth_get_ops(master)->send(master, dsa_packet, length);
+}
+
+/* Receive a frame from master Ethernet, process it and pass it on */
+static int dsa_port_recv(struct udevice *pdev, int flags, uchar **packetp)
+{
+	struct udevice *dev = dev_get_parent(pdev);
+	struct dsa_perdev_platdata *platdata = dev_get_platdata(dev);
+	struct udevice *master = dsa_port_get_master(pdev, true);
+	struct dsa_port_platdata *ppriv = dev_get_priv(pdev);
+	struct dsa_ops *ops = dsa_get_ops(dev);
+	int head = platdata->headroom, tail = platdata->tailroom;
+	int length, port_index, err;
+
+	if (!master)
+		return -EINVAL;
+
+	length = eth_get_ops(master)->recv(master, flags, packetp);
+	if (length <= 0)
+		return length;
+
+	/*
+	 * if we receive frames from a different port or frames that DSA driver
+	 * doesn't like we discard them here.
+	 * In case of discard we return with no frame and expect to be called
+	 * again instead of looping here, so upper layer can deal with timeouts
+	 * and ctrl-c
+	 */
+	err = ops->rcv(dev, &port_index, *packetp, length);
+	if (err || port_index != ppriv->index || (length <= head + tail)) {
+		if (eth_get_ops(master)->free_pkt)
+			eth_get_ops(master)->free_pkt(master, *packetp, length);
+		return -EAGAIN;
+	}
+
+	/*
+	 * We move the pointer over headroom here to avoid a copy.  If free_pkt
+	 * gets called we move the pointer back before calling master free_pkt.
+	 */
+	*packetp += head;
+
+	return length - head - tail;
+}
+
+static int dsa_port_free_pkt(struct udevice *pdev, uchar *packet, int length)
+{
+	struct udevice *dev = dev_get_parent(pdev);
+	struct dsa_perdev_platdata *platdata = dev_get_platdata(dev);
+	struct udevice *master = dsa_port_get_master(pdev, true);
+
+	if (!master)
+		return -EINVAL;
+
+	if (eth_get_ops(master)->free_pkt) {
+		/* return the original pointer and length to master Eth */
+		packet -= platdata->headroom;
+		length += platdata->headroom - platdata->tailroom;
+
+		return eth_get_ops(master)->free_pkt(master, packet, length);
+	}
+
+	return 0;
+}
+
+static const struct eth_ops dsa_port_ops = {
+	.start		= dsa_port_start,
+	.send		= dsa_port_send,
+	.recv		= dsa_port_recv,
+	.stop		= dsa_port_stop,
+	.free_pkt	= dsa_port_free_pkt,
+};
+
+U_BOOT_DRIVER(dsa_port) = {
+	.name	= DSA_PORT_CHILD_DRV_NAME,
+	.id	= UCLASS_ETH,
+	.ops	= &dsa_port_ops,
+	.platdata_auto_alloc_size = sizeof(struct eth_pdata),
+};
+
+/*
+ * reads the DT properties of the given DSA port.
+ * If the return value is != 0 then the port is skipped
+ */
+static int dsa_port_parse_dt(struct udevice *dev, int port_index,
+			     ofnode ports_node, bool *is_cpu)
+{
+	struct dsa_perdev_platdata *platdata = dev_get_platdata(dev);
+	struct dsa_port_platdata *port = &platdata->port[port_index];
+	ofnode temp_node;
+	u32 ethernet;
+
+	/*
+	 * if we don't have a DT we don't do anything here but the port is
+	 * registered normally
+	 */
+	if (!ofnode_valid(ports_node))
+		return 0;
+
+	ofnode_for_each_subnode(temp_node, ports_node) {
+		const char *port_label;
+		u32 reg;
+
+		if (ofnode_read_u32(temp_node, "reg", &reg) ||
+		    reg != port_index)
+			continue;
+
+		/* if the port is explicitly disabled in DT skip it */
+		if (!ofnode_is_available(temp_node))
+			return -ENODEV;
+
+		port->node = temp_node;
+
+		dev_dbg(dev, "port %d node %s\n", port->index,
+			ofnode_get_name(port->node));
+
+		/* Use 'label' if present in DT */
+		port_label = ofnode_read_string(port->node, "label");
+		if (port_label)
+			strncpy(port->name, port_label, DSA_PORT_NAME_LENGTH);
+
+		*is_cpu = !ofnode_read_u32(port->node, "ethernet",
+					   &ethernet);
+
+		if (*is_cpu) {
+			platdata->master_node =
+				ofnode_get_by_phandle(ethernet);
+			platdata->cpu_port = port_index;
+
+			dev_dbg(dev, "master node %s on port %d\n",
+				ofnode_get_name(platdata->master_node),
+				port_index);
+		}
+		break;
+	}
+
+	return 0;
+}
+
+/**
+ * This function mostly deals with pulling information out of the device tree
+ * into the platdata structure.
+ * It goes through the list of switch ports, registers an Eth device for each
+ * front panel port and identifies the cpu port connected to master Eth device.
+ * TODO: support cascaded switches
+ */
+static int dm_dsa_post_bind(struct udevice *dev)
+{
+	struct dsa_perdev_platdata *platdata = dev_get_platdata(dev);
+	ofnode ports_node = ofnode_null();
+	int first_err = 0, err = 0, i;
+
+	if (!platdata) {
+		dev_err(dev, "missing plaform data\n");
+		return -EINVAL;
+	}
+
+	if (platdata->num_ports <= 0 || platdata->num_ports > DSA_MAX_PORTS) {
+		dev_err(dev, "unexpected num_ports value (%d)\n",
+			platdata->num_ports);
+		return -EINVAL;
+	}
+
+	platdata->master_node = ofnode_null();
+
+	if (!ofnode_valid(dev->node)) {
+		dev_dbg(dev, "Device doesn't have a valid DT node!\n");
+	} else {
+		ports_node = ofnode_find_subnode(dev->node, "ports");
+		if (!ofnode_valid(ports_node))
+			dev_dbg(dev,
+				"ports node is missing under DSA device!\n");
+	}
+
+	for (i = 0; i < platdata->num_ports; i++) {
+		struct dsa_port_platdata *port = &platdata->port[i];
+		bool skip_port, is_cpu = false;
+
+		port->index = i;
+
+		/*
+		 * If the driver set up port names in _bind use those, otherwise
+		 * use default ones.
+		 * If present, DT label is used as name and overrides anything
+		 * we may have here.
+		 */
+		if (!strlen(port->name))
+			snprintf(port->name, DSA_PORT_NAME_LENGTH, "%s@%d",
+				 dev->name, i);
+
+		skip_port = !!dsa_port_parse_dt(dev, i, ports_node, &is_cpu);
+
+		/*
+		 * if this is the CPU port don't register it as an ETH device,
+		 * we skip it on purpose since I/O to/from it from the CPU
+		 * isn't useful
+		 * TODO: cpu port may have a PHY and we don't handle that yet.
+		 */
+		if (is_cpu || skip_port)
+			continue;
+
+		err = device_bind_driver_to_node(dev, DSA_PORT_CHILD_DRV_NAME,
+						 port->name, port->node,
+						 &port->dev);
+
+		/* try to bind all ports but keep 1st error */
+		if (err && !first_err)
+			first_err = err;
+	}
+
+	if (!ofnode_valid(platdata->master_node))
+		dev_dbg(dev, "DSA master Eth device is missing!\n");
+
+	return first_err;
+}
+
+/**
+ * This function deals with additional devices around the switch as these should
+ * have been bound to drivers by now.
+ * TODO: pick up references to other switch devices here, if we're cascaded.
+ */
+static int dm_dsa_pre_probe(struct udevice *dev)
+{
+	struct dsa_perdev_platdata *platdata = dev_get_platdata(dev);
+	int i;
+
+	if (!platdata)
+		return -EINVAL;
+
+	if (ofnode_valid(platdata->master_node))
+		uclass_find_device_by_ofnode(UCLASS_ETH, platdata->master_node,
+					     &platdata->master_dev);
+
+	for (i = 0; i < platdata->num_ports; i++) {
+		struct dsa_port_platdata *port = &platdata->port[i];
+
+		if (port->dev) {
+			port->dev->priv = port;
+			port->phy = dm_eth_phy_connect(port->dev);
+		}
+	}
+
+	return 0;
+}
+
+UCLASS_DRIVER(dsa) = {
+	.id = UCLASS_DSA,
+	.name = "dsa",
+	.post_bind  = dm_dsa_post_bind,
+	.pre_probe = dm_dsa_pre_probe,
+	.per_device_platdata_auto_alloc_size =
+			sizeof(struct dsa_perdev_platdata),
+};