Add support for CDC ECM (Communications Device Class, Ethernet Control Model) devices.
References: - [USBECM12] USB Subclass Specification for Ethernet Control Model 1.2. - [USBCDC12] USB Class Definitions for Communications Devices 1.2. - [USB20] USB Specification 2.0. Signed-off-by: Jonas Karlman <[email protected]> --- doc/README.usb | 1 + drivers/usb/eth/Kconfig | 9 ++ drivers/usb/eth/Makefile | 1 + drivers/usb/eth/cdc_common.c | 153 +++++++++++++++++++ drivers/usb/eth/cdc_common.h | 26 ++++ drivers/usb/eth/cdc_ecm.c | 284 +++++++++++++++++++++++++++++++++++ 6 files changed, 474 insertions(+) create mode 100644 drivers/usb/eth/cdc_common.c create mode 100644 drivers/usb/eth/cdc_common.h create mode 100644 drivers/usb/eth/cdc_ecm.c diff --git a/doc/README.usb b/doc/README.usb index 650a6daae0a5..65bafe7030f7 100644 --- a/doc/README.usb +++ b/doc/README.usb @@ -133,6 +133,7 @@ and one or more of the following for individual adapter hardware: CONFIG_USB_ETHER_ASIX=y CONFIG_USB_ETHER_ASIX88179=y +CONFIG_USB_ETHER_CDC_ECM=y CONFIG_USB_ETHER_LAN75XX=y CONFIG_USB_ETHER_LAN78XX=y CONFIG_USB_ETHER_MCS7830=y diff --git a/drivers/usb/eth/Kconfig b/drivers/usb/eth/Kconfig index b9b77f467437..0b1aae69ccac 100644 --- a/drivers/usb/eth/Kconfig +++ b/drivers/usb/eth/Kconfig @@ -20,6 +20,15 @@ config USB_ETHER_ASIX88179 Say Y here if you would like to support ASIX AX88179 based USB 3.0 Ethernet Devices. +config USB_ETHER_CDC_ECM + bool "CDC ECM support" + depends on USB_HOST_ETHER + help + Say Y here if you would like to support CDC ECM (Communications Device + Class, Ethernet Control Model) devices. The USB Communications Class + Subclass Specifications for Ethernet Control Model Devices is + available from <http://www.usb.org/>. + config USB_ETHER_LAN75XX bool "Microchip LAN75XX support" depends on USB_HOST_ETHER diff --git a/drivers/usb/eth/Makefile b/drivers/usb/eth/Makefile index 2e5d0782e888..3ddace19578a 100644 --- a/drivers/usb/eth/Makefile +++ b/drivers/usb/eth/Makefile @@ -7,6 +7,7 @@ obj-$(CONFIG_USB_HOST_ETHER) += usb_ether.o obj-$(CONFIG_USB_ETHER_ASIX) += asix.o obj-$(CONFIG_USB_ETHER_ASIX88179) += asix88179.o +obj-$(CONFIG_USB_ETHER_CDC_ECM) += cdc_common.o cdc_ecm.o obj-$(CONFIG_USB_ETHER_MCS7830) += mcs7830.o obj-$(CONFIG_USB_ETHER_SMSC95XX) += smsc95xx.o obj-$(CONFIG_USB_ETHER_LAN75XX) += lan7x.o lan75xx.o diff --git a/drivers/usb/eth/cdc_common.c b/drivers/usb/eth/cdc_common.c new file mode 100644 index 000000000000..2342a56310f4 --- /dev/null +++ b/drivers/usb/eth/cdc_common.c @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright Contributors to the U-Boot project. + +#define LOG_CATEGORY UCLASS_ETH + +#include <console.h> +#include <env.h> +#include <hexdump.h> +#include <memalign.h> +#include <time.h> +#include <usb.h> +#include <linux/delay.h> +#include <linux/if_ether.h> +#include <linux/usb/cdc.h> +#include "cdc_common.h" + +int cdc_bulk_xfer(struct usb_device *udev, u32 pipe, void *data, int length, + int *actual_length, int timeout) +{ + int ret; + + *actual_length = 0; + ret = usb_bulk_msg(udev, pipe, data, length, actual_length, timeout); + + log_debug("Xfer: length = %u, actual = %u, ret = %d, status = %lx\n", + length, *actual_length, ret, udev->status); + + if (ret && udev->status & USB_ST_STALLED) + usb_clear_halt(udev, pipe); + + return ret; +} + +int cdc_get_mac_address(struct usb_device *udev, int index, u8 *data) +{ + u8 tmpbuf[13]; + int ret; + + /* The string descriptor holds the 48bit Ethernet MAC address */ + ret = usb_string(udev, index, tmpbuf, sizeof(tmpbuf)); + if (ret == 12 && !hex2bin(data, tmpbuf, ETH_ALEN)) + return 0; + + memset(data, 0, ETH_ALEN); + return -EINVAL; +} + +int cdc_set_ethernet_packet_filter(struct usb_device *udev, u16 index, u16 value) +{ + int ret; + + /* 6.2.4 SetEthernetPacketFilter [USBECM12] */ + ret = usb_control_msg(udev, usb_sndctrlpipe(udev, 0), + USB_CDC_SET_ETHERNET_PACKET_FILTER, + USB_TYPE_CLASS | USB_RECIP_INTERFACE, + value, index, NULL, 0, USB_CTRL_SET_TIMEOUT); + log_debug("SetEthernetPacketFilter(%x): ret=%d, status=%lx\n", + value, ret, udev->status); + + return ret; +} + +int cdc_set_interface(struct usb_device *udev, u16 index, u16 value) +{ + int ret; + + /* 9.4.10 Set Interface [USB20] */ + ret = usb_control_msg(udev, usb_sndctrlpipe(udev, 0), + USB_REQ_SET_INTERFACE, USB_RECIP_INTERFACE, + value, index, NULL, 0, USB_CTRL_SET_TIMEOUT); + log_debug("SetInterface(%u, %u): ret=%d, status=%lx\n", + index, value, ret, udev->status); + + return ret; +} + +int cdc_wait_on_connection(struct usb_device *udev, u32 pipe, int interval) +{ + struct usb_cdc_notification *notification; + ulong start, timeout; + int maxpacket, ret; + ALLOC_CACHE_ALIGN_BUFFER(u8, tmpbuf, 64); + + if (!pipe) + return 0; + + maxpacket = usb_maxpacket(udev, pipe); + if (maxpacket < sizeof(struct usb_cdc_notification) || maxpacket > 64) + return -EINVAL; + + timeout = env_get_ulong("phy_aneg_timeout", 10, + config_opt_enabled(CONFIG_PHYLIB, + CONFIG_PHY_ANEG_TIMEOUT, 4000)); + start = get_timer(0); + do { + if (ctrlc()) + return -EINTR; + + ret = usb_int_msg(udev, pipe, tmpbuf, maxpacket, interval, false); + if (ret < 0) + return ret; + + notification = (struct usb_cdc_notification *)tmpbuf; + if (!notification->bmRequestType) { + log_warning("Invalid notification: [%x,%x,%x,%u,%u]\n", + notification->bmRequestType, + notification->bNotificationType, + le16_to_cpu(notification->wValue), + le16_to_cpu(notification->wIndex), + le16_to_cpu(notification->wLength)); + return -EINVAL; + } + + switch (notification->bNotificationType) { + /* 6.3.1 NetworkConnection [USBCDC12] */ + case USB_CDC_NOTIFY_NETWORK_CONNECTION: + if (le16_to_cpu(notification->wValue) == 1) { + log_debug("Network connection established\n"); + return 0; + } + mdelay(50); + break; + } + } while (get_timer(start) < timeout); + + return -ETIMEDOUT; +} + +struct usb_config_descriptor * +cdc_get_config_descriptor(struct usb_device *udev, int cfgno) +{ + struct usb_config_descriptor *config; + int length, ret; + + length = usb_get_configuration_len(udev, cfgno); + if (length <= 0) + return NULL; + + config = malloc_cache_aligned(length); + if (!config) + return NULL; + + /* 9.4.3 Get Descriptor [USB20] */ + ret = usb_get_configuration_no(udev, cfgno, (u8 *)config, length); + if (ret < 0 || + config->bDescriptorType != USB_DT_CONFIG || + config->bLength != USB_DT_CONFIG_SIZE) { + free(config); + return NULL; + } + + return config; +} diff --git a/drivers/usb/eth/cdc_common.h b/drivers/usb/eth/cdc_common.h new file mode 100644 index 000000000000..200860f63ffd --- /dev/null +++ b/drivers/usb/eth/cdc_common.h @@ -0,0 +1,26 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* Copyright Contributors to the U-Boot project. */ + +#ifndef _USB_ETH_CDC_COMMON_H +#define _USB_ETH_CDC_COMMON_H + +#define USB_BULK_RECV_TIMEOUT 500 +#define USB_BULK_SEND_TIMEOUT 5000 +#define USB_CTRL_GET_TIMEOUT 5000 +#define USB_CTRL_SET_TIMEOUT 5000 + +int cdc_bulk_xfer(struct usb_device *udev, u32 pipe, void *data, int length, + int *actual_length, int timeout); + +int cdc_get_mac_address(struct usb_device *udev, int index, u8 *data); + +int cdc_set_ethernet_packet_filter(struct usb_device *udev, u16 index, u16 value); + +int cdc_set_interface(struct usb_device *udev, u16 index, u16 value); + +int cdc_wait_on_connection(struct usb_device *udev, u32 pipe, int interval); + +struct usb_config_descriptor *cdc_get_config_descriptor(struct usb_device *udev, + int cfgno); + +#endif diff --git a/drivers/usb/eth/cdc_ecm.c b/drivers/usb/eth/cdc_ecm.c new file mode 100644 index 000000000000..115152e4964a --- /dev/null +++ b/drivers/usb/eth/cdc_ecm.c @@ -0,0 +1,284 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright Contributors to the U-Boot project. + +#define LOG_CATEGORY UCLASS_ETH + +#include <dm.h> +#include <memalign.h> +#include <net-common.h> +#include <usb.h> +#include <linux/usb/cdc.h> +#include "cdc_common.h" + +#define RX_BUFFER_SIZE SZ_2K + +struct cdc_ecm_priv { + struct usb_cdc_ether_desc ether_desc; + bool normal_operation; + u16 control; + u16 data; + u16 data_default; + u16 data_normal; + u32 intpipe; + int intinterval; + u32 bulkinpipe; + u32 bulkoutpipe; + u8 *rx_buffer; +}; + +static void cdc_ecm_stop(struct udevice *dev) +{ + struct usb_device *udev = dev_get_parent_priv(dev); + struct cdc_ecm_priv *priv = dev_get_priv(dev); + + /* 6.2.4 SetEthernetPacketFilter [USBECM12] */ + cdc_set_ethernet_packet_filter(udev, priv->control, 0); +} + +static int cdc_ecm_start(struct udevice *dev) +{ + struct usb_device *udev = dev_get_parent_priv(dev); + struct cdc_ecm_priv *priv = dev_get_priv(dev); + int ret; + + /* 6.2.4 SetEthernetPacketFilter [USBECM12] */ + cdc_set_ethernet_packet_filter(udev, priv->control, + USB_CDC_PACKET_TYPE_DIRECTED | + USB_CDC_PACKET_TYPE_BROADCAST | + USB_CDC_PACKET_TYPE_MULTICAST); + + if (!priv->normal_operation) { + /* Set non-default state to enable normal operation */ + ret = cdc_set_interface(udev, priv->data, priv->data_normal); + if (ret < 0) + return ret; + + priv->normal_operation = true; + } + + ret = cdc_wait_on_connection(udev, priv->intpipe, priv->intinterval); + if (ret < 0) + cdc_ecm_stop(dev); + + return ret; +} + +static int cdc_ecm_send(struct udevice *dev, void *packet, int length) +{ + struct usb_device *udev = dev_get_parent_priv(dev); + struct cdc_ecm_priv *priv = dev_get_priv(dev); + int actual_length; + + if (length < ETH_HLEN || length > priv->ether_desc.wMaxSegmentSize) + return -EINVAL; + + return cdc_bulk_xfer(udev, priv->bulkoutpipe, packet, length, + &actual_length, USB_BULK_SEND_TIMEOUT); +} + +static int cdc_ecm_recv(struct udevice *dev, int flags, uchar **packetp) +{ + struct usb_device *udev = dev_get_parent_priv(dev); + struct cdc_ecm_priv *priv = dev_get_priv(dev); + int actual_length, ret; + + if (flags & ETH_RECV_CHECK_DEVICE) { + ret = cdc_bulk_xfer(udev, priv->bulkinpipe, + priv->rx_buffer, RX_BUFFER_SIZE, + &actual_length, USB_BULK_RECV_TIMEOUT); + if (!ret && actual_length >= ETH_HLEN) { + *packetp = priv->rx_buffer; + return actual_length; + } + } + + *packetp = NULL; + return -EAGAIN; +} + +static int cdc_ecm_read_rom_hwaddr(struct udevice *dev) +{ + struct usb_device *udev = dev_get_parent_priv(dev); + struct cdc_ecm_priv *priv = dev_get_priv(dev); + struct eth_pdata *pdata = dev_get_plat(dev); + + return cdc_get_mac_address(udev, priv->ether_desc.iMACAddress, + pdata->enetaddr); +} + +static int cdc_ecm_parse_config_descriptor(struct udevice *dev, + struct usb_config_descriptor *config) +{ + struct usb_device *udev = dev_get_parent_priv(dev); + struct cdc_ecm_priv *priv = dev_get_priv(dev); + struct usb_interface_descriptor *iface_desc; + struct usb_endpoint_descriptor *ep_desc; + struct usb_descriptor_header *head; + bool comm_if_found = false; + bool data_if_found = false; + u8 *buffer = (u8 *)config; + int ep, index; + + priv->control = U16_MAX; + priv->data = U16_MAX; + priv->data_default = U16_MAX; + priv->data_normal = U16_MAX; + priv->intpipe = 0; + priv->intinterval = 0; + priv->bulkinpipe = 0; + priv->bulkoutpipe = 0; + memset(&priv->ether_desc, 0, sizeof(priv->ether_desc)); + + index = config->bLength; + head = (struct usb_descriptor_header *)&buffer[index]; + while (index + 1 < config->wTotalLength && head->bLength) { + if (index + head->bLength > config->wTotalLength) + return -EINVAL; + switch (head->bDescriptorType) { + /* 9.6.5 Interface [USB20] */ + case USB_DT_INTERFACE: + if (head->bLength != USB_DT_INTERFACE_SIZE) + return -EINVAL; + iface_desc = (struct usb_interface_descriptor *)head; + comm_if_found = (iface_desc->bInterfaceClass == USB_CLASS_COMM && + iface_desc->bInterfaceSubClass == USB_CDC_SUBCLASS_ETHERNET && + iface_desc->bInterfaceProtocol == USB_CDC_PROTO_NONE); + data_if_found = (iface_desc->bInterfaceClass == USB_CLASS_CDC_DATA && + iface_desc->bInterfaceSubClass == 0 && + iface_desc->bInterfaceProtocol == USB_CDC_PROTO_NONE); + if (comm_if_found) { + priv->control = iface_desc->bInterfaceNumber; + } else if (data_if_found && iface_desc->bNumEndpoints == 2) { + priv->data = iface_desc->bInterfaceNumber; + priv->data_normal = iface_desc->bAlternateSetting; + } else if (data_if_found && iface_desc->bNumEndpoints == 0) { + priv->data_default = iface_desc->bAlternateSetting; + } + break; + /* 9.6.6 Endpoint [USB20] */ + case USB_DT_ENDPOINT: + if (head->bLength != USB_DT_ENDPOINT_SIZE && + head->bLength != USB_DT_ENDPOINT_AUDIO_SIZE) + return -EINVAL; + ep_desc = (struct usb_endpoint_descriptor *)head; + if (comm_if_found && usb_endpoint_is_int_in(ep_desc)) { + ep = usb_endpoint_num(ep_desc); + priv->intpipe = usb_rcvintpipe(udev, ep); + priv->intinterval = ep_desc->bInterval; + } else if (data_if_found && usb_endpoint_is_bulk_in(ep_desc)) { + ep = usb_endpoint_num(ep_desc); + priv->bulkinpipe = usb_rcvbulkpipe(udev, ep); + } else if (data_if_found && usb_endpoint_is_bulk_out(ep_desc)) { + ep = usb_endpoint_num(ep_desc); + priv->bulkoutpipe = usb_sndbulkpipe(udev, ep); + } + break; + /* 5.2.3 Functional Descriptors [USBCDC12] */ + case USB_DT_CS_INTERFACE: + if (head->bLength < 4) + return -EINVAL; + if (!comm_if_found) + break; + switch (buffer[index + 2]) { /* bDescriptorSubType */ + /* 5.4 Ethernet Networking Functional Descriptor [USBECM12] */ + case USB_CDC_ETHERNET_TYPE: + memcpy(&priv->ether_desc, head, sizeof(priv->ether_desc)); + le32_to_cpus(&priv->ether_desc.bmEthernetStatistics); + le16_to_cpus(&priv->ether_desc.wMaxSegmentSize); + le16_to_cpus(&priv->ether_desc.wNumberMCFilters); + break; + } + break; + } + index += head->bLength; + head = (struct usb_descriptor_header *)&buffer[index]; + } + + if (priv->control == U16_MAX || priv->data == U16_MAX || + priv->data_default == U16_MAX || priv->data_normal == U16_MAX || + !priv->bulkinpipe || !priv->bulkoutpipe) + return -ENODEV; + + return 0; +} + +static int cdc_ecm_probe(struct udevice *dev) +{ + struct usb_device *udev = dev_get_parent_priv(dev); + struct cdc_ecm_priv *priv = dev_get_priv(dev); + bool configuration_found = false; + int cfgno, ret; + + /* Find the CDC ECM configuration */ + for (cfgno = 0; cfgno < udev->descriptor.bNumConfigurations; cfgno++) { + struct usb_config_descriptor *config; + + /* 9.4.3 Get Descriptor [USB20] */ + config = cdc_get_config_descriptor(udev, cfgno); + if (!config) + return -ENODEV; + + ret = cdc_ecm_parse_config_descriptor(dev, config); + if (!ret) { + configuration_found = true; + free(config); + break; + } + + free(config); + } + if (!configuration_found) + return -ENODEV; + + /* 9.4.7 Set Configuration [USB20] */ + ret = usb_select_configuration_no(udev, cfgno); + if (ret < 0) + return -ENODEV; + + priv->rx_buffer = malloc_cache_aligned(RX_BUFFER_SIZE); + if (!priv->rx_buffer) + return -ENOMEM; + + return 0; +} + +static int cdc_ecm_remove(struct udevice *dev) +{ + struct usb_device *udev = dev_get_parent_priv(dev); + struct cdc_ecm_priv *priv = dev_get_priv(dev); + + if (priv->normal_operation) { + /* Set default state to disable normal operation */ + cdc_set_interface(udev, priv->data, priv->data_default); + } + + free(priv->rx_buffer); + + return 0; +} + +static const struct eth_ops cdc_ecm_eth_ops = { + .start = cdc_ecm_start, + .send = cdc_ecm_send, + .recv = cdc_ecm_recv, + .stop = cdc_ecm_stop, + .read_rom_hwaddr = cdc_ecm_read_rom_hwaddr, +}; + +U_BOOT_DRIVER(usb_cdc_2_ecm) = { + .name = "cdc_ecm", + .id = UCLASS_ETH, + .probe = cdc_ecm_probe, + .remove = cdc_ecm_remove, + .ops = &cdc_ecm_eth_ops, + .priv_auto = sizeof(struct cdc_ecm_priv), + .plat_auto = sizeof(struct eth_pdata), +}; + +static const struct usb_device_id cdc_ecm_id_table[] = { + { USB_INTERFACE_INFO(USB_CLASS_COMM, USB_CDC_SUBCLASS_ETHERNET, + USB_CDC_PROTO_NONE), }, + { /* sentinel */ } +}; + +U_BOOT_USB_DEVICE(usb_cdc_2_ecm, cdc_ecm_id_table); -- 2.54.0

