[PATCH 5/8] usb: eth: Add CDC ECM support

Jonas Karlman jonas at kwiboo.se
Sat Jul 11 18:08:03 CEST 2026


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 <jonas at kwiboo.se>
---
 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



More information about the U-Boot mailing list