From 547197eea960752bdae1a556da38a34820b55173 Mon Sep 17 00:00:00 2001 From: Josuah Demangeon Date: Fri, 8 Aug 2025 14:13:16 +0000 Subject: [PATCH 1/8] usb: host: add host status to the host context Add a "struct usbh_status" that contains a bitmask of flags to keep track of the global state of the host context, like done for the device_next implementation. Signed-off-by: Josuah Demangeon --- include/zephyr/usb/usbh.h | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/include/zephyr/usb/usbh.h b/include/zephyr/usb/usbh.h index e2e6f3074000e..b6526af828bc9 100644 --- a/include/zephyr/usb/usbh.h +++ b/include/zephyr/usb/usbh.h @@ -33,6 +33,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 +53,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 */ From f273bc01ce927fc866c5f6513fe9aae423f2d50c Mon Sep 17 00:00:00 2001 From: Josuah Demangeon Date: Thu, 14 Aug 2025 14:45:40 +0000 Subject: [PATCH 2/8] usb: host: class: implement more of the class/data struct fields Add a "struct usbh_class_api" for the host implementation, and move all the function poitners to it. Add more fields to "struct usbh_class_data". Signed-off-by: Josuah Demangeon --- include/zephyr/usb/usbh.h | 51 ++++++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/include/zephyr/usb/usbh.h b/include/zephyr/usb/usbh.h index b6526af828bc9..86c50e1a47db7 100644 --- a/include/zephyr/usb/usbh.h +++ b/include/zephyr/usb/usbh.h @@ -84,28 +84,45 @@ struct usbh_code_triple { uint8_t proto; }; +struct usbh_class_data; + /** - * @brief USB host class data and class instance API + * @brief USB host class instance API + */ +struct usbh_class_api { + /** Initialization of the class implementation */ + int (*init)(struct usbh_class_data *const c_data); + /** Request completion event handler */ + int (*request)(struct usbh_class_data *const c_data, + struct uhc_transfer *const xfer, int err); + /** Device connected handler */ + int (*connected)(struct usbh_class_data *const c_data, + void *const desc_start_addr, + void *const desc_end_addr); + /** Device removed handler */ + int (*removed)(struct usbh_class_data *const c_data); + /** Bus remote wakeup handler */ + int (*rwup)(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 */ 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; /** Class code supported by this instance */ struct usbh_code_triple code; - - /** Initialization of the class implementation */ - /* int (*init)(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); + /** Pointer to host support class API */ + struct usbh_class_api *api; + /** Pointer to private data */ + void *priv; }; /** From 63889f5b6742e14458bd6d7bd4e54301b8bd4846 Mon Sep 17 00:00:00 2001 From: Josuah Demangeon Date: Fri, 15 Aug 2025 16:10:46 +0000 Subject: [PATCH 3/8] usb: host: introduce wrappers to access the class function pointers Add API wrappers around the function pointers in struct usbh_class_api, while also documenting the USB host class internal API. Signed-off-by: Josuah Demangeon --- include/zephyr/usb/usbh.h | 27 +++--- subsys/usb/host/usbh_class_api.h | 158 +++++++++++++++++++++++++++++++ 2 files changed, 172 insertions(+), 13 deletions(-) create mode 100644 subsys/usb/host/usbh_class_api.h diff --git a/include/zephyr/usb/usbh.h b/include/zephyr/usb/usbh.h index 86c50e1a47db7..7461e1f7e533e 100644 --- a/include/zephyr/usb/usbh.h +++ b/include/zephyr/usb/usbh.h @@ -90,19 +90,18 @@ struct usbh_class_data; * @brief USB host class instance API */ struct usbh_class_api { - /** Initialization of the class implementation */ - int (*init)(struct usbh_class_data *const c_data); + /** 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_class_data *const c_data, - struct uhc_transfer *const xfer, int err); - /** Device connected handler */ - int (*connected)(struct usbh_class_data *const c_data, - void *const desc_start_addr, - void *const desc_end_addr); - /** Device removed handler */ + 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 remote wakeup handler */ - int (*rwup)(struct usbh_class_data *const c_data); /** Bus suspended handler */ int (*suspended)(struct usbh_class_data *const c_data); /** Bus resumed handler */ @@ -117,8 +116,10 @@ struct usbh_class_data { const char *name; /** Pointer to USB host stack context structure */ struct usbh_context *uhs_ctx; - /** Class code supported by this instance */ - struct usbh_code_triple code; + /** 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 */ 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 */ From 31cc07503f58280408f084f429e01c30a0368f2b Mon Sep 17 00:00:00 2001 From: Josuah Demangeon Date: Fri, 15 Aug 2025 19:04:59 +0000 Subject: [PATCH 4/8] usb: host: introduce usbh_class with init/remove functions Add functions to probe/remove all classes as part of a new usbh_class.c and a matching usbh_class.h. These functions are called from the function usbh_init_device_intl() in usbh_core.c to initialize every class upon connection of a device. Every class driver provide filters to match the interfaces of the device. Co-authored-by: Aiden Hu Signed-off-by: Josuah Demangeon --- include/zephyr/usb/usbh.h | 63 +++++++++-- subsys/usb/host/CMakeLists.txt | 4 +- subsys/usb/host/usbh_class.c | 186 +++++++++++++++++++++++++++++++++ subsys/usb/host/usbh_class.h | 67 ++++++++++++ subsys/usb/host/usbh_core.c | 21 ++-- subsys/usb/host/usbh_data.ld | 2 +- subsys/usb/host/usbh_desc.c | 179 +++++++++++++++++++++++++++++++ subsys/usb/host/usbh_desc.h | 132 +++++++++++++++++++++++ 8 files changed, 634 insertions(+), 20 deletions(-) create mode 100644 subsys/usb/host/usbh_class.c create mode 100644 subsys/usb/host/usbh_class.h create mode 100644 subsys/usb/host/usbh_desc.c create mode 100644 subsys/usb/host/usbh_desc.h diff --git a/include/zephyr/usb/usbh.h b/include/zephyr/usb/usbh.h index 7461e1f7e533e..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 */ @@ -72,20 +73,26 @@ 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; }; -struct usbh_class_data; - /** * @brief USB host class instance API */ @@ -127,10 +134,44 @@ struct usbh_class_data { }; /** + * @cond INTERNAL_HIDDEN + * + * Variables used by the USB host stack but not exposed to the class + * through the class API. */ -#define USBH_DEFINE_CLASS(name) \ - static STRUCT_SECTION_ITERABLE(usbh_class_data, name) +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/host/CMakeLists.txt b/subsys/usb/host/CMakeLists.txt index f48f5b8c71483..b9afd0a65801d 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 ) 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_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 */ From da49fd00c38795b520cef0d75df711ae51734e71 Mon Sep 17 00:00:00 2001 From: Josuah Demangeon Date: Sat, 26 Jul 2025 15:42:36 +0000 Subject: [PATCH 5/8] usb: host: ensure device configuration descriptors are nil-terminated Allocate extra room at the end of the USB descriptor buffer to ensure that the device ends with `desc->bLength == 0`. For now, validating `desc->bLength` from the device to ensure no access past the buffer is still not done. Signed-off-by: Josuah Demangeon --- subsys/usb/host/usbh_device.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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"); From c95eaffb49b7ddea94a9a7da80d624f40da414bb Mon Sep 17 00:00:00 2001 From: Santhosh Charles Date: Wed, 29 Oct 2025 11:47:00 +0530 Subject: [PATCH 6/8] subsys: usb: add common directory for shared host/device support Introduce subsys/usb/common as a new directory intended to hold sources and configuration that are shared between USB host and device implementations. Integrate the new common directory into the build system. Signed-off-by: Santhosh Charles Signed-off-by: Josuah Demangeon --- subsys/usb/CMakeLists.txt | 1 + subsys/usb/common/CMakeLists.txt | 6 ++++ subsys/usb/common/include/usb_cdc_ecm.h | 37 +++++++++++++++++++++++++ subsys/usb/common/usb_cdc_ecm.c | 35 +++++++++++++++++++++++ 4 files changed, 79 insertions(+) create mode 100644 subsys/usb/common/CMakeLists.txt create mode 100644 subsys/usb/common/include/usb_cdc_ecm.h create mode 100644 subsys/usb/common/usb_cdc_ecm.c 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; +} From 49b0d523942e4575e679ce403939ad0d1e823c38 Mon Sep 17 00:00:00 2001 From: Santhosh Charles Date: Wed, 29 Oct 2025 23:37:54 +0530 Subject: [PATCH 7/8] dts: bindings: usb: Add DT bindings for USB Host CDC ECM class Add devicetree binding file describing the USB Host CDC Ethernet Control Model (ECM) implementation for Zephyr. Signed-off-by: Santhosh Charles --- dts/bindings/usb/zephyr,usbh-cdc-ecm.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 dts/bindings/usb/zephyr,usbh-cdc-ecm.yaml 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" From 3ba2e92e25b09468cc251143a7fc18d474473c95 Mon Sep 17 00:00:00 2001 From: Santhosh Charles Date: Wed, 29 Oct 2025 12:10:59 +0530 Subject: [PATCH 8/8] subsys: usb: host: class: Add CDC ECM class support for USBH Add support for the USB CDC ECM (Ethernet Control Model) class to the USB host subsystem. This implementation enables virtual Ethernet functionality for USB host devices. Signed-off-by: Santhosh Charles --- subsys/usb/host/CMakeLists.txt | 5 + subsys/usb/host/Kconfig | 1 + subsys/usb/host/class/Kconfig | 7 + subsys/usb/host/class/Kconfig.cdc_ecm_host | 20 + subsys/usb/host/class/usbh_cdc_ecm.c | 767 +++++++++++++++++++++ 5 files changed, 800 insertions(+) create mode 100644 subsys/usb/host/class/Kconfig create mode 100644 subsys/usb/host/class/Kconfig.cdc_ecm_host create mode 100644 subsys/usb/host/class/usbh_cdc_ecm.c diff --git a/subsys/usb/host/CMakeLists.txt b/subsys/usb/host/CMakeLists.txt index b9afd0a65801d..5cab660403286 100644 --- a/subsys/usb/host/CMakeLists.txt +++ b/subsys/usb/host/CMakeLists.txt @@ -18,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);