diff --git a/dts/bindings/usb/zephyr,usbh-cdc-ecm.yaml b/dts/bindings/usb/zephyr,usbh-cdc-ecm.yaml new file mode 100644 index 0000000000000..75919ab0462f7 --- /dev/null +++ b/dts/bindings/usb/zephyr,usbh-cdc-ecm.yaml @@ -0,0 +1,15 @@ +# Copyright (c) 2025 Linumiz GmbH +# SPDX-License-Identifier: Apache-2.0 + +description: | + USB Host CDC Ethernet Control Model (ECM) implementation. + Supports communication with CDC ECM-compliant USB Ethernet devices. + +compatible: "zephyr,usbh-cdc-ecm" + +include: base.yaml + +properties: + local-mac-address: + type: uint8-array + description: "Local MAC address for the CDC ECM device" diff --git a/include/zephyr/usb/usbh.h b/include/zephyr/usb/usbh.h index e2e6f3074000e..46fe733ae6828 100644 --- a/include/zephyr/usb/usbh.h +++ b/include/zephyr/usb/usbh.h @@ -1,5 +1,6 @@ /* - * Copyright (c) 2022 Nordic Semiconductor ASA + * Copyright (c) 2025 Nordic Semiconductor ASA + * Copyright 2025 NXP * * SPDX-License-Identifier: Apache-2.0 */ @@ -33,6 +34,16 @@ extern "C" { * @{ */ +/** + * USB host support status + */ +struct usbh_status { + /** USB host support is initialized */ + unsigned int initialized : 1; + /** USB host support is enabled */ + unsigned int enabled : 1; +}; + /** * USB host support runtime context */ @@ -43,6 +54,8 @@ struct usbh_context { struct k_mutex mutex; /** Pointer to UHC device struct */ const struct device *dev; + /** Status of the USB host support */ + struct usbh_status status; /** USB device list */ sys_dlist_t udevs; /** USB root device */ @@ -60,47 +73,105 @@ struct usbh_context { .addr_ba = &ba_##device_name, \ } +struct usbh_class_data; + /** - * @brief USB Class Code triple + * @brief Information about a device, which is relevant for matching a particular class. */ -struct usbh_code_triple { - /** Device Class Code */ - uint8_t dclass; - /** Class Subclass Code */ +struct usbh_class_filter { + /** Vendor ID */ + uint16_t vid; + /** Product ID */ + uint16_t pid; + /** Class Code */ + uint8_t class; + /** Subclass Code */ uint8_t sub; - /** Class Protocol Code */ + /** Protocol Code */ uint8_t proto; + /** Flags that tell which field to match */ + uint8_t flags; }; /** - * @brief USB host class data and class instance API + * @brief USB host class instance API */ -struct usbh_class_data { - /** Class code supported by this instance */ - struct usbh_code_triple code; - - /** Initialization of the class implementation */ - /* int (*init)(struct usbh_context *const uhs_ctx); */ +struct usbh_class_api { + /** Host init handler, before any device is connected */ + int (*init)(struct usbh_class_data *const c_data, + struct usbh_context *const uhs_ctx); /** Request completion event handler */ - int (*request)(struct usbh_context *const uhs_ctx, - struct uhc_transfer *const xfer, int err); - /** Device connected handler */ - int (*connected)(struct usbh_context *const uhs_ctx); - /** Device removed handler */ - int (*removed)(struct usbh_context *const uhs_ctx); - /** Bus remote wakeup handler */ - int (*rwup)(struct usbh_context *const uhs_ctx); - /** Bus suspended handler */ - int (*suspended)(struct usbh_context *const uhs_ctx); - /** Bus resumed handler */ - int (*resumed)(struct usbh_context *const uhs_ctx); + int (*completion_cb)(struct usbh_class_data *const c_data, + struct uhc_transfer *const xfer); + /** Device connection handler */ + int (*probe)(struct usbh_class_data *const c_data, + struct usb_device *const udev, + const uint8_t iface); + /** Device removal handler */ + int (*removed)(struct usbh_class_data *const c_data); + /** Bus suspended handler */ + int (*suspended)(struct usbh_class_data *const c_data); + /** Bus resumed handler */ + int (*resumed)(struct usbh_class_data *const c_data); }; /** + * @brief USB host class instance data */ -#define USBH_DEFINE_CLASS(name) \ - static STRUCT_SECTION_ITERABLE(usbh_class_data, name) +struct usbh_class_data { + /** Name of the USB host class instance */ + const char *name; + /** Pointer to USB host stack context structure */ + struct usbh_context *uhs_ctx; + /** Pointer to USB device this class is used for */ + struct usb_device *udev; + /** Interface number for which this class matched */ + uint8_t iface; + /** Pointer to host support class API */ + struct usbh_class_api *api; + /** Pointer to private data */ + void *priv; +}; +/** + * @cond INTERNAL_HIDDEN + * + * Variables used by the USB host stack but not exposed to the class + * through the class API. + */ +struct usbh_class_node { + /** Class information exposed to host class implementations (drivers). */ + struct usbh_class_data *const c_data; + /** Filter rules to match this USB host class instance against a device class **/ + struct usbh_class_filter *filters; + /** Number of filters in the array */ + size_t num_filters; +}; +/* @endcond */ + +/** + * @brief Define USB host support class data + * + * Macro defines class (function) data, as well as corresponding node + * structures used internally by the stack. + * + * @param[in] class_name Class name + * @param[in] class_api Pointer to struct usbh_class_api + * @param[in] class_priv Class private data + * @param[in] filt Array of @ref usbh_class_filter to match this class or NULL to match everything + * @param[in] num_filt Number of filters in the array + */ +#define USBH_DEFINE_CLASS(class_name, class_api, class_priv, filt, num_filt) \ + static struct usbh_class_data class_data_##class_name = { \ + .name = STRINGIFY(class_name), \ + .api = class_api, \ + .priv = class_priv, \ + }; \ + static STRUCT_SECTION_ITERABLE(usbh_class_node, class_name) = { \ + .c_data = &class_data_##class_name, \ + .filters = filt, \ + .num_filters = num_filt, \ + }; /** * @brief Initialize the USB host support; diff --git a/subsys/usb/CMakeLists.txt b/subsys/usb/CMakeLists.txt index 6b968a4bf894e..b8367352d06de 100644 --- a/subsys/usb/CMakeLists.txt +++ b/subsys/usb/CMakeLists.txt @@ -5,3 +5,4 @@ add_subdirectory_ifdef(CONFIG_USB_DEVICE_STACK device) add_subdirectory_ifdef(CONFIG_USB_DEVICE_STACK_NEXT device_next) add_subdirectory_ifdef(CONFIG_USB_HOST_STACK host) add_subdirectory_ifdef(CONFIG_USBC_STACK usb_c) +add_subdirectory(common) diff --git a/subsys/usb/common/CMakeLists.txt b/subsys/usb/common/CMakeLists.txt new file mode 100644 index 0000000000000..1b38551ffa05a --- /dev/null +++ b/subsys/usb/common/CMakeLists.txt @@ -0,0 +1,6 @@ +# Copyright (c) 2025 Linumiz GmbH +# SPDX-License-Identifier: Apache-2.0 + +zephyr_include_directories(include) + +zephyr_library_sources_ifdef(CONFIG_USBH_CDC_ECM_CLASS usb_cdc_ecm.c) diff --git a/subsys/usb/common/include/usb_cdc_ecm.h b/subsys/usb/common/include/usb_cdc_ecm.h new file mode 100644 index 0000000000000..5a96873f2b117 --- /dev/null +++ b/subsys/usb/common/include/usb_cdc_ecm.h @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2017 Intel Corporation + * Copyright (c) 2023 Nordic Semiconductor ASA + * Copyright (c) 2025 Linumiz GmbH + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_INCLUDE_COMMON_USB_CDC_ECM_H_ +#define ZEPHYR_INCLUDE_COMMON_USB_CDC_ECM_H_ + +#include +#include +#include +#include +#include +#include + +#define CDC_ECM_EP_MPS_INT 16U +#define CDC_ECM_INTERVAL_DEFAULT 10000UL +#define CDC_ECM_FS_INT_EP_INTERVAL USB_FS_INT_EP_INTERVAL(10000U) +#define CDC_ECM_HS_INT_EP_INTERVAL USB_HS_INT_EP_INTERVAL(10000U) + +struct cdc_ecm_notification { + union { + uint8_t bmRequestType; + struct usb_req_type_field RequestType; + }; + uint8_t bNotificationType; + uint16_t wValue; + uint16_t wIndex; + uint16_t wLength; +} __packed; + +size_t ecm_eth_size(void *const ecm_pkt, const size_t len); + +#endif /* ZEPHYR_INCLUDE_COMMON_USB_CDC_ECM_H_ */ diff --git a/subsys/usb/common/usb_cdc_ecm.c b/subsys/usb/common/usb_cdc_ecm.c new file mode 100644 index 0000000000000..bdebc3f4b30e0 --- /dev/null +++ b/subsys/usb/common/usb_cdc_ecm.c @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2025 Linumiz GmbH + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "usb_cdc_ecm.h" + +/* Retrieve expected pkt size from ethernet/ip header */ +size_t ecm_eth_size(void *const ecm_pkt, const size_t len) +{ + uint8_t *ip_data = (uint8_t *)ecm_pkt + sizeof(struct net_eth_hdr); + struct net_eth_hdr *hdr = (void *)ecm_pkt; + uint16_t ip_len; + + if (len < NET_IPV6H_LEN + sizeof(struct net_eth_hdr)) { + /* Too short */ + return 0; + } + + switch (ntohs(hdr->type)) { + case NET_ETH_PTYPE_IP: + __fallthrough; + case NET_ETH_PTYPE_ARP: + ip_len = ntohs(((struct net_ipv4_hdr *)ip_data)->len); + break; + case NET_ETH_PTYPE_IPV6: + ip_len = ntohs(((struct net_ipv6_hdr *)ip_data)->len); + break; + default: + return 0; + } + + return sizeof(struct net_eth_hdr) + ip_len; +} diff --git a/subsys/usb/host/CMakeLists.txt b/subsys/usb/host/CMakeLists.txt index f48f5b8c71483..5cab660403286 100644 --- a/subsys/usb/host/CMakeLists.txt +++ b/subsys/usb/host/CMakeLists.txt @@ -5,9 +5,11 @@ zephyr_library() zephyr_library_include_directories(${CMAKE_CURRENT_SOURCE_DIR}) zephyr_library_sources( + usbh_api.c usbh_ch9.c + usbh_class.c usbh_core.c - usbh_api.c + usbh_desc.c usbh_device.c ) @@ -16,6 +18,11 @@ zephyr_library_sources_ifdef( usbh_shell.c ) +zephyr_library_sources_ifdef( + CONFIG_USBH_CDC_ECM_CLASS + class/usbh_cdc_ecm.c +) + zephyr_library_sources_ifdef( CONFIG_USBIP usbip.c diff --git a/subsys/usb/host/Kconfig b/subsys/usb/host/Kconfig index 8da3c95ebf7be..f77d0835be841 100644 --- a/subsys/usb/host/Kconfig +++ b/subsys/usb/host/Kconfig @@ -54,5 +54,6 @@ config USBH_MAX_UHC_MSG Maximum number of USB host controller events that can be queued. rsource "Kconfig.usbip" +rsource "class/Kconfig" endif # USB_HOST_STACK diff --git a/subsys/usb/host/class/Kconfig b/subsys/usb/host/class/Kconfig new file mode 100644 index 0000000000000..c3cd8de92805e --- /dev/null +++ b/subsys/usb/host/class/Kconfig @@ -0,0 +1,7 @@ +# +# Copyright (c) 2025 Linumiz GmbH +# +# SPDX-License-Identifier: Apache-2.0 +# + +rsource "Kconfig.cdc_ecm_host" diff --git a/subsys/usb/host/class/Kconfig.cdc_ecm_host b/subsys/usb/host/class/Kconfig.cdc_ecm_host new file mode 100644 index 0000000000000..388dd579537e4 --- /dev/null +++ b/subsys/usb/host/class/Kconfig.cdc_ecm_host @@ -0,0 +1,20 @@ +# Copyright (c) 2025 Linumiz GmbH +# SPDX-License-Identifier: Apache-2.0 + +config USBH_CDC_ECM_CLASS + bool "USB Host CDC ECM Class implementation" + default y + depends on NET_L2_ETHERNET + depends on DT_HAS_ZEPHYR_USBH_CDC_ECM_ENABLED + help + USB Host CDC Ethernet Control Model (ECM) implementation. + Supports communication with CDC ECM-compliant USB Ethernet devices. + +if USBH_CDC_ECM_CLASS + +module = USBH_CDC_ECM +module-str = "usbh cdc_ecm" +default-count = 1 +source "subsys/logging/Kconfig.template.log_config" + +endif # USBH_CDC_ECM_CLASS diff --git a/subsys/usb/host/class/usbh_cdc_ecm.c b/subsys/usb/host/class/usbh_cdc_ecm.c new file mode 100644 index 0000000000000..8ea8a17561129 --- /dev/null +++ b/subsys/usb/host/class/usbh_cdc_ecm.c @@ -0,0 +1,767 @@ +/* + * Copyright (c) 2025 Linumiz GmbH + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#define DT_DRV_COMPAT zephyr_usbh_cdc_ecm + +#include +#include + +#include +#include + +#include "usbh_class.h" +#include "usbh_device.h" +#include "usbh_desc.h" +#include "usbh_ch9.h" + +#include "usb_cdc_ecm.h" + +LOG_MODULE_REGISTER(usbh_cdc_ecm, CONFIG_USBH_CDC_ECM_LOG_LEVEL); + +/* CDC ECM constants */ +#define CDC_ECM_ETH_MAX_FRAME_SIZE 1514 +#define CDC_ECM_REQUEST_TIMEOUT_MS 5000 + +#define USB_DESC_INTERFACE_MASK (1U << USB_DESC_INTERFACE) +#define USB_DESC_CS_INTERFACE_MASK (1ULL << USB_DESC_CS_INTERFACE) + +#define CDC_ECM_ETHERNET_PACKET_FILTER_ALL 0x000F + +static int cdc_ecm_start_rx(struct usbh_class_data *c_data); +static int cdc_ecm_start_int(struct usbh_class_data *c_data); + +enum cdc_ecm_state { + CDC_ECM_DISCONNECTED, + CDC_ECM_CONNECTED, + CDC_ECM_CONFIGURED, + CDC_ECM_SUSPENDED, +}; + +struct usbh_cdc_ecm_data { + struct usbh_class_data *c_data; + + struct net_if *iface; + uint8_t mac_addr[6]; + enum cdc_ecm_state state; + + struct usb_device *udev; + uint16_t bulk_mps; + uint16_t int_mps; + uint8_t ctrl_iface; + uint8_t data_iface; + uint8_t bulk_in_ep; + uint8_t bulk_out_ep; + uint8_t int_in_ep; + + struct k_mutex tx_mutex; + struct k_sem tx_comp_sem; + struct net_stats_eth stats; +}; + +static void cleanup_xfer(struct usb_device *udev, struct uhc_transfer *xfer) +{ + if (xfer->buf) { + usbh_xfer_buf_free(udev, xfer->buf); + } + + usbh_xfer_free(udev, xfer); +} + +static int submit_xfer(struct usbh_cdc_ecm_data *priv, uint8_t ep, + usbh_udev_cb_t cb, size_t buf_size, struct net_pkt *pkt) +{ + struct uhc_transfer *xfer; + struct net_buf *buf; + int ret; + + xfer = usbh_xfer_alloc(priv->udev, ep, cb, priv->c_data); + if (!xfer) { + return -ENOMEM; + } + + buf = usbh_xfer_buf_alloc(priv->udev, buf_size); + if (!buf) { + usbh_xfer_free(priv->udev, xfer); + return -ENOMEM; + } + + ret = usbh_xfer_buf_add(priv->udev, xfer, buf); + if (ret < 0) { + usbh_xfer_buf_free(priv->udev, buf); + cleanup_xfer(priv->udev, xfer); + return ret; + } + + /* Read from pkt if provided and buf_size > 0 */ + if (pkt && buf_size > 0) { + if (net_pkt_read(pkt, buf->data, buf_size) != 0) { + cleanup_xfer(priv->udev, xfer); + return -EIO; + } + net_buf_add(buf, buf_size); + } else if (buf_size == 0) { + /* ZLP: Set len to 0 */ + net_buf_add(buf, 0); + } + + ret = usbh_xfer_enqueue(priv->udev, xfer); + if (ret < 0) { + cleanup_xfer(priv->udev, xfer); + return ret; + } + + return 0; +} + +static int cdc_ecm_int_in_cb(struct usb_device *udev, struct uhc_transfer *xfer) +{ + struct usbh_class_data *c_data = xfer->priv; + struct usbh_cdc_ecm_data *priv = c_data->priv; + struct cdc_ecm_notification *notif; + bool connected; + int ret = 0; + + if (xfer->err) { + LOG_ERR("Interrupt transfer error: %d", xfer->err); + } else if (xfer->buf && xfer->buf->len >= 8) { + /* Handle ECM notifications */ + notif = (struct cdc_ecm_notification *)xfer->buf->data; + + if (notif->bNotificationType == USB_CDC_NETWORK_CONNECTION) { + connected = !!sys_le16_to_cpu(notif->wValue); + LOG_DBG("Network connection: %s", connected ? "connected" : "disconnected"); + if (connected) { + net_if_carrier_on(priv->iface); + } else { + net_if_carrier_off(priv->iface); + } + } + } + + cleanup_xfer(priv->udev, xfer); + + if (priv->state == CDC_ECM_CONFIGURED) { + ret = cdc_ecm_start_int(c_data); + } + + if (ret) { + LOG_ERR("Failed to resubmit intr in xfer : %d", ret); + } + + return ret; +} + +static int cdc_ecm_bulk_in_cb(struct usb_device *udev, struct uhc_transfer *xfer) +{ + struct usbh_class_data *c_data = xfer->priv; + struct usbh_cdc_ecm_data *priv = c_data->priv; + struct net_pkt *pkt; + int ret = 0; + + if (xfer->err) { + LOG_ERR("Bulk in transfer error: %d", xfer->err); + priv->stats.errors.rx++; + } + + if (xfer->buf && xfer->buf->len > 0) { + /* Handle potential ZLP delimiter (trailing null byte) */ + if (xfer->buf->data[xfer->buf->len - 1] == 0U) { + /* Last byte is null */ + if (ecm_eth_size(xfer->buf->data, xfer->buf->len) == (xfer->buf->len - 1)) { + /* Last byte appended as delimiter, drop it */ + net_buf_remove_u8(xfer->buf); + LOG_DBG("Stripped ZLP delimiter from RX frame"); + } + } + + pkt = net_pkt_rx_alloc_with_buffer(priv->iface, xfer->buf->len, + AF_UNSPEC, 0, K_NO_WAIT); + if (pkt) { + if (net_pkt_write(pkt, xfer->buf->data, xfer->buf->len) == 0) { + priv->stats.bytes.received += xfer->buf->len; + priv->stats.pkts.rx++; + + if (net_recv_data(priv->iface, pkt) < 0) { + net_pkt_unref(pkt); + priv->stats.errors.rx++; + } + } else { + net_pkt_unref(pkt); + priv->stats.errors.rx++; + } + } else { + LOG_ERR("No net_pkt available for received data"); + priv->stats.errors.rx++; + } + } + + cleanup_xfer(priv->udev, xfer); + + if (priv->state == CDC_ECM_CONFIGURED) { + ret = cdc_ecm_start_rx(c_data); + } + + if (ret) { + LOG_ERR("Failed to resubmit bulk in xfer : %d", ret); + } + + return ret; +} + +static int cdc_ecm_start_rx(struct usbh_class_data *c_data) +{ + struct usbh_cdc_ecm_data *priv = c_data->priv; + + return submit_xfer(priv, priv->bulk_in_ep, cdc_ecm_bulk_in_cb, priv->bulk_mps, NULL); +} + +static int cdc_ecm_start_int(struct usbh_class_data *c_data) +{ + struct usbh_cdc_ecm_data *priv = c_data->priv; + + return submit_xfer(priv, priv->int_in_ep, cdc_ecm_int_in_cb, priv->int_mps, NULL); +} + +static int cdc_ecm_send_cmd(struct usbh_class_data *c_data, + uint8_t request, uint16_t value, + uint16_t index, void *data, size_t len) +{ + struct usbh_cdc_ecm_data *priv = c_data->priv; + struct usb_setup_packet *setup; + struct net_buf *buf = NULL; + struct uhc_transfer *xfer; + int ret; + + xfer = usbh_xfer_alloc(priv->udev, 0, NULL, NULL); + if (!xfer) { + LOG_ERR("Failed to allocate control transfer"); + return -ENOMEM; + } + + setup = (struct usb_setup_packet *)xfer->setup_pkt; + setup->bmRequestType = USB_REQTYPE_TYPE_CLASS | USB_REQTYPE_RECIPIENT_INTERFACE; + setup->bRequest = request; + setup->wValue = sys_cpu_to_le16(value); + setup->wIndex = sys_cpu_to_le16(index); + setup->wLength = sys_cpu_to_le16(len); + + if (len > 0 && data) { + buf = usbh_xfer_buf_alloc(priv->udev, len); + if (!buf) { + ret = -ENOMEM; + goto error; + } + + net_buf_add_mem(buf, data, len); + ret = usbh_xfer_buf_add(priv->udev, xfer, buf); + if (ret) { + goto error; + } + } + + ret = usbh_xfer_enqueue(priv->udev, xfer); + if (ret) { + LOG_ERR("Failed to enqueue control transfer: %d", ret); + goto error; + } + + /* Wait for transfer completion */ + k_sleep(K_MSEC(CDC_ECM_REQUEST_TIMEOUT_MS)); + + return 0; + +error: + if (buf) { + usbh_xfer_buf_free(priv->udev, buf); + } + usbh_xfer_free(priv->udev, xfer); + + return ret; +} + +static int cdc_ecm_set_ethernet_packet_filter(struct usbh_class_data *c_data, + uint16_t filter) +{ + struct usbh_cdc_ecm_data *priv = c_data->priv; + + return cdc_ecm_send_cmd(c_data, SET_ETHERNET_PACKET_FILTER, + filter, priv->ctrl_iface, NULL, 0); +} + +static int cdc_ecm_bulk_out_cb(struct usb_device *udev, struct uhc_transfer *xfer) +{ + struct usbh_class_data *c_data = xfer->priv; + struct usbh_cdc_ecm_data *priv = c_data->priv; + + if (xfer->err) { + LOG_ERR("Bulk out transfer error: %d", xfer->err); + priv->stats.errors.tx++; + } else { + priv->stats.bytes.sent += xfer->buf->len; + priv->stats.pkts.tx++; + } + + cleanup_xfer(priv->udev, xfer); + + k_sem_give(&priv->tx_comp_sem); + + return 0; +} + +static int cdc_ecm_parse_descriptors(struct usbh_class_data *c_data) +{ + struct usbh_cdc_ecm_data *priv = c_data->priv; + + const uint8_t *desc_beg = usbh_desc_get_cfg_beg(priv->udev); + const uint8_t *desc_end = usbh_desc_get_cfg_end(priv->udev); + + const struct usb_desc_header *desc; + const struct usb_if_descriptor *if_desc; + const struct usb_ep_descriptor *ep_desc; + + const struct cdc_union_descriptor *union_desc = NULL; + const struct cdc_ecm_descriptor *ecm_desc = NULL; + + uint8_t data_iface = 0; + bool found_ecm = false; + + /* Find CDC ECM control interface */ + desc = usbh_desc_get_by_type(desc_beg, desc_end, USB_DESC_INTERFACE_MASK); + while (desc != NULL) { + if_desc = (const struct usb_if_descriptor *)desc; + + if (if_desc->bInterfaceClass == USB_BCC_CDC_CONTROL && + if_desc->bInterfaceSubClass == ECM_SUBCLASS) { + priv->ctrl_iface = if_desc->bInterfaceNumber; + found_ecm = true; + + /* Parse control interface for functional descriptors AND endpoints */ + const struct usb_desc_header *ctrl_desc = desc; + + /* Move to first descriptor after interface */ + ctrl_desc = usbh_desc_get_next(ctrl_desc, desc_end); + + /* Parse all descriptors until next interface */ + while (ctrl_desc != NULL && + ctrl_desc->bDescriptorType != USB_DESC_INTERFACE) { + + if (ctrl_desc->bDescriptorType == USB_DESC_CS_INTERFACE) { + uint8_t subtype = ((const uint8_t *)ctrl_desc)[2]; + + if (subtype == UNION_FUNC_DESC) { + union_desc = (const struct cdc_union_descriptor *)ctrl_desc; + data_iface = union_desc->bSubordinateInterface0; + } else if (subtype == ETHERNET_FUNC_DESC) { + ecm_desc = (const struct cdc_ecm_descriptor *)ctrl_desc; + } + } + /* Also look for INTERRUPT endpoint in control interface */ + else if (ctrl_desc->bDescriptorType == USB_DESC_ENDPOINT) { + ep_desc = (const struct usb_ep_descriptor *)ctrl_desc; + if (ep_desc->bmAttributes == USB_EP_TYPE_INTERRUPT) { + priv->int_in_ep = ep_desc->bEndpointAddress; + priv->int_mps = sys_le16_to_cpu(ep_desc->wMaxPacketSize); + LOG_DBG("Found interrupt endpoint: 0x%02x", + priv->int_in_ep); + } + } + + ctrl_desc = usbh_desc_get_next(ctrl_desc, desc_end); + } + break; + } + + desc = usbh_desc_get_next(desc, desc_end); + desc = usbh_desc_get_by_type(desc, desc_end, USB_DESC_INTERFACE_MASK); + } + + if (!found_ecm) { + LOG_ERR("CDC ECM control interface not found"); + return -ENODEV; + } + + if (!union_desc || !ecm_desc) { + LOG_ERR("CDC ECM functional descriptors not found"); + return -ENODEV; + } + + priv->data_iface = data_iface; + + /* Find data interface with endpoints (alternate setting 1) */ + desc = usbh_desc_get_by_iface(desc_beg, desc_end, data_iface); + while (desc != NULL) { + if_desc = (const struct usb_if_descriptor *)desc; + + /* Look for alternate setting with endpoints */ + if (if_desc->bAlternateSetting >= 1 && if_desc->bNumEndpoints >= 2) { + /* Found data interface with endpoints */ + break; + } + desc = usbh_desc_get_next(desc, desc_end); + } + + if (!desc || if_desc->bNumEndpoints < 2) { + LOG_ERR("CDC ECM data interface with endpoints not found"); + return -ENODEV; + } + + /* Parse endpoints in data interface */ + desc = usbh_desc_get_next(desc, desc_end); + while (desc != NULL && desc->bDescriptorType == USB_DESC_ENDPOINT) { + ep_desc = (const struct usb_ep_descriptor *)desc; + + if (ep_desc->bmAttributes == USB_EP_TYPE_BULK) { + if (USB_EP_DIR_IS_IN(ep_desc->bEndpointAddress)) { + /* BULK IN endpoint - for RX (Device -> Host) */ + priv->bulk_in_ep = ep_desc->bEndpointAddress; + priv->bulk_mps = sys_le16_to_cpu(ep_desc->wMaxPacketSize); + } else { + /* BULK OUT endpoint - for TX (Host -> Device) */ + priv->bulk_out_ep = ep_desc->bEndpointAddress; + LOG_DBG("Found BULK OUT endpoint: 0x%02x", + priv->bulk_out_ep); + } + } + + desc = usbh_desc_get_next(desc, desc_end); + } + + if (!priv->bulk_in_ep || !priv->bulk_out_ep) { + LOG_ERR("CDC ECM bulk endpoints not found"); + return -ENODEV; + } + + if (!priv->int_in_ep) { + LOG_ERR("CDC ECM interrupt endpoint not found"); + } + + + LOG_DBG("CDC ECM parsed: ctrl_iface=%u, data_iface=%u, " + "bulk_in=0x%02x (RX), bulk_out=0x%02x (TX), " + "int_in=0x%02x, bulk_mps=%d int_in_mps=%d", + priv->ctrl_iface, priv->data_iface, priv->bulk_in_ep, + priv->bulk_out_ep, priv->int_in_ep, priv->bulk_mps, priv->int_mps); + + return 0; +} + +static int usbh_cdc_ecm_init(struct usbh_class_data *const c_data, + struct usbh_context *const uhs_ctx) +{ + return 0; +} + +static int usbh_cdc_ecm_completion_cb(struct usbh_class_data *const c_data, + struct uhc_transfer *const xfer) +{ + return 0; +} + +static int usbh_cdc_ecm_probe(struct usbh_class_data *const c_data, + struct usb_device *const udev, + const uint8_t iface) +{ + struct usbh_cdc_ecm_data *priv = c_data->priv; + const uint8_t *desc_beg = usbh_desc_get_cfg_beg(udev); + const uint8_t *desc_end = usbh_desc_get_cfg_end(udev); + const struct usb_desc_header *desc; + const struct usb_if_descriptor *if_desc = NULL; + int ret; + + /* The host stack might pass us the IAD first, we need to find the actual control interface */ + desc = usbh_desc_get_by_iface(desc_beg, desc_end, iface); + + if (desc == NULL) { + LOG_ERR("No descriptor found for interface %u", iface); + return -ENODEV; + } + + LOG_DBG("Descriptor type: %u", desc->bDescriptorType); + + /* Handle Interface Association Descriptor case */ + if (desc->bDescriptorType == USB_DESC_INTERFACE_ASSOC) { + struct usb_association_descriptor *iad = (struct usb_association_descriptor *)desc; + + LOG_DBG("IAD Found: first_iface=%u, count=%u, class=%u, subclass=%u, protocol=%u", + iad->bFirstInterface, iad->bInterfaceCount, + iad->bFunctionClass, iad->bFunctionSubClass, iad->bFunctionProtocol); + + /* Now find the actual interface descriptor for the control interface */ + desc = usbh_desc_get_by_type(desc_beg, desc_end, BIT(USB_DESC_INTERFACE)); + while (desc != NULL) { + if_desc = (const struct usb_if_descriptor *)desc; + + /* Look for the control interface */ + if (if_desc->bInterfaceNumber == iad->bFirstInterface && + if_desc->bAlternateSetting == 0) { + LOG_DBG("Found control interface: iface=%u, alt=%u, class=%u, subclass=%u", + if_desc->bInterfaceNumber, if_desc->bAlternateSetting, + if_desc->bInterfaceClass, if_desc->bInterfaceSubClass); + break; + } + desc = usbh_desc_get_next(desc, desc_end); + desc = usbh_desc_get_by_type(desc, desc_end, BIT(USB_DESC_INTERFACE)); + } + + if (if_desc == NULL) { + LOG_ERR("Control interface %u not found", iad->bFirstInterface); + return -ENODEV; + } + } + /* Handle regular Interface Descriptor case */ + else if (desc->bDescriptorType == USB_DESC_INTERFACE) { + if_desc = (const struct usb_if_descriptor *)desc; + + LOG_DBG("Interface descriptor: iface=%u, alt=%u, class=%u, subclass=%u", + if_desc->bInterfaceNumber, if_desc->bAlternateSetting, + if_desc->bInterfaceClass, if_desc->bInterfaceSubClass); + } + else { + LOG_ERR("Unexpected descriptor type: %u", desc->bDescriptorType); + return -ENODEV; + } + + /* Verify this is a CDC ECM control interface */ + if (if_desc->bInterfaceClass != USB_BCC_CDC_CONTROL || + if_desc->bInterfaceSubClass != ECM_SUBCLASS) { + LOG_ERR("Not a CDC ECM control interface: class=%u, subclass=%u", + if_desc->bInterfaceClass, if_desc->bInterfaceSubClass); + return -ENOTSUP; + } + + LOG_INF("Found CDC ECM device at interface %u (control)", if_desc->bInterfaceNumber); + + priv->udev = udev; + priv->c_data = c_data; + priv->state = CDC_ECM_CONNECTED; + + /* Parse descriptors to find endpoints */ + ret = cdc_ecm_parse_descriptors(c_data); + if (ret) { + LOG_ERR("Failed to parse CDC ECM descriptors"); + return ret; + } + + /* Set the data interface alternate setting */ + ret = usbh_device_interface_set(udev, priv->data_iface, 1, false); + if (ret) { + LOG_ERR("Failed to set data interface alternate setting"); + return ret; + } + + /* Set Ethernet packet filter to receive all packets */ + ret = cdc_ecm_set_ethernet_packet_filter(c_data, CDC_ECM_ETHERNET_PACKET_FILTER_ALL); + if (ret) { + LOG_ERR("Failed to set packet filter: %d", ret); + return ret; + } + + priv->state = CDC_ECM_CONFIGURED; + + /* Start interrupt endpoint for notifications */ + ret = cdc_ecm_start_int(c_data); + if (ret) { + LOG_ERR("Failed to start interrupt endpoint: %d", ret); + return ret; + } + + /* Start receiving data */ + ret = cdc_ecm_start_rx(c_data); + if (ret) { + LOG_ERR("Failed to start RX transfers: %d", ret); + return ret; + } + + net_if_carrier_on(priv->iface); + + return 0; +} + +static int usbh_cdc_ecm_removed(struct usbh_class_data *const c_data) +{ + return 0; +} + +static int usbh_cdc_ecm_suspended(struct usbh_class_data *const c_data) +{ + return 0; +} + +static int usbh_cdc_ecm_resumed(struct usbh_class_data *const c_data) +{ + return 0; +} + +static void cdc_ecm_host_iface_init(struct net_if *iface) +{ + const struct device *dev = net_if_get_device(iface); + struct usbh_cdc_ecm_data *priv = dev->data; + + priv->iface = iface; + + net_if_set_link_addr(iface, priv->mac_addr, sizeof(priv->mac_addr), NET_LINK_ETHERNET); + net_if_carrier_off(iface); + + LOG_DBG("CDC ECM network interface initialized"); +} + +#if defined(CONFIG_NET_STATISTICS_ETHERNET) +static struct net_stats_eth *cdc_ecm_host_get_stats(struct device *dev) +{ + struct usbh_cdc_ecm_data *priv = (struct usbh_cdc_ecm_data *)dev->data; + + return &priv->stats; +} +#endif + +static int cdc_ecm_host_iface_start(const struct device *dev) +{ + struct usbh_cdc_ecm_data *priv = (struct usbh_cdc_ecm_data *)dev->data; + + if (priv->state == CDC_ECM_CONFIGURED) { + net_if_carrier_on(priv->iface); + } + + return 0; +} + +static int cdc_ecm_host_iface_stop(const struct device *dev) +{ + struct usbh_cdc_ecm_data *priv = (struct usbh_cdc_ecm_data *)dev->data; + net_if_carrier_off(priv->iface); + + return 0; +} + +static enum ethernet_hw_caps cdc_ecm_host_get_capabilities(const struct device *dev) +{ + ARG_UNUSED(dev); + + return ETHERNET_LINK_10BASE; +} + +static int cdc_ecm_host_set_config(const struct device *dev, + enum ethernet_config_type type, + const struct ethernet_config *config) +{ + struct usbh_cdc_ecm_data *priv = (struct usbh_cdc_ecm_data *)dev->data; + + if (type == ETHERNET_CONFIG_TYPE_MAC_ADDRESS) { + memcpy(priv->mac_addr, config->mac_address.addr, sizeof(priv->mac_addr)); + return 0; + } + + return -ENOTSUP; +} + +static int cdc_ecm_host_send(const struct device *dev, struct net_pkt *pkt) +{ + struct usbh_cdc_ecm_data *priv = (struct usbh_cdc_ecm_data *)dev->data; + size_t len, remain, chunk_size; + bool need_zlp = false; + int ret = 0; + + + len = net_pkt_get_len(pkt); + if (len > CDC_ECM_ETH_MAX_FRAME_SIZE) { + return -EMSGSIZE; + } + + if (priv->state != CDC_ECM_CONFIGURED) { + return -ENETDOWN; + } + + if (k_mutex_lock(&priv->tx_mutex, K_MSEC(1000)) != 0) { + return -EBUSY; + } + + remain = len; + need_zlp = (len % priv->bulk_mps == 0); + + while(remain > 0) { + chunk_size = MIN(remain, (size_t)priv->bulk_mps); + + ret = submit_xfer(priv, priv->bulk_out_ep, cdc_ecm_bulk_out_cb, chunk_size, pkt); + + if (ret < 0) { + goto error; + } + + if (k_sem_take(&priv->tx_comp_sem, K_MSEC(5000)) != 0) { + ret = -ETIMEDOUT; + goto error; + } + + remain -= chunk_size; + } + + if (need_zlp) { + ret = submit_xfer(priv, priv->bulk_out_ep, cdc_ecm_bulk_out_cb, 0, NULL); + + if (ret < 0) { + goto error; + } + + if (k_sem_take(&priv->tx_comp_sem, K_MSEC(5000)) != 0) { + ret = -ETIMEDOUT; + } + } +error: + k_mutex_unlock(&priv->tx_mutex); + + return ret; +} + +static struct usbh_class_api usbh_cdc_ecm_class_api = { + .init = usbh_cdc_ecm_init, + .completion_cb = usbh_cdc_ecm_completion_cb, + .probe = usbh_cdc_ecm_probe, + .removed = usbh_cdc_ecm_removed, + .suspended = usbh_cdc_ecm_suspended, + .resumed = usbh_cdc_ecm_resumed, +}; + +static const struct ethernet_api cdc_ecm_eth_api = { + .iface_api.init = cdc_ecm_host_iface_init, +#if defined(CONFIG_NET_STATISTICS_ETHERNET) + .get_stats = cdc_ecm_host_get_stats, +#endif + .start = cdc_ecm_host_iface_start, + .stop = cdc_ecm_host_iface_stop, + .get_capabilities = cdc_ecm_host_get_capabilities, + .set_config = cdc_ecm_host_set_config, + .send = cdc_ecm_host_send, +}; + +static struct usbh_class_filter cdc_ecm_filters[] = { + { + .flags = USBH_CLASS_MATCH_CLASS | USBH_CLASS_MATCH_SUB, + .class = USB_BCC_CDC_CONTROL, + .sub = ECM_SUBCLASS, + } +}; + +#define USBH_CDC_ECM_DT_DEVICE_DEFINE(index) \ + \ + static struct usbh_cdc_ecm_data cdc_ecm_data_##index = { \ + .state = CDC_ECM_DISCONNECTED, \ + .mac_addr = DT_INST_PROP_OR(index, local_mac_address, {0}), \ + .tx_mutex = Z_MUTEX_INITIALIZER(cdc_ecm_data_##index.tx_mutex), \ + .tx_comp_sem = Z_SEM_INITIALIZER(cdc_ecm_data_##index.tx_comp_sem, 0, 1), \ + }; \ + \ + NET_DEVICE_DT_INST_DEFINE(index, NULL, NULL, \ + &cdc_ecm_data_##index, NULL, \ + CONFIG_ETH_INIT_PRIORITY, \ + &cdc_ecm_eth_api, \ + ETHERNET_L2, \ + NET_L2_GET_CTX_TYPE(ETHERNET_L2), \ + NET_ETH_MTU); \ + \ + USBH_DEFINE_CLASS(cdc_ecm_##index, &usbh_cdc_ecm_class_api, &cdc_ecm_data_##index, \ + cdc_ecm_filters, ARRAY_SIZE(cdc_ecm_filters)); + +DT_INST_FOREACH_STATUS_OKAY(USBH_CDC_ECM_DT_DEVICE_DEFINE); diff --git a/subsys/usb/host/usbh_class.c b/subsys/usb/host/usbh_class.c new file mode 100644 index 0000000000000..fffef794f678c --- /dev/null +++ b/subsys/usb/host/usbh_class.c @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * Copyright 2025 NXP + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include "usbh_class.h" +#include "usbh_class_api.h" +#include "usbh_desc.h" + +LOG_MODULE_REGISTER(usbh_class, CONFIG_USBH_LOG_LEVEL); + +int usbh_class_init_all(struct usbh_context *const uhs_ctx) +{ + int ret; + + STRUCT_SECTION_FOREACH(usbh_class_node, c_node) { + ret = usbh_class_init(c_node->c_data, uhs_ctx); + if (ret != 0) { + LOG_ERR("Failed to initialize all registered class instances"); + return ret; + } + } + + return 0; +} + +int usbh_class_remove_all(const struct usb_device *const udev) +{ + int ret; + + STRUCT_SECTION_FOREACH(usbh_class_node, c_node) { + if (c_node->c_data->udev == udev) { + ret = usbh_class_removed(c_node->c_data); + if (ret != 0) { + LOG_ERR("Failed to handle device removal for each classes"); + return ret; + } + + /* The class instance is now free to bind to a new device */ + c_node->c_data->udev = NULL; + } + } + + return 0; +} + +static int usbh_class_probe_each_iface(struct usbh_class_node *const c_node, + struct usb_device *const udev) +{ + const void *const desc_beg = usbh_desc_get_cfg_beg(udev); + const void *const desc_end = usbh_desc_get_cfg_end(udev); + const struct usb_desc_header *desc = desc_beg; + struct usbh_class_data *const c_data = c_node->c_data; + struct usbh_class_filter info = { + .vid = udev->dev_desc.idVendor, + .pid = udev->dev_desc.idProduct, + }; + uint8_t iface; + int ret; + + while (true) { + desc = usbh_desc_get_next_function(desc, desc_end); + if (desc == NULL) { + break; + } + + if (desc->bDescriptorType == USB_DESC_INTERFACE_ASSOC) { + iface = ((struct usb_association_descriptor *)desc)->bFirstInterface; + } else if (desc->bDescriptorType == USB_DESC_INTERFACE) { + iface = ((struct usb_if_descriptor *)desc)->bInterfaceNumber; + } else { + __ASSERT(false, "bad implementation of usbh_desc_get_next_function()"); + return -EINVAL; + } + + ret = usbh_desc_get_iface_info(desc, &info, &iface); + if (ret < 0) { + LOG_ERR("Failed to collect class codes for matching interface %u", iface); + return ret; + } + + if (!usbh_class_is_matching(c_node->filters, c_node->num_filters, &info)) { + LOG_DBG("Class %s not matching interface %u", c_data->name, iface); + continue; + } + + ret = usbh_class_probe(c_data, udev, iface); + if (ret == -ENOTSUP) { + LOG_DBG("Class %s not supporting this device, skipping", c_data->name); + continue; + } + if (ret != 0) { + LOG_ERR("Error while probing the class %s for interface %u", + c_data->name, iface); + return ret; + } + + LOG_INF("Class %s matches this device's interface %u", c_data->name, iface); + c_data->udev = udev; + c_data->iface = iface; + + return 0; + } + + return -ENOTSUP; +} + +int usbh_class_probe_all(struct usb_device *const udev) +{ + bool matching = false; + int ret; + + STRUCT_SECTION_FOREACH(usbh_class_node, c_node) { + struct usbh_class_data *const c_data = c_node->c_data; + + /* If the class is not already having a device */ + if (c_data->udev == NULL) { + ret = usbh_class_probe_each_iface(c_node, udev); + if (ret == -ENOTSUP) { + LOG_DBG("USB device not matching %s", c_node->c_data->name); + continue; + } + if (ret != 0) { + return ret; + } + matching = true; + } + } + + if (!matching) { + LOG_WRN("Could not find any class for this device"); + return -ENOTSUP; + } + + return 0; +} + +bool usbh_class_is_matching(struct usbh_class_filter *const filters, size_t num_filters, + struct usbh_class_filter *const info) +{ + /* Make empty filter set match everything (use class_api->probe() only) */ + if (num_filters == 0) { + return true; + } + + /* Try to find a rule that matches completely */ + for (int i = 0; i < num_filters; i++) { + const struct usbh_class_filter *filt = &filters[i]; + + if ((filt->flags & USBH_CLASS_MATCH_VID) && + info->vid != filt->vid) { + continue; + } + + if ((filt->flags & USBH_CLASS_MATCH_PID) && + info->pid != filt->pid) { + continue; + } + + if (filt->flags & USBH_CLASS_MATCH_CLASS && + info->class != filt->class) { + continue; + } + + if ((filt->flags & USBH_CLASS_MATCH_SUB) && + info->sub != filt->sub) { + continue; + } + + if ((filt->flags & USBH_CLASS_MATCH_PROTO) && + info->proto != filt->proto) { + continue; + } + + /* All the selected filters did match */ + return true; + } + + /* At the end of the filter table and still no match */ + return false; +} diff --git a/subsys/usb/host/usbh_class.h b/subsys/usb/host/usbh_class.h new file mode 100644 index 0000000000000..5dcd0b330023c --- /dev/null +++ b/subsys/usb/host/usbh_class.h @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * Copyright 2025 NXP + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_INCLUDE_USBD_CLASS_H +#define ZEPHYR_INCLUDE_USBD_CLASS_H + +#include + +/** Match a device's vendor ID */ +#define USBH_CLASS_MATCH_VID BIT(1) + +/** Match a device's product ID */ +#define USBH_CLASS_MATCH_PID BIT(2) + +/** Match a class code */ +#define USBH_CLASS_MATCH_CLASS BIT(3) + +/** Match a subclass code */ +#define USBH_CLASS_MATCH_SUB BIT(4) + +/** Match a protocol code */ +#define USBH_CLASS_MATCH_PROTO BIT(5) + +/** Match a code triple */ +#define USBH_CLASS_MATCH_CODE_TRIPLE \ + (USBH_CLASS_MATCH_CLASS | USBH_CLASS_MATCH_SUB | USBH_CLASS_MATCH_PROTO) + +/** + * @brief Match an USB host class (a driver) against a device descriptor. + * + * @param[in] filters Array of filter rules to match + * @param[in] num_filters Number of rules in the array. + * @param[in] device_info Device information filled by this function + * @retval true if the USB Device descriptor matches at least one rule. + */ +bool usbh_class_is_matching(struct usbh_class_filter *const filters, size_t num_filters, + struct usbh_class_filter *const device_info); + +/** + * @brief Initialize every class instantiated on the system + * + * @param[in] uhs_ctx USB Host context to pass to the class. + * @retval 0 on success or negative error code on failure + */ +int usbh_class_init_all(struct usbh_context *const uhs_ctx); + +/** + * @brief Probe all classes to against a newly connected USB device. + * + * @param[in] udev USB device to probe. + * @retval 0 on success or negative error code on failure + */ +int usbh_class_probe_all(struct usb_device *const udev); + +/** + * @brief Call the device removal handler for every class configured with it + * + * @param[in] udev USB device that got removed. + * @retval 0 on success or negative error code on failure + */ +int usbh_class_remove_all(const struct usb_device *const udev); + +#endif /* ZEPHYR_INCLUDE_USBD_CLASS_H */ diff --git a/subsys/usb/host/usbh_class_api.h b/subsys/usb/host/usbh_class_api.h new file mode 100644 index 0000000000000..b2ec156ca2dd9 --- /dev/null +++ b/subsys/usb/host/usbh_class_api.h @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file + * @brief USB host stack class instances API + * + * This file contains the USB host stack class instances API. + */ + +#ifndef ZEPHYR_INCLUDE_USBH_CLASS_API_H +#define ZEPHYR_INCLUDE_USBH_CLASS_API_H + +#include + +/** + * @brief Initialization of the class implementation + * + * This is called for each instance during the initialization phase, + * for every registered class. + * It can be used to initialize underlying systems. + * + * @param[in] c_data Pointer to USB host class data + * @param[in] uhs_ctx USB host context to assign to this class + * + * @return 0 on success, negative error code on failure. + */ + +static inline int usbh_class_init(struct usbh_class_data *const c_data, + struct usbh_context *const uhs_ctx) +{ + const struct usbh_class_api *api = c_data->api; + + if (api->init != NULL) { + return api->init(c_data, uhs_ctx); + } + + return -ENOTSUP; +} + +/** + * @brief Request completion event handler + * + * Called upon completion of a request made by the host to this class. + * + * @param[in] c_data Pointer to USB host class data + * @param[in] udev USB device connected + * @param[in] xfer Completed transfer + * + * @return 0 on success, negative error code on failure. + */ +static inline int usbh_class_completion_cb(struct usbh_class_data *const c_data, + struct uhc_transfer *const xfer) +{ + const struct usbh_class_api *api = c_data->api; + + if (api->completion_cb != NULL) { + return api->completion_cb(c_data, xfer); + } + + return -ENOTSUP; +} + +/** + * @brief Device initialization handler + * + * Called when a device is connected to the bus for every device. + * + * @param[in] c_data Pointer to USB host class data + * @param[in] udev USB device connected + * @param[in] iface The @c bInterfaceNumber or @c bFirstInterface of this class + * + * @return 0 on success, negative error code on failure. + * @return -ENOTSUP if the class is not matching + */ +static inline int usbh_class_probe(struct usbh_class_data *const c_data, + struct usb_device *const udev, + const uint8_t iface) +{ + const struct usbh_class_api *api = c_data->api; + + if (api->probe != NULL) { + return api->probe(c_data, udev, iface); + } + + return -ENOTSUP; +} + +/** + * @brief Device removed handler + * + * Called when the device is removed from the bus + * and it matches the class filters of this instance. + * + * @param[in] c_data Pointer to USB host class data + * @param[in] udev USB device connected + * + * @return 0 on success, negative error code on failure. + */ +static inline int usbh_class_removed(struct usbh_class_data *const c_data) +{ + const struct usbh_class_api *api = c_data->api; + + if (api->removed != NULL) { + return api->removed(c_data); + } + + return -ENOTSUP; +} + +/** + * @brief Bus suspended handler + * + * Called when the host has suspending the bus. + * It can be used to suspend underlying systems. + * + * @param[in] c_data Pointer to USB host class data + * @param[in] udev USB device connected + * + * @return 0 on success, negative error code on failure. + */ +static inline int usbh_class_suspended(struct usbh_class_data *const c_data) +{ + const struct usbh_class_api *api = c_data->api; + + if (api->suspended != NULL) { + return api->suspended(c_data); + } + + return -ENOTSUP; +} + +/** + * @brief Bus resumed handler + * + * Called when the host resumes the activity on a bus. + * It can be used to wake-up underlying systems. + * + * @param[in] c_data Pointer to USB host class data + * @param[in] udev USB device connected + * + * @return 0 on success, negative error code on failure. + */ +static inline int usbh_class_resumed(struct usbh_class_data *const c_data) +{ + const struct usbh_class_api *api = c_data->api; + + if (api->resumed != NULL) { + return api->resumed(c_data); + } + + return -ENOTSUP; +} + +#endif /* ZEPHYR_INCLUDE_USBD_CLASS_API_H */ diff --git a/subsys/usb/host/usbh_core.c b/subsys/usb/host/usbh_core.c index 1b654b5041133..2b10ae3eab2a2 100644 --- a/subsys/usb/host/usbh_core.c +++ b/subsys/usb/host/usbh_core.c @@ -10,9 +10,12 @@ #include #include #include +#include -#include "usbh_internal.h" +#include "usbh_class.h" +#include "usbh_class_api.h" #include "usbh_device.h" +#include "usbh_internal.h" #include LOG_MODULE_REGISTER(uhs, CONFIG_USBH_LOG_LEVEL); @@ -46,6 +49,7 @@ static int usbh_event_carrier(const struct device *dev, static void dev_connected_handler(struct usbh_context *const ctx, const struct uhc_event *const event) { + int ret; LOG_DBG("Device connected event"); if (ctx->root != NULL) { @@ -71,6 +75,11 @@ static void dev_connected_handler(struct usbh_context *const ctx, if (usbh_device_init(ctx->root)) { LOG_ERR("Failed to reset new USB device"); } + + ret = usbh_class_probe_all(ctx->root); + if (ret != 0) { + LOG_ERR("Failed to probe all classes for this new USB device"); + } } static void dev_removed_handler(struct usbh_context *const ctx) @@ -194,12 +203,10 @@ int usbh_init_device_intl(struct usbh_context *const uhs_ctx) sys_dlist_init(&uhs_ctx->udevs); - STRUCT_SECTION_FOREACH(usbh_class_data, cdata) { - /* - * For now, we have not implemented any class drivers, - * so just keep it as placeholder. - */ - break; + ret = usbh_class_init_all(uhs_ctx); + if (ret != 0) { + LOG_ERR("Failed to initialize all classes"); + return ret; } return 0; diff --git a/subsys/usb/host/usbh_data.ld b/subsys/usb/host/usbh_data.ld index 4363154f29474..bc53c86119914 100644 --- a/subsys/usb/host/usbh_data.ld +++ b/subsys/usb/host/usbh_data.ld @@ -1,4 +1,4 @@ #include ITERABLE_SECTION_RAM(usbh_context, Z_LINK_ITERABLE_SUBALIGN) -ITERABLE_SECTION_RAM(usbh_class_data, Z_LINK_ITERABLE_SUBALIGN) +ITERABLE_SECTION_RAM(usbh_class_node, Z_LINK_ITERABLE_SUBALIGN) diff --git a/subsys/usb/host/usbh_desc.c b/subsys/usb/host/usbh_desc.c new file mode 100644 index 0000000000000..6f22f2592ac07 --- /dev/null +++ b/subsys/usb/host/usbh_desc.c @@ -0,0 +1,179 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * Copyright NXP + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include "usbh_class.h" +#include "usbh_desc.h" + +const void *usbh_desc_get_next(const void *const desc_beg, const void *const desc_end) +{ + const struct usb_desc_header *const desc = desc_beg; + const struct usb_desc_header *next; + + if (desc_beg == NULL || desc_end == NULL) { + return NULL; + } + + next = (const struct usb_desc_header *)((uint8_t *)desc + desc->bLength); + + /* Validate the bLength field to make sure it does not point past the end */ + if ((uint8_t *)next >= (uint8_t *)desc_end || + (uint8_t *)next + next->bLength > (uint8_t *)desc_end || + desc->bLength == 0) { + return NULL; + } + + return next; +} + +const bool usbh_desc_size_is_valid(const void *const desc, size_t expected_size, + const void *const desc_end) +{ + return desc != NULL && desc_end != NULL && + (uint8_t *)desc + expected_size <= (uint8_t *)desc_end; +} + +const struct usb_desc_header *usbh_desc_get_by_type(const void *const desc_beg, + const void *const desc_end, + uint32_t type_mask) +{ + const struct usb_desc_header *desc; + + if (desc_beg == NULL || desc_end == NULL) { + return NULL; + } + + for (desc = desc_beg; desc != NULL; desc = usbh_desc_get_next(desc, desc_end)) { + if ((BIT(desc->bDescriptorType) & type_mask) != 0) { + return desc; + } + } + + return NULL; +} + +const void *usbh_desc_get_by_iface(const void *const desc_beg, const void *const desc_end, + const uint8_t iface) +{ + const struct usb_desc_header *desc; + + if (desc_beg == NULL || desc_end == NULL) { + return NULL; + } + + for (desc = desc_beg; desc != NULL; desc = usbh_desc_get_next(desc, desc_end)) { + if (desc->bDescriptorType == USB_DESC_INTERFACE && + ((struct usb_if_descriptor *)desc)->bInterfaceNumber == iface) { + return desc; + } + if (desc->bDescriptorType == USB_DESC_INTERFACE_ASSOC && + ((struct usb_association_descriptor *)desc)->bFirstInterface == iface) { + return desc; + } + } + + return NULL; +} + +const void *usbh_desc_get_cfg_beg(const struct usb_device *udev) +{ + return udev->cfg_desc; +} + +const void *usbh_desc_get_cfg_end(const struct usb_device *udev) +{ + const struct usb_cfg_descriptor *cfg_desc; + + if (udev->cfg_desc == NULL) { + return NULL; + } + + cfg_desc = (struct usb_cfg_descriptor *)udev->cfg_desc; + + return (uint8_t *)cfg_desc + cfg_desc->wTotalLength; +} + +const int usbh_desc_get_iface_info(const struct usb_desc_header *desc, + struct usbh_class_filter *const iface_code, + uint8_t *const iface_num) +{ + if (desc->bDescriptorType == USB_DESC_INTERFACE_ASSOC) { + struct usb_association_descriptor *iad_desc = (void *)desc; + + iface_code->class = iad_desc->bFunctionClass; + iface_code->sub = iad_desc->bFunctionSubClass; + iface_code->proto = iad_desc->bFunctionProtocol; + if (iface_num != NULL) { + *iface_num = iad_desc->bFirstInterface; + } + return 0; + } + + if (desc->bDescriptorType == USB_DESC_INTERFACE) { + struct usb_if_descriptor *if_desc = (void *)desc; + + iface_code->class = if_desc->bInterfaceClass; + iface_code->sub = if_desc->bInterfaceSubClass; + iface_code->proto = if_desc->bInterfaceProtocol; + if (iface_num != NULL) { + *iface_num = if_desc->bInterfaceNumber; + } + return 0; + } + + return -EINVAL; +} + +const void *usbh_desc_get_next_function(const void *const desc_beg, const void *const desc_end) +{ + const struct usb_desc_header *desc = desc_beg; + const struct usb_association_descriptor *iad_desc = desc_beg; + const struct usb_if_descriptor *if_desc = desc_beg; + uint32_t type_mask = BIT(USB_DESC_INTERFACE_ASSOC) | BIT(USB_DESC_INTERFACE); + size_t skip_num; + int8_t iface; + + if (desc_beg == NULL || desc_end == NULL) { + return NULL; + } + + if (desc->bDescriptorType == USB_DESC_INTERFACE_ASSOC) { + iface = iad_desc->bFirstInterface; + skip_num = iad_desc->bInterfaceCount; + } else if (desc->bDescriptorType == USB_DESC_INTERFACE) { + iface = if_desc->bInterfaceNumber; + skip_num = 1; + } else { + return usbh_desc_get_by_type(desc_beg, desc_end, type_mask); + } + + while (skip_num > 0) { + desc = usbh_desc_get_by_type(desc, desc_end, type_mask); + if (desc == NULL) { + return NULL; + } + + if (desc->bDescriptorType == USB_DESC_INTERFACE_ASSOC) { + /* No alternate setting */ + return desc; + } else if (desc->bDescriptorType == USB_DESC_INTERFACE) { + if_desc = (struct usb_if_descriptor *)desc; + + /* Skip all the alternate settings for the same interface number */ + if (if_desc->bInterfaceNumber != iface) { + iface = if_desc->bInterfaceNumber; + skip_num--; + } + } else { + __ASSERT(false, "invalid implementation of usbh_desc_get_by_iface()"); + return NULL; + } + } + + return desc; +} diff --git a/subsys/usb/host/usbh_desc.h b/subsys/usb/host/usbh_desc.h new file mode 100644 index 0000000000000..dbae100017747 --- /dev/null +++ b/subsys/usb/host/usbh_desc.h @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file + * @brief Descriptor matching and searching utilities + * + * This file contains utilities to browse USB descriptors returned by a device + * according to the USB Specification 2.0 Chapter 9. + */ + +#ifndef ZEPHYR_INCLUDE_USBH_DESC_H +#define ZEPHYR_INCLUDE_USBH_DESC_H + +#include + +#include + +/** + * @brief Get the next descriptor in an array of descriptors. + * + * This + * to search either an interface or interface association descriptor: + * + * @code{.c} + * BIT(USB_DESC_INTERFACE) | BIT(USB_DESC_INTERFACE_ASSOC) + * @endcode + * + * @param[in] desc_beg Pointer to the beginning of the descriptor array; to search. May be NULL. + * @param[in] desc_end Pointer after the end of the descriptor array to search + * @param[in] type_mask Bitmask of bDescriptorType values to match + * @return A pointer to the first descriptor matching + */ +const void *usbh_desc_get_next(const void *const desc_beg, const void *const desc_end); + +/** + * @brief Search the first descriptor matching the selected type(s). + * + * As an example, the @p type_mask argument can be constructed this way + * to search either an interface or interface association descriptor: + * + * @code{.c} + * BIT(USB_DESC_INTERFACE) | BIT(USB_DESC_INTERFACE_ASSOC) + * @endcode + * + * @param[in] desc_beg Pointer to the beginning of the descriptor array; to search. May be NULL. + * @param[in] desc_end Pointer after the end of the descriptor array to search + * @param[in] type_mask Bitmask of bDescriptorType values to match + * @return A pointer to the first descriptor matching + */ +const struct usb_desc_header *usbh_desc_get_by_type(const void *const desc_beg, + const void *const desc_end, + uint32_t type_mask); + +/** + * @brief Search a descriptor matching the interace number wanted. + * + * The descriptors are going to be scanned until either an interface association + * @c bFirstInterface field or an interface @c bInterfaceNumber field match. + * + * The descriptors following it can be browsed using @ref usbh_desc_get_next + * + * @param[in] desc_beg Pointer to the beginning of the descriptor array; to search. May be NULL. + * @param[in] desc_end Pointer after the end of the descriptor array to search + * @param[in] iface The interface number to search + * @return A pointer to the first descriptor matching + */ +const void *usbh_desc_get_by_iface(const void *const desc_beg, const void *const desc_end, + const uint8_t iface); + +/** + * @brief Get the start of a device's configuration descriptor. + * + * @param[in] udev The device holding the confiugration descriptor + * @return A pointer to the beginning of the descriptor + */ +const void *usbh_desc_get_cfg_beg(const struct usb_device *udev); + +/** + * @brief Get the end of a device's configuration descriptor. + * + * @param[in] udev The device holding the confiugration descriptor + * @return A pointer to the beginning of the descriptor + */ +const void *usbh_desc_get_cfg_end(const struct usb_device *udev); + +/** + * @brief Extract information from an interface or interface association descriptors + * + * @param[in] desc The descriptor to use + * @param[out] device_info Device information filled by this function + * @param[out] iface_num Pointer filled with the interface number, or NULL + * @return 0 on success or negative error code on failure. + */ +const int usbh_desc_get_iface_info(const struct usb_desc_header *desc, + struct usbh_class_filter *const iface_code, + uint8_t *const iface_num); + +/** + * Checks that the pointed descriptor is not truncated. + * + * @param[in] desc The descriptor to use + * @return true if the descriptor size is valid + */ +const bool usbh_desc_size_is_valid(const void *const desc, size_t expected_size, + const void *const desc_end); + +/** + * @brief Get the next function in the descriptor list. + * + * This returns the interface or interface association of the next USB function, if found. + * This can be used to walk through the list of USB functions to associate a class to each. + * + * If @p desc_beg is an interface association descriptor, it will return a pointer to the interface + * after all those related to the association descriptor. + * + * If @p desc_beg is an interface descriptor, it will skip all the interface alternate settings + * and return a pointer to the next interface with a different number. + * + * If @p desc_beg is not one of these types of descriptors, it will return a pointer to the first + * interface or interface association descriptor following @p desc_beg. + * + * @param[in] desc_beg Pointer to the beginning of the descriptor array; to search. May be NULL. + * @param[in] desc_end Pointer after the end of the descriptor array to search. + * @return A pointer to the next interface after following @p desc_beg or NULL if none. + */ +const void *usbh_desc_get_next_function(const void *const desc_beg, const void *const desc_end); + +#endif /* ZEPHYR_INCLUDE_USBH_DESC_H */ diff --git a/subsys/usb/host/usbh_device.c b/subsys/usb/host/usbh_device.c index 92ecf6ae7aee1..9c27419baf8dc 100644 --- a/subsys/usb/host/usbh_device.c +++ b/subsys/usb/host/usbh_device.c @@ -397,7 +397,7 @@ int usbh_device_set_configuration(struct usb_device *const udev, const uint8_t n } udev->cfg_desc = k_heap_alloc(&usb_device_heap, - cfg_desc.wTotalLength, + cfg_desc.wTotalLength + sizeof(struct usb_desc_header), K_NO_WAIT); if (udev->cfg_desc == NULL) { LOG_ERR("Failed to allocate memory for configuration descriptor");