diff --git a/drivers/net/Kconfig b/drivers/net/Kconfig
index 4d1013c984..863314284b 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 598f65ea7a..6fc52b72aa 100644
--- a/include/dm/uclass-id.h
+++ b/include/dm/uclass-id.h
@@ -44,6 +44,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.h b/include/net.h
index 82500eeb30..155f49196e 100644
--- a/include/net.h
+++ b/include/net.h
@@ -489,7 +489,13 @@ struct icmp_hdr {
  * maximum packet size and multiple of 32 bytes =  1536
  */
 #define PKTSIZE			1522
+#ifndef CONFIG_DM_DSA
 #define PKTSIZE_ALIGN		1536
+#else
+/* Maximum DSA tagging overhead (headroom or tailroom) */
+#define DSA_MAX_OVR		256
+#define PKTSIZE_ALIGN		(1536 + DSA_MAX_OVR)
+#endif
 
 /*
  * Maximum receive ring size; that is, the number of packets
diff --git a/include/net/dsa.h b/include/net/dsa.h
new file mode 100644
index 0000000000..45f1d42e7e
--- /dev/null
+++ b/include/net/dsa.h
@@ -0,0 +1,137 @@
+/* SPDX-License-Identifier: GPL-2.0+ */
+/*
+ * Copyright 2019-2020 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.
+ * - 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
+
+/**
+ * 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 fef71b940a..32f5a7824e 100644
--- a/net/Makefile
+++ b/net/Makefile
@@ -27,6 +27,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..32573fe45c
--- /dev/null
+++ b/net/dsa-uclass.c
@@ -0,0 +1,395 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright 2019-2020 NXP
+ */
+
+#include <net/dsa.h>
+#include <dm/lists.h>
+#include <dm/device_compat.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)
+{
+	struct udevice *dev = dev_get_parent(pdev);
+	struct dsa_perdev_platdata *platdata = dev_get_platdata(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);
+	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);
+	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);
+	int head = platdata->headroom, tail = platdata->tailroom;
+	struct udevice *master = dsa_port_get_master(pdev);
+	struct dsa_port_platdata *ppriv = dev_get_priv(pdev);
+	struct dsa_ops *ops = dsa_get_ops(dev);
+	uchar dsa_packet_tmp[PKTSIZE_ALIGN];
+	int err;
+
+	if (!master)
+		return -EINVAL;
+
+	if (length + head + tail > PKTSIZE_ALIGN)
+		return -EINVAL;
+
+	memset(dsa_packet_tmp, 0, head);
+	memset(dsa_packet_tmp + head + length, 0, tail);
+	memcpy(dsa_packet_tmp + head, packet, length);
+	length += head + tail;
+	/* copy back to preserve original buffer alignment */
+	memcpy(packet, dsa_packet_tmp, length);
+
+	err = ops->xmit(dev, ppriv->index, packet, length);
+	if (err)
+		return err;
+
+	return eth_get_ops(master)->send(master, 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);
+	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);
+
+	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 int dsa_port_probe(struct udevice *pdev)
+{
+	struct udevice *master = dsa_port_get_master(pdev);
+	unsigned char env_enetaddr[ARP_HLEN];
+
+	/* If there is no MAC address in the environment, inherit it
+	 * from the DSA master.
+	 */
+	eth_env_get_enetaddr_by_index("eth", pdev->seq, env_enetaddr);
+	if (!is_zero_ethaddr(env_enetaddr))
+		return 0;
+
+	if (master) {
+		struct eth_pdata *meth = dev_get_platdata(master);
+		struct eth_pdata *peth = dev_get_platdata(pdev);
+
+		memcpy(peth->enetaddr, meth->enetaddr, ARP_HLEN);
+		eth_env_set_enetaddr_by_index("eth", pdev->seq,
+					      meth->enetaddr);
+	}
+
+	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,
+	.probe  = dsa_port_probe,
+	.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;
+
+		port->node = temp_node;
+		/* if the port is explicitly disabled in DT skip it */
+		if (!ofnode_is_available(temp_node))
+			return -ENODEV;
+
+		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];
+
+		/* non-cpu ports only */
+		if (!port->dev)
+			continue;
+
+		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),
+};
