From patchwork Tue May 5 17:43:03 2020 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Claudiu Manoil X-Patchwork-Id: 245159 List-Id: U-Boot discussion From: claudiu.manoil at nxp.com (Claudiu Manoil) Date: Tue, 5 May 2020 20:43:03 +0300 Subject: [PATCH v4 1/6] net: introduce DSA class for Ethernet switches In-Reply-To: <1588700588-8587-1-git-send-email-claudiu.manoil@nxp.com> References: <1588700588-8587-1-git-send-email-claudiu.manoil@nxp.com> Message-ID: <1588700588-8587-2-git-send-email-claudiu.manoil@nxp.com> From: Alex Marginean 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 Signed-off-by: Vladimir Oltean Signed-off-by: Claudiu Manoil --- drivers/net/Kconfig | 13 ++ include/dm/uclass-id.h | 1 + include/net.h | 6 + include/net/dsa.h | 137 ++++++++++++++ net/Makefile | 1 + net/dsa-uclass.c | 395 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 553 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 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 +#include +#include + +/** + * 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)| + * ---------------------------- + * | | + * |--------------------------------------------| + * | 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 +#include +#include +#include +#include +#include + +#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 != 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", + ðernet); + + 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), +};