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..34fed5f84aaa7 --- /dev/null +++ b/subsys/usb/common/include/usb_cdc_ecm.h @@ -0,0 +1,54 @@ +/* + * 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 ECM_CTRL_MASK BIT(0) +#define ECM_FUNC_MASK BIT(1) +#define ECM_INTR_IN_EP_MASK BIT(2) +#define ECM_BULK_IN_EP_MASK BIT(3) +#define ECM_BULK_OUT_EP_MASK BIT(4) +#define ECM_DATA_MASK BIT(5) +#define ECM_UNION_MASK BIT(5) + +/* Combined mask representing all required ECM descriptors */ +#define ECM_MASK_ALL GENMASK(5, 0) + +#define CDC_ECM_SEND_TIMEOUT_MS K_MSEC(1000) +#define CDC_ECM_ETH_PKT_FILTER_ALL 0x000F +#define CDC_ECM_ETH_MAX_FRAME_SIZE 1514 + +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; + +enum cdc_ecm_state { + CDC_ECM_DISCONNECTED, + CDC_ECM_CONNECTED, + CDC_ECM_CONFIGURED, + CDC_ECM_SUSPENDED, +}; + +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..4b55717ee3444 100644 --- a/subsys/usb/host/CMakeLists.txt +++ b/subsys/usb/host/CMakeLists.txt @@ -5,15 +5,22 @@ zephyr_library() zephyr_library_include_directories(${CMAKE_CURRENT_SOURCE_DIR}) zephyr_library_sources( - usbh_ch9.c - usbh_core.c - usbh_api.c - usbh_device.c + usbh_api.c + usbh_ch9.c + usbh_class.c + usbh_core.c + usbh_desc.c + usbh_device.c ) zephyr_library_sources_ifdef( - CONFIG_USBH_SHELL - usbh_shell.c + CONFIG_USBH_SHELL + usbh_shell.c +) + +zephyr_library_sources_ifdef( + CONFIG_USBH_CDC_ECM_CLASS + class/usbh_cdc_ecm.c ) zephyr_library_sources_ifdef( 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..066203d0396d5 --- /dev/null +++ b/subsys/usb/host/class/usbh_cdc_ecm.c @@ -0,0 +1,651 @@ +/* + * 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); + +struct usbh_cdc_ecm_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 intr_mps; + uint8_t ctrl_iface; + uint8_t data_iface; + uint8_t bulk_in_ep; + uint8_t bulk_out_ep; + uint8_t intr_in_ep; + + struct k_mutex tx_mutex; + struct k_sem tx_comp_sem; + struct net_stats_eth stats; +}; + +static int cdc_ecm_start_rx(struct usbh_cdc_ecm_data *priv); +static int cdc_ecm_start_intr(struct usbh_cdc_ecm_data *priv); + +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); + 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_intr_in_cb(struct usb_device *udev, struct uhc_transfer *xfer) +{ + struct usbh_cdc_ecm_data *priv = xfer->priv; + struct cdc_ecm_notification *notif; + bool connected; + int ret = 0; + + if (xfer->err) { + LOG_DBG("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_intr(priv); + } + + 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_cdc_ecm_data *priv = xfer->priv; + struct net_pkt *pkt; + int ret = 0; + + if (xfer->err) { + LOG_DBG("Bulk in transfer error: %d", xfer->err); + priv->stats.errors.rx++; + } + + if (xfer->buf && xfer->buf->len > 0) { + 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_DBG("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(priv); + } + + if (ret) { + LOG_ERR("Failed to resubmit bulk in xfer : %d", ret); + } + + return ret; +} + +static int cdc_ecm_start_rx(struct usbh_cdc_ecm_data *priv) +{ + return submit_xfer(priv, priv->bulk_in_ep, cdc_ecm_bulk_in_cb, priv->bulk_mps, NULL); +} + +static int cdc_ecm_start_intr(struct usbh_cdc_ecm_data *priv) +{ + return submit_xfer(priv, priv->intr_in_ep, cdc_ecm_intr_in_cb, priv->intr_mps, NULL); +} + +static int cdc_ecm_send_cmd(struct usbh_cdc_ecm_data *priv, + uint8_t request, uint16_t value, + uint16_t index, void *data, size_t len) +{ + struct net_buf *buf = NULL; + int ret; + + if (len > 0 && data) { + buf = usbh_xfer_buf_alloc(priv->udev, len); + if (!buf) { + return -ENOMEM; + } + net_buf_add_mem(buf, data, len); + } + + ret = usbh_req_setup(priv->udev, USB_REQTYPE_TYPE_CLASS | USB_REQTYPE_RECIPIENT_INTERFACE, + request, value, index, len, buf); + + if (buf) { + usbh_xfer_buf_free(priv->udev, buf); + } + + return ret; +} + +static int cdc_ecm_bulk_out_cb(struct usb_device *udev, struct uhc_transfer *xfer) +{ + struct usbh_cdc_ecm_data *priv = xfer->priv; + + if (xfer->err) { + LOG_DBG("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_cdc_ecm_data *priv) +{ + const void *const desc_beg = usbh_desc_get_cfg_beg(priv->udev); + const void *const desc_end = usbh_desc_get_cfg_end(priv->udev); + const struct usb_desc_header *desc = desc_beg; + const struct cdc_union_descriptor *union_desc; + const struct usb_if_descriptor *if_desc; + const struct usb_desc_header *ctrl_desc; + const struct usb_ep_descriptor *ep_desc; + + uint8_t subtype; + uint8_t ecm_mask = 0; + + while (desc != NULL) { + switch (desc->bDescriptorType) { + case USB_DESC_INTERFACE: + if_desc = (const struct usb_if_descriptor *)desc; + /* CDC Control Interface */ + if (if_desc->bInterfaceClass == USB_BCC_CDC_CONTROL && + if_desc->bInterfaceSubClass == ECM_SUBCLASS) { + priv->ctrl_iface = if_desc->bInterfaceNumber; + ecm_mask |= ECM_CTRL_MASK; + } + + /* CDC Data Interface */ + else if (if_desc->bInterfaceClass == USB_BCC_CDC_DATA) { + priv->data_iface = if_desc->bInterfaceNumber; + ecm_mask |= ECM_DATA_MASK; + } + break; + + case USB_DESC_CS_INTERFACE: + ctrl_desc = (const struct usb_desc_header *)desc; + subtype = ((const uint8_t *)ctrl_desc)[2]; + if (subtype == UNION_FUNC_DESC) { + union_desc = (const struct cdc_union_descriptor *)desc; + priv->data_iface = union_desc->bSubordinateInterface0; + ecm_mask |= ECM_UNION_MASK; + } else if (subtype == ETHERNET_FUNC_DESC) { + ecm_mask |= ECM_FUNC_MASK; + } + break; + + case USB_DESC_ENDPOINT: + ep_desc = (const struct usb_ep_descriptor *)desc; + + /* Interrupt endpoint (Notification) */ + if (ep_desc->bmAttributes == USB_EP_TYPE_INTERRUPT && + (ep_desc->bEndpointAddress & USB_EP_DIR_MASK)) { + priv->intr_in_ep = ep_desc->bEndpointAddress; + priv->intr_mps = sys_le16_to_cpu(ep_desc->wMaxPacketSize); + ecm_mask |= ECM_INTR_IN_EP_MASK; + } + + /* Bulk IN endpoint */ + else if (ep_desc->bmAttributes == USB_EP_TYPE_BULK && + (ep_desc->bEndpointAddress & USB_EP_DIR_MASK)) { + priv->bulk_in_ep = ep_desc->bEndpointAddress; + priv->bulk_mps = sys_le16_to_cpu(ep_desc->wMaxPacketSize); + ecm_mask |= ECM_BULK_IN_EP_MASK; + } + + /* Bulk OUT endpoint */ + else if (ep_desc->bmAttributes == USB_EP_TYPE_BULK && + !(ep_desc->bEndpointAddress & USB_EP_DIR_MASK)) { + priv->bulk_out_ep = ep_desc->bEndpointAddress; + ecm_mask |= ECM_BULK_OUT_EP_MASK; + } + break; + + default: + break; + } + + desc = usbh_desc_get_next(desc, desc_end); + } + + if ((ecm_mask & ECM_MASK_ALL) != ECM_MASK_ALL) { + LOG_ERR("ECM descriptor incomplete (mask=0x%02x)", ecm_mask); + return -ENODEV; + } + + LOG_INF("CDC ECM parse success: ctrl_iface = %u data_iface = %u bulk_in = 0x%02x " + "bulk_out = 0x%02x intr_in = 0x%02x", priv->ctrl_iface, priv->data_iface, + priv->bulk_in_ep, priv->bulk_out_ep, priv->intr_in_ep); + + return 0; +} + +static int usbh_cdc_ecm_init(struct usbh_class_data *const c_data, + struct usbh_context *const uhs_ctx) +{ + ARG_UNUSED(c_data); + ARG_UNUSED(uhs_ctx); + + return 0; +} + +static int usbh_cdc_ecm_completion_cb(struct usbh_class_data *const c_data, + struct uhc_transfer *const xfer) +{ + ARG_UNUSED(c_data); + ARG_UNUSED(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_association_descriptor *iad; + const struct usb_if_descriptor *if_desc = NULL; + const struct usb_desc_header *desc; + int ret; + + /* The host stack might pass us the IAD first, we need to find the 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); + + if (desc->bDescriptorType == USB_DESC_INTERFACE_ASSOC) { + iad = (const void *)desc; + LOG_DBG("IAD: first_iface=%u count=%u class=%u subclass=%u", + iad->bFirstInterface, iad->bInterfaceCount, + iad->bFunctionClass, iad->bFunctionSubClass); + + /* Fetch the control interface following IAD */ + if_desc = usbh_desc_get_by_iface(desc_beg, desc_end, iad->bFirstInterface); + if (!if_desc) { + LOG_ERR("Control interface %u not found", iad->bFirstInterface); + return -ENODEV; + } + } else if (desc->bDescriptorType == USB_DESC_INTERFACE) { + if_desc = (const struct usb_if_descriptor *)desc; + } else { + LOG_ERR("Unexpected descriptor type: %u", desc->bDescriptorType); + return -ENODEV; + } + + LOG_INF("Found CDC ECM device at interface %u (control)", if_desc->bInterfaceNumber); + + priv->udev = udev; + priv->state = CDC_ECM_CONNECTED; + + /* Parse descriptors to find endpoints */ + ret = cdc_ecm_parse_descriptors(priv); + 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; + } + + priv->state = CDC_ECM_CONFIGURED; + + /* Start interrupt endpoint for notifications */ + ret = cdc_ecm_start_intr(priv); + if (ret) { + LOG_ERR("Failed to start interrupt transfer: %d", ret); + return ret; + } + + /* Start receiving data */ + ret = cdc_ecm_start_rx(priv); + if (ret) { + LOG_ERR("Failed to start RX transfers: %d", ret); + return ret; + } + + if (priv->iface == NULL) { + return -ENETDOWN; + } + + net_if_carrier_on(priv->iface); + + return 0; +} + +static int usbh_cdc_ecm_removed(struct usbh_class_data *const c_data) +{ + struct usbh_cdc_ecm_data *priv = c_data->priv; + + LOG_INF("CDC ECM device removed"); + + priv->state = CDC_ECM_DISCONNECTED; + + /* Stop network operations */ + if (priv->iface) { + net_if_carrier_off(priv->iface); + } + + /* Clear USB device references */ + priv->bulk_mps = 0; + priv->intr_mps = 0; + priv->ctrl_iface = 0; + priv->data_iface = 0; + priv->bulk_in_ep = 0; + priv->bulk_out_ep = 0; + priv->intr_in_ep = 0; + + k_sem_reset(&priv->tx_comp_sem); + + return 0; +} + +static int usbh_cdc_ecm_suspended(struct usbh_class_data *const c_data) +{ + ARG_UNUSED(c_data); + + return 0; +} + +static int usbh_cdc_ecm_resumed(struct usbh_class_data *const c_data) +{ + ARG_UNUSED(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_INF("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; + int ret = 0; + + switch (type) { + case ETHERNET_CONFIG_TYPE_MAC_ADDRESS: + memcpy(priv->mac_addr, config->mac_address.addr, sizeof(priv->mac_addr)); + break; + + case ETHERNET_CONFIG_TYPE_FILTER: { + ret = cdc_ecm_send_cmd(priv, SET_ETHERNET_PACKET_FILTER, + CDC_ECM_ETH_PKT_FILTER_ALL, + priv->ctrl_iface, NULL, 0); + break; + } + + default: + ret = -ENOTSUP; + break; + } + + return ret; +} + +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; + int ret = 0; + bool need_zlp = false; + + + 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; + } + + net_pkt_cursor_init(pkt); + + 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, CDC_ECM_SEND_TIMEOUT_MS) != 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, CDC_ECM_SEND_TIMEOUT_MS) != 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..d4b0b98c26ad5 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,11 +75,17 @@ 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) { if (ctx->root != NULL) { + usbh_class_remove_all(ctx->root); usbh_device_free(ctx->root); ctx->root = NULL; LOG_DBG("Device removed"); @@ -194,12 +204,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..1ec03bca4395b --- /dev/null +++ b/subsys/usb/host/usbh_desc.c @@ -0,0 +1,192 @@ +/* + * 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); + } + + desc = usbh_desc_get_next(desc_beg, desc_end); + if (desc == NULL) { + return NULL; + } + + 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--; + if(skip_num == 0) { + return desc; + } + } + + desc = usbh_desc_get_next(desc, desc_end); + if (desc == NULL) { + return NULL; + } + } 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..0e536270b7929 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"); @@ -487,18 +487,6 @@ int usbh_device_init(struct usb_device *const udev) goto error; } - err = usbh_req_desc_dev(udev, sizeof(udev->dev_desc), &udev->dev_desc); - if (err) { - LOG_ERR("Failed to read device descriptor"); - goto error; - } - - if (!udev->dev_desc.bNumConfigurations) { - LOG_ERR("Device has no configurations, bNumConfigurations %d", - udev->dev_desc.bNumConfigurations); - goto error; - } - err = alloc_device_address(udev, &new_addr); if (err) { LOG_ERR("Failed to allocate device address"); @@ -518,6 +506,18 @@ int usbh_device_init(struct usb_device *const udev) LOG_INF("New device with address %u state %u", udev->addr, udev->state); + err = usbh_req_desc_dev(udev, sizeof(udev->dev_desc), &udev->dev_desc); + if (err) { + LOG_ERR("Failed to read device descriptor"); + goto error; + } + + if (!udev->dev_desc.bNumConfigurations) { + LOG_ERR("Device has no configurations, bNumConfigurations %d", + udev->dev_desc.bNumConfigurations); + goto error; + } + err = usbh_device_set_configuration(udev, 1); if (err) { LOG_ERR("Failed to configure new device with address %u", udev->addr);