From bf1a6609595fb2cf42af530fc62c93a496bae648 Mon Sep 17 00:00:00 2001 From: Josuah Demangeon Date: Fri, 8 Aug 2025 14:13:16 +0000 Subject: [PATCH 01/34] 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 958d08fa5a60de8de80af02c961b95b16eab7e42 Mon Sep 17 00:00:00 2001 From: Josuah Demangeon Date: Thu, 23 Oct 2025 20:08:48 +0000 Subject: [PATCH 02/34] usb: host: add copyright notice to linker script Add missing copyright notice for the linker script to help with check_compliance.py. Signed-off-by: Josuah Demangeon --- subsys/usb/host/usbh_data.ld | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/subsys/usb/host/usbh_data.ld b/subsys/usb/host/usbh_data.ld index 4363154f29474..555539bb21110 100644 --- a/subsys/usb/host/usbh_data.ld +++ b/subsys/usb/host/usbh_data.ld @@ -1,3 +1,9 @@ +/* + * Copyright 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + #include ITERABLE_SECTION_RAM(usbh_context, Z_LINK_ITERABLE_SUBALIGN) From bd7462c47b7088f3d17fded3e96c96833db4a319 Mon Sep 17 00:00:00 2001 From: Josuah Demangeon Date: Thu, 14 Aug 2025 14:45:40 +0000 Subject: [PATCH 03/34] 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 789795ad13b237e25d4b951259d3d9a3e0c5dbdf Mon Sep 17 00:00:00 2001 From: Josuah Demangeon Date: Fri, 15 Aug 2025 16:10:46 +0000 Subject: [PATCH 04/34] 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 | 147 +++++++++++++++++++++++++++++++ 2 files changed, 161 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..759cffe777bb6 --- /dev/null +++ b/subsys/usb/host/usbh_class_api.h @@ -0,0 +1,147 @@ +/* + * 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] 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 + * @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 suspended the bus. + * It can be used to suspend underlying systems. + * + * @param[in] c_data Pointer to USB host class data + * @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 its activity on the bus. + * It can be used to wake-up underlying systems. + * + * @param[in] c_data Pointer to USB host class data + * @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 210673d3fd20d41d49c55d369df5a821dbb6e3da Mon Sep 17 00:00:00 2001 From: Josuah Demangeon Date: Fri, 15 Aug 2025 19:04:59 +0000 Subject: [PATCH 05/34] 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 | 82 +++++++++++-- subsys/usb/host/CMakeLists.txt | 4 +- subsys/usb/host/usbh_class.c | 197 +++++++++++++++++++++++++++++++ subsys/usb/host/usbh_class.h | 79 +++++++++++++ subsys/usb/host/usbh_core.c | 17 ++- subsys/usb/host/usbh_data.ld | 2 +- subsys/usb/host/usbh_desc.c | 210 +++++++++++++++++++++++++++++++++ subsys/usb/host/usbh_desc.h | 148 +++++++++++++++++++++++ 8 files changed, 717 insertions(+), 22 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..b4f93029679e6 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 */ @@ -17,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -72,20 +74,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 +135,62 @@ struct usbh_class_data { }; /** + * @cond INTERNAL_HIDDEN + * + * Internal state of an USB class. Not corresponding to an USB protocol state, + * but instead software life cycle. */ -#define USBH_DEFINE_CLASS(name) \ - static STRUCT_SECTION_ITERABLE(usbh_class_data, name) +enum usbh_class_state { + /** The class is available to be associated to an USB device function. */ + USBH_CLASS_STATE_IDLE, + /** The class got bound to an USB function of a particular device on the bus. */ + USBH_CLASS_STATE_BOUND, + /** The class failed to initialize and cannot be used. */ + USBH_CLASS_STATE_ERROR, +}; +/* @endcond */ +/** + * @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; + /** State of the USB class instance */ + enum usbh_class_state state; +}; +/* @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 UTIL_CAT(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 = &UTIL_CAT(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 4e8f7b9d28dc8..cafa3162b3c94 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..adc8cfb8a3f18 --- /dev/null +++ b/subsys/usb/host/usbh_class.c @@ -0,0 +1,197 @@ +/* + * 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); + +void usbh_class_init_all(struct usbh_context *const uhs_ctx) +{ + int ret; + + STRUCT_SECTION_FOREACH(usbh_class_node, c_node) { + struct usbh_class_data *const c_data = c_node->c_data; + + if (c_node->state != USBH_CLASS_STATE_IDLE) { + LOG_DBG("Skipping '%s' in state %u", c_data->name, c_node->state); + continue; + } + + ret = usbh_class_init(c_data, uhs_ctx); + if (ret != 0) { + LOG_WRN("Failed to initialize class %s (%d)", c_data->name, ret); + c_node->state = USBH_CLASS_STATE_ERROR; + } + } +} + +void usbh_class_remove_all(struct usb_device *const udev) +{ + int ret; + + k_mutex_lock(&udev->mutex, K_FOREVER); + + STRUCT_SECTION_FOREACH(usbh_class_node, c_node) { + struct usbh_class_data *const c_data = c_node->c_data; + + if (c_data->udev == udev) { + ret = usbh_class_removed(c_data); + if (ret != 0) { + LOG_ERR("Failed to handle device removal for each class (%d)", ret); + c_node->state = USBH_CLASS_STATE_ERROR; + continue; + } + + /* The class instance is now free to bind to a new device */ + c_data->udev = NULL; + c_node->state = USBH_CLASS_STATE_IDLE; + } + } + + k_mutex_unlock(&udev->mutex); +} + +/* + * Probe an USB device function against all available classes of the system. + * + * Try to match a class from the global list of all system classes, using their filter rules + * and return status to tell if a class matches or not. + * + * The first matched class will stop the loop, and the status will be updated so that classes + * are only matched for a single USB function at a time. + * + * USB functions will only have one class matching, and calling usbh_class_probe_function() + * multiple times consequently has no effect. + */ +static void usbh_class_probe_function(struct usb_device *const udev, + struct usbh_class_filter *const info, uint8_t iface) +{ + int ret; + + /* Assumes that udev->mutex is locked */ + + /* First check if any interface is already bound to this */ + STRUCT_SECTION_FOREACH(usbh_class_node, c_node) { + struct usbh_class_data *const c_data = c_node->c_data; + + if (c_node->state == USBH_CLASS_STATE_BOUND && + c_data->udev == udev && c_data->iface == iface) { + LOG_DBG("Interface %u bound to '%s', skipping", iface, c_data->name); + return; + } + } + + /* Then try to match this function against all interfaces */ + STRUCT_SECTION_FOREACH(usbh_class_node, c_node) { + struct usbh_class_data *const c_data = c_node->c_data; + + if (c_node->state != USBH_CLASS_STATE_IDLE) { + LOG_DBG("Class %s already matched, skipping", c_data->name); + continue; + } + + 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; + } + + LOG_INF("Class '%s' matches interface %u", c_data->name, iface); + c_node->state = USBH_CLASS_STATE_BOUND; + c_data->udev = udev; + c_data->iface = iface; + break; + } +} + +void usbh_class_probe_device(struct usb_device *const udev) +{ + const void *desc_end; + const struct usb_desc_header *desc; + struct usbh_class_filter info; + uint8_t iface; + int ret; + + k_mutex_lock(&udev->mutex, K_FOREVER); + + desc = usbh_desc_get_cfg_beg(udev); + desc_end = usbh_desc_get_cfg_end(udev); + info.vid = udev->dev_desc.idVendor; + info.pid = udev->dev_desc.idProduct; + + while (true) { + desc = usbh_desc_get_next_function(desc, desc_end); + if (desc == NULL) { + break; + } + + ret = usbh_desc_get_iface_info(desc, &info, &iface); + if (ret < 0) { + LOG_ERR("Failed to collect class codes for matching interface %u", iface); + continue; + } + + usbh_class_probe_function(udev, &info, iface); + } + + k_mutex_unlock(&udev->mutex); +} + +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..f0971518c033f --- /dev/null +++ b/subsys/usb/host/usbh_class.h @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * Copyright 2025 NXP + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_INCLUDE_USBH_CLASS_H +#define ZEPHYR_INCLUDE_USBH_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. + * + * An empty filter set matches everything. + * This can be used to only rely on @c class_api->probe() return value. + * + * @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 + */ +void usbh_class_init_all(struct usbh_context *const uhs_ctx); + +/** + * @brief Probe an USB device function against all available classes. + * + * Try to match a class from the global list of all system classes using their filter rules + * and return status to update the state of each matched class. + * + * The first matched class + * + * @param[in] udev USB device to probe. + * + * @retval 0 on success or negative error code on failure + */ +void usbh_class_probe_device(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 + */ +void usbh_class_remove_all(struct usb_device *const udev); + +#endif /* ZEPHYR_INCLUDE_USBH_CLASS_H */ diff --git a/subsys/usb/host/usbh_core.c b/subsys/usb/host/usbh_core.c index 1b654b5041133..1b30f2079b20d 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,7 +49,6 @@ 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) { - LOG_DBG("Device connected event"); if (ctx->root != NULL) { LOG_ERR("Device already connected"); @@ -71,11 +73,14 @@ static void dev_connected_handler(struct usbh_context *const ctx, if (usbh_device_init(ctx->root)) { LOG_ERR("Failed to reset new USB device"); } + + usbh_class_probe_device(ctx->root); } 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,13 +199,7 @@ 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; - } + usbh_class_init_all(uhs_ctx); return 0; } diff --git a/subsys/usb/host/usbh_data.ld b/subsys/usb/host/usbh_data.ld index 555539bb21110..d568296320c19 100644 --- a/subsys/usb/host/usbh_data.ld +++ b/subsys/usb/host/usbh_data.ld @@ -7,4 +7,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..009e0197d65fe --- /dev/null +++ b/subsys/usb/host/usbh_desc.c @@ -0,0 +1,210 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * Copyright NXP + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include "usbh_class.h" +#include "usbh_desc.h" + +LOG_MODULE_REGISTER(usbh_desc, CONFIG_USBH_LOG_LEVEL); + +const bool usbh_desc_is_valid(const void *const desc_ptr, const void *const desc_end, + size_t expected_size) +{ + const struct usb_desc_header *desc = desc_ptr; + + /* Block invalid input */ + if (desc == NULL || desc_end == NULL || expected_size < sizeof(struct usb_desc_header)) { + return false; + } + + /* Avoid out of order pointers */ + if ((uint8_t *)desc > (uint8_t *)desc_end) { + return false; + } + + /* Avoid too short descriptors */ + if ((uint8_t *)desc + expected_size > (uint8_t *)desc_end) { + return false; + } + + /* Avoid too short bLength field */ + if (desc->bLength < expected_size) { + return false; + } + + return true; +} + +bool usbh_desc_is_valid_interface(const void *const desc, const void *const desc_end) +{ + return usbh_desc_is_valid(desc, desc_end, sizeof(struct usb_if_descriptor)) && + ((struct usb_desc_header *)desc)->bDescriptorType == USB_DESC_INTERFACE; +} + +bool usbh_desc_is_valid_association(const void *const desc, const void *const desc_end) +{ + return usbh_desc_is_valid(desc, desc_end, sizeof(struct usb_association_descriptor)) && + ((struct usb_desc_header *)desc)->bDescriptorType == USB_DESC_INTERFACE_ASSOC; +} + +bool usbh_desc_is_valid_configuration(const void *const desc, const void *const desc_end) +{ + return usbh_desc_is_valid(desc, desc_end, sizeof(struct usb_cfg_descriptor)) && + ((struct usb_desc_header *)desc)->bDescriptorType == USB_DESC_CONFIGURATION; +} + +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 (!usbh_desc_is_valid(desc, desc_end, sizeof(const struct usb_desc_header))) { + return NULL; + } + + next = (const struct usb_desc_header *)((uint8_t *)desc + desc->bLength); + + if (!usbh_desc_is_valid(next, desc_end, sizeof(const struct usb_desc_header))) { + return NULL; + } + + return next; +} + +const void *usbh_desc_get_next_alt_setting(const void *const desc_beg, const void *const desc_end) +{ + const struct usb_desc_header *desc = desc_beg; + + /* Expect desc to already be pointing at an interface descriptor */ + if (desc_beg == NULL || desc_end == NULL || desc->bDescriptorType != USB_DESC_INTERFACE) { + return NULL; + } + + /* Skip the current interface descriptor */ + desc = usbh_desc_get_next(desc, desc_end); + + /* Seek to the nextAlternate Setting for this interface */ + for (; desc != NULL; desc = usbh_desc_get_next(desc, desc_end)) { + if (desc->bDescriptorType == USB_DESC_INTERFACE) { + /* Non-zero Alternate Setting */ + if (usbh_desc_is_valid_interface(desc, desc_end) && + ((struct usb_if_descriptor *)desc)->bAlternateSetting != 0) { + return desc; + } + + /* Do not continue to the next interface */ + return NULL; + } + } + + 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 (usbh_desc_is_valid_interface(desc, desc_end) && + ((struct usb_if_descriptor *)desc)->bInterfaceNumber == iface) { + return desc; + } + if (usbh_desc_is_valid_association(desc, desc_end) && + ((struct usb_association_descriptor *)desc)->bFirstInterface == iface) { + return desc; + } + } + + return NULL; +} + +const void *usbh_desc_get_cfg_beg(const struct usb_device *const udev) +{ + return udev->cfg_desc; +} + +const void *usbh_desc_get_cfg_end(const struct usb_device *const udev) +{ + const struct usb_cfg_descriptor *const cfg_desc = udev->cfg_desc; + + __ASSERT_NO_MSG(cfg_desc != NULL); + + /* Relies on wTotalLength being validated in usbh_device_set_configuration() */ + return (uint8_t *)cfg_desc + cfg_desc->wTotalLength; +} + +int usbh_desc_get_iface_info(const struct usb_desc_header *const desc, + struct usbh_class_filter *const iface_code, uint8_t *const iface_num) +{ + if (desc->bDescriptorType == USB_DESC_INTERFACE_ASSOC) { + const struct usb_association_descriptor *iad_desc = + (const struct usb_association_descriptor *)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) { + const struct usb_if_descriptor *if_desc = + (const struct usb_if_descriptor *)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; + uint8_t skip_num = 1; + + if (desc_beg == NULL || desc_end == NULL) { + return NULL; + } + + /* Skip all interfaces the Association descriptor contains */ + if (usbh_desc_is_valid_association(desc, desc_end)) { + skip_num = ((struct usb_association_descriptor *)desc)->bInterfaceCount; + } + + while (desc != NULL && skip_num > 0) { + /* If already on an Interface Association or Interface, this will skip it */ + desc = usbh_desc_get_next(desc, desc_end); + + /* Association descriptor: this is always a new function */ + if (usbh_desc_is_valid_association(desc, desc_end)) { + return desc; + } + + /* Only count the first Alternate Setting of an Interface */ + if (usbh_desc_is_valid_interface(desc, desc_end) && + ((struct usb_if_descriptor *)desc)->bAlternateSetting == 0) { + skip_num--; + } + } + + return desc; +} diff --git a/subsys/usb/host/usbh_desc.h b/subsys/usb/host/usbh_desc.h new file mode 100644 index 0000000000000..f039da8ef3d27 --- /dev/null +++ b/subsys/usb/host/usbh_desc.h @@ -0,0 +1,148 @@ +/* + * 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. + * + * @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 first descriptor matching + */ +const void *usbh_desc_get_next(const void *const desc_beg, const void *const desc_end); + +/** + * @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 configuration descriptor + * + * @return A pointer to the beginning of the descriptor + */ +const void *usbh_desc_get_cfg_beg(const struct usb_device *const udev); + +/** + * @brief Get the end of a device's configuration descriptor. + * + * @param[in] udev The device holding the configuration descriptor + * + * @return A pointer to the beginning of the descriptor + */ +const void *usbh_desc_get_cfg_end(const struct usb_device *const udev); + +/** + * @brief Extract information from an interface or interface association descriptors + * + * @param[in] desc The descriptor to use + * @param[out] iface_code 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. + */ +int usbh_desc_get_iface_info(const struct usb_desc_header *desc, + struct usbh_class_filter *const iface_code, + uint8_t *const iface_num); + +/** + * @brief Checks that the pointed descriptor is not truncated. + * + * @param[in] desc The descriptor to validate + * @param[in] desc_end Pointer after the end of the descriptor array to search. + * @param[in] expected_size The size of the descriptor. + * + * @return true if the descriptor size is valid + */ +const bool usbh_desc_is_valid(const void *const desc, const void *const desc_end, + size_t expected_size); + +/** + * @brief Checks that the pointed descriptor is a valid interface descriptor. + * + * @param[in] desc The descriptor to validate + * @param[in] desc_end Pointer after the end of the descriptor array to search. + * + * @return true if the descriptor size is the expected type and size + */ +bool usbh_desc_is_valid_interface(const void *const desc, const void *const desc_end); + +/** + * @brief Checks that the pointed descriptor is a valid interface descriptor. + * + * @param[in] desc The descriptor to validate + * @param[in] desc_end Pointer after the end of the descriptor array to search. + * + * @return true if the descriptor size is the expected type and size + */ +bool usbh_desc_is_valid_association(const void *const desc, 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 bAlternateSetting of 0. + * + * If @p desc_beg is another type of descriptors, it will seek to a matching descriptor type and + * return it, without skipping to the next one after it. + * + * @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 Pointer to the next matching descriptor or NULL. + */ +const void *usbh_desc_get_next_function(const void *const desc_beg, const void *const desc_end); + +/** + * @brief Get the next alternate setting in the current interface. + * + * The @p desc_beg descriptor is expected to point at an interface descriptor, and the descriptor + * returned will be different, with bAlternateSetting > 0 and same bInterfaceNumber, or NULL if + * none was found or on invalid descriptor. + * + * @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 Pointer to the next matching descriptor or NULL. + */ +const void *usbh_desc_get_next_alt_setting(const void *const desc_beg, const void *const desc_end); + +#endif /* ZEPHYR_INCLUDE_USBH_DESC_H */ From 750162eeefdd4ab8f32a46add4a7fd254b6b77f4 Mon Sep 17 00:00:00 2001 From: Josuah Demangeon Date: Fri, 25 Jul 2025 12:09:06 +0000 Subject: [PATCH 06/34] usb: uvc: move the header definition to include/ Move the UVC header with all the definitions from the UVC standard to share it between USB host and device class implementation. Signed-off-by: Josuah Demangeon --- subsys/usb/CMakeLists.txt | 1 + subsys/usb/common/CMakeLists.txt | 4 ++++ .../class/usbd_uvc.h => common/include/usb_uvc.h} | 0 subsys/usb/device_next/class/usbd_uvc.c | 3 ++- 4 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 subsys/usb/common/CMakeLists.txt rename subsys/usb/{device_next/class/usbd_uvc.h => common/include/usb_uvc.h} (100%) 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..1dfd227bd5081 --- /dev/null +++ b/subsys/usb/common/CMakeLists.txt @@ -0,0 +1,4 @@ +# Copyright (c) 2025 Nordic Semiconductor +# SPDX-License-Identifier: Apache-2.0 + +zephyr_include_directories(include) diff --git a/subsys/usb/device_next/class/usbd_uvc.h b/subsys/usb/common/include/usb_uvc.h similarity index 100% rename from subsys/usb/device_next/class/usbd_uvc.h rename to subsys/usb/common/include/usb_uvc.h diff --git a/subsys/usb/device_next/class/usbd_uvc.c b/subsys/usb/device_next/class/usbd_uvc.c index 2b80084079e37..169e4f30bcb0a 100644 --- a/subsys/usb/device_next/class/usbd_uvc.c +++ b/subsys/usb/device_next/class/usbd_uvc.c @@ -25,7 +25,8 @@ #include #include -#include "usbd_uvc.h" +#include "usb_uvc.h" + #include "../../../drivers/video/video_ctrls.h" #include "../../../drivers/video/video_device.h" From 1c89dc3e032cdc41557e9e6bb8a2a8ecfb3d9bd3 Mon Sep 17 00:00:00 2001 From: Josuah Demangeon Date: Thu, 11 Sep 2025 19:32:25 +0000 Subject: [PATCH 07/34] tests: usb: host: test basic API functionality Add tests making sure the USB Host class APIs introduced build and run as expected. Signed-off-by: Josuah Demangeon --- .../subsys/usb/host/class_api/CMakeLists.txt | 12 + .../usb/host/class_api/boards/native_sim.conf | 1 + .../host/class_api/boards/native_sim.overlay | 19 ++ .../host/class_api/boards/native_sim_64.conf | 1 + .../class_api/boards/native_sim_64.overlay | 7 + .../class_api/boards/qemu_cortex_m3.overlay | 17 + tests/subsys/usb/host/class_api/prj.conf | 8 + tests/subsys/usb/host/class_api/src/main.c | 306 ++++++++++++++++++ tests/subsys/usb/host/class_api/testcase.yaml | 17 + .../usb/host/common/include/test_descriptor.h | 86 +++++ tests/subsys/usb/host/desc/CMakeLists.txt | 12 + .../usb/host/desc/boards/native_sim.conf | 1 + .../usb/host/desc/boards/native_sim.overlay | 19 ++ .../usb/host/desc/boards/native_sim_64.conf | 1 + .../host/desc/boards/native_sim_64.overlay | 7 + .../host/desc/boards/qemu_cortex_m3.overlay | 17 + tests/subsys/usb/host/desc/prj.conf | 8 + tests/subsys/usb/host/desc/src/main.c | 74 +++++ tests/subsys/usb/host/desc/testcase.yaml | 9 + 19 files changed, 622 insertions(+) create mode 100644 tests/subsys/usb/host/class_api/CMakeLists.txt create mode 100644 tests/subsys/usb/host/class_api/boards/native_sim.conf create mode 100644 tests/subsys/usb/host/class_api/boards/native_sim.overlay create mode 100644 tests/subsys/usb/host/class_api/boards/native_sim_64.conf create mode 100644 tests/subsys/usb/host/class_api/boards/native_sim_64.overlay create mode 100644 tests/subsys/usb/host/class_api/boards/qemu_cortex_m3.overlay create mode 100644 tests/subsys/usb/host/class_api/prj.conf create mode 100644 tests/subsys/usb/host/class_api/src/main.c create mode 100644 tests/subsys/usb/host/class_api/testcase.yaml create mode 100644 tests/subsys/usb/host/common/include/test_descriptor.h create mode 100644 tests/subsys/usb/host/desc/CMakeLists.txt create mode 100644 tests/subsys/usb/host/desc/boards/native_sim.conf create mode 100644 tests/subsys/usb/host/desc/boards/native_sim.overlay create mode 100644 tests/subsys/usb/host/desc/boards/native_sim_64.conf create mode 100644 tests/subsys/usb/host/desc/boards/native_sim_64.overlay create mode 100644 tests/subsys/usb/host/desc/boards/qemu_cortex_m3.overlay create mode 100644 tests/subsys/usb/host/desc/prj.conf create mode 100644 tests/subsys/usb/host/desc/src/main.c create mode 100644 tests/subsys/usb/host/desc/testcase.yaml diff --git a/tests/subsys/usb/host/class_api/CMakeLists.txt b/tests/subsys/usb/host/class_api/CMakeLists.txt new file mode 100644 index 0000000000000..d99885afb1d47 --- /dev/null +++ b/tests/subsys/usb/host/class_api/CMakeLists.txt @@ -0,0 +1,12 @@ +# Copyright (c) 2025 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(test_usb_host) + +target_include_directories(app PRIVATE ${ZEPHYR_BASE}/subsys/usb/host) +target_include_directories(app PRIVATE ${ZEPHYR_BASE}/tests/subsys/usb/host/common/include) + +target_sources(app PRIVATE src/main.c) diff --git a/tests/subsys/usb/host/class_api/boards/native_sim.conf b/tests/subsys/usb/host/class_api/boards/native_sim.conf new file mode 100644 index 0000000000000..68e22b2de6a0c --- /dev/null +++ b/tests/subsys/usb/host/class_api/boards/native_sim.conf @@ -0,0 +1 @@ +CONFIG_SYS_CLOCK_TICKS_PER_SEC=1000000 diff --git a/tests/subsys/usb/host/class_api/boards/native_sim.overlay b/tests/subsys/usb/host/class_api/boards/native_sim.overlay new file mode 100644 index 0000000000000..d352f555c8c89 --- /dev/null +++ b/tests/subsys/usb/host/class_api/boards/native_sim.overlay @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/delete-node/ &zephyr_udc0; + +/ { + zephyr_uhc0: uhc_vrt0 { + compatible = "zephyr,uhc-virtual"; + + zephyr_udc0: udc_vrt0 { + compatible = "zephyr,udc-virtual"; + num-bidir-endpoints = <8>; + maximum-speed = "high-speed"; + }; + }; +}; diff --git a/tests/subsys/usb/host/class_api/boards/native_sim_64.conf b/tests/subsys/usb/host/class_api/boards/native_sim_64.conf new file mode 100644 index 0000000000000..68e22b2de6a0c --- /dev/null +++ b/tests/subsys/usb/host/class_api/boards/native_sim_64.conf @@ -0,0 +1 @@ +CONFIG_SYS_CLOCK_TICKS_PER_SEC=1000000 diff --git a/tests/subsys/usb/host/class_api/boards/native_sim_64.overlay b/tests/subsys/usb/host/class_api/boards/native_sim_64.overlay new file mode 100644 index 0000000000000..e3ea1991fbdfe --- /dev/null +++ b/tests/subsys/usb/host/class_api/boards/native_sim_64.overlay @@ -0,0 +1,7 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "native_sim.overlay" diff --git a/tests/subsys/usb/host/class_api/boards/qemu_cortex_m3.overlay b/tests/subsys/usb/host/class_api/boards/qemu_cortex_m3.overlay new file mode 100644 index 0000000000000..4dd05c0fe9975 --- /dev/null +++ b/tests/subsys/usb/host/class_api/boards/qemu_cortex_m3.overlay @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/ { + zephyr_uhc0: uhc_vrt0 { + compatible = "zephyr,uhc-virtual"; + + zephyr_udc0: udc_vrt0 { + compatible = "zephyr,udc-virtual"; + num-bidir-endpoints = <8>; + maximum-speed = "high-speed"; + }; + }; +}; diff --git a/tests/subsys/usb/host/class_api/prj.conf b/tests/subsys/usb/host/class_api/prj.conf new file mode 100644 index 0000000000000..74f0aae233c58 --- /dev/null +++ b/tests/subsys/usb/host/class_api/prj.conf @@ -0,0 +1,8 @@ +# Copyright (c) 2025 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +CONFIG_LOG=y +CONFIG_ZTEST=y + +CONFIG_USB_HOST_STACK=y +CONFIG_UHC_DRIVER=y diff --git a/tests/subsys/usb/host/class_api/src/main.c b/tests/subsys/usb/host/class_api/src/main.c new file mode 100644 index 0000000000000..4f9fa72c86c5b --- /dev/null +++ b/tests/subsys/usb/host/class_api/src/main.c @@ -0,0 +1,306 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include "usbh_ch9.h" +#include "usbh_class_api.h" +#include "usbh_desc.h" +#include "usbh_host.h" +#include "usbh_class.h" + +#include "test_descriptor.h" + +#include +LOG_MODULE_REGISTER(usb_test, LOG_LEVEL_DBG); + +USBH_CONTROLLER_DEFINE(uhs_ctx, DEVICE_DT_GET(DT_NODELABEL(zephyr_uhc0))); + +#define TEST_VID 0x2FE3 +#define TEST_PID 0x0002 + +static const uint8_t test_hub_descriptor[] = { + TEST_HUB_DESCRIPTOR +}; + +struct usb_device test_udev = { + .cfg_desc = (void *)test_hub_descriptor, +}; + +/* Private class data, here just an integer but usually a custom struct. */ +struct test_class_priv { + enum { + /* Test value stored before the class is initialized */ + TEST_CLASS_PRIV_INACTIVE, + /* Test value stored after the class is initialized */ + TEST_CLASS_PRIV_IDLE, + /* Test value stored after the class is probed */ + TEST_CLASS_PRIV_ENABLED, + /* Test value stored after the class is initialized */ + TEST_CLASS_PRIV_INITIALIZED, + /* Test value stored after the class is suspended */ + TEST_CLASS_PRIV_SUSPENDED, + } state; +}; + +static struct test_class_priv test_class_priv = { + .state = TEST_CLASS_PRIV_INACTIVE, +}; + +static int test_class_init(struct usbh_class_data *const c_data, + struct usbh_context *const uhs_ctx) +{ + struct test_class_priv *priv = c_data->priv; + + LOG_DBG("initializing %p, priv value 0x%x", c_data, *priv); + + zassert_equal(priv->state, TEST_CLASS_PRIV_INACTIVE, + "Class should be initialized only once"); + + priv->state = TEST_CLASS_PRIV_IDLE; + + return 0; +} + +static int test_class_completion_cb(struct usbh_class_data *const c_data, + struct uhc_transfer *const xfer) +{ + struct test_class_priv *priv = c_data->priv; + + LOG_DBG("completion callback for %p, transfer %p", c_data, xfer); + + zassert_equal(priv->state, TEST_CLASS_PRIV_ENABLED); + + return -ENOTSUP; +} + +static int test_class_probe(struct usbh_class_data *const c_data, + struct usb_device *const udev, + const uint8_t iface) +{ + struct test_class_priv *priv = c_data->priv; + const void *const desc_beg = test_hub_descriptor; + const void *const desc_end = test_hub_descriptor + sizeof(test_hub_descriptor); + const struct usb_desc_header *desc = desc_beg; + const struct usb_if_descriptor *if_desc; + + zassert_equal(priv->state, TEST_CLASS_PRIV_IDLE); + + desc = usbh_desc_get_by_iface(desc, desc_end, iface); + if (desc == NULL) { + return -ENOENT; + } + + if (desc->bDescriptorType != USB_DESC_INTERFACE) { + return -ENOTSUP; + } + + if_desc = (struct usb_if_descriptor *)desc; + if (if_desc->bInterfaceClass != USB_HUB_CLASSCODE || + if_desc->bInterfaceSubClass != 0x00) { + return -ENOTSUP; + } + + priv->state = TEST_CLASS_PRIV_ENABLED; + + return 0; +} + +static int test_class_removed(struct usbh_class_data *const c_data) +{ + struct test_class_priv *priv = c_data->priv; + + zassert_equal(priv->state, TEST_CLASS_PRIV_ENABLED); + + priv->state = TEST_CLASS_PRIV_IDLE; + + return 0; +} + +static int test_class_suspended(struct usbh_class_data *const c_data) +{ + struct test_class_priv *priv = c_data->priv; + + zassert_equal(priv->state, TEST_CLASS_PRIV_ENABLED); + + priv->state = TEST_CLASS_PRIV_SUSPENDED; + + return 0; +} + +static int test_class_resumed(struct usbh_class_data *const c_data) +{ + struct test_class_priv *priv = c_data->priv; + + zassert_equal(priv->state, TEST_CLASS_PRIV_SUSPENDED); + + priv->state = TEST_CLASS_PRIV_ENABLED; + + return 0; +} + +static struct usbh_class_api test_class_api = { + .init = &test_class_init, + .completion_cb = &test_class_completion_cb, + .probe = &test_class_probe, + .removed = &test_class_removed, + .suspended = &test_class_suspended, + .resumed = &test_class_resumed, +}; + +struct usbh_class_filter test_filters[] = { + { + .class = 9, + .sub = 0, + .proto = 1, + .flags = USBH_CLASS_MATCH_CODE_TRIPLE, + }, + { + .vid = TEST_VID, + .pid = TEST_PID, + .flags = USBH_CLASS_MATCH_VID | USBH_CLASS_MATCH_PID, + }, +}; + +/* Define a class used in the tests */ +USBH_DEFINE_CLASS(test_class, &test_class_api, &test_class_priv, + test_filters, ARRAY_SIZE(test_filters)); + +ZTEST(host_class, test_class_matchiing) +{ + struct usb_device *udev = &test_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; + struct usbh_class_filter info_ok = { + .vid = TEST_VID, + .pid = TEST_PID, + }; + struct usbh_class_filter info_wrong_vid = { + .vid = TEST_VID + 1, + .pid = TEST_PID, + }; + struct usbh_class_filter info_wrong_pid = { + .vid = TEST_VID, + .pid = TEST_PID + 1, + }; + int ret; + + zassert(usbh_class_is_matching(test_filters, ARRAY_SIZE(test_filters), &info_ok), + "Filtering on valid VID:PID should match"); + + zassert(!usbh_class_is_matching(test_filters, ARRAY_SIZE(test_filters), &info_wrong_vid), + "Filtering on invalid VID only should not match"); + + zassert(!usbh_class_is_matching(test_filters, ARRAY_SIZE(test_filters), &info_wrong_pid), + "Filtering on invalid PID only should not match"); + + desc = usbh_desc_get_next_function(desc_beg, desc_end); + zassert_not_null(desc, "There should be at least a function descriptor"); + + ret = usbh_desc_get_iface_info(desc, &info_ok, 0); + zassert_ok(ret, "Expecting the class info to be found"); + + ret = usbh_desc_get_iface_info(desc, &info_wrong_pid, 0); + zassert_ok(ret, "Expecting the class info to be found"); + + ret = usbh_desc_get_iface_info(desc, &info_wrong_vid, 0); + zassert_ok(ret, "Expecting the class info to be found"); + + zassert(usbh_class_is_matching(test_filters, ARRAY_SIZE(test_filters), &info_ok), + "Filteringon valid VID:PID and code triple should match"); + + zassert(usbh_class_is_matching(test_filters, ARRAY_SIZE(test_filters), &info_wrong_vid), + "Filtering on code triple only should match"); + + zassert(usbh_class_is_matching(test_filters, ARRAY_SIZE(test_filters), &info_wrong_pid), + "Filtering on code triple only should match"); +} + +ZTEST(host_class, test_class_fake_device) +{ + struct usbh_class_data *c_data = test_class.c_data; + struct usb_device *udev = &test_udev; + struct test_class_priv *priv = c_data->priv; + int ret; + + zassert_equal(priv->state, TEST_CLASS_PRIV_IDLE, + "The class should have been initialized by usbh_init()"); + + ret = usbh_class_probe(c_data, udev, 1); + zassert_equal(ret, -ENOENT, "There is no interface 1 so should be rejected"); + zassert_equal(priv->state, TEST_CLASS_PRIV_IDLE, + "The class should not be enabled if probing failed"); + + ret = usbh_class_probe(c_data, udev, 0); + zassert_ok(ret, "The interface 0 should match in this example class (%s)", strerror(-ret)); + zassert_equal(priv->state, TEST_CLASS_PRIV_ENABLED, + "The class should be enabled if probing succeeded"); + + ret = usbh_class_suspended(c_data); + zassert_ok(ret, "Susupending the class while it is running should succeed"); + zassert_equal(priv->state, TEST_CLASS_PRIV_SUSPENDED, + "The class private state should have been updated"); + + ret = usbh_class_resumed(c_data); + zassert_ok(ret, "Resuming the class after suspending should succeed"); + zassert_equal(priv->state, TEST_CLASS_PRIV_ENABLED, + "The class private state should have been updated"); + + ret = usbh_class_removed(c_data); + zassert_ok(ret, "Removing the class after probing it should succeed"); + zassert_equal(priv->state, TEST_CLASS_PRIV_IDLE, + "The class should be back to inactive "); + + ret = usbh_class_probe(c_data, udev, 0); + zassert_ok(ret, "Probing the class again should succeed"); + zassert_equal(priv->state, TEST_CLASS_PRIV_ENABLED, + "The class should be back to active "); +} + +static void *usb_test_enable(void) +{ + int ret; + + ret = usbh_init(&uhs_ctx); + zassert_ok(ret, "Failed to initialize USB host (%s)", strerror(-ret)); + + ret = usbh_enable(&uhs_ctx); + zassert_ok(ret, "Failed to enable USB host (%s)", strerror(-ret)); + + ret = uhc_bus_reset(uhs_ctx.dev); + zassert_ok(ret, "Failed to signal bus reset (%s)", strerror(-ret)); + + ret = uhc_bus_resume(uhs_ctx.dev); + zassert_ok(ret, "Failed to signal bus resume (%s)", strerror(-ret)); + + ret = uhc_sof_enable(uhs_ctx.dev); + zassert_ok(ret, "Failed to enable SoF generator (%s)", strerror(-ret)); + + LOG_INF("Host controller enabled"); + + /* Allow the host time to reset the host. */ + k_msleep(200); + + return NULL; +} + +static void usb_test_shutdown(void *f) +{ + int ret; + + ret = usbh_disable(&uhs_ctx); + zassert_ok(ret, "Failed to enable host support (%s)", strerror(-ret)); + + ret = usbh_shutdown(&uhs_ctx); + zassert_ok(ret, "Failed to shutdown host support (%s)", strerror(-ret)); + + LOG_INF("Host controller disabled"); +} + +ZTEST_SUITE(host_class, NULL, usb_test_enable, NULL, NULL, usb_test_shutdown); diff --git a/tests/subsys/usb/host/class_api/testcase.yaml b/tests/subsys/usb/host/class_api/testcase.yaml new file mode 100644 index 0000000000000..3ff951fc47a9f --- /dev/null +++ b/tests/subsys/usb/host/class_api/testcase.yaml @@ -0,0 +1,17 @@ +tests: + usb.host.class_api: + platform_allow: + - native_sim + - native_sim/native/64 + - qemu_cortex_m3 + integration_platforms: + - native_sim + tags: usb + usb.host.class_api.build_all: + platform_allow: + - native_sim + - native_sim/native/64 + integration_platforms: + - native_sim + tags: usb + build_only: true diff --git a/tests/subsys/usb/host/common/include/test_descriptor.h b/tests/subsys/usb/host/common/include/test_descriptor.h new file mode 100644 index 0000000000000..02b5cfdab36e8 --- /dev/null +++ b/tests/subsys/usb/host/common/include/test_descriptor.h @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_TEST_USB_HOST_SAMPLE_DESC_H_ +#define ZEPHYR_TEST_USB_HOST_SAMPLE_DESC_H_ + +#include +#include + +#define _LE16(n) ((n) & 0xff), ((n) >> 8) + +/* bInterfaceClass of these descriptors */ +#define USB_HUB_CLASSCODE 0x09 + +/* + * Obtained with lsusb then verified against the + * USB 2.0 standard's sample HUB descriptor + */ + +#define TEST_HUB_DEVICE_DESCRIPTOR \ + 18, /* bLength */ \ + 1, /* bDescriptorType */ \ + _LE16(0x0200), /* bcdUSB */ \ + 0x09, /* bDeviceClass */ \ + 0x00, /* bDeviceSubClass */ \ + 0x02, /* bDeviceProtocol */ \ + 64, /* bMaxPacketSize0 */ \ + _LE16(0x0bda), /* idVendor */ \ + _LE16(0x5411), /* idProduct */ \ + _LE16(0x0001), /* bcdDevice */ \ + 0, /* iManufacturer */ \ + 0, /* iProduct */ \ + 0, /* iSerial */ \ + 1, /* bNumConfigurations */ + +#define TEST_HUB_CONFIG_DESCRIPTOR \ + 9, /* bLength */ \ + 2, /* bDescriptorType */ \ + _LE16(0x0029), /* wTotalLength */ \ + 1, /* bNumInterfaces */ \ + 1, /* bConfigurationValue */ \ + 0, /* iConfiguration */ \ + 0xe0, /* bmAttributes */ \ + 0, /* MaxPower */ + +#define TEST_HUB_INTERFACE_ALT0_DESCRIPTOR \ + 9, /* bLength */ \ + 4, /* bDescriptorType */ \ + 0, /* bInterfaceNumber */ \ + 0, /* bAlternateSetting */ \ + 1, /* bNumEndpoints */ \ + 9, /* bInterfaceClass */ \ + 0, /* bInterfaceSubClass */ \ + 1, /* bInterfaceProtocol */ \ + 0, /* iInterface */ + +#define TEST_HUB_INTERFACE_ALT1_DESCRIPTOR \ + 9, /* bLength */ \ + 4, /* bDescriptorType */ \ + 0, /* bInterfaceNumber */ \ + 1, /* bAlternateSetting */ \ + 1, /* bNumEndpoints */ \ + 9, /* bInterfaceClass */ \ + 0, /* bInterfaceSubClass */ \ + 2, /* bInterfaceProtocol */ \ + 0, /* iInterface */ + +#define TEST_HUB_ENDPOINT_DESCRIPTOR \ + 7, /* bLength */ \ + 5, /* bDescriptorType */ \ + 0x81, /* bEndpointAddress */ \ + 0x03, /* bmAttributes */ \ + _LE16(1), /* wMaxPacketSize */ \ + 12, /* bInterval */ + +#define TEST_HUB_DESCRIPTOR \ + TEST_HUB_CONFIG_DESCRIPTOR \ + TEST_HUB_INTERFACE_ALT0_DESCRIPTOR \ + TEST_HUB_ENDPOINT_DESCRIPTOR \ + TEST_HUB_INTERFACE_ALT1_DESCRIPTOR \ + TEST_HUB_ENDPOINT_DESCRIPTOR + +#endif /* ZEPHYR_TEST_USB_HOST_SAMPLE_DESC_H_ */ diff --git a/tests/subsys/usb/host/desc/CMakeLists.txt b/tests/subsys/usb/host/desc/CMakeLists.txt new file mode 100644 index 0000000000000..d99885afb1d47 --- /dev/null +++ b/tests/subsys/usb/host/desc/CMakeLists.txt @@ -0,0 +1,12 @@ +# Copyright (c) 2025 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(test_usb_host) + +target_include_directories(app PRIVATE ${ZEPHYR_BASE}/subsys/usb/host) +target_include_directories(app PRIVATE ${ZEPHYR_BASE}/tests/subsys/usb/host/common/include) + +target_sources(app PRIVATE src/main.c) diff --git a/tests/subsys/usb/host/desc/boards/native_sim.conf b/tests/subsys/usb/host/desc/boards/native_sim.conf new file mode 100644 index 0000000000000..68e22b2de6a0c --- /dev/null +++ b/tests/subsys/usb/host/desc/boards/native_sim.conf @@ -0,0 +1 @@ +CONFIG_SYS_CLOCK_TICKS_PER_SEC=1000000 diff --git a/tests/subsys/usb/host/desc/boards/native_sim.overlay b/tests/subsys/usb/host/desc/boards/native_sim.overlay new file mode 100644 index 0000000000000..d352f555c8c89 --- /dev/null +++ b/tests/subsys/usb/host/desc/boards/native_sim.overlay @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/delete-node/ &zephyr_udc0; + +/ { + zephyr_uhc0: uhc_vrt0 { + compatible = "zephyr,uhc-virtual"; + + zephyr_udc0: udc_vrt0 { + compatible = "zephyr,udc-virtual"; + num-bidir-endpoints = <8>; + maximum-speed = "high-speed"; + }; + }; +}; diff --git a/tests/subsys/usb/host/desc/boards/native_sim_64.conf b/tests/subsys/usb/host/desc/boards/native_sim_64.conf new file mode 100644 index 0000000000000..68e22b2de6a0c --- /dev/null +++ b/tests/subsys/usb/host/desc/boards/native_sim_64.conf @@ -0,0 +1 @@ +CONFIG_SYS_CLOCK_TICKS_PER_SEC=1000000 diff --git a/tests/subsys/usb/host/desc/boards/native_sim_64.overlay b/tests/subsys/usb/host/desc/boards/native_sim_64.overlay new file mode 100644 index 0000000000000..e3ea1991fbdfe --- /dev/null +++ b/tests/subsys/usb/host/desc/boards/native_sim_64.overlay @@ -0,0 +1,7 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "native_sim.overlay" diff --git a/tests/subsys/usb/host/desc/boards/qemu_cortex_m3.overlay b/tests/subsys/usb/host/desc/boards/qemu_cortex_m3.overlay new file mode 100644 index 0000000000000..4dd05c0fe9975 --- /dev/null +++ b/tests/subsys/usb/host/desc/boards/qemu_cortex_m3.overlay @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/ { + zephyr_uhc0: uhc_vrt0 { + compatible = "zephyr,uhc-virtual"; + + zephyr_udc0: udc_vrt0 { + compatible = "zephyr,udc-virtual"; + num-bidir-endpoints = <8>; + maximum-speed = "high-speed"; + }; + }; +}; diff --git a/tests/subsys/usb/host/desc/prj.conf b/tests/subsys/usb/host/desc/prj.conf new file mode 100644 index 0000000000000..74f0aae233c58 --- /dev/null +++ b/tests/subsys/usb/host/desc/prj.conf @@ -0,0 +1,8 @@ +# Copyright (c) 2025 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +CONFIG_LOG=y +CONFIG_ZTEST=y + +CONFIG_USB_HOST_STACK=y +CONFIG_UHC_DRIVER=y diff --git a/tests/subsys/usb/host/desc/src/main.c b/tests/subsys/usb/host/desc/src/main.c new file mode 100644 index 0000000000000..ee2b6ad47c88e --- /dev/null +++ b/tests/subsys/usb/host/desc/src/main.c @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include "usbh_desc.h" + +#include "test_descriptor.h" + +static const uint8_t test_hub_descriptor[] = {TEST_HUB_DESCRIPTOR}; + +static const uint8_t test_hub_config_descriptor[] = {TEST_HUB_CONFIG_DESCRIPTOR}; +static const uint8_t test_hub_interface_alt0_descriptor[] = {TEST_HUB_INTERFACE_ALT0_DESCRIPTOR}; +static const uint8_t test_hub_interface_alt1_descriptor[] = {TEST_HUB_INTERFACE_ALT1_DESCRIPTOR}; +static const uint8_t test_hub_endpoint_descriptor[] = {TEST_HUB_ENDPOINT_DESCRIPTOR}; + +ZTEST(host_desc, test_desc_get_by_type) +{ + const uint8_t *const start_addr = test_hub_descriptor; + const uint8_t *const end_addr = test_hub_descriptor + sizeof(test_hub_descriptor); + const struct usb_desc_header *desc; + + /* From "test_descriptor.h": + * #0 TEST_HUB_CONFIG_DESCRIPTOR + * #1 TEST_HUB_INTERFACE_ALT0_DESCRIPTOR + * #2 TEST_HUB_ENDPOINT_DESCRIPTOR + * #3 TEST_HUB_INTERFACE_ALT1_DESCRIPTOR + * #4 TEST_HUB_ENDPOINT_DESCRIPTOR + */ + desc = (const struct usb_desc_header *)start_addr; + + /* desc at #0 TEST_HUB_CONFIG_DESCRIPTOR */ + + zassert_mem_equal(desc, test_hub_config_descriptor, + sizeof(test_hub_config_descriptor), + "needs to be able to reach the config descriptor"); + + /* desc at #0 TEST_HUB_CONFIG_DESCRIPTOR */ + + desc = usbh_desc_get_next(desc, end_addr); + zassert_not_null(desc); + zassert_mem_equal(desc, test_hub_interface_alt0_descriptor, + sizeof(test_hub_interface_alt0_descriptor), + "needs to return the first match (interface 0 alt 1) only"); + + /* desc at #1 TEST_HUB_INTERFACE_ALT0_DESCRIPTOR */ + + desc = usbh_desc_get_next_alt_setting(desc, end_addr); + zassert_not_null(desc); + zassert_mem_equal(desc, test_hub_interface_alt1_descriptor, + sizeof(test_hub_interface_alt1_descriptor), + "needs to return the interface 0 alt 1 descriptor"); + + /* desc at #3 TEST_HUB_INTERFACE_ALT1_DESCRIPTOR */ + + desc = usbh_desc_get_next(desc, end_addr); + zassert_not_null(desc); + zassert_mem_equal(desc, test_hub_endpoint_descriptor, + sizeof(test_hub_endpoint_descriptor), + "needs to return the last descriptor (endpoint)"); + + /* desc at #4 TEST_HUB_ENDPOINT_DESCRIPTOR */ + + desc = usbh_desc_get_next(desc, end_addr); + zassert_is_null(desc, + "needs to return NULL when running past the end"); +} + +ZTEST_SUITE(host_desc, NULL, NULL, NULL, NULL, NULL); diff --git a/tests/subsys/usb/host/desc/testcase.yaml b/tests/subsys/usb/host/desc/testcase.yaml new file mode 100644 index 0000000000000..3eef037a273dc --- /dev/null +++ b/tests/subsys/usb/host/desc/testcase.yaml @@ -0,0 +1,9 @@ +tests: + usb.host.desc: + platform_allow: + - native_sim + - native_sim/native/64 + - qemu_cortex_m3 + integration_platforms: + - native_sim + tags: usb From dc61cc2712d8671bb533ec93f3843078ac9b1ea0 Mon Sep 17 00:00:00 2001 From: Josuah Demangeon Date: Thu, 11 Sep 2025 19:32:25 +0000 Subject: [PATCH 08/34] tests: usb: host: test basic API functionality Add tests making sure the USB Host class APIs introduced build and run as expected. Signed-off-by: Josuah Demangeon --- .../host/class_api/include/test_descriptor.h | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 tests/subsys/usb/host/class_api/include/test_descriptor.h diff --git a/tests/subsys/usb/host/class_api/include/test_descriptor.h b/tests/subsys/usb/host/class_api/include/test_descriptor.h new file mode 100644 index 0000000000000..ac29e50aefee7 --- /dev/null +++ b/tests/subsys/usb/host/class_api/include/test_descriptor.h @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_TEST_USB_HOST_SAMPLE_DESC_H_ +#define ZEPHYR_TEST_USB_HOST_SAMPLE_DESC_H_ + +#include +#include + +#define _LE16(n) ((n) & 0xff), ((n) >> 8) + +/* + * Obtained with lsusb then verified against the + * USB 2.0 standard's sample HUB descriptor + */ + +#define TEST_HUB_DEVICE_DESCRIPTOR \ + 18, /* bLength */ \ + 1, /* bDescriptorType */ \ + _LE16(0x0200), /* bcdUSB */ \ + 0x09, /* bDeviceClass */ \ + 0x00, /* bDeviceSubClass */ \ + 0x02, /* bDeviceProtocol */ \ + 64, /* bMaxPacketSize0 */ \ + _LE16(0x0bda), /* idVendor */ \ + _LE16(0x5411), /* idProduct */ \ + _LE16(0x0001), /* bcdDevice */ \ + 0, /* iManufacturer */ \ + 0, /* iProduct */ \ + 0, /* iSerial */ \ + 1, /* bNumConfigurations */ + +#define TEST_HUB_CONFIG_DESCRIPTOR \ + 9, /* bLength */ \ + 2, /* bDescriptorType */ \ + _LE16(0x0029), /* wTotalLength */ \ + 1, /* bNumInterfaces */ \ + 1, /* bConfigurationValue */ \ + 0, /* iConfiguration */ \ + 0xe0, /* bmAttributes */ \ + 0, /* MaxPower */ + +#define TEST_HUB_INTERFACE_ALT0_DESCRIPTOR \ + 9, /* bLength */ \ + 4, /* bDescriptorType */ \ + 0, /* bInterfaceNumber */ \ + 0, /* bAlternateSetting */ \ + 1, /* bNumEndpoints */ \ + 9, /* bInterfaceClass */ \ + 0, /* bInterfaceSubClass */ \ + 1, /* bInterfaceProtocol */ \ + 0, /* iInterface */ + +#define TEST_HUB_INTERFACE_ALT1_DESCRIPTOR \ + 9, /* bLength */ \ + 4, /* bDescriptorType */ \ + 0, /* bInterfaceNumber */ \ + 1, /* bAlternateSetting */ \ + 1, /* bNumEndpoints */ \ + 9, /* bInterfaceClass */ \ + 0, /* bInterfaceSubClass */ \ + 2, /* bInterfaceProtocol */ \ + 0, /* iInterface */ + +#define TEST_HUB_ENDPOINT_DESCRIPTOR \ + 7, /* bLength */ \ + 5, /* bDescriptorType */ \ + 0x81, /* bEndpointAddress */ \ + 0x03, /* bmAttributes */ \ + _LE16(1), /* wMaxPacketSize */ \ + 12, /* bInterval */ + +#define TEST_HUB_DESCRIPTOR \ + TEST_HUB_DEVICE_DESCRIPTOR \ + TEST_HUB_CONFIG_DESCRIPTOR \ + TEST_HUB_INTERFACE_ALT0_DESCRIPTOR \ + TEST_HUB_ENDPOINT_DESCRIPTOR \ + TEST_HUB_INTERFACE_ALT1_DESCRIPTOR \ + TEST_HUB_ENDPOINT_DESCRIPTOR + +#endif /* ZEPHYR_TEST_USB_HOST_SAMPLE_DESC_H_ */ From 949e79207a3162e60efa198658ecbc177a3e85a7 Mon Sep 17 00:00:00 2001 From: Josuah Demangeon Date: Fri, 25 Jul 2025 12:09:06 +0000 Subject: [PATCH 09/34] usb: uvc: move the header definition to include/ Move the UVC header with all the definitions from the UVC standard to share it between USB host and device class implementation. Signed-off-by: Josuah Demangeon --- subsys/usb/common/CMakeLists.txt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/subsys/usb/common/CMakeLists.txt b/subsys/usb/common/CMakeLists.txt index 1dfd227bd5081..c95c8594654b0 100644 --- a/subsys/usb/common/CMakeLists.txt +++ b/subsys/usb/common/CMakeLists.txt @@ -1,4 +1,6 @@ -# Copyright (c) 2025 Nordic Semiconductor +# Copyright (c) 2025 Nordic Semiconductor ASA # SPDX-License-Identifier: Apache-2.0 -zephyr_include_directories(include) +if(CONFIG_USBD_VIDEO_CLASS OR CONFIG_USBH_VIDEO_CLASS) + zephyr_include_directories(include) +endif() From c9dd480ec2ab24fbbc5f92a9ea28ac97bcc3372b Mon Sep 17 00:00:00 2001 From: Josuah Demangeon Date: Sun, 17 Aug 2025 19:18:33 +0000 Subject: [PATCH 10/34] uvc: device: move helper functions to a common directory Move UVC helper functions to a file shared between UVC host and device. The arrays are not visible anymore from either USB host or device, but instead accessed through a front-end funciton. Signed-off-by: Josuah Demangeon --- subsys/usb/common/CMakeLists.txt | 1 + subsys/usb/common/include/usb_common_uvc.h | 81 ++++++ subsys/usb/common/usb_common_uvc.c | 234 ++++++++++++++++++ subsys/usb/device_next/class/usbd_uvc.c | 271 ++------------------- 4 files changed, 332 insertions(+), 255 deletions(-) create mode 100644 subsys/usb/common/include/usb_common_uvc.h create mode 100644 subsys/usb/common/usb_common_uvc.c diff --git a/subsys/usb/common/CMakeLists.txt b/subsys/usb/common/CMakeLists.txt index c95c8594654b0..5015d5c438030 100644 --- a/subsys/usb/common/CMakeLists.txt +++ b/subsys/usb/common/CMakeLists.txt @@ -3,4 +3,5 @@ if(CONFIG_USBD_VIDEO_CLASS OR CONFIG_USBH_VIDEO_CLASS) zephyr_include_directories(include) + zephyr_sources(usb_common_uvc.c) endif() diff --git a/subsys/usb/common/include/usb_common_uvc.h b/subsys/usb/common/include/usb_common_uvc.h new file mode 100644 index 0000000000000..aed1f23fde6dc --- /dev/null +++ b/subsys/usb/common/include/usb_common_uvc.h @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_INCLUDE_USB_COMMON_UVC_H +#define ZEPHYR_INCLUDE_USB_COMMON_UVC_H + +#include + +/** + * @brief Type of value used by the USB protocol for this control. + */ +enum uvc_control_type { + /** Signed integer control type */ + UVC_CONTROL_SIGNED, + /** Unsigned integer control type */ + UVC_CONTROL_UNSIGNED, +}; + +/** + * @brief Mapping between UVC controls and Video controls + */ +struct uvc_control_map { + /* Video CID to use for this control */ + uint32_t cid; + /* Size to write out */ + uint8_t size; + /* Bit position in the UVC control */ + uint8_t bit; + /* UVC selector identifying this control */ + uint8_t selector; + /* Whether the UVC value is signed, always false for bitmaps and boolean */ + enum uvc_control_type type; +}; + +/** + * @brief Mapping between UVC GUIDs and standard FourCC. + */ +struct uvc_guid_quirk { + /* A Video API format identifier, for which the UVC format GUID is not standard. */ + uint32_t fourcc; + /* GUIDs are 16-bytes long, with the first four bytes being the Four Character Code of the + * format and the rest constant, except for some exceptions listed in this table. + */ + uint8_t guid[16]; +}; + +/** + * @brief Get a conversion table for a given control unit type + * + * The mappings contains information about how UVC control structures are related to + * video control structures. + * + * @param subtype The field bDescriptorSubType of a descriptor of type USB_DESC_CS_INTERFACE. + * @param map Filled with a pointer to the conversion table + * @param length Filled with the number of elements in that conversion table. + * + * @return 0 on success, negative code on error. + */ +int uvc_get_control_map(uint8_t subtype, const struct uvc_control_map **map, size_t *length); + +/** + * @brief Convert a standard FourCC to an equivalent UVC GUID. + * + * @param guid Array to a GUID, filled in binary format + * @param fourcc Four character code + */ +void uvc_fourcc_to_guid(uint8_t guid[16], const uint32_t fourcc); + +/** + * @brief Convert an UVC GUID to a standard FourCC + * + * @param guid GUID, to convert + * + * @return Four Character Code + */ +uint32_t uvc_guid_to_fourcc(const uint8_t guid[16]); + +#endif /* ZEPHYR_INCLUDE_USB_COMMON_UVC_H */ diff --git a/subsys/usb/common/usb_common_uvc.c b/subsys/usb/common/usb_common_uvc.c new file mode 100644 index 0000000000000..980724ae799d9 --- /dev/null +++ b/subsys/usb/common/usb_common_uvc.c @@ -0,0 +1,234 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include +#include +#include + +#include "usb_common_uvc.h" +#include "usb_uvc.h" + +static const struct uvc_guid_quirk uvc_guid_quirks[] = { + { + .fourcc = VIDEO_PIX_FMT_YUYV, + .guid = UVC_FORMAT_GUID("YUY2"), + }, + { + .fourcc = VIDEO_PIX_FMT_GREY, + .guid = UVC_FORMAT_GUID("Y800"), + }, +}; + +static const struct uvc_control_map uvc_control_map_ct[] = { + { + .size = 1, + .bit = 1, + .selector = UVC_CT_AE_MODE_CONTROL, + .cid = VIDEO_CID_EXPOSURE_AUTO, + .type = UVC_CONTROL_UNSIGNED, + }, + { + .size = 1, + .bit = 2, + .selector = UVC_CT_AE_PRIORITY_CONTROL, + .cid = VIDEO_CID_EXPOSURE_AUTO_PRIORITY, + .type = UVC_CONTROL_UNSIGNED, + }, + { + .size = 4, + .bit = 3, + .selector = UVC_CT_EXPOSURE_TIME_ABS_CONTROL, + .cid = VIDEO_CID_EXPOSURE, + .type = UVC_CONTROL_UNSIGNED, + }, + { + .size = 2, + .bit = 5, + .selector = UVC_CT_FOCUS_ABS_CONTROL, + .cid = VIDEO_CID_FOCUS_ABSOLUTE, + .type = UVC_CONTROL_UNSIGNED, + }, + { + .size = 2, + .bit = 6, + .selector = UVC_CT_FOCUS_REL_CONTROL, + .cid = VIDEO_CID_FOCUS_RELATIVE, + .type = UVC_CONTROL_SIGNED, + }, + { + .size = 2, + .bit = 7, + .selector = UVC_CT_IRIS_ABS_CONTROL, + .cid = VIDEO_CID_IRIS_ABSOLUTE, + .type = UVC_CONTROL_UNSIGNED, + }, + { + .size = 1, + .bit = 8, + .selector = UVC_CT_IRIS_REL_CONTROL, + .cid = VIDEO_CID_IRIS_RELATIVE, + .type = UVC_CONTROL_SIGNED, + }, + { + .size = 2, + .bit = 9, + .selector = UVC_CT_ZOOM_ABS_CONTROL, + .cid = VIDEO_CID_ZOOM_ABSOLUTE, + .type = UVC_CONTROL_UNSIGNED, + }, + { + .size = 3, + .bit = 10, + .selector = UVC_CT_ZOOM_REL_CONTROL, + .cid = VIDEO_CID_ZOOM_RELATIVE, + .type = UVC_CONTROL_SIGNED, + }, +}; + +static const struct uvc_control_map uvc_control_map_pu[] = { + { + .size = 2, + .bit = 0, + .selector = UVC_PU_BRIGHTNESS_CONTROL, + .cid = VIDEO_CID_BRIGHTNESS, + .type = UVC_CONTROL_SIGNED, + }, + { + .size = 1, + .bit = 1, + .selector = UVC_PU_CONTRAST_CONTROL, + .cid = VIDEO_CID_CONTRAST, + .type = UVC_CONTROL_UNSIGNED, + }, + { + .size = 2, + .bit = 9, + .selector = UVC_PU_GAIN_CONTROL, + .cid = VIDEO_CID_GAIN, + .type = UVC_CONTROL_UNSIGNED, + }, + { + .size = 2, + .bit = 3, + .selector = UVC_PU_SATURATION_CONTROL, + .cid = VIDEO_CID_SATURATION, + .type = UVC_CONTROL_UNSIGNED, + }, + { + .size = 2, + .bit = 6, + .selector = UVC_PU_WHITE_BALANCE_TEMP_CONTROL, + .cid = VIDEO_CID_WHITE_BALANCE_TEMPERATURE, + .type = UVC_CONTROL_UNSIGNED, + }, +}; + +static const struct uvc_control_map uvc_control_map_su[] = { + { + .size = 1, + .bit = 0, + .selector = UVC_SU_INPUT_SELECT_CONTROL, + .cid = VIDEO_CID_TEST_PATTERN, + .type = UVC_CONTROL_UNSIGNED, + }, +}; + +static const struct uvc_control_map uvc_control_map_xu[] = { + { + .size = 4, + .bit = 0, + .selector = UVC_XU_BASE_CONTROL + 0, + .cid = VIDEO_CID_PRIVATE_BASE + 0, + .type = UVC_CONTROL_UNSIGNED, + }, + { + .size = 4, + .bit = 1, + .selector = UVC_XU_BASE_CONTROL + 1, + .cid = VIDEO_CID_PRIVATE_BASE + 1, + .type = UVC_CONTROL_UNSIGNED, + }, + { + .size = 4, + .bit = 2, + .selector = UVC_XU_BASE_CONTROL + 2, + .cid = VIDEO_CID_PRIVATE_BASE + 2, + .type = UVC_CONTROL_UNSIGNED, + }, + { + .size = 4, + .bit = 3, + .selector = UVC_XU_BASE_CONTROL + 3, + .cid = VIDEO_CID_PRIVATE_BASE + 3, + .type = UVC_CONTROL_UNSIGNED, + }, +}; + +int uvc_get_control_map(uint8_t subtype, const struct uvc_control_map **map, size_t *length) +{ + switch (subtype) { + case UVC_VC_INPUT_TERMINAL: + *map = uvc_control_map_ct; + *length = ARRAY_SIZE(uvc_control_map_ct); + break; + case UVC_VC_SELECTOR_UNIT: + *map = uvc_control_map_su; + *length = ARRAY_SIZE(uvc_control_map_su); + break; + case UVC_VC_PROCESSING_UNIT: + *map = uvc_control_map_pu; + *length = ARRAY_SIZE(uvc_control_map_pu); + break; + case UVC_VC_EXTENSION_UNIT: + *map = uvc_control_map_xu; + *length = ARRAY_SIZE(uvc_control_map_xu); + break; + default: + return -EINVAL; + } + + return 0; +} + +void uvc_fourcc_to_guid(uint8_t guid[16], const uint32_t fourcc) +{ + uint32_t fourcc_le; + + /* Lookup in the "quirk table" if the UVC format GUID is custom */ + for (int i = 0; i < ARRAY_SIZE(uvc_guid_quirks); i++) { + if (uvc_guid_quirks[i].fourcc == fourcc) { + memcpy(guid, uvc_guid_quirks[i].guid, 16); + return; + } + } + + /* By default, UVC GUIDs are the four character code followed by a common suffix */ + fourcc_le = sys_cpu_to_le32(fourcc); + /* Copy the common suffix with the GUID set to 'XXXX' */ + memcpy(guid, UVC_FORMAT_GUID("XXXX"), 16); + /* Replace the 'XXXX' by the actual GUID of the format */ + memcpy(guid, &fourcc_le, 4); +} + +uint32_t uvc_guid_to_fourcc(const uint8_t guid[16]) +{ + uint32_t fourcc; + + /* Lookup in the "quirk table" if the UVC format GUID is custom */ + for (int i = 0; i < ARRAY_SIZE(uvc_guid_quirks); i++) { + if (memcmp(guid, uvc_guid_quirks[i].guid, 16) == 0) { + return uvc_guid_quirks[i].fourcc; + } + } + + /* Extract the four character code out of the leading 4 bytes of the GUID */ + memcpy(&fourcc, guid, 4); + fourcc = sys_le32_to_cpu(fourcc); + + return fourcc; +} diff --git a/subsys/usb/device_next/class/usbd_uvc.c b/subsys/usb/device_next/class/usbd_uvc.c index 169e4f30bcb0a..a2f9fb78d2b39 100644 --- a/subsys/usb/device_next/class/usbd_uvc.c +++ b/subsys/usb/device_next/class/usbd_uvc.c @@ -25,6 +25,7 @@ #include #include +#include "usb_common_uvc.h" #include "usb_uvc.h" #include "../../../drivers/video/video_ctrls.h" @@ -63,11 +64,6 @@ enum uvc_unit_id { UVC_UNIT_ID_OT, }; -enum uvc_control_type { - UVC_CONTROL_SIGNED, - UVC_CONTROL_UNSIGNED, -}; - union uvc_fmt_desc { struct usb_desc_header hdr; struct uvc_format_descriptor fmt; @@ -155,29 +151,6 @@ struct uvc_buf_info { struct video_buffer *vbuf; } __packed; -/* Mapping between UVC controls and Video controls */ -struct uvc_control_map { - /* Video CID to use for this control */ - uint32_t cid; - /* Size to write out */ - uint8_t size; - /* Bit position in the UVC control */ - uint8_t bit; - /* UVC selector identifying this control */ - uint8_t selector; - /* Whether the UVC value is signed, always false for bitmaps and boolean */ - enum uvc_control_type type; -}; - -struct uvc_guid_quirk { - /* A Video API format identifier, for which the UVC format GUID is not standard. */ - uint32_t fourcc; - /* GUIDs are 16-bytes long, with the first four bytes being the Four Character Code of the - * format and the rest constant, except for some exceptions listed in this table. - */ - uint8_t guid[16]; -}; - #define UVC_TOTAL_BUFS (DT_NUM_INST_STATUS_OKAY(DT_DRV_COMPAT) * CONFIG_USBD_VIDEO_NUM_BUFS) UDC_BUF_POOL_VAR_DEFINE(uvc_buf_pool, UVC_TOTAL_BUFS, UVC_TOTAL_BUFS * USBD_MAX_BULK_MPS, @@ -187,202 +160,6 @@ static void uvc_flush_queue(const struct device *dev); /* UVC helper functions */ -static const struct uvc_guid_quirk uvc_guid_quirks[] = { - { - .fourcc = VIDEO_PIX_FMT_YUYV, - .guid = UVC_FORMAT_GUID("YUY2"), - }, - { - .fourcc = VIDEO_PIX_FMT_GREY, - .guid = UVC_FORMAT_GUID("Y800"), - }, -}; - -static void uvc_fourcc_to_guid(uint8_t guid[16], const uint32_t fourcc) -{ - uint32_t fourcc_le; - - /* Lookup in the "quirk table" if the UVC format GUID is custom */ - for (int i = 0; i < ARRAY_SIZE(uvc_guid_quirks); i++) { - if (uvc_guid_quirks[i].fourcc == fourcc) { - memcpy(guid, uvc_guid_quirks[i].guid, 16); - return; - } - } - - /* By default, UVC GUIDs are the four character code followed by a common suffix */ - fourcc_le = sys_cpu_to_le32(fourcc); - /* Copy the common suffix with the GUID set to 'XXXX' */ - memcpy(guid, UVC_FORMAT_GUID("XXXX"), 16); - /* Replace the 'XXXX' by the actual GUID of the format */ - memcpy(guid, &fourcc_le, 4); -} - -static uint32_t uvc_guid_to_fourcc(const uint8_t guid[16]) -{ - uint32_t fourcc; - - /* Lookup in the "quirk table" if the UVC format GUID is custom */ - for (int i = 0; i < ARRAY_SIZE(uvc_guid_quirks); i++) { - if (memcmp(guid, uvc_guid_quirks[i].guid, 16) == 0) { - return uvc_guid_quirks[i].fourcc; - } - } - - /* Extract the four character code out of the leading 4 bytes of the GUID */ - memcpy(&fourcc, guid, 4); - fourcc = sys_le32_to_cpu(fourcc); - - return fourcc; -} - -/* UVC control handling */ - -static const struct uvc_control_map uvc_control_map_ct[] = { - { - .size = 1, - .bit = 1, - .selector = UVC_CT_AE_MODE_CONTROL, - .cid = VIDEO_CID_EXPOSURE_AUTO, - .type = UVC_CONTROL_UNSIGNED, - }, - { - .size = 1, - .bit = 2, - .selector = UVC_CT_AE_PRIORITY_CONTROL, - .cid = VIDEO_CID_EXPOSURE_AUTO_PRIORITY, - .type = UVC_CONTROL_UNSIGNED, - }, - { - .size = 4, - .bit = 3, - .selector = UVC_CT_EXPOSURE_TIME_ABS_CONTROL, - .cid = VIDEO_CID_EXPOSURE, - .type = UVC_CONTROL_UNSIGNED, - }, - { - .size = 2, - .bit = 5, - .selector = UVC_CT_FOCUS_ABS_CONTROL, - .cid = VIDEO_CID_FOCUS_ABSOLUTE, - .type = UVC_CONTROL_UNSIGNED, - }, - { - .size = 2, - .bit = 6, - .selector = UVC_CT_FOCUS_REL_CONTROL, - .cid = VIDEO_CID_FOCUS_RELATIVE, - .type = UVC_CONTROL_SIGNED, - }, - { - .size = 2, - .bit = 7, - .selector = UVC_CT_IRIS_ABS_CONTROL, - .cid = VIDEO_CID_IRIS_ABSOLUTE, - .type = UVC_CONTROL_UNSIGNED, - }, - { - .size = 1, - .bit = 8, - .selector = UVC_CT_IRIS_REL_CONTROL, - .cid = VIDEO_CID_IRIS_RELATIVE, - .type = UVC_CONTROL_SIGNED, - }, - { - .size = 2, - .bit = 9, - .selector = UVC_CT_ZOOM_ABS_CONTROL, - .cid = VIDEO_CID_ZOOM_ABSOLUTE, - .type = UVC_CONTROL_UNSIGNED, - }, - { - .size = 3, - .bit = 10, - .selector = UVC_CT_ZOOM_REL_CONTROL, - .cid = VIDEO_CID_ZOOM_RELATIVE, - .type = UVC_CONTROL_SIGNED, - }, -}; - -static const struct uvc_control_map uvc_control_map_pu[] = { - { - .size = 2, - .bit = 0, - .selector = UVC_PU_BRIGHTNESS_CONTROL, - .cid = VIDEO_CID_BRIGHTNESS, - .type = UVC_CONTROL_SIGNED, - }, - { - .size = 1, - .bit = 1, - .selector = UVC_PU_CONTRAST_CONTROL, - .cid = VIDEO_CID_CONTRAST, - .type = UVC_CONTROL_UNSIGNED, - }, - { - .size = 2, - .bit = 9, - .selector = UVC_PU_GAIN_CONTROL, - .cid = VIDEO_CID_GAIN, - .type = UVC_CONTROL_UNSIGNED, - }, - { - .size = 2, - .bit = 3, - .selector = UVC_PU_SATURATION_CONTROL, - .cid = VIDEO_CID_SATURATION, - .type = UVC_CONTROL_UNSIGNED, - }, - { - .size = 2, - .bit = 6, - .selector = UVC_PU_WHITE_BALANCE_TEMP_CONTROL, - .cid = VIDEO_CID_WHITE_BALANCE_TEMPERATURE, - .type = UVC_CONTROL_UNSIGNED, - }, -}; - -static const struct uvc_control_map uvc_control_map_su[] = { - { - .size = 1, - .bit = 0, - .selector = UVC_SU_INPUT_SELECT_CONTROL, - .cid = VIDEO_CID_TEST_PATTERN, - .type = UVC_CONTROL_UNSIGNED, - }, -}; - -static const struct uvc_control_map uvc_control_map_xu[] = { - { - .size = 4, - .bit = 0, - .selector = UVC_XU_BASE_CONTROL + 0, - .cid = VIDEO_CID_PRIVATE_BASE + 0, - .type = UVC_CONTROL_UNSIGNED, - }, - { - .size = 4, - .bit = 1, - .selector = UVC_XU_BASE_CONTROL + 1, - .cid = VIDEO_CID_PRIVATE_BASE + 1, - .type = UVC_CONTROL_UNSIGNED, - }, - { - .size = 4, - .bit = 2, - .selector = UVC_XU_BASE_CONTROL + 2, - .cid = VIDEO_CID_PRIVATE_BASE + 2, - .type = UVC_CONTROL_UNSIGNED, - }, - { - .size = 4, - .bit = 3, - .selector = UVC_XU_BASE_CONTROL + 3, - .cid = VIDEO_CID_PRIVATE_BASE + 3, - .type = UVC_CONTROL_UNSIGNED, - }, -}; - /* Get the format and frame descriptors selected for the given VideoStreaming interface. */ static void uvc_get_vs_fmtfrm_desc(const struct device *dev, struct uvc_format_descriptor **const format_desc, @@ -1168,6 +945,7 @@ static int uvc_get_control_op(const struct device *dev, const struct usb_setup_p uint8_t unit_id = setup->wIndex >> 8; uint8_t selector = setup->wValue >> 8; uint8_t subtype = 0; + int ret; /* VideoStreaming operation */ @@ -1200,17 +978,16 @@ static int uvc_get_control_op(const struct device *dev, const struct usb_setup_p for (int i = UVC_IDX_VC_UNIT;; i++) { struct uvc_unit_descriptor *desc = (void *)cfg->fs_desc[i]; - if (desc->bDescriptorType != USB_DESC_CS_INTERFACE || - (desc->bDescriptorSubtype != UVC_VC_INPUT_TERMINAL && - desc->bDescriptorSubtype != UVC_VC_ENCODING_UNIT && - desc->bDescriptorSubtype != UVC_VC_SELECTOR_UNIT && - desc->bDescriptorSubtype != UVC_VC_EXTENSION_UNIT && - desc->bDescriptorSubtype != UVC_VC_PROCESSING_UNIT)) { + if (desc->bDescriptorType != USB_DESC_CS_INTERFACE) { break; } + ret = uvc_get_control_map(desc->bDescriptorSubtype, &list, &list_sz); + if (ret != 0) { + goto err; + } + if (unit_id == desc->bUnitID) { - subtype = desc->bDescriptorSubtype; break; } } @@ -1219,27 +996,6 @@ static int uvc_get_control_op(const struct device *dev, const struct usb_setup_p goto err; } - switch (subtype) { - case UVC_VC_INPUT_TERMINAL: - list = uvc_control_map_ct; - list_sz = ARRAY_SIZE(uvc_control_map_ct); - break; - case UVC_VC_SELECTOR_UNIT: - list = uvc_control_map_su; - list_sz = ARRAY_SIZE(uvc_control_map_su); - break; - case UVC_VC_PROCESSING_UNIT: - list = uvc_control_map_pu; - list_sz = ARRAY_SIZE(uvc_control_map_pu); - break; - case UVC_VC_EXTENSION_UNIT: - list = uvc_control_map_xu; - list_sz = ARRAY_SIZE(uvc_control_map_xu); - break; - default: - CODE_UNREACHABLE; - } - *map = NULL; for (int i = 0; i < list_sz; i++) { if (list[i].selector == selector) { @@ -1763,7 +1519,9 @@ void uvc_set_video_dev(const struct device *const dev, const struct device *cons { struct uvc_data *data = dev->data; const struct uvc_config *cfg = dev->config; + const struct uvc_control_map *map = NULL; uint32_t mask = 0; + size_t map_sz = 0; data->video_dev = video_dev; @@ -1771,17 +1529,20 @@ void uvc_set_video_dev(const struct device *const dev, const struct device *cons cfg->desc->if0_hdr.baInterfaceNr[0] = cfg->desc->if1.bInterfaceNumber; - mask = uvc_get_mask(data->video_dev, uvc_control_map_ct, ARRAY_SIZE(uvc_control_map_ct)); + uvc_get_control_map(UVC_VC_INPUT_TERMINAL, &map, &map_sz); + mask = uvc_get_mask(data->video_dev, map, map_sz); cfg->desc->if0_ct.bmControls[0] = mask >> 0; cfg->desc->if0_ct.bmControls[1] = mask >> 8; cfg->desc->if0_ct.bmControls[2] = mask >> 16; - mask = uvc_get_mask(data->video_dev, uvc_control_map_pu, ARRAY_SIZE(uvc_control_map_pu)); + uvc_get_control_map(UVC_VC_PROCESSING_UNIT, &map, &map_sz); + mask = uvc_get_mask(data->video_dev, map, map_sz); cfg->desc->if0_pu.bmControls[0] = mask >> 0; cfg->desc->if0_pu.bmControls[1] = mask >> 8; cfg->desc->if0_pu.bmControls[2] = mask >> 16; - mask = uvc_get_mask(data->video_dev, uvc_control_map_xu, ARRAY_SIZE(uvc_control_map_xu)); + uvc_get_control_map(UVC_VC_EXTENSION_UNIT, &map, &map_sz); + mask = uvc_get_mask(data->video_dev, map, map_sz); cfg->desc->if0_xu.bmControls[0] = mask >> 0; cfg->desc->if0_xu.bmControls[1] = mask >> 8; cfg->desc->if0_xu.bmControls[2] = mask >> 16; From 764c81639b70596e9e67d113d8cb466ec4f2c8f2 Mon Sep 17 00:00:00 2001 From: Josuah Demangeon Date: Thu, 27 Nov 2025 18:57:42 +0000 Subject: [PATCH 11/34] usb: uvc: common: correct the size for UVC_PU_CONTRAST_CONTROL The USB control size field was wrong for UVC_PU_CONTRAST_CONTROL. Correct it to the correct value from the standard. Signed-off-by: Josuah Demangeon --- subsys/usb/common/usb_common_uvc.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/subsys/usb/common/usb_common_uvc.c b/subsys/usb/common/usb_common_uvc.c index 980724ae799d9..15f09f19a4d5f 100644 --- a/subsys/usb/common/usb_common_uvc.c +++ b/subsys/usb/common/usb_common_uvc.c @@ -99,7 +99,7 @@ static const struct uvc_control_map uvc_control_map_pu[] = { .type = UVC_CONTROL_SIGNED, }, { - .size = 1, + .size = 2, .bit = 1, .selector = UVC_PU_CONTRAST_CONTROL, .cid = VIDEO_CID_CONTRAST, From a58ea381f0b368a43702ddf20d3ed244ecff7b78 Mon Sep 17 00:00:00 2001 From: Josuah Demangeon Date: Sat, 26 Jul 2025 15:47:34 +0000 Subject: [PATCH 12/34] usb: host: uvc: placeholder implementation Loop through each of the VideoStreaming and VideoControl descriptor to parse them. This is meant as a stub for the purpose of testing the class API. Signed-off-by: Josuah Demangeon --- subsys/usb/host/class/usbh_uvc.c | 411 +++++++++++++++++++++++++++++++ 1 file changed, 411 insertions(+) create mode 100644 subsys/usb/host/class/usbh_uvc.c diff --git a/subsys/usb/host/class/usbh_uvc.c b/subsys/usb/host/class/usbh_uvc.c new file mode 100644 index 0000000000000..ddd7955f70c42 --- /dev/null +++ b/subsys/usb/host/class/usbh_uvc.c @@ -0,0 +1,411 @@ +/* + * Copyright (c) 2025 tinyVision.ai Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#define DT_DRV_COMPAT zephyr_uvc_host + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "usbh_ch9.h" +#include "usbh_class.h" +#include "usbh_desc.h" +#include "usbh_device.h" +#include "usb_uvc.h" + +#include "../../../drivers/video/video_ctrls.h" +#include "../../../drivers/video/video_device.h" + +LOG_MODULE_REGISTER(usbh_uvc, CONFIG_USBH_VIDEO_LOG_LEVEL); + +struct usbh_uvc_data { + int todo; +}; + +/* + * Descriptor parsing utilities + * Validate and parse the video streaming and video control descriptors. + */ + +static bool usbh_uvc_desc_is_valid_vs_header(const void *const desc, + const void *const desc_end) +{ + const struct uvc_stream_header_descriptor *const header_desc = desc; + + return usbh_desc_is_valid(desc, desc_end, sizeof(struct uvc_stream_header_descriptor)) && + header_desc->bDescriptorType == USB_DESC_CS_INTERFACE && + (header_desc->bDescriptorSubtype == UVC_VS_OUTPUT_HEADER || + header_desc->bDescriptorSubtype == UVC_VS_INPUT_HEADER); +} + +static bool usbh_uvc_desc_is_valid_vc_header(const void *const desc, + const void *const desc_end) +{ + const struct uvc_control_header_descriptor *const header_desc = desc; + + return usbh_desc_is_valid(desc, desc_end, sizeof(struct uvc_control_header_descriptor)) && + header_desc->bDescriptorType == USB_DESC_CS_INTERFACE && + header_desc->bDescriptorSubtype == UVC_VC_HEADER; +} + +const void *usbh_uvc_desc_get_vs_end(const struct usb_if_descriptor *if_desc, + const void *const desc_end) +{ + const struct uvc_stream_header_descriptor *const header_desc = + usbh_desc_get_next(if_desc, desc_end); + const void *vs_end; + + if (!usbh_uvc_desc_is_valid_vs_header(header_desc, desc_end)) { + return NULL; + } + + vs_end = (uint8_t *)header_desc + header_desc->wTotalLength; + if (vs_end > desc_end) { + return NULL; + } + + return vs_end; +} + +const void *usbh_uvc_desc_get_vc_end(const struct usb_if_descriptor *if_desc, + const void *const desc_end) +{ + const struct uvc_control_header_descriptor *const header_desc = + usbh_desc_get_next(if_desc, desc_end); + const void *vc_end; + + if (!usbh_uvc_desc_is_valid_vc_header(header_desc, desc_end)) { + return NULL; + } + + vc_end = (uint8_t *)header_desc + header_desc->wTotalLength; + if (vc_end > desc_end) { + LOG_WRN("vc_end %p > desc_end %p", vc_end, desc_end); + return NULL; + } + + return vc_end; +} + +static int usbh_uvc_parse_vc_desc(struct usbh_class_data *const c_data, + const void *const desc_beg, + const void *const desc_end) +{ + /* Skip the interface descriptor itself */ + const struct usb_desc_header *desc = usbh_desc_get_next(desc_beg, desc_end); + + for (; desc != NULL; desc = usbh_desc_get_next(desc, desc_end)) { + if (desc->bDescriptorType == USB_DESC_INTERFACE || + desc->bDescriptorType == USB_DESC_INTERFACE_ASSOC || + desc->bDescriptorType == 0) { + break; + } else if (desc->bDescriptorType == USB_DESC_CS_INTERFACE) { + const struct uvc_if_descriptor *const if_desc = (const void *)desc; + + switch (if_desc->bDescriptorSubtype) { + case UVC_VC_HEADER: + LOG_DBG("VideoControl interface: Header"); + break; + case UVC_VC_OUTPUT_TERMINAL: + LOG_DBG("VideoControl interface: Output Terminal"); + break; + case UVC_VC_INPUT_TERMINAL: + LOG_DBG("VideoControl interface: Input/Camera Terminal"); + break; + case UVC_VC_SELECTOR_UNIT: + LOG_DBG("VideoControl interface: Selector Unit"); + break; + case UVC_VC_PROCESSING_UNIT: + LOG_DBG("VideoControl interface: Processing Unit"); + break; + case UVC_VC_EXTENSION_UNIT: + LOG_DBG("VideoControl interface: Extension Unit"); + break; + case UVC_VC_ENCODING_UNIT: + LOG_DBG("VideoControl interface: Encoding Unit"); + break; + default: + LOG_WRN("VideoControl interface: unknown subtype %u, skipping", + if_desc->bDescriptorSubtype); + } + } else { + LOG_WRN("VideoControl descriptor: unknown type %u, skipping", + desc->bDescriptorType); + } + } + + return 0; +} + +static int usbh_uvc_parse_vs_desc(struct usbh_class_data *const c_data, + const void *const desc_beg, + const void *const desc_end) +{ + /* Skip the interface descriptor itself */ + const struct usb_desc_header *desc = usbh_desc_get_next(desc_beg, desc_end); + + for (; desc != NULL; desc = usbh_desc_get_next(desc, desc_end)) { + if (desc->bDescriptorType == USB_DESC_INTERFACE || + desc->bDescriptorType == USB_DESC_INTERFACE_ASSOC || + desc->bDescriptorType == 0) { + break; + } else if (desc->bDescriptorType == USB_DESC_CS_INTERFACE) { + const struct uvc_if_descriptor *const if_desc = (const void *)desc; + + switch (if_desc->bDescriptorSubtype) { + case UVC_VS_INPUT_HEADER: + LOG_DBG("VideoStreaming interface: Input header"); + break; + case UVC_VS_OUTPUT_HEADER: + LOG_DBG("VideoStreaming interface: Output header"); + break; + case UVC_VS_FORMAT_UNCOMPRESSED: + LOG_DBG("VideoStreaming interface: Uncompressed format"); + break; + case UVC_VS_FORMAT_MJPEG: + LOG_DBG("VideoStreaming interface: MJPEG format"); + break; + case UVC_VS_FRAME_UNCOMPRESSED: + LOG_DBG("VideoStreaming interface: Uncompressed Frame"); + break; + case UVC_VS_FRAME_MJPEG: + LOG_DBG("VideoStreaming interface: MJPEG Frame"); + break; + case UVC_VS_COLORFORMAT: + LOG_DBG("VideoStreaming interface: Color"); + break; + default: + LOG_DBG("VideoStreaming descriptor: unknown subtype %u, skipping", + if_desc->bDescriptorSubtype); + } + } else if (desc->bDescriptorType == USB_DESC_ENDPOINT) { + const struct usb_ep_descriptor *const ep_desc = (const void *)desc; + + LOG_DBG("VideoStreaming Endpoint", ep_desc->bEndpointAddress); + } else { + LOG_WRN("VideoStreaming descriptor: unknown type %u, skipping", + desc->bDescriptorType); + } + } + + return 0; +} + +static int usbh_uvc_probe(struct usbh_class_data *const c_data, + struct usb_device *const udev, uint8_t iface) +{ + 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_association_descriptor *const iad_desc = + usbh_desc_get_by_iface(desc_beg, desc_end, iface); + const struct usb_desc_header *desc; + bool has_vc_if = false; + bool has_vs_if = false; + int ret; + + if (iad_desc == NULL) { + LOG_ERR("failed to find interface or interface association number %u", iface); + return -ENOSYS; + } + + if (iad_desc->bDescriptorType != USB_DESC_INTERFACE_ASSOC) { + LOG_WRN("Interface %u is not a valid %s, skipping", iface, "interface assoc"); + return -ENOTSUP; + } + + desc = (struct usb_desc_header *)usbh_desc_get_next(iad_desc, desc_end); + if (desc == NULL) { + return -EBADMSG; + } + + for (int i = 0; i < iad_desc->bInterfaceCount; i++) { + const struct usb_if_descriptor *if_desc; + + if_desc = (const void *)usbh_desc_get_by_iface(desc, desc_end, c_data->iface + i); + if (if_desc == NULL) { + LOG_ERR("Not as many interfaces (%u) as announced (%u)", + i, iad_desc->bInterfaceCount); + return -EBADMSG; + } + + if (if_desc->bInterfaceClass == USB_BCC_VIDEO && + if_desc->bInterfaceSubClass == UVC_SC_VIDEOCONTROL) { + const void *vc_end; + + if (has_vc_if) { + LOG_WRN("Skipping extra VideoControl interface"); + continue; + } + + vc_end = usbh_uvc_desc_get_vc_end(if_desc, desc_end); + if (vc_end == NULL) { + LOG_ERR("Invalid VideoControl interface descriptor"); + return -EBADMSG; + } + + ret = usbh_uvc_parse_vc_desc(c_data, if_desc, vc_end); + if (ret != 0) { + LOG_ERR("Failed to parse VC descriptor"); + return ret; + } + + has_vc_if = true; + } + + if (if_desc->bInterfaceClass == USB_BCC_VIDEO && + if_desc->bInterfaceSubClass == UVC_SC_VIDEOSTREAMING) { + const void *vs_end; + + if (has_vs_if) { + LOG_WRN("Skipping extra VideoStreaming interface"); + continue; + } + + vs_end = usbh_uvc_desc_get_vs_end(if_desc, desc_end); + if (vs_end == NULL) { + LOG_ERR("Invalid VideoStreaming interface descriptor"); + return -EBADMSG; + } + + ret = usbh_uvc_parse_vs_desc(c_data, if_desc, vs_end); + if (ret != 0) { + return ret; + } + + has_vs_if = true; + } + } + + if (!has_vs_if) { + LOG_ERR("No VideoStreaming interface found"); + return -EINVAL; + } + + if (!has_vc_if) { + LOG_DBG("No VideoControl interface found"); + return -EINVAL; + } + + LOG_INF("Interface %u associated with UVC class", iface); + + return 0; +} + +static int usbh_uvc_removed(struct usbh_class_data *const c_data) +{ + return 0; +} + +static int usbh_uvc_init(struct usbh_class_data *const c_data, + struct usbh_context *const uhs_ctx) +{ + return 0; +} + +static int usbh_uvc_completion_cb(struct usbh_class_data *const c_data, + struct uhc_transfer *const xfer) +{ + return 0; +} + +static int usbh_uvc_suspended(struct usbh_class_data *const c_data) +{ + return 0; +} + +static int usbh_uvc_resumed(struct usbh_class_data *const c_data) +{ + return 0; +} + +static int usbh_uvc_preinit(const struct device *dev) +{ + LOG_DBG("%s", dev->name); + + return 0; +} + +static struct usbh_class_api uvc_class_api = { + .init = usbh_uvc_init, + .completion_cb = usbh_uvc_completion_cb, + .probe = usbh_uvc_probe, + .removed = usbh_uvc_removed, + .suspended = usbh_uvc_suspended, + .resumed = usbh_uvc_resumed, +}; + +int usbh_uvc_get_caps(const struct device *const dev, struct video_caps *const caps) +{ + return 0; +} + +int usbh_uvc_get_format(const struct device *const dev, struct video_format *const fmt) +{ + return 0; +} + +int usbh_uvc_set_stream(const struct device *const dev, bool enable, enum video_buf_type type) +{ + return 0; +} + +int usbh_uvc_enqueue(const struct device *const dev, struct video_buffer *const vbuf) +{ + return 0; +} + +int usbh_uvc_dequeue(const struct device *const dev, struct video_buffer **const vbuf, + k_timeout_t timeout) +{ + return 0; +} + +static DEVICE_API(video, uvc_video_api) = { + .get_caps = usbh_uvc_get_caps, + .get_format = usbh_uvc_get_format, + .set_stream = usbh_uvc_set_stream, + .enqueue = usbh_uvc_enqueue, + .dequeue = usbh_uvc_dequeue, +}; + +static struct usbh_class_filter usbh_uvc_filters[] = { + { + .flags = USBH_CLASS_MATCH_CLASS | USBH_CLASS_MATCH_SUB, + .class = USB_BCC_VIDEO, + .sub = UVC_SC_VIDEO_INTERFACE_COLLECTION, + }, +}; + +#define USBH_VIDEO_DT_DEVICE_DEFINE(n) \ + struct usbh_uvc_data usbh_uvc_data_##n = { \ + }; \ + \ + USBH_DEFINE_CLASS(uvc_c_data_##n, &uvc_class_api, \ + (void *)DEVICE_DT_INST_GET(n), \ + usbh_uvc_filters, ARRAY_SIZE(usbh_uvc_filters)); \ + \ + DEVICE_DT_INST_DEFINE(n, usbh_uvc_preinit, NULL, \ + &usbh_uvc_data_##n, NULL, \ + POST_KERNEL, CONFIG_VIDEO_INIT_PRIORITY, \ + &uvc_video_api); \ + \ + VIDEO_DEVICE_DEFINE(uvc_host_##n, DEVICE_DT_INST_GET(n), NULL); + +DT_INST_FOREACH_STATUS_OKAY(USBH_VIDEO_DT_DEVICE_DEFINE) From a6557368d60c6e5fb97a3c18da1a213a03ffe502 Mon Sep 17 00:00:00 2001 From: Josuah Demangeon Date: Thu, 24 Jul 2025 11:58:41 +0000 Subject: [PATCH 13/34] tests: usb: uvc: connect USB device and host via UVB Add a test to run the USB Video Class host support by using the existing Zephyr USB Video Class device support. This allows running implementing the host side from the device side. A draft implementation of UVC is added leveraging this test. Signed-off-by: Josuah Demangeon --- dts/bindings/usb/zephyr,uvc-host.yaml | 15 +++ subsys/usb/host/CMakeLists.txt | 5 + subsys/usb/host/Kconfig | 1 + subsys/usb/host/class/Kconfig | 5 + subsys/usb/host/class/Kconfig.uvc | 19 ++++ tests/subsys/usb/uvc/CMakeLists.txt | 13 +++ tests/subsys/usb/uvc/app.overlay | 26 +++++ tests/subsys/usb/uvc/prj.conf | 15 +++ tests/subsys/usb/uvc/src/main.c | 156 ++++++++++++++++++++++++++ tests/subsys/usb/uvc/testcase.yaml | 11 ++ 10 files changed, 266 insertions(+) create mode 100644 dts/bindings/usb/zephyr,uvc-host.yaml create mode 100644 subsys/usb/host/class/Kconfig create mode 100644 subsys/usb/host/class/Kconfig.uvc create mode 100644 tests/subsys/usb/uvc/CMakeLists.txt create mode 100644 tests/subsys/usb/uvc/app.overlay create mode 100644 tests/subsys/usb/uvc/prj.conf create mode 100644 tests/subsys/usb/uvc/src/main.c create mode 100644 tests/subsys/usb/uvc/testcase.yaml diff --git a/dts/bindings/usb/zephyr,uvc-host.yaml b/dts/bindings/usb/zephyr,uvc-host.yaml new file mode 100644 index 0000000000000..fa720c5a64301 --- /dev/null +++ b/dts/bindings/usb/zephyr,uvc-host.yaml @@ -0,0 +1,15 @@ +# Copyright (c) 2025 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +description: | + USB Video Class (UVC) host instance. + + Each UVC instance added to the USB Host Controller (UHC) node will be visible + as a new camera from Zephyr point of view. + + as soon as a camera is connected to USB this device will be usable by the application as a + video device, following the video API. + +compatible: "zephyr,uvc-host" + +include: base.yaml diff --git a/subsys/usb/host/CMakeLists.txt b/subsys/usb/host/CMakeLists.txt index cafa3162b3c94..8bd3e2819f558 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_VIDEO_CLASS + class/usbh_uvc.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..f9d957ad1a8ac --- /dev/null +++ b/subsys/usb/host/class/Kconfig @@ -0,0 +1,5 @@ +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: Apache-2.0 + +rsource "Kconfig.uvc" diff --git a/subsys/usb/host/class/Kconfig.uvc b/subsys/usb/host/class/Kconfig.uvc new file mode 100644 index 0000000000000..53516ab27453a --- /dev/null +++ b/subsys/usb/host/class/Kconfig.uvc @@ -0,0 +1,19 @@ +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: Apache-2.0 + +config USBH_VIDEO_CLASS + bool "USB Video Class implementation [EXPERIMENTAL]" + depends on DT_HAS_ZEPHYR_UVC_HOST_ENABLED + select EXPERIMENTAL + help + USB Host Video Class (UVC) implementation. + +if USBH_VIDEO_CLASS + +module = USBH_VIDEO +module-str = usbh uvc +default-count = 1 +source "subsys/logging/Kconfig.template.log_config" + +endif # USBH_VIDEO_CLASS diff --git a/tests/subsys/usb/uvc/CMakeLists.txt b/tests/subsys/usb/uvc/CMakeLists.txt new file mode 100644 index 0000000000000..ff98cb58e5977 --- /dev/null +++ b/tests/subsys/usb/uvc/CMakeLists.txt @@ -0,0 +1,13 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(uvc_uvb) + +# This sample cannot use the common USB sample infrastructure as we do not want +# to initialize the board default `zephyr_udc0` but instead an unrelated `virtual_udc0`. +#include(${ZEPHYR_BASE}/samples/subsys/usb/common/common.cmake) + +FILE(GLOB app_sources src/*.c) +target_sources(app PRIVATE ${app_sources}) +zephyr_include_directories(${ZEPHYR_BASE}/subsys/usb/host) diff --git a/tests/subsys/usb/uvc/app.overlay b/tests/subsys/usb/uvc/app.overlay new file mode 100644 index 0000000000000..a0b35c939004c --- /dev/null +++ b/tests/subsys/usb/uvc/app.overlay @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/ { + virtual_uhc0: uhc-virtual { + compatible = "zephyr,uhc-virtual"; + maximum-speed = "full-speed"; + + virtual_udc0: udc-virtual { + compatible = "zephyr,udc-virtual"; + num-bidir-endpoints = <4>; + maximum-speed = "full-speed"; + }; + }; + + uvc_device: uvc-device { + compatible = "zephyr,uvc-device"; + }; + + uvc_host: uvc-host { + compatible = "zephyr,uvc-host"; + }; +}; diff --git a/tests/subsys/usb/uvc/prj.conf b/tests/subsys/usb/uvc/prj.conf new file mode 100644 index 0000000000000..ed2322f383032 --- /dev/null +++ b/tests/subsys/usb/uvc/prj.conf @@ -0,0 +1,15 @@ +CONFIG_LOG=y +CONFIG_UDC_BUF_POOL_SIZE=2048 +CONFIG_UDC_DRIVER_LOG_LEVEL_INF=y +CONFIG_UHC_DRIVER_LOG_LEVEL_INF=y +CONFIG_USBD_VIDEO_CLASS=y +CONFIG_USBD_VIDEO_LOG_LEVEL_INF=y +CONFIG_USBH_LOG_LEVEL_INF=y +CONFIG_USBH_VIDEO_CLASS=y +CONFIG_USBH_VIDEO_LOG_LEVEL_INF=y +CONFIG_USB_DEVICE_STACK_NEXT=y +CONFIG_USB_HOST_STACK=y +CONFIG_UVB_LOG_LEVEL_INF=y +CONFIG_VIDEO=y +CONFIG_VIDEO_LOG_LEVEL_INF=y +CONFIG_ZTEST=y diff --git a/tests/subsys/usb/uvc/src/main.c b/tests/subsys/usb/uvc/src/main.c new file mode 100644 index 0000000000000..0952067d78a01 --- /dev/null +++ b/tests/subsys/usb/uvc/src/main.c @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include + +#include "../../../../../drivers/video/video_common.h" + +LOG_MODULE_REGISTER(app, LOG_LEVEL_INF); + +/* + * Minimal USB Host initialization. + */ + +USBH_CONTROLLER_DEFINE(app_usbh, + DEVICE_DT_GET(DT_NODELABEL(virtual_uhc0))); + +struct usbh_context *app_usbh_init_controller(void) +{ + int err; + + err = usbh_init(&app_usbh); + if (err) { + LOG_ERR("Failed to initialize host support"); + return NULL; + } + + return &app_usbh; +} + +USBD_DEVICE_DEFINE(app_usbd, DEVICE_DT_GET(DT_NODELABEL(virtual_udc0)), 0x2fe3, 0x9999); +USBD_DESC_LANG_DEFINE(app_lang); +USBD_DESC_MANUFACTURER_DEFINE(app_mfr, "Nordic"); +USBD_DESC_PRODUCT_DEFINE(app_product, "Virtual UVC device"); +USBD_DESC_CONFIG_DEFINE(fs_cfg_desc, "FS Configuration"); +USBD_CONFIGURATION_DEFINE(app_fs_config, 0, 200, &fs_cfg_desc); + +/* + * Minimal USB Device initialization. + */ + +struct usbd_context *app_usbd_init_device(void) +{ + int ret; + + ret = usbd_add_descriptor(&app_usbd, &app_lang); + if (ret != 0) { + LOG_ERR("Failed to initialize language descriptor (%d)", ret); + return NULL; + } + + ret = usbd_add_descriptor(&app_usbd, &app_mfr); + if (ret != 0) { + LOG_ERR("Failed to initialize manufacturer descriptor (%d)", ret); + return NULL; + } + + ret = usbd_add_descriptor(&app_usbd, &app_product); + if (ret != 0) { + LOG_ERR("Failed to initialize product descriptor (%d)", ret); + return NULL; + } + + ret = usbd_add_configuration(&app_usbd, USBD_SPEED_FS, &app_fs_config); + if (ret != 0) { + LOG_ERR("Failed to add Full-Speed configuration"); + return NULL; + } + + ret = usbd_register_all_classes(&app_usbd, USBD_SPEED_FS, 1, NULL); + if (ret != 0) { + LOG_ERR("Failed to add register classes"); + return NULL; + } + + usbd_device_set_code_triple(&app_usbd, USBD_SPEED_FS, USB_BCC_MISCELLANEOUS, 0x02, 0x01); + + usbd_self_powered(&app_usbd, USB_SCD_SELF_POWERED); + + ret = usbd_init(&app_usbd); + if (ret != 0) { + LOG_ERR("Failed to initialize device support"); + return NULL; + } + + return &app_usbd; +} + +/* + * Test using this host connected to this device via UVB + */ + +const struct video_format test_formats[] = { + {.pixelformat = VIDEO_PIX_FMT_YUYV, .width = 640, .height = 480}, + {.pixelformat = VIDEO_PIX_FMT_YUYV, .width = 320, .height = 240}, + {.pixelformat = VIDEO_PIX_FMT_YUYV, .width = 160, .height = 120}, +}; + +const struct device *const uvc_dev = DEVICE_DT_GET(DT_NODELABEL(uvc_device)); +const struct device *const video_dev = DEVICE_DT_GET(DT_CHOSEN(zephyr_camera)); + +ZTEST(usb_uvc, test_virtual_device_virtual_host) +{ + struct usbd_context *usbd_ctx; + struct usbh_context *usbh_ctx; + int ret; + + uvc_set_video_dev(uvc_dev, video_dev); + + for (size_t i = 0; i < ARRAY_SIZE(test_formats); i++) { + struct video_format fmt = test_formats[i]; + + ret = video_estimate_fmt_size(&fmt); + zassert_ok(ret); + + ret = uvc_add_format(uvc_dev, &fmt); + zassert_ok(ret); + } + + usbd_ctx = app_usbd_init_device(); + zassert_not_null(usbd_ctx, "USB device initialization must succeed"); + + usbh_ctx = app_usbh_init_controller(); + zassert_not_null(usbh_ctx, "USB host initialization must succeed"); + + ret = usbd_enable(usbd_ctx); + zassert_ok(ret, "USB device error code %d must be 0", ret); + + ret = usbh_enable(usbh_ctx); + zassert_ok(ret, "USB host enable error code %d must be 0", ret); + + k_sleep(K_MSEC(500)); + + /* TODO: test the video devices here. */ + + ret = usbh_disable(usbh_ctx); + zassert_ok(ret, "USB host disable error code %d must be 0", ret); + + ret = usbd_disable(usbd_ctx); + zassert_ok(ret, "USB device disable error code %d must be 0", ret); + + ret = usbh_shutdown(usbh_ctx); + zassert_ok(ret, "USB host shutdown error code %d must be 0", ret); + + ret = usbd_shutdown(usbd_ctx); + zassert_ok(ret, "USB device shutdown error code %d must be 0", ret); +} + +ZTEST_SUITE(usb_uvc, NULL, NULL, NULL, NULL, NULL); diff --git a/tests/subsys/usb/uvc/testcase.yaml b/tests/subsys/usb/uvc/testcase.yaml new file mode 100644 index 0000000000000..a6ab1f92033ad --- /dev/null +++ b/tests/subsys/usb/uvc/testcase.yaml @@ -0,0 +1,11 @@ +tests: + usb.uvc: + tags: + - usb + - video + platform_allow: + - native_sim/native + integration_platforms: + - native_sim/native + extra_args: + - platform:native_sim/native:SNIPPET="video-sw-generator" From 6396988a524d90a2c1a1b4ecc0d09c2f1670da78 Mon Sep 17 00:00:00 2001 From: Josuah Demangeon Date: Wed, 29 Oct 2025 22:48:52 +0000 Subject: [PATCH 14/34] usb: device_next: uvc: fix public API include The public API file lacked an include to making it fail depending on the order of the includes. Signed-off-by: Josuah Demangeon --- include/zephyr/usb/class/usbd_uvc.h | 1 + 1 file changed, 1 insertion(+) diff --git a/include/zephyr/usb/class/usbd_uvc.h b/include/zephyr/usb/class/usbd_uvc.h index 9cd3ad1d1931b..43f62b88b9942 100644 --- a/include/zephyr/usb/class/usbd_uvc.h +++ b/include/zephyr/usb/class/usbd_uvc.h @@ -13,6 +13,7 @@ #define ZEPHYR_INCLUDE_USB_CLASS_USBD_UVC_H #include +#include /** * @brief USB Video Class (UVC) device API From d235b65e62811d82fcf42a2e19a7f2245271a343 Mon Sep 17 00:00:00 2001 From: Santhosh Charles Date: Thu, 13 Nov 2025 23:50:32 +0530 Subject: [PATCH 15/34] usb: host: device: Read descriptor after setting device address Modify the USB device int sequence to read the device descriptor only after setting a valid device address. Signed-off-by: Santhosh Charles --- subsys/usb/host/usbh_device.c | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/subsys/usb/host/usbh_device.c b/subsys/usb/host/usbh_device.c index 92ecf6ae7aee1..68e68a400f889 100644 --- a/subsys/usb/host/usbh_device.c +++ b/subsys/usb/host/usbh_device.c @@ -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); From c2b4bdec990886e548e59d2c1fca3fd90a00ba30 Mon Sep 17 00:00:00 2001 From: Aiden Hu Date: Tue, 18 Nov 2025 22:12:25 +0800 Subject: [PATCH 16/34] subsys: usb: host: support multiple devices under hub When hub is used, need to consider about multiple devices are attached. Signed-off-by: Aiden Hu --- subsys/usb/host/usbh_core.c | 28 +++++++++++++++++----------- subsys/usb/host/usbh_device.c | 17 ++++++++++++----- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/subsys/usb/host/usbh_core.c b/subsys/usb/host/usbh_core.c index 1b30f2079b20d..9703c2f4f8900 100644 --- a/subsys/usb/host/usbh_core.c +++ b/subsys/usb/host/usbh_core.c @@ -49,29 +49,35 @@ 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) { + struct usb_device *udev; + LOG_DBG("Device connected event"); - if (ctx->root != NULL) { - LOG_ERR("Device already connected"); - usbh_device_free(ctx->root); - ctx->root = NULL; - } - ctx->root = usbh_device_alloc(ctx); - if (ctx->root == NULL) { + udev = usbh_device_alloc(ctx); + if (udev == NULL) { LOG_ERR("Failed allocate new device"); return; } - ctx->root->state = USB_STATE_DEFAULT; + udev->state = USB_STATE_DEFAULT; if (event->type == UHC_EVT_DEV_CONNECTED_HS) { - ctx->root->speed = USB_SPEED_SPEED_HS; + udev->speed = USB_SPEED_SPEED_HS; } else { - ctx->root->speed = USB_SPEED_SPEED_FS; + udev->speed = USB_SPEED_SPEED_FS; + } + + k_mutex_lock(&ctx->mutex, K_FOREVER); + sys_dlist_append(&ctx->udevs, &udev->node); + + if (ctx->root == NULL) { + ctx->root = udev; } + k_mutex_unlock(&ctx->mutex); - if (usbh_device_init(ctx->root)) { + if (usbh_device_init(udev)) { LOG_ERR("Failed to reset new USB device"); + sys_dlist_remove(&udev->node); } usbh_class_probe_device(ctx->root); diff --git a/subsys/usb/host/usbh_device.c b/subsys/usb/host/usbh_device.c index 68e68a400f889..a5c5daea41242 100644 --- a/subsys/usb/host/usbh_device.c +++ b/subsys/usb/host/usbh_device.c @@ -452,6 +452,7 @@ int usbh_device_init(struct usb_device *const udev) struct usbh_context *const uhs_ctx = udev->ctx; uint8_t new_addr; int err; + uint8_t device_count = 0; if (udev->state != USB_STATE_DEFAULT) { LOG_ERR("USB device is not in default state"); @@ -464,11 +465,17 @@ int usbh_device_init(struct usb_device *const udev) return err; } - /* FIXME: The port to which the device is connected should be reset. */ - err = uhc_bus_reset(uhs_ctx->dev); - if (err) { - LOG_ERR("Failed to signal bus reset"); - return err; + k_mutex_lock(&uhs_ctx->mutex, K_FOREVER); + device_count = sys_dlist_len(&uhs_ctx->udevs); + k_mutex_unlock(&uhs_ctx->mutex); + + /* Only reset bus if this is the root device. */ + if (device_count == 1U) { + err = uhc_bus_reset(uhs_ctx->dev); + if (err) { + LOG_ERR("Failed to signal bus reset"); + return err; + } } /* From ece50f38f69e8c5e77a9789347dc071233720db9 Mon Sep 17 00:00:00 2001 From: Aiden Hu Date: Wed, 19 Nov 2025 20:23:12 +0800 Subject: [PATCH 17/34] subsys: usb: host: remove root member for usbh_context add usbh_device_get_root and usbh_device_is_root function to check root device Signed-off-by: Aiden Hu --- include/zephyr/usb/usbh.h | 2 -- subsys/usb/host/usbh_core.c | 18 +++++++++--------- subsys/usb/host/usbh_device.c | 28 ++++++++++++++++++++++++++++ subsys/usb/host/usbh_device.h | 7 +++++++ 4 files changed, 44 insertions(+), 11 deletions(-) diff --git a/include/zephyr/usb/usbh.h b/include/zephyr/usb/usbh.h index b4f93029679e6..0f444bb3ca290 100644 --- a/include/zephyr/usb/usbh.h +++ b/include/zephyr/usb/usbh.h @@ -59,8 +59,6 @@ struct usbh_context { struct usbh_status status; /** USB device list */ sys_dlist_t udevs; - /** USB root device */ - struct usb_device *root; /** Allocated device addresses bit array */ struct sys_bitarray *addr_ba; }; diff --git a/subsys/usb/host/usbh_core.c b/subsys/usb/host/usbh_core.c index 9703c2f4f8900..43c8ac65c8aee 100644 --- a/subsys/usb/host/usbh_core.c +++ b/subsys/usb/host/usbh_core.c @@ -69,10 +69,6 @@ static void dev_connected_handler(struct usbh_context *const ctx, k_mutex_lock(&ctx->mutex, K_FOREVER); sys_dlist_append(&ctx->udevs, &udev->node); - - if (ctx->root == NULL) { - ctx->root = udev; - } k_mutex_unlock(&ctx->mutex); if (usbh_device_init(udev)) { @@ -80,19 +76,23 @@ static void dev_connected_handler(struct usbh_context *const ctx, sys_dlist_remove(&udev->node); } - usbh_class_probe_device(ctx->root); + usbh_class_probe_device(usbh_device_get_root(ctx)); } 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; + struct usb_device *udev = NULL; + + udev = usbh_device_get_root(ctx); + + if (udev) { + usbh_class_remove_all(udev); + usbh_device_free(udev); LOG_DBG("Device removed"); } else { LOG_DBG("Spurious device removed event"); } + /* TODO: handle remove for all of classes for the unattached device */ } static int discard_ep_request(struct usbh_context *const ctx, diff --git a/subsys/usb/host/usbh_device.c b/subsys/usb/host/usbh_device.c index a5c5daea41242..5f2b999f90327 100644 --- a/subsys/usb/host/usbh_device.c +++ b/subsys/usb/host/usbh_device.c @@ -447,6 +447,34 @@ int usbh_device_set_configuration(struct usb_device *const udev, const uint8_t n return err; } +struct usb_device *usbh_device_get_root(struct usbh_context *const ctx) +{ + sys_dnode_t *node; + + if (ctx == NULL) { + return NULL; + } + + node = sys_dlist_peek_head(&ctx->udevs); + if (node == NULL) { + /* No devices in the list */ + return NULL; + } + + /* Get the usb_device structure from the node */ + return CONTAINER_OF(node, struct usb_device, node); +} + +bool usbh_device_is_root(struct usbh_context *const ctx, + struct usb_device *const udev) +{ + if (ctx == NULL || udev == NULL) { + return false; + } + + return sys_dlist_peek_head(&ctx->udevs) == &udev->node; +} + int usbh_device_init(struct usb_device *const udev) { struct usbh_context *const uhs_ctx = udev->ctx; diff --git a/subsys/usb/host/usbh_device.h b/subsys/usb/host/usbh_device.h index 5c5637fb0bee8..d45257c58b975 100644 --- a/subsys/usb/host/usbh_device.h +++ b/subsys/usb/host/usbh_device.h @@ -36,6 +36,13 @@ int usbh_device_interface_set(struct usb_device *const udev, const uint8_t iface, const uint8_t alt, const bool dry); +/* Get root USB device */ +struct usb_device *usbh_device_get_root(struct usbh_context *const ctx); + +/* Check if USB device is root */ +bool usbh_device_is_root(struct usbh_context *const ctx, + struct usb_device *const udev); + /* Wrappers around to avoid glue UHC calls. */ static inline struct uhc_transfer *usbh_xfer_alloc(struct usb_device *udev, const uint8_t ep, From 4596079254f37a42b4ba6fd33de3fba48e20d522 Mon Sep 17 00:00:00 2001 From: Aiden Hu Date: Tue, 18 Nov 2025 19:40:16 +0800 Subject: [PATCH 18/34] drivers: uhc: use correct endpoint type and interval For usb xfer, set endpoint type and interval by the selected endpoint desc. Signed-off-by: Aiden Hu --- drivers/usb/uhc/uhc_common.c | 8 ++++++++ drivers/usb/uhc/uhc_mcux_common.c | 10 ++-------- include/zephyr/drivers/usb/uhc.h | 2 ++ 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/drivers/usb/uhc/uhc_common.c b/drivers/usb/uhc/uhc_common.c index 097d4a01457e0..e06048c4e926a 100644 --- a/drivers/usb/uhc/uhc_common.c +++ b/drivers/usb/uhc/uhc_common.c @@ -101,6 +101,8 @@ struct uhc_transfer *uhc_xfer_alloc(const struct device *dev, const struct uhc_api *api = dev->api; struct uhc_transfer *xfer = NULL; uint16_t mps; + uint16_t interval; + uint8_t type; api->lock(dev); @@ -109,6 +111,8 @@ struct uhc_transfer *uhc_xfer_alloc(const struct device *dev, } if (ep_idx == 0) { + interval = 0; + type = USB_EP_TYPE_CONTROL; mps = udev->dev_desc.bMaxPacketSize0; } else { struct usb_ep_descriptor *ep_desc; @@ -125,6 +129,8 @@ struct uhc_transfer *uhc_xfer_alloc(const struct device *dev, } mps = ep_desc->wMaxPacketSize; + interval = ep_desc->bInterval; + type = ep_desc->bmAttributes & USB_EP_TRANSFER_TYPE_MASK; } LOG_DBG("Allocate xfer, ep 0x%02x mps %u cb %p", ep, mps, cb); @@ -137,6 +143,8 @@ struct uhc_transfer *uhc_xfer_alloc(const struct device *dev, memset(xfer, 0, sizeof(struct uhc_transfer)); xfer->ep = ep; xfer->mps = mps; + xfer->interval = interval; + xfer->type = type; xfer->udev = udev; xfer->cb = cb; xfer->priv = cb_priv; diff --git a/drivers/usb/uhc/uhc_mcux_common.c b/drivers/usb/uhc/uhc_mcux_common.c index 7ed7aa169e8f5..7399adab668d0 100644 --- a/drivers/usb/uhc/uhc_mcux_common.c +++ b/drivers/usb/uhc/uhc_mcux_common.c @@ -243,8 +243,7 @@ static usb_host_pipe_handle uhc_mcux_check_hal_ep(const struct device *dev, } } - /* TODO: need to check endpoint type too */ - if (mcux_ep != NULL && + if (mcux_ep != NULL && mcux_ep->pipeType == xfer->type && (mcux_ep->maxPacketSize != xfer->mps || mcux_ep->interval != xfer->interval)) { /* re-initialize the ep */ @@ -289,12 +288,7 @@ usb_host_pipe_t *uhc_mcux_init_hal_ep(const struct device *dev, struct uhc_trans */ pipe_init.numberPerUframe = 0; /* TODO: need right way to implement it. */ pipe_init.interval = xfer->interval; - /* TODO: need right way to implement it. */ - if (pipe_init.endpointAddress == 0) { - pipe_init.pipeType = USB_ENDPOINT_CONTROL; - } else { - pipe_init.pipeType = USB_ENDPOINT_BULK; - } + pipe_init.pipeType = xfer->type; status = priv->mcux_if->controllerOpenPipe(priv->mcux_host.controllerHandle, (usb_host_pipe_handle *)&mcux_ep, &pipe_init); diff --git a/include/zephyr/drivers/usb/uhc.h b/include/zephyr/drivers/usb/uhc.h index 1e0acd71aad1f..5bacfdc19556f 100644 --- a/include/zephyr/drivers/usb/uhc.h +++ b/include/zephyr/drivers/usb/uhc.h @@ -120,6 +120,8 @@ struct uhc_transfer { struct net_buf *buf; /** Endpoint to which request is associated */ uint8_t ep; + /** Endpoint type */ + uint8_t type; /** Maximum packet size */ uint16_t mps; /** Interval, used for periodic transfers only */ From d22fe3b5236b0db8bb52d2cd7cd136814f04136a Mon Sep 17 00:00:00 2001 From: Aiden Hu Date: Tue, 18 Nov 2025 20:56:49 +0800 Subject: [PATCH 19/34] drivers: uhc: correct interval usage for usb xfer Convert xfer's interval to actual value because mcux_ep->interval is already calculated. Signed-off-by: Aiden Hu --- drivers/usb/uhc/uhc_mcux_common.c | 4 +++- drivers/usb/uhc/uhc_mcux_common.h | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/drivers/usb/uhc/uhc_mcux_common.c b/drivers/usb/uhc/uhc_mcux_common.c index 7399adab668d0..41312bc2b101b 100644 --- a/drivers/usb/uhc/uhc_mcux_common.c +++ b/drivers/usb/uhc/uhc_mcux_common.c @@ -245,7 +245,7 @@ static usb_host_pipe_handle uhc_mcux_check_hal_ep(const struct device *dev, if (mcux_ep != NULL && mcux_ep->pipeType == xfer->type && (mcux_ep->maxPacketSize != xfer->mps || - mcux_ep->interval != xfer->interval)) { + priv->mcux_eps_interval[i] != xfer->interval)) { /* re-initialize the ep */ status = priv->mcux_if->controllerClosePipe(priv->mcux_host.controllerHandle, mcux_ep); @@ -255,6 +255,7 @@ static usb_host_pipe_handle uhc_mcux_check_hal_ep(const struct device *dev, uhc_mcux_lock(dev); priv->mcux_eps[i] = NULL; + priv->mcux_eps_interval[i] = 0; uhc_mcux_unlock(dev); mcux_ep = NULL; } @@ -308,6 +309,7 @@ usb_host_pipe_t *uhc_mcux_init_hal_ep(const struct device *dev, struct uhc_trans for (i = 0; i < USB_HOST_CONFIG_MAX_PIPES; i++) { if (priv->mcux_eps[i] == NULL) { priv->mcux_eps[i] = mcux_ep; + priv->mcux_eps_interval[i] = xfer->interval; break; } } diff --git a/drivers/usb/uhc/uhc_mcux_common.h b/drivers/usb/uhc/uhc_mcux_common.h index d80ccdadcfcf0..3e9014af85a6a 100644 --- a/drivers/usb/uhc/uhc_mcux_common.h +++ b/drivers/usb/uhc/uhc_mcux_common.h @@ -12,6 +12,7 @@ struct uhc_mcux_data { const usb_host_controller_interface_t *mcux_if; /* TODO: Maybe make it to link with udev->ep_in and udev->ep_out */ usb_host_pipe_t *mcux_eps[USB_HOST_CONFIG_MAX_PIPES]; + uint16_t mcux_eps_interval[USB_HOST_CONFIG_MAX_PIPES]; usb_host_instance_t mcux_host; struct k_thread drv_stack_data; uint8_t controller_id; /* MCUX hal controller id, 0xFF is invalid value */ From 3a73f60d51fa5f84853757a5d632bb066f7d07b1 Mon Sep 17 00:00:00 2001 From: Aiden Hu Date: Wed, 19 Nov 2025 08:34:03 +0800 Subject: [PATCH 20/34] drivers: uhc: set right value for pipe by xfer's mps maxPacketSize and numberPerUframe of pipe should be set considering additional transactions. Signed-off-by: Aiden Hu --- drivers/usb/uhc/uhc_mcux_common.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/usb/uhc/uhc_mcux_common.c b/drivers/usb/uhc/uhc_mcux_common.c index 41312bc2b101b..d8e96d42d580d 100644 --- a/drivers/usb/uhc/uhc_mcux_common.c +++ b/drivers/usb/uhc/uhc_mcux_common.c @@ -280,14 +280,14 @@ usb_host_pipe_t *uhc_mcux_init_hal_ep(const struct device *dev, struct uhc_trans /* USB_HostHelperGetPeripheralInformation uses this value as first parameter */ pipe_init.devInstance = xfer->udev; pipe_init.nakCount = USB_HOST_CONFIG_MAX_NAK; - pipe_init.maxPacketSize = xfer->mps; + pipe_init.maxPacketSize = USB_MPS_EP_SIZE(xfer->mps); pipe_init.endpointAddress = USB_EP_GET_IDX(xfer->ep); pipe_init.direction = USB_EP_GET_IDX(xfer->ep) == 0 ? USB_OUT : USB_EP_GET_DIR(xfer->ep) ? USB_IN : USB_OUT; /* Current Zephyr Host stack is experimental, the endpoint's interval, * 'number per uframe' and the endpoint type cannot be got yet. */ - pipe_init.numberPerUframe = 0; /* TODO: need right way to implement it. */ + pipe_init.numberPerUframe = USB_MPS_ADDITIONAL_TRANSACTIONS(xfer->mps); pipe_init.interval = xfer->interval; pipe_init.pipeType = xfer->type; From 7bf8093ed4a0da5109b04b9c468fc9b45ec5586d Mon Sep 17 00:00:00 2001 From: Aiden Hu Date: Wed, 26 Nov 2025 15:17:41 +0800 Subject: [PATCH 21/34] subsys: usb: host: refine connect/disconnect handling Add two functions: usbh_connect_device() for device connection usbh_disconnect_device() for device disconnection These functions centralize the logic for device attach/detach, including class probe and remove handling. They can be invoked by the hub class as well as dev_connected_handler and dev_removed_handler, improving code clarity and reuse. Signed-off-by: Aiden Hu --- subsys/usb/host/usbh_core.c | 35 ++++++---------------------- subsys/usb/host/usbh_device.c | 43 +++++++++++++++++++++++++++++++++++ subsys/usb/host/usbh_device.h | 6 +++++ 3 files changed, 56 insertions(+), 28 deletions(-) diff --git a/subsys/usb/host/usbh_core.c b/subsys/usb/host/usbh_core.c index 43c8ac65c8aee..69cbc82bd118d 100644 --- a/subsys/usb/host/usbh_core.c +++ b/subsys/usb/host/usbh_core.c @@ -12,8 +12,6 @@ #include #include -#include "usbh_class.h" -#include "usbh_class_api.h" #include "usbh_device.h" #include "usbh_internal.h" @@ -50,33 +48,18 @@ static void dev_connected_handler(struct usbh_context *const ctx, const struct uhc_event *const event) { struct usb_device *udev; - - LOG_DBG("Device connected event"); - - udev = usbh_device_alloc(ctx); - if (udev == NULL) { - LOG_ERR("Failed allocate new device"); - return; - } - - udev->state = USB_STATE_DEFAULT; + uint8_t speed; if (event->type == UHC_EVT_DEV_CONNECTED_HS) { - udev->speed = USB_SPEED_SPEED_HS; + speed = USB_SPEED_SPEED_HS; } else { - udev->speed = USB_SPEED_SPEED_FS; + speed = USB_SPEED_SPEED_FS; } - k_mutex_lock(&ctx->mutex, K_FOREVER); - sys_dlist_append(&ctx->udevs, &udev->node); - k_mutex_unlock(&ctx->mutex); - - if (usbh_device_init(udev)) { - LOG_ERR("Failed to reset new USB device"); - sys_dlist_remove(&udev->node); + udev = usbh_connect_device(ctx, speed); + if (udev == NULL) { + return; } - - usbh_class_probe_device(usbh_device_get_root(ctx)); } static void dev_removed_handler(struct usbh_context *const ctx) @@ -84,15 +67,11 @@ static void dev_removed_handler(struct usbh_context *const ctx) struct usb_device *udev = NULL; udev = usbh_device_get_root(ctx); - if (udev) { - usbh_class_remove_all(udev); - usbh_device_free(udev); - LOG_DBG("Device removed"); + usbh_disconnect_device(ctx, udev); } else { LOG_DBG("Spurious device removed event"); } - /* TODO: handle remove for all of classes for the unattached device */ } static int discard_ep_request(struct usbh_context *const ctx, diff --git a/subsys/usb/host/usbh_device.c b/subsys/usb/host/usbh_device.c index 5f2b999f90327..15db67a4d5a7a 100644 --- a/subsys/usb/host/usbh_device.c +++ b/subsys/usb/host/usbh_device.c @@ -9,6 +9,8 @@ #include "usbh_device.h" #include "usbh_ch9.h" +#include "usbh_class.h" +#include "usbh_class_api.h" #include LOG_MODULE_REGISTER(usbh_dev, CONFIG_USBH_LOG_LEVEL); @@ -475,6 +477,47 @@ bool usbh_device_is_root(struct usbh_context *const ctx, return sys_dlist_peek_head(&ctx->udevs) == &udev->node; } +struct usb_device *usbh_connect_device(struct usbh_context *const ctx, + uint8_t speed) +{ + struct usb_device *udev; + + LOG_DBG("Device connected event"); + + /* Allocate new device */ + udev = usbh_device_alloc(ctx); + if (udev == NULL) { + LOG_ERR("Failed allocate new device"); + return NULL; + } + + udev->state = USB_STATE_DEFAULT; + udev->speed = speed; + + if (usbh_device_init(udev)) { + LOG_ERR("Failed to init new USB device"); + usbh_device_free(udev); + return NULL; + } + + usbh_class_probe_device(udev); + + return udev; +} + +void usbh_disconnect_device(struct usbh_context *ctx, struct usb_device *udev) +{ + if (!ctx || !udev) { + return; + } + + usbh_class_remove_all(udev); + + usbh_device_free(udev); + + LOG_DBG("Device removed"); +} + int usbh_device_init(struct usb_device *const udev) { struct usbh_context *const uhs_ctx = udev->ctx; diff --git a/subsys/usb/host/usbh_device.h b/subsys/usb/host/usbh_device.h index d45257c58b975..bccb7d65d81f8 100644 --- a/subsys/usb/host/usbh_device.h +++ b/subsys/usb/host/usbh_device.h @@ -43,6 +43,12 @@ struct usb_device *usbh_device_get_root(struct usbh_context *const ctx); bool usbh_device_is_root(struct usbh_context *const ctx, struct usb_device *const udev); +/* Connect a new USB device */ +struct usb_device *usbh_connect_device(struct usbh_context *const ctx, + uint8_t device_speed); +/* Disconnect USB device */ +void usbh_disconnect_device(struct usbh_context *ctx, struct usb_device *udev); + /* Wrappers around to avoid glue UHC calls. */ static inline struct uhc_transfer *usbh_xfer_alloc(struct usb_device *udev, const uint8_t ep, From 44fbdfb9967153784dc795efbbf56c12ad476661 Mon Sep 17 00:00:00 2001 From: Santhosh Charles Date: Wed, 29 Oct 2025 12:10:59 +0530 Subject: [PATCH 22/34] 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 Ethernet functionality for USB host. Signed-off-by: Santhosh Charles --- subsys/usb/host/CMakeLists.txt | 5 + subsys/usb/host/class/Kconfig | 3 + subsys/usb/host/class/Kconfig.cdc_ecm_host | 20 + subsys/usb/host/class/usbh_cdc_ecm.c | 651 +++++++++++++++++++++ 4 files changed, 679 insertions(+) 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 8bd3e2819f558..2d15a63f3fc80 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_USBH_VIDEO_CLASS class/usbh_uvc.c diff --git a/subsys/usb/host/class/Kconfig b/subsys/usb/host/class/Kconfig index f9d957ad1a8ac..84cfc3f762abf 100644 --- a/subsys/usb/host/class/Kconfig +++ b/subsys/usb/host/class/Kconfig @@ -1,5 +1,8 @@ # Copyright (c) 2025 Nordic Semiconductor ASA +# Copyright (c) 2025 Linumiz GmbH # # SPDX-License-Identifier: Apache-2.0 +# +rsource "Kconfig.cdc_ecm_host" rsource "Kconfig.uvc" 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); From 42741a1eb076bb33418e751544ce1e5fce9608bb Mon Sep 17 00:00:00 2001 From: Santhosh Charles Date: Wed, 29 Oct 2025 23:37:54 +0530 Subject: [PATCH 23/34] 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 a01c220e2cc79d1e197fcee761569924a8c79f9e Mon Sep 17 00:00:00 2001 From: Santhosh Charles Date: Wed, 29 Oct 2025 11:47:00 +0530 Subject: [PATCH 24/34] 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/common/CMakeLists.txt | 6 +++ subsys/usb/common/include/usb_cdc_ecm.h | 54 +++++++++++++++++++++++++ subsys/usb/common/usb_cdc_ecm.c | 35 ++++++++++++++++ 3 files changed, 95 insertions(+) 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/common/CMakeLists.txt b/subsys/usb/common/CMakeLists.txt index 5015d5c438030..fd890ad8d7e3f 100644 --- a/subsys/usb/common/CMakeLists.txt +++ b/subsys/usb/common/CMakeLists.txt @@ -1,7 +1,13 @@ # Copyright (c) 2025 Nordic Semiconductor ASA +# Copyright (c) 2025 Linumiz GmbH +# # SPDX-License-Identifier: Apache-2.0 if(CONFIG_USBD_VIDEO_CLASS OR CONFIG_USBH_VIDEO_CLASS) zephyr_include_directories(include) zephyr_sources(usb_common_uvc.c) endif() + +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; +} From 563b297420aed8f2886bc999c90b1d4b3d9b35e5 Mon Sep 17 00:00:00 2001 From: Dv Alan Date: Mon, 1 Dec 2025 16:43:37 +0800 Subject: [PATCH 25/34] Revert "usb: add common directory for shared host/device support" This reverts commit a01c220e2cc79d1e197fcee761569924a8c79f9e. --- subsys/usb/common/CMakeLists.txt | 6 --- subsys/usb/common/include/usb_cdc_ecm.h | 54 ------------------------- subsys/usb/common/usb_cdc_ecm.c | 35 ---------------- 3 files changed, 95 deletions(-) delete mode 100644 subsys/usb/common/include/usb_cdc_ecm.h delete mode 100644 subsys/usb/common/usb_cdc_ecm.c diff --git a/subsys/usb/common/CMakeLists.txt b/subsys/usb/common/CMakeLists.txt index fd890ad8d7e3f..5015d5c438030 100644 --- a/subsys/usb/common/CMakeLists.txt +++ b/subsys/usb/common/CMakeLists.txt @@ -1,13 +1,7 @@ # Copyright (c) 2025 Nordic Semiconductor ASA -# Copyright (c) 2025 Linumiz GmbH -# # SPDX-License-Identifier: Apache-2.0 if(CONFIG_USBD_VIDEO_CLASS OR CONFIG_USBH_VIDEO_CLASS) zephyr_include_directories(include) zephyr_sources(usb_common_uvc.c) endif() - -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 deleted file mode 100644 index 34fed5f84aaa7..0000000000000 --- a/subsys/usb/common/include/usb_cdc_ecm.h +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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 deleted file mode 100644 index bdebc3f4b30e0..0000000000000 --- a/subsys/usb/common/usb_cdc_ecm.c +++ /dev/null @@ -1,35 +0,0 @@ -/* - * 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 c3a18facfd237fb9f3a22053a38fa0446c6e3bf5 Mon Sep 17 00:00:00 2001 From: Dv Alan Date: Mon, 1 Dec 2025 17:08:16 +0800 Subject: [PATCH 26/34] subsys: usb: host: add missing usbh_class.h include Add missing include for usbh_class.h header file in usbh_core.c to fix implicit declaration of function 'usbh_class_init_all' in usbh_core.c Signed-off-by: Dv Alan --- subsys/usb/host/usbh_core.c | 1 + 1 file changed, 1 insertion(+) diff --git a/subsys/usb/host/usbh_core.c b/subsys/usb/host/usbh_core.c index 69cbc82bd118d..d5fc614330612 100644 --- a/subsys/usb/host/usbh_core.c +++ b/subsys/usb/host/usbh_core.c @@ -13,6 +13,7 @@ #include #include "usbh_device.h" +#include "usbh_class.h" #include "usbh_internal.h" #include From 948c8363028f25eaa2c69c319c2f234f74cf88e5 Mon Sep 17 00:00:00 2001 From: Dv Alan Date: Mon, 1 Dec 2025 17:40:32 +0800 Subject: [PATCH 27/34] usb: host: ch9: Add string descriptor request helper function Add usbh_req_desc_str() helper function to retrieve USB string descriptors from a device. This function wraps the generic descriptor request with proper handling for string descriptors, including memory allocation, data copying, and endianness conversion for the bString field. Signed-off-by: Dv Alan --- subsys/usb/host/usbh_ch9.c | 33 +++++++++++++++++++++++++++++++++ subsys/usb/host/usbh_ch9.h | 6 ++++++ 2 files changed, 39 insertions(+) diff --git a/subsys/usb/host/usbh_ch9.c b/subsys/usb/host/usbh_ch9.c index 80783e430559b..961e369a74d64 100644 --- a/subsys/usb/host/usbh_ch9.c +++ b/subsys/usb/host/usbh_ch9.c @@ -222,6 +222,39 @@ int usbh_req_get_cfg(struct usb_device *const udev, return ret; } +int usbh_req_desc_str(struct usb_device *const udev, + const uint8_t index, + const uint16_t len, + const uint16_t langid, + struct usb_string_descriptor *const desc) +{ + const uint8_t type = USB_DESC_STRING; + const uint16_t wLength = len; + struct net_buf *buf; + uint8_t *bString_bytes = NULL; + uint16_t bString_val; + int ret; + + buf = usbh_xfer_buf_alloc(udev, len); + if (!buf) { + return -ENOMEM; + } + + ret = usbh_req_desc(udev, type, index, langid, wLength, buf); + if (ret == 0) { + memcpy(desc, buf->data, len); + bString_bytes = (uint8_t *)&desc->bString; + for (size_t i = 0; i < (len - 2) / 2; i++) { + bString_val = sys_get_le16(&bString_bytes[i * 2]); + sys_put_le16(bString_val, &bString_bytes[i * 2]); + } + } + + usbh_xfer_buf_free(udev, buf); + + return ret; +} + int usbh_req_set_alt(struct usb_device *const udev, const uint8_t iface, const uint8_t alt) { diff --git a/subsys/usb/host/usbh_ch9.h b/subsys/usb/host/usbh_ch9.h index 19879dae6e2df..76fbde56bf9b2 100644 --- a/subsys/usb/host/usbh_ch9.h +++ b/subsys/usb/host/usbh_ch9.h @@ -41,6 +41,12 @@ int usbh_req_desc_cfg(struct usb_device *const udev, const uint16_t len, struct usb_cfg_descriptor *const desc); +int usbh_req_desc_str(struct usb_device *const udev, + const uint8_t index, + const uint16_t len, + const uint16_t langid, + struct usb_string_descriptor *const desc); + int usbh_req_set_alt(struct usb_device *const udev, const uint8_t iface, const uint8_t alt); From 0b5b4c30a3abbfe8e92f6f61a6ead993703a688f Mon Sep 17 00:00:00 2001 From: Dv Alan Date: Mon, 1 Dec 2025 17:42:05 +0800 Subject: [PATCH 28/34] usb: class: cdc: Add ECM statistics and notification definitions Add Ethernet Statistics Feature Selector enumeration defining all standard CDC ECM statistics counters (XMIT_OK, RCV_OK, etc.) as per the USB CDC specification. Add CDC notification packet structure for handling device notifications. Add CONNECTION_SPEED_CHANGE notification code and Ethernet Power Management Pattern activation constants. Signed-off-by: Dv Alan --- include/zephyr/usb/class/usb_cdc.h | 53 +++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/include/zephyr/usb/class/usb_cdc.h b/include/zephyr/usb/class/usb_cdc.h index d07dd76bfcb80..0aa4c2cae9ca3 100644 --- a/include/zephyr/usb/class/usb_cdc.h +++ b/include/zephyr/usb/class/usb_cdc.h @@ -2,6 +2,7 @@ /* * Copyright (c) 2017 PHYTEC Messtechnik GmbH + * Copyright (c) 2025 NXP * * SPDX-License-Identifier: Apache-2.0 */ @@ -74,8 +75,9 @@ #define USB_CDC_AUX_JACK_HOOK_STATE 0x08 #define USB_CDC_RING_DETECT 0x09 #define USB_CDC_SERIAL_STATE 0x20 -#define USB_CDC_CALL_STATE_CHANGE 0x28 #define USB_CDC_LINE_STATE_CHANGE 0x23 +#define USB_CDC_CALL_STATE_CHANGE 0x28 +#define USB_CDC_CONNECTION_SPEED_CHANGE 0x2A /** * @brief PSTN UART State Bitmap Values @@ -158,6 +160,10 @@ #define GET_CRC_MODE 0x89 #define SET_CRC_MODE 0x8A +/** Ethernet Power Management Pattern Activation */ +#define ETHERNET_PM_PATTERN_ACTIVE_TRUE 0x0001 +#define ETHERNET_PM_PATTERN_ACTIVE_FALSE 0x0000 + /** Ethernet Packet Filter Bitmap */ #define PACKET_TYPE_MULTICAST 0x10 #define PACKET_TYPE_BROADCAST 0x08 @@ -229,6 +235,39 @@ struct cdc_ecm_descriptor { uint8_t bNumberPowerFilters; } __packed; +/** Ethernet Statistics Feature Selector */ +enum cdc_ecm_ethernet_stats { + XMIT_OK = 0x01, /* Frames transmitted without errors */ + RCV_OK = 0x02, /* Frames received without errors */ + XMIT_ERROR = 0x03, /* Frames not transmitted, or transmitted with */ + RCV_ERROR = 0x04, /* Frames received with errors */ + RCV_NO_BUFFER = 0x05, /* Frames missed, no buffers */ + DIRECTED_BYTES_XMIT = 0x06, /* Directed bytes transmitted without errors */ + DIRECTED_FRAMES_XMIT = 0x07, /* Directed frames transmitted without errors */ + MULTICAST_BYTES_XMIT = 0x08, /* Multicast bytes transmitted without errors */ + MULTICAST_FRAMES_XMIT = 0x09, /* Multicast frames transmitted without errors */ + BROADCAST_BYTES_XMIT = 0x0A, /* Broadcast bytes transmitted without errors */ + BROADCAST_FRAMES_XMIT = 0x0B, /* Broadcast frames transmitted without errors */ + DIRECTED_BYTES_RCV = 0x0C, /* Directed bytes received without errors */ + DIRECTED_FRAMES_RCV = 0x0D, /* Directed frames received without errors */ + MULTICAST_BYTES_RCV = 0x0E, /* Multicast bytes received without errors */ + MULTICAST_FRAMES_RCV = 0x0F, /* Multicast frames received without errors */ + BROADCAST_BYTES_RCV = 0x10, /* Broadcast bytes received without errors */ + BROADCAST_FRAMES_RCV = 0x11, /* Broadcast frames received without errors */ + RCV_CRC_ERROR = 0x12, /* Frames received with circular redundancy check (CRC) or frame check sequence (FCS) error */ + TRANSMIT_QUEUE_LENGTH = 0x13, /* Length of transmit queue */ + RCV_ERROR_ALIGNMENT = 0x14, /* Frames received with alignment error */ + XMIT_ONE_COLLISION = 0x15, /* Frames transmitted with one collision */ + XMIT_MORE_COLLISIONS = 0x16, /* Frames transmitted with more than one collision */ + XMIT_DEFERRED = 0x17, /* Frames transmitted after deferral */ + XMIT_MAX_COLLISIONS = 0x18, /* Frames not transmitted due to collisions */ + RCV_OVERRUN = 0x19, /* Frames not received due to overrun */ + XMIT_UNDERRUN = 0x1A, /* Frames not transmitted due to underrun */ + XMIT_HEARTBEAT_FAILURE = 0x1B, /* Frames transmitted with heartbeat failure */ + XMIT_TIMES_CRS_LOST = 0x1C, /* Times carrier sense signal lost during transmission */ + XMIT_LATE_COLLISIONS = 0x1D, /* Late collisions detected */ +}; + /** Ethernet Network Control Model (NCM) Descriptor */ struct cdc_ncm_descriptor { uint8_t bFunctionLength; @@ -238,4 +277,16 @@ struct cdc_ncm_descriptor { uint8_t bmNetworkCapabilities; } __packed; +/** CDC Notification Packet */ +struct cdc_notification_packet { + union { + uint8_t bmRequestType; + struct usb_req_type_field RequestType; + } __packed; + uint8_t bNotification; + uint16_t wValue; + uint16_t wIndex; + uint16_t wLength; +} __packed; + #endif /* ZEPHYR_INCLUDE_USB_CLASS_USB_CDC_H_ */ From b191fed3fdf745b9f90e43d1a11e9f6c16711601 Mon Sep 17 00:00:00 2001 From: Dv Alan Date: Mon, 1 Dec 2025 17:50:18 +0800 Subject: [PATCH 29/34] usb: host: class: cdc_ecm: Rewrite CDC-ECM host driver implementation Complete rewrite of the USB host CDC-ECM (Ethernet Control Model) driver with improved architecture and functionality: - Restructure driver to use proper Ethernet API with ETH_NET_DEVICE_DT_INST_DEFINE - Implement asynchronous RX handling using k_poll signals and dedicated thread - Add support for fragmented network buffers with linearization - Implement proper packet filter management (promiscuous, multicast, broadcast) - Add support for connection speed change notifications and link capabilities - Implement MAC address retrieval from USB string descriptors with multi-language support - Add comprehensive CDC-ECM class requests (SET_ETHERNET_PACKET_FILTER, GET_ETHERNET_STATISTIC, SET_ETHERNET_MULTICAST_FILTERS, etc.) - Use separate buffer pools for TX and RX data transfers - Add proper ZLP (Zero Length Packet) handling for bulk transfers - Implement automatic RX restart mechanism with error recovery - Add network interface up/down state management - Update Kconfig with configurable buffer counts and stack size - Rename Kconfig file from Kconfig.cdc_ecm_host to Kconfig.cdc_ecm - Change DT compatible from "zephyr,usbh-cdc-ecm" to "zephyr,cdc-ecm-host" - Mark driver as EXPERIMENTAL in Kconfig The rewritten driver provides better error handling, improved performance through asynchronous operations, and more complete CDC-ECM feature support. Signed-off-by: Dv Alan --- subsys/usb/host/class/Kconfig | 5 +- subsys/usb/host/class/Kconfig.cdc_ecm | 46 + subsys/usb/host/class/Kconfig.cdc_ecm_host | 20 - subsys/usb/host/class/usbh_cdc_ecm.c | 1286 ++++++++++++++------ 4 files changed, 944 insertions(+), 413 deletions(-) create mode 100644 subsys/usb/host/class/Kconfig.cdc_ecm delete mode 100644 subsys/usb/host/class/Kconfig.cdc_ecm_host diff --git a/subsys/usb/host/class/Kconfig b/subsys/usb/host/class/Kconfig index 84cfc3f762abf..8444acaeec009 100644 --- a/subsys/usb/host/class/Kconfig +++ b/subsys/usb/host/class/Kconfig @@ -1,8 +1,7 @@ # Copyright (c) 2025 Nordic Semiconductor ASA +# Copyright (c) 2025 NXP # Copyright (c) 2025 Linumiz GmbH -# # SPDX-License-Identifier: Apache-2.0 -# -rsource "Kconfig.cdc_ecm_host" +rsource "Kconfig.cdc_ecm" rsource "Kconfig.uvc" diff --git a/subsys/usb/host/class/Kconfig.cdc_ecm b/subsys/usb/host/class/Kconfig.cdc_ecm new file mode 100644 index 0000000000000..2a50ad13365fc --- /dev/null +++ b/subsys/usb/host/class/Kconfig.cdc_ecm @@ -0,0 +1,46 @@ +# Copyright (c) 2025 NXP +# Copyright (c) 2025 Linumiz GmbH +# SPDX-License-Identifier: Apache-2.0 + +config USBH_CDC_ECM_CLASS + bool "USB Communication Device Class - Ethernet Control Mode (CDC-ECM) implementation [EXPERIMENTAL]" + depends on DT_HAS_ZEPHYR_CDC_ECM_HOST_ENABLED + select EXPERIMENTAL + select NET_L2_ETHERNET + select NET_L2_ETHERNET_MGMT + help + USB Host Communication Device Class - Ethernet Control Mode (CDC-ECM) implementation. + +if USBH_CDC_ECM_CLASS + +config USBH_CDC_ECM_STACK_SIZE + int "USB CDC-ECM thread stack size" + default 1024 + help + USB CDC-ECM thread stack size in bytes. + +config USBH_CDC_ECM_DATA_TX_BUF_COUNT + int "Number of CDC-ECM data tx buffers" + default 8 + help + Number of CDC-ECM data tx buffers per instance for transmitting data. + +config USBH_CDC_ECM_DATA_RX_BUF_COUNT + int "Number of CDC-ECM data rx buffers" + default 16 + help + Number of CDC-ECM data rx buffers per instance for receiving data. + +config USBH_CDC_ECM_DATA_BUF_POOL_SIZE + int "USB CDC-ECM buffer pool size" + default 1514 + range 60 9000 + help + USB CDC-ECM buffer pool size in bytes. + +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/Kconfig.cdc_ecm_host b/subsys/usb/host/class/Kconfig.cdc_ecm_host deleted file mode 100644 index 388dd579537e4..0000000000000 --- a/subsys/usb/host/class/Kconfig.cdc_ecm_host +++ /dev/null @@ -1,20 +0,0 @@ -# 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 index 066203d0396d5..8cd0f009d2617 100644 --- a/subsys/usb/host/class/usbh_cdc_ecm.c +++ b/subsys/usb/host/class/usbh_cdc_ecm.c @@ -1,334 +1,824 @@ /* + * Copyright (c) 2025 NXP * Copyright (c) 2025 Linumiz GmbH * * SPDX-License-Identifier: Apache-2.0 */ -#define DT_DRV_COMPAT zephyr_usbh_cdc_ecm - -#include +#include +#include #include - -#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" +#define DT_DRV_COMPAT zephyr_cdc_ecm_host LOG_MODULE_REGISTER(usbh_cdc_ecm, CONFIG_USBH_CDC_ECM_LOG_LEVEL); +#define USBH_CDC_ECM_INSTANCE_COUNT DT_NUM_INST_STATUS_OKAY(DT_DRV_COMPAT) + +NET_BUF_POOL_DEFINE(usbh_cdc_ecm_data_tx_pool, + USBH_CDC_ECM_INSTANCE_COUNT *CONFIG_USBH_CDC_ECM_DATA_TX_BUF_COUNT, + CONFIG_USBH_CDC_ECM_DATA_BUF_POOL_SIZE, 0, NULL); + +NET_BUF_POOL_DEFINE(usbh_cdc_ecm_data_rx_pool, + USBH_CDC_ECM_INSTANCE_COUNT *CONFIG_USBH_CDC_ECM_DATA_RX_BUF_COUNT, + CONFIG_USBH_CDC_ECM_DATA_BUF_POOL_SIZE, 0, NULL); + +#define USBH_CDC_ECM_SIG_COMM_RX_IDLE BIT(0) +#define USBH_CDC_ECM_SIG_DATA_RX_IDLE BIT(1) + struct usbh_cdc_ecm_data { + struct usbh_class_data *c_data; + uint8_t comm_if_num; + uint8_t data_if_num; + uint8_t data_alt_num; + uint8_t comm_in_ep_addr; + uint8_t data_in_ep_addr; + uint8_t data_out_ep_addr; + uint16_t data_out_ep_mps; + uint8_t mac_str_desc_idx; + uint16_t max_segment_size; + atomic_t eth_pkt_filter_bitmap; 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; + enum ethernet_hw_caps caps; +#if defined(CONFIG_NET_STATISTICS_ETHERNET) struct net_stats_eth stats; +#endif + atomic_t auto_rx_enabled; + atomic_t rx_pending_sig_vals; + struct k_poll_signal *rx_sig; + uint8_t dev_idx; }; -static int cdc_ecm_start_rx(struct usbh_cdc_ecm_data *priv); -static int cdc_ecm_start_intr(struct usbh_cdc_ecm_data *priv); +struct usbh_cdc_ecm_req_params { + uint8_t bRequest; + union { + struct { + uint8_t (*m_addr)[6]; + uint16_t len; + } multicast_filter_list; + struct { + uint16_t num; + uint16_t mask_size; + uint8_t *mask_bitmask; + uint8_t *pattern; + uint16_t pattern_size; + } pm_pattern_filter; + struct { + uint16_t num; + uint16_t active; + } pm_pattern_activation; + uint16_t eth_pkt_filter_bitmap; + struct { + uint16_t feature_sel; + uint32_t data; + } eth_stats; + }; +}; -static void cleanup_xfer(struct usb_device *udev, struct uhc_transfer *xfer) +struct usbh_cdc_ecm_xfer_params { + uint8_t ep_addr; + struct net_buf *buf; + usbh_udev_cb_t cb; + void *cb_priv; + struct uhc_transfer *xfer; +}; + +static struct k_poll_event usbh_cdc_ecm_data_events[USBH_CDC_ECM_INSTANCE_COUNT]; +static struct k_poll_signal usbh_cdc_ecm_data_signals[USBH_CDC_ECM_INSTANCE_COUNT]; +static struct usbh_cdc_ecm_data *usbh_cdc_ecm_data_instances[USBH_CDC_ECM_INSTANCE_COUNT] = {0}; + +static int usbh_cdc_ecm_req(struct usbh_cdc_ecm_data *const data, struct usb_device *const udev, + struct usbh_cdc_ecm_req_params *const param) { - if (xfer->buf) { - usbh_xfer_buf_free(udev, xfer->buf); + uint8_t bmRequestType = USB_REQTYPE_TYPE_CLASS << 5 | USB_REQTYPE_RECIPIENT_INTERFACE; + uint16_t wValue = 0; + uint16_t wLength; + struct net_buf *req_buf = NULL; + uint16_t pm_pattern_filter_mask_size; + int ret; + + switch (param->bRequest) { + case SET_ETHERNET_MULTICAST_FILTERS: + if (param->multicast_filter_list.len > UINT16_MAX / 6) { + return -EINVAL; + } + bmRequestType |= USB_REQTYPE_DIR_TO_DEVICE << 7; + wValue = param->multicast_filter_list.len; + wLength = param->multicast_filter_list.len * 6; + req_buf = usbh_xfer_buf_alloc(udev, wLength); + if (!req_buf) { + return -ENOMEM; + } + if (!net_buf_add_mem(req_buf, param->multicast_filter_list.m_addr, wLength)) { + ret = -ENOMEM; + goto cleanup; + } + break; + case SET_ETHERNET_PM_FILTER: + if (param->pm_pattern_filter.mask_size > UINT16_MAX - 2 || + param->pm_pattern_filter.pattern_size > + UINT16_MAX - 2 - param->pm_pattern_filter.mask_size) { + return -EINVAL; + } + bmRequestType |= USB_REQTYPE_DIR_TO_DEVICE << 7; + wValue = param->pm_pattern_filter.num; + wLength = 2 + param->pm_pattern_filter.mask_size + + param->pm_pattern_filter.pattern_size; + req_buf = usbh_xfer_buf_alloc(udev, wLength); + if (!req_buf) { + return -ENOMEM; + } + pm_pattern_filter_mask_size = sys_cpu_to_le16(param->pm_pattern_filter.mask_size); + if (!net_buf_add_mem(req_buf, &pm_pattern_filter_mask_size, 2)) { + ret = -ENOMEM; + goto cleanup; + } + if (!net_buf_add_mem(req_buf, param->pm_pattern_filter.mask_bitmask, + param->pm_pattern_filter.mask_size)) { + ret = -ENOMEM; + goto cleanup; + } + if (!net_buf_add_mem(req_buf, param->pm_pattern_filter.pattern, + param->pm_pattern_filter.pattern_size)) { + ret = -ENOMEM; + goto cleanup; + } + break; + case GET_ETHERNET_PM_FILTER: + bmRequestType |= USB_REQTYPE_DIR_TO_HOST << 7; + wValue = param->pm_pattern_activation.num; + wLength = 2; + req_buf = usbh_xfer_buf_alloc(udev, wLength); + if (!req_buf) { + return -ENOMEM; + } + break; + case SET_ETHERNET_PACKET_FILTER: + bmRequestType |= USB_REQTYPE_DIR_TO_DEVICE << 7; + wValue = param->eth_pkt_filter_bitmap; + wLength = 0; + req_buf = NULL; + break; + case GET_ETHERNET_STATISTIC: + bmRequestType |= USB_REQTYPE_DIR_TO_HOST << 7; + wValue = param->eth_stats.feature_sel; + wLength = 4; + req_buf = usbh_xfer_buf_alloc(udev, wLength); + if (!req_buf) { + return -ENOMEM; + } + break; + default: + return -ENOTSUP; } - usbh_xfer_free(udev, xfer); + ret = usbh_req_setup(udev, bmRequestType, param->bRequest, wValue, data->comm_if_num, + wLength, req_buf); + + if (!ret && req_buf) { + switch (param->bRequest) { + case GET_ETHERNET_PM_FILTER: + if (req_buf->len == 2 && !req_buf->frags) { + param->pm_pattern_activation.active = sys_get_le16(req_buf->data); + } else { + ret = -EIO; + } + break; + case GET_ETHERNET_STATISTIC: + if (req_buf->len == 4 && !req_buf->frags) { + param->eth_stats.data = sys_get_le32(req_buf->data); + } else { + ret = -EIO; + } + break; + } + } + +cleanup: + if (req_buf) { + usbh_xfer_buf_free(udev, req_buf); + } + + return ret; } -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) +static int usbh_cdc_ecm_xfer(struct usb_device *const udev, + struct usbh_cdc_ecm_xfer_params *const param) { - struct uhc_transfer *xfer; - struct net_buf *buf; int ret; - xfer = usbh_xfer_alloc(priv->udev, ep, cb, priv); - if (!xfer) { - return -ENOMEM; + param->xfer = NULL; + + if (!param || !param->ep_addr || !param->cb || !param->buf) { + return -EINVAL; } - buf = usbh_xfer_buf_alloc(priv->udev, buf_size); - if (!buf) { - usbh_xfer_free(priv->udev, xfer); + param->xfer = usbh_xfer_alloc(udev, param->ep_addr, param->cb, param->cb_priv); + if (!param->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; + ret = usbh_xfer_buf_add(udev, param->xfer, param->buf); + if (ret) { + goto cleanup; } - /* 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(udev, param->xfer); + if (ret) { + goto cleanup; } - ret = usbh_xfer_enqueue(priv->udev, xfer); - if (ret < 0) { - cleanup_xfer(priv->udev, xfer); - return ret; - } + goto done; - return 0; +cleanup: + (void)usbh_xfer_free(udev, param->xfer); + +done: + return ret; } -static int cdc_ecm_intr_in_cb(struct usb_device *udev, struct uhc_transfer *xfer) +static int usbh_cdc_ecm_comm_rx(struct usbh_cdc_ecm_data *const data); +static int usbh_cdc_ecm_data_rx(struct usbh_cdc_ecm_data *const data); + +static void usbh_cdc_ecm_sig_raise(struct usbh_cdc_ecm_data *const data, unsigned int result) +{ + (void)atomic_or(&data->rx_pending_sig_vals, result); + (void)k_poll_signal_raise(data->rx_sig, 0); +} + +static void usbh_cdc_ecm_start_auto_rx(struct usbh_cdc_ecm_data *const data) +{ + (void)atomic_set(&data->auto_rx_enabled, 1); + + usbh_cdc_ecm_sig_raise(data, USBH_CDC_ECM_SIG_COMM_RX_IDLE | USBH_CDC_ECM_SIG_DATA_RX_IDLE); +} + +static void usbh_cdc_ecm_stop_auto_rx(struct usbh_cdc_ecm_data *const data) +{ + (void)atomic_clear(&data->auto_rx_enabled); + (void)atomic_clear(&data->rx_pending_sig_vals); + + k_poll_signal_reset(data->rx_sig); +} + +static int usbh_cdc_ecm_comm_rx_cb(struct usb_device *const udev, struct uhc_transfer *const xfer) { struct usbh_cdc_ecm_data *priv = xfer->priv; - struct cdc_ecm_notification *notif; - bool connected; - int ret = 0; + struct cdc_notification_packet *notif; + uint32_t *link_speeds; + int ret = xfer->err; + + if (atomic_get(&priv->auto_rx_enabled)) { + if (usbh_cdc_ecm_comm_rx(priv)) { + usbh_cdc_ecm_sig_raise(priv, USBH_CDC_ECM_SIG_COMM_RX_IDLE); + } + } 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); + LOG_ERR("comm rx xfer callback error (%d)", ret); + goto cleanup; + } + + notif = (struct cdc_notification_packet *)xfer->buf->data; + switch (notif->bNotification) { + case USB_CDC_NETWORK_CONNECTION: + if (xfer->buf->len != sizeof(struct cdc_notification_packet)) { + ret = -EBADMSG; + goto cleanup; + } + if (sys_le16_to_cpu(notif->wValue)) { + net_if_carrier_on(priv->iface); + } else { + usbh_cdc_ecm_stop_auto_rx(priv); + net_if_carrier_off(priv->iface); + } + break; + case USB_CDC_CONNECTION_SPEED_CHANGE: + if (xfer->buf->len != (sizeof(struct cdc_notification_packet) + 8)) { + ret = -EBADMSG; + goto cleanup; + } + + link_speeds = (uint32_t *)(notif + 1); + + for (size_t i = 0; i < 2; i++) { + link_speeds[i] = sys_le32_to_cpu(link_speeds[i]); + switch (link_speeds[i]) { + case 2500 * 1000000U: + priv->caps |= ETHERNET_LINK_2500BASE; + case 1000 * 1000000U: + priv->caps |= ETHERNET_LINK_1000BASE; + case 100 * 1000000U: + priv->caps |= ETHERNET_LINK_100BASE; + case 10 * 1000000U: + priv->caps |= ETHERNET_LINK_10BASE; + break; + default: + break; } } + break; + default: + break; + } + +cleanup: + if (xfer->buf) { + usbh_xfer_buf_free(priv->c_data->udev, xfer->buf); } - cleanup_xfer(priv->udev, xfer); + (void)usbh_xfer_free(priv->c_data->udev, xfer); + + return ret; +} + +static int usbh_cdc_ecm_comm_rx(struct usbh_cdc_ecm_data *const data) +{ + struct usbh_cdc_ecm_xfer_params param; + int ret; - if (priv->state == CDC_ECM_CONFIGURED) { - ret = cdc_ecm_start_intr(priv); + struct net_buf *buf = + usbh_xfer_buf_alloc(data->c_data->udev, sizeof(struct cdc_notification_packet) + 8); + if (!buf) { + LOG_WRN("comm rx xfer buffer allocation failed"); + return -ENOMEM; } + param.buf = buf; + param.cb = usbh_cdc_ecm_comm_rx_cb; + param.cb_priv = data; + param.ep_addr = data->comm_in_ep_addr; + + ret = usbh_cdc_ecm_xfer(data->c_data->udev, ¶m); if (ret) { - LOG_ERR("Failed to resubmit intr in xfer : %d", ret); + LOG_ERR("comm rx xfer request failed (%d)", ret); + usbh_xfer_buf_free(data->c_data->udev, buf); } return ret; } -static int cdc_ecm_bulk_in_cb(struct usb_device *udev, struct uhc_transfer *xfer) +static int usbh_cdc_ecm_data_rx_cb(struct usb_device *const udev, struct uhc_transfer *const xfer) { struct usbh_cdc_ecm_data *priv = xfer->priv; struct net_pkt *pkt; - int ret = 0; + int ret = xfer->err; - if (xfer->err) { - LOG_DBG("Bulk in transfer error: %d", xfer->err); - priv->stats.errors.rx++; + if (atomic_get(&priv->auto_rx_enabled)) { + if (usbh_cdc_ecm_data_rx(priv)) { + usbh_cdc_ecm_sig_raise(priv, USBH_CDC_ECM_SIG_DATA_RX_IDLE); + } } - 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 (xfer->err) { + LOG_ERR("data rx xfer callback error (%d)", ret); + goto cleanup; + } - 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++; - } + if (!xfer->buf->len) { + LOG_DBG("data rx xfer callback discard 0 length packet"); + goto cleanup; } - cleanup_xfer(priv->udev, xfer); + if (xfer->buf->len > priv->max_segment_size) { + LOG_WRN("data rx xfer callback dropped data (length: %d) with exceeding max " + "segment size (%d)", + xfer->buf->len, priv->max_segment_size); + goto cleanup; + } - if (priv->state == CDC_ECM_CONFIGURED) { - ret = cdc_ecm_start_rx(priv); + pkt = net_pkt_rx_alloc_on_iface(priv->iface, K_NO_WAIT); + if (!pkt) { + LOG_WRN("data rx xfer callback alloc net pkt failed and lost data"); + ret = -ENOMEM; + goto cleanup; } + net_pkt_set_family(pkt, AF_UNSPEC); + net_pkt_append_buffer(pkt, xfer->buf); + xfer->buf = NULL; + + ret = net_recv_data(priv->iface, pkt); if (ret) { - LOG_ERR("Failed to resubmit bulk in xfer : %d", ret); + LOG_ERR("data rx xfer callback transmits data into network stack failed (error: " + "%d)", + ret); + net_pkt_unref(pkt); + } + + goto done; + +cleanup: + if (xfer->buf) { + net_buf_unref(xfer->buf); } +done: + (void)usbh_xfer_free(priv->c_data->udev, xfer); + return ret; } -static int cdc_ecm_start_rx(struct usbh_cdc_ecm_data *priv) +static int usbh_cdc_ecm_data_rx(struct usbh_cdc_ecm_data *const data) { - return submit_xfer(priv, priv->bulk_in_ep, cdc_ecm_bulk_in_cb, priv->bulk_mps, NULL); + struct usbh_cdc_ecm_xfer_params param; + int ret; + + struct net_buf *buf = net_buf_alloc(&usbh_cdc_ecm_data_rx_pool, K_NO_WAIT); + if (!buf) { + LOG_WRN("data rx xfer buffer allocation failed"); + return -ENOMEM; + } + + param.buf = buf; + param.cb = usbh_cdc_ecm_data_rx_cb; + param.cb_priv = data; + param.ep_addr = data->data_in_ep_addr; + + ret = usbh_cdc_ecm_xfer(data->c_data->udev, ¶m); + if (ret) { + LOG_ERR("data rx xfer request failed (%d)", ret); + net_buf_unref(buf); + } + + return ret; } -static int cdc_ecm_start_intr(struct usbh_cdc_ecm_data *priv) +static int usbh_cdc_ecm_data_tx_cb(struct usb_device *const udev, struct uhc_transfer *const xfer) { - return submit_xfer(priv, priv->intr_in_ep, cdc_ecm_intr_in_cb, priv->intr_mps, NULL); + struct usbh_cdc_ecm_data *priv = xfer->priv; + int ret = xfer->err; + + if (ret) { + LOG_ERR("data tx xfer callback error (%d)", ret); + } + + if (xfer->buf) { + net_buf_unref(xfer->buf); + } + + (void)usbh_xfer_free(priv->c_data->udev, xfer); + + return ret; } -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) +static int usbh_cdc_ecm_data_tx(struct usbh_cdc_ecm_data *const data, struct net_buf *buf) { - struct net_buf *buf = NULL; - int ret; + struct usbh_cdc_ecm_xfer_params param; + struct uhc_transfer *fst_xfer = NULL; + struct net_buf *tx_buf = NULL; + struct net_buf *zlp_buf = NULL; + struct net_buf *frag; + size_t total_len; + int ret = 0; - if (len > 0 && data) { - buf = usbh_xfer_buf_alloc(priv->udev, len); - if (!buf) { - return -ENOMEM; + if (!buf) { + LOG_ERR("data tx xfer get NULL buffer"); + return -EINVAL; + } + + total_len = net_buf_frags_len(buf); + if (!total_len || total_len > data->max_segment_size) { + LOG_ERR("data tx xfer invalid buffer length (%zu)", total_len); + return -EMSGSIZE; + } + + if (!buf->frags) { + tx_buf = net_buf_ref(buf); + } else { + frag = buf; + while (frag) { + frag = net_buf_ref(frag); + frag = frag->frags; + } + + tx_buf = net_buf_alloc(&usbh_cdc_ecm_data_tx_pool, K_NO_WAIT); + if (!tx_buf) { + LOG_WRN("data tx xfer linearized buffer allocation failed"); + ret = -ENOMEM; + goto unref_frags; + } + + if (net_buf_linearize(tx_buf->data, total_len, buf, 0, total_len) != total_len) { + LOG_ERR("data tx xfer linearize fragmented buffer error"); + ret = -EIO; + (void)net_buf_unref(tx_buf); + goto unref_frags; + } + + (void)net_buf_add(tx_buf, total_len); + +unref_frags: + frag = buf; + while (frag) { + struct net_buf *next = frag->frags; + (void)net_buf_unref(frag); + frag = next; + } + + if (ret) { + return ret; + } + } + + param.buf = tx_buf; + param.cb = usbh_cdc_ecm_data_tx_cb; + param.cb_priv = data; + param.ep_addr = data->data_out_ep_addr; + + ret = usbh_cdc_ecm_xfer(data->c_data->udev, ¶m); + if (ret) { + LOG_ERR("data tx xfer request failed (%d)", ret); + (void)net_buf_unref(tx_buf); + return ret; + } + + fst_xfer = param.xfer; + + if (!(total_len % data->data_out_ep_mps)) { + zlp_buf = net_buf_alloc(&usbh_cdc_ecm_data_tx_pool, K_NO_WAIT); + if (!zlp_buf) { + LOG_WRN("data tx xfer zlp buffer allocation failed"); + ret = -ENOMEM; + goto dequeue_first; + } + + param.buf = zlp_buf; + + ret = usbh_cdc_ecm_xfer(data->c_data->udev, ¶m); + if (ret) { + LOG_ERR("data tx xfer (zlp) request failed (%d)", ret); + (void)net_buf_unref(zlp_buf); + goto dequeue_first; } - 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); + return 0; - if (buf) { - usbh_xfer_buf_free(priv->udev, buf); +dequeue_first: + if (!usbh_xfer_dequeue(data->c_data->udev, fst_xfer)) { + (void)net_buf_unref(tx_buf); + (void)usbh_xfer_free(data->c_data->udev, fst_xfer); } return ret; } -static int cdc_ecm_bulk_out_cb(struct usb_device *udev, struct uhc_transfer *xfer) +static int usbh_cdc_ecm_set_pkt_filter(struct usbh_cdc_ecm_data *const data, + struct usb_device *const udev, uint16_t packet_type) { - struct usbh_cdc_ecm_data *priv = xfer->priv; + struct usbh_cdc_ecm_req_params param; + uint16_t current_filter, target_filter; + int ret; - 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++; + current_filter = (uint16_t)atomic_or(&data->eth_pkt_filter_bitmap, packet_type); + target_filter = current_filter | packet_type; + + if (current_filter == target_filter) { + return 0; } - cleanup_xfer(priv->udev, xfer); - k_sem_give(&priv->tx_comp_sem); + param.bRequest = SET_ETHERNET_PACKET_FILTER; + param.eth_pkt_filter_bitmap = target_filter; + ret = usbh_cdc_ecm_req(data, udev, ¶m); + if (ret) { + (void)atomic_and(&data->eth_pkt_filter_bitmap, ~packet_type); + } - return 0; + return ret; } -static int cdc_ecm_parse_descriptors(struct usbh_cdc_ecm_data *priv) +static int usbh_cdc_ecm_unset_pkt_filter(struct usbh_cdc_ecm_data *const data, + struct usb_device *const udev, uint16_t packet_type) { - 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) { + struct usbh_cdc_ecm_req_params param; + uint16_t current_filter, target_filter; + int ret; + + current_filter = (uint16_t)atomic_and(&data->eth_pkt_filter_bitmap, ~packet_type); + target_filter = current_filter & ~packet_type; + + if (current_filter == target_filter) { + return 0; + } + + param.bRequest = SET_ETHERNET_PACKET_FILTER; + param.eth_pkt_filter_bitmap = target_filter; + ret = usbh_cdc_ecm_req(data, udev, ¶m); + if (ret) { + (void)atomic_or(&data->eth_pkt_filter_bitmap, packet_type); + } + + return ret; +} + +static int usbh_cdc_ecm_parse_descriptors(struct usbh_cdc_ecm_data *const data, + struct usb_device *const udev, + const struct usb_desc_header *desc) +{ + const void *desc_end = usbh_desc_get_cfg_end(udev); + struct usb_if_descriptor *if_desc = NULL; + struct cdc_header_descriptor *cdc_header_desc = NULL; + struct cdc_union_descriptor *cdc_union_desc = NULL; + struct cdc_ecm_descriptor *cdc_ecm_desc = NULL; + struct usb_ep_descriptor *ep_desc = NULL; + bool cdc_header_func_ready = false; + bool cdc_union_func_ready = false; + + while (desc) { switch (desc->bDescriptorType) { case USB_DESC_INTERFACE: - if_desc = (const struct usb_if_descriptor *)desc; - /* CDC Control Interface */ + if_desc = (struct usb_if_descriptor *)desc; 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; + data->comm_if_num = if_desc->bInterfaceNumber; + } else if (if_desc->bInterfaceClass == USB_BCC_CDC_DATA) { + data->data_if_num = if_desc->bInterfaceNumber; + if (if_desc->bNumEndpoints) { + data->data_alt_num = if_desc->bAlternateSetting; + } + } else { + return -ENOTSUP; } 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; + cdc_header_desc = (struct cdc_header_descriptor *)desc; + if (cdc_header_desc->bDescriptorSubtype == HEADER_FUNC_DESC) { + cdc_header_func_ready = true; + } else if (cdc_header_desc->bDescriptorSubtype == UNION_FUNC_DESC && + cdc_header_func_ready) { + cdc_union_func_ready = true; + cdc_union_desc = (struct cdc_union_descriptor *)desc; + if (cdc_union_desc->bControlInterface != data->comm_if_num) { + return -ENODEV; + } + } else if (cdc_header_desc->bDescriptorSubtype == ETHERNET_FUNC_DESC && + cdc_union_func_ready) { + cdc_ecm_desc = (struct cdc_ecm_descriptor *)desc; + data->mac_str_desc_idx = cdc_ecm_desc->iMACAddress; + /** TODO: Ethernet Statistics Feature */ + data->max_segment_size = + sys_le16_to_cpu(cdc_ecm_desc->wMaxSegmentSize); + /** TODO: MCFilter Feature */ + /** TODO: Power Filter Feature */ } 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; + ep_desc = (struct usb_ep_descriptor *)desc; + if (!if_desc) { + return -ENODEV; } - - /* 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; + if (if_desc->bInterfaceClass == USB_BCC_CDC_CONTROL) { + if ((ep_desc->bEndpointAddress & USB_EP_DIR_MASK) == + USB_EP_DIR_IN) { + data->comm_in_ep_addr = ep_desc->bEndpointAddress; + } else { + return -ENODEV; + } + } else if (if_desc->bInterfaceClass == USB_BCC_CDC_DATA) { + if ((ep_desc->bEndpointAddress & USB_EP_DIR_MASK) == + USB_EP_DIR_IN) { + data->data_in_ep_addr = ep_desc->bEndpointAddress; + } else { + data->data_out_ep_addr = ep_desc->bEndpointAddress; + data->data_out_ep_mps = + sys_le16_to_cpu(ep_desc->wMaxPacketSize); + } + } else { + return -ENOTSUP; } 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); + if (!cdc_header_func_ready || !cdc_union_func_ready) { + return -ENODEV; + } + + if (data->mac_str_desc_idx == 0) { 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); + if (!data->comm_in_ep_addr || !data->data_in_ep_addr || !data->data_out_ep_addr) { + return -ENODEV; + } return 0; } +static int usbh_cdc_ecm_get_mac_address(struct usbh_cdc_ecm_data *const data, + struct usb_device *const udev) +{ + struct usb_string_descriptor zero_str_desc_head; + struct usb_string_descriptor *zero_str_desc = NULL; + bool zero_str_desc_allocated = false; + size_t langid_size = 0; + uint8_t *langid_data = NULL; + uint8_t mac_str_desc_buf[2 + NET_ETH_ADDR_LEN * 2 * 2]; + struct usb_string_descriptor *mac_str_desc = + (struct usb_string_descriptor *)mac_str_desc_buf; + uint8_t *mac_utf16le = NULL; + char mac_str[NET_ETH_ADDR_LEN * 2 + 1] = {0}; + bool found_mac = false; + struct ethernet_req_params eth_req_params; + int ret; + + if (!data->mac_str_desc_idx) { + return -EINVAL; + } + + ret = usbh_req_desc_str(udev, 0, sizeof(zero_str_desc_head), 0, &zero_str_desc_head); + if (ret) { + return ret; + } + + langid_size = (zero_str_desc_head.bLength - 2) / 2; + + if (zero_str_desc_head.bLength > sizeof(zero_str_desc_head)) { + zero_str_desc = k_malloc(zero_str_desc_head.bLength); + if (!zero_str_desc) { + return -ENOMEM; + } + + zero_str_desc_allocated = true; + + ret = usbh_req_desc_str(udev, 0, zero_str_desc_head.bLength, 0, zero_str_desc); + if (ret) { + goto cleanup; + } + } else if (zero_str_desc_head.bLength < sizeof(zero_str_desc_head)) { + return -ENODEV; + } else { + zero_str_desc = &zero_str_desc_head; + } + + langid_data = (uint8_t *)&zero_str_desc->bString; + + for (size_t i = 0; i < langid_size; i++) { + ret = usbh_req_desc_str(udev, data->mac_str_desc_idx, ARRAY_SIZE(mac_str_desc_buf), + sys_get_le16(&langid_data[i * 2]), mac_str_desc); + if (ret) { + continue; + } + + if (mac_str_desc->bLength != ARRAY_SIZE(mac_str_desc_buf)) { + continue; + } + + mac_utf16le = (uint8_t *)&mac_str_desc->bString; + + for (size_t j = 0; j < NET_ETH_ADDR_LEN * 2; j++) { + mac_str[j] = (char)sys_get_le16(&mac_utf16le[j * 2]); + } + + if (hex2bin(mac_str, NET_ETH_ADDR_LEN * 2, eth_req_params.mac_address.addr, + NET_ETH_ADDR_LEN) == NET_ETH_ADDR_LEN) { + if (net_eth_is_addr_valid(ð_req_params.mac_address)) { + found_mac = true; + break; + } + } + } + + if (!found_mac) { + ret = -ENODEV; + goto cleanup; + } + + ret = net_mgmt(NET_REQUEST_ETHERNET_SET_MAC_ADDRESS, data->iface, ð_req_params, + sizeof(eth_req_params)); + if (ret) { + LOG_ERR("net management set mac address error (%d)", ret); + } + +cleanup: + if (zero_str_desc_allocated) { + k_free(zero_str_desc); + } + + return ret; +} + static int usbh_cdc_ecm_init(struct usbh_class_data *const c_data, struct usbh_context *const uhs_ctx) { - ARG_UNUSED(c_data); + struct device *dev = c_data->priv; + struct usbh_cdc_ecm_data *priv = dev->data; + ARG_UNUSED(uhs_ctx); + priv->c_data = c_data; + usbh_cdc_ecm_data_instances[priv->dev_idx] = priv; + return 0; } @@ -341,114 +831,116 @@ static int usbh_cdc_ecm_completion_cb(struct usbh_class_data *const c_data, return 0; } -static int usbh_cdc_ecm_probe(struct usbh_class_data *const c_data, - struct usb_device *const udev, +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; + struct device *dev = c_data->priv; + struct usbh_cdc_ecm_data *priv = dev->data; + 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; + struct net_linkaddr *linkaddr; 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); + if (!desc) { + 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); + const struct usb_association_descriptor *assoc_desc = + (struct usb_association_descriptor *)desc; + desc = usbh_desc_get_by_iface(desc, desc_end, assoc_desc->bFirstInterface); + if (!desc) { + LOG_ERR("no descriptor (iad) found for interface %u", iface); 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->comm_if_num = 0; + priv->data_if_num = 0; + priv->data_alt_num = 0; + priv->comm_in_ep_addr = 0; + priv->data_in_ep_addr = 0; + priv->data_out_ep_addr = 0; - priv->udev = udev; - priv->state = CDC_ECM_CONNECTED; - - /* Parse descriptors to find endpoints */ - ret = cdc_ecm_parse_descriptors(priv); + ret = usbh_cdc_ecm_parse_descriptors(priv, udev, desc); if (ret) { - LOG_ERR("Failed to parse CDC ECM descriptors"); + LOG_ERR("parse descriptor error (%d)", ret); 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; + LOG_INF("communication interface %d, IN endpoint addr 0x%02x", priv->comm_if_num, + priv->comm_in_ep_addr); + LOG_INF("data interface %d, IN endpoint addr 0x%02x, OUT endpoint addr 0x%02x", + priv->data_if_num, priv->data_in_ep_addr, priv->data_out_ep_addr); + LOG_INF("device wMaxSegmentSize is %d", priv->max_segment_size); + + if (priv->data_alt_num) { + ret = usbh_device_interface_set(udev, priv->data_if_num, priv->data_alt_num, false); + if (ret) { + LOG_ERR("set data interface alternate setting error (%d)", ret); + return ret; + } } - priv->state = CDC_ECM_CONFIGURED; + priv->caps = 0; + (void)atomic_clear(&priv->eth_pkt_filter_bitmap); - /* Start interrupt endpoint for notifications */ - ret = cdc_ecm_start_intr(priv); + ret = net_if_down(priv->iface); + if (ret && ret != -EALREADY) { + LOG_ERR("down network interface error (%d)", ret); + return ret; + } + + ret = usbh_cdc_ecm_get_mac_address(priv, udev); if (ret) { - LOG_ERR("Failed to start interrupt transfer: %d", ret); + LOG_ERR("get mac address error (%d)", ret); return ret; } - /* Start receiving data */ - ret = cdc_ecm_start_rx(priv); + linkaddr = net_if_get_link_addr(priv->iface); + LOG_INF("device mac address %02x:%02x:%02x:%02x:%02x:%02x", linkaddr->addr[0], + linkaddr->addr[1], linkaddr->addr[2], linkaddr->addr[3], linkaddr->addr[4], + linkaddr->addr[5]); + + ret = usbh_cdc_ecm_set_pkt_filter(priv, udev, + PACKET_TYPE_BROADCAST | PACKET_TYPE_DIRECTED | + PACKET_TYPE_ALL_MULTICAST); if (ret) { - LOG_ERR("Failed to start RX transfers: %d", ret); + LOG_ERR("set packet filter error (%d)", ret); return ret; } - if (priv->iface == NULL) { - return -ENETDOWN; + ret = net_if_up(priv->iface); + if (ret) { + LOG_ERR("bring up network interface error (%d)", ret); + return ret; } - net_if_carrier_on(priv->iface); + usbh_cdc_ecm_start_auto_rx(priv); return 0; } static int usbh_cdc_ecm_removed(struct usbh_class_data *const c_data) { - struct usbh_cdc_ecm_data *priv = c_data->priv; + struct device *dev = c_data->priv; + struct usbh_cdc_ecm_data *priv = dev->data; + int ret; - LOG_INF("CDC ECM device removed"); + net_if_carrier_off(priv->iface); - priv->state = CDC_ECM_DISCONNECTED; + usbh_cdc_ecm_stop_auto_rx(priv); - /* Stop network operations */ - if (priv->iface) { - net_if_carrier_off(priv->iface); + ret = net_if_down(priv->iface); + if (ret && ret != -EALREADY) { + LOG_WRN("down network interface error (%d)", ret); } - /* 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); + priv->caps = 0; + (void)atomic_clear(&priv->eth_pkt_filter_bitmap); return 0; } @@ -467,74 +959,65 @@ 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) +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 void eth_usbh_cdc_ecm_iface_init(struct net_if *iface) { - const struct device *dev = net_if_get_device(iface); - struct usbh_cdc_ecm_data *priv = dev->data; + struct usbh_cdc_ecm_data *priv = net_if_get_device(iface)->data; priv->iface = iface; - net_if_set_link_addr(iface, priv->mac_addr, sizeof(priv->mac_addr), NET_LINK_ETHERNET); + ethernet_init(iface); + net_if_flag_clear(iface, NET_IF_UP); 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) +static enum ethernet_hw_caps eth_usbh_cdc_ecm_get_capabilities(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); - } + struct usbh_cdc_ecm_data *priv = dev->data; - return 0; + return priv->caps | ETHERNET_LINK_100BASE | ETHERNET_LINK_1000BASE; } -static int cdc_ecm_host_iface_stop(const struct device *dev) +#if defined(CONFIG_NET_STATISTICS_ETHERNET) +struct net_stats_eth *eth_usbh_cdc_ecm_get_stats(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); + struct usbh_cdc_ecm_data *priv = dev->data; - return ETHERNET_LINK_10BASE; + return &priv->stats; } +#endif -static int cdc_ecm_host_set_config(const struct device *dev, - enum ethernet_config_type type, - const struct ethernet_config *config) +static int eth_usbh_cdc_ecm_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; + struct usbh_cdc_ecm_data *priv = 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)); + ret = net_if_set_link_addr(priv->iface, (uint8_t *)config->mac_address.addr, + NET_ETH_ADDR_LEN, NET_LINK_ETHERNET); 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); +#if defined(CONFIG_NET_PROMISCUOUS_MODE) + case ETHERNET_CONFIG_TYPE_PROMISC_MODE: + if (config->promisc_mode) { + ret = usbh_cdc_ecm_set_pkt_filter(priv, priv->c_data->udev, + PACKET_TYPE_PROMISCUOUS); + } else { + ret = usbh_cdc_ecm_unset_pkt_filter(priv, priv->c_data->udev, + PACKET_TYPE_PROMISCUOUS); + } break; - } - +#endif default: ret = -ENOTSUP; break; @@ -543,109 +1026,132 @@ static int cdc_ecm_host_set_config(const struct device *dev, return ret; } -static int cdc_ecm_host_send(const struct device *dev, struct net_pkt *pkt) +static int eth_usbh_cdc_ecm_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; - } + struct usbh_cdc_ecm_data *priv = dev->data; - if (priv->state != CDC_ECM_CONFIGURED) { + if (priv->c_data->udev->state != USB_STATE_CONFIGURED) { return -ENETDOWN; } - if (k_mutex_lock(&priv->tx_mutex, K_MSEC(1000)) != 0) { - return -EBUSY; + if (!pkt || !pkt->frags) { + return -EINVAL; } - net_pkt_cursor_init(pkt); + return usbh_cdc_ecm_data_tx(priv, pkt->frags); +} - remain = len; - need_zlp = (len % priv->bulk_mps == 0); +static struct ethernet_api eth_usbh_cdc_ecm_api = { + .iface_api.init = eth_usbh_cdc_ecm_iface_init, + .get_capabilities = eth_usbh_cdc_ecm_get_capabilities, +#if defined(CONFIG_NET_STATISTICS_ETHERNET) + .get_stats = eth_usbh_cdc_ecm_get_stats, +#endif + .set_config = eth_usbh_cdc_ecm_set_config, + .send = eth_usbh_cdc_ecm_send, +}; - while (remain > 0) { - chunk_size = MIN(remain, (size_t)priv->bulk_mps); +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, +}}; - ret = submit_xfer(priv, priv->bulk_out_ep, cdc_ecm_bulk_out_cb, chunk_size, pkt); +static void usbh_cdc_ecm_thread_entry(void *arg1, void *arg2, void *arg3) +{ + struct usbh_cdc_ecm_data *data; + struct k_poll_signal *sig; + struct k_poll_event *evt; + unsigned int signaled; + int result; + unsigned int pending_sig_val; + int ret; - if (ret < 0) { - goto error; - } + ARG_UNUSED(arg1); + ARG_UNUSED(arg2); + ARG_UNUSED(arg3); + + for (size_t i = 0; i < USBH_CDC_ECM_INSTANCE_COUNT; i++) { + k_poll_signal_init(&usbh_cdc_ecm_data_signals[i]); + k_poll_event_init(&usbh_cdc_ecm_data_events[i], K_POLL_TYPE_SIGNAL, + K_POLL_MODE_NOTIFY_ONLY, &usbh_cdc_ecm_data_signals[i]); + } - if (k_sem_take(&priv->tx_comp_sem, CDC_ECM_SEND_TIMEOUT_MS) != 0) { - ret = -ETIMEDOUT; - goto error; + while (true) { + ret = k_poll(usbh_cdc_ecm_data_events, ARRAY_SIZE(usbh_cdc_ecm_data_events), + K_FOREVER); + if (ret) { + k_sleep(K_MSEC(1000)); + continue; } - remain -= chunk_size; - } + for (size_t i = 0; i < ARRAY_SIZE(usbh_cdc_ecm_data_events); i++) { + evt = &usbh_cdc_ecm_data_events[i]; + sig = &usbh_cdc_ecm_data_signals[i]; + data = usbh_cdc_ecm_data_instances[i]; - if (need_zlp) { - ret = submit_xfer(priv, priv->bulk_out_ep, cdc_ecm_bulk_out_cb, 0, NULL); + if (evt->state != K_POLL_STATE_SIGNALED) { + continue; + } - if (ret < 0) { - goto error; - } + k_poll_signal_check(sig, &signaled, &result); - if (k_sem_take(&priv->tx_comp_sem, CDC_ECM_SEND_TIMEOUT_MS) != 0) { - ret = -ETIMEDOUT; - } - } -error: - k_mutex_unlock(&priv->tx_mutex); + evt->state = K_POLL_STATE_NOT_READY; - return ret; -} + if (signaled) { + k_poll_signal_reset(sig); -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, -}; + if (!data) { + continue; + } -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, -}; + if (!atomic_get(&data->auto_rx_enabled)) { + continue; + } + + result = atomic_clear(&data->rx_pending_sig_vals); + pending_sig_val = 0; + + if (result & USBH_CDC_ECM_SIG_COMM_RX_IDLE) { + ret = usbh_cdc_ecm_comm_rx(data); + if (ret) { + pending_sig_val |= USBH_CDC_ECM_SIG_COMM_RX_IDLE; + } + } + + if (result & USBH_CDC_ECM_SIG_DATA_RX_IDLE) { + ret = usbh_cdc_ecm_data_rx(data); + if (ret) { + pending_sig_val |= USBH_CDC_ECM_SIG_DATA_RX_IDLE; + } + } -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, + if (pending_sig_val) { + if (result & pending_sig_val) { + k_sleep(K_MSEC(500)); + } + usbh_cdc_ecm_sig_raise(data, pending_sig_val); + } + } + } } -}; +} -#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)); +K_THREAD_DEFINE(usbh_cdc_ecm_thread, CONFIG_USBH_CDC_ECM_STACK_SIZE, usbh_cdc_ecm_thread_entry, + NULL, NULL, NULL, CONFIG_SYSTEM_WORKQUEUE_PRIORITY, 0, 0); + +#define USBH_CDC_ECM_DT_DEVICE_DEFINE(n) \ + static struct usbh_cdc_ecm_data cdc_ecm_data_##n = { \ + .dev_idx = n, \ + .rx_sig = &usbh_cdc_ecm_data_signals[n], \ + }; \ + \ + ETH_NET_DEVICE_DT_INST_DEFINE(n, NULL, NULL, &cdc_ecm_data_##n, NULL, \ + CONFIG_ETH_INIT_PRIORITY, ð_usbh_cdc_ecm_api, \ + NET_ETH_MTU); \ + \ + USBH_DEFINE_CLASS(cdc_ecm_c_data_##n, &usbh_cdc_ecm_class_api, \ + (void *)DEVICE_DT_INST_GET(n), cdc_ecm_filters, \ + ARRAY_SIZE(cdc_ecm_filters)) DT_INST_FOREACH_STATUS_OKAY(USBH_CDC_ECM_DT_DEVICE_DEFINE); From 2e9f7ea568ba10cd6253de924ce9c462b51a08b1 Mon Sep 17 00:00:00 2001 From: Dv Alan Date: Mon, 1 Dec 2025 17:52:47 +0800 Subject: [PATCH 30/34] dts: bindings: usb: Rename CDC-ECM host binding file Rename the CDC-ECM host device tree binding file from zephyr,usbh-cdc-ecm.yaml to zephyr,cdc-ecm-host.yaml to align with the updated compatible string used in the rewritten driver. Update the binding description to clarify that each CDC-ECM instance represents an ethernet interface visible to Zephyr's networking stack. Remove the local-mac-address property as MAC address retrieval is now handled directly from USB string descriptors in the driver implementation. Signed-off-by: Dv Alan --- dts/bindings/usb/zephyr,cdc-ecm-host.yaml | 16 ++++++++++++++++ dts/bindings/usb/zephyr,usbh-cdc-ecm.yaml | 15 --------------- 2 files changed, 16 insertions(+), 15 deletions(-) create mode 100644 dts/bindings/usb/zephyr,cdc-ecm-host.yaml delete mode 100644 dts/bindings/usb/zephyr,usbh-cdc-ecm.yaml diff --git a/dts/bindings/usb/zephyr,cdc-ecm-host.yaml b/dts/bindings/usb/zephyr,cdc-ecm-host.yaml new file mode 100644 index 0000000000000..761730cae24ab --- /dev/null +++ b/dts/bindings/usb/zephyr,cdc-ecm-host.yaml @@ -0,0 +1,16 @@ +# Copyright (c) 2025 NXP +# Copyright (c) 2025 Linumiz GmbH +# SPDX-License-Identifier: Apache-2.0 + +description: | + USB CDC Ethernet Control Model (CDC-ECM) host instance. + Each CDC-ECM instance added to the USB Host Controller (UHC) node + will be visible as a new ethernet interface from Zephyr point of + view. + As soon as a ethernet interface is connected to USB this device will + be usable by the application as a ethernet interface device, + following the networking API. + +compatible: "zephyr,cdc-ecm-host" + +include: base.yaml diff --git a/dts/bindings/usb/zephyr,usbh-cdc-ecm.yaml b/dts/bindings/usb/zephyr,usbh-cdc-ecm.yaml deleted file mode 100644 index 75919ab0462f7..0000000000000 --- a/dts/bindings/usb/zephyr,usbh-cdc-ecm.yaml +++ /dev/null @@ -1,15 +0,0 @@ -# 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 15a9ae539d7c42aa580fde84fbf9d847c17d1369 Mon Sep 17 00:00:00 2001 From: Dv Alan Date: Mon, 1 Dec 2025 17:53:43 +0800 Subject: [PATCH 31/34] samples: net: dhcpv4_client: Add USB host stack initialization support Add USB host stack initialization to the DHCPv4 client sample to enable testing with USB CDC-ECM host devices. Initialize and enable the USB host controller (zephyr_uhc0) when CONFIG_USB_HOST_STACK is enabled, allowing the sample to work with USB Ethernet adapters. Add conditional includes for USB host stack headers and define the USB host controller context using USBH_CONTROLLER_DEFINE macro. Initialize and enable the USB host stack in main() before setting up network management callbacks. Signed-off-by: Dv Alan --- samples/net/dhcpv4_client/src/main.c | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/samples/net/dhcpv4_client/src/main.c b/samples/net/dhcpv4_client/src/main.c index f307e3401e97b..b29f71da38b45 100644 --- a/samples/net/dhcpv4_client/src/main.c +++ b/samples/net/dhcpv4_client/src/main.c @@ -3,6 +3,7 @@ /* * Copyright (c) 2017 ARM Ltd. * Copyright (c) 2016 Intel Corporation. + * Copyright (c) 2025 NXP. * * SPDX-License-Identifier: Apache-2.0 */ @@ -20,6 +21,15 @@ LOG_MODULE_REGISTER(net_dhcpv4_client_sample, LOG_LEVEL_DBG); #include #include +#if defined(CONFIG_USB_HOST_STACK) +#include +#include +#endif + +#if defined(CONFIG_USB_HOST_STACK) +USBH_CONTROLLER_DEFINE(uhs_ctx, DEVICE_DT_GET(DT_NODELABEL(zephyr_uhc0))); +#endif + #define DHCP_OPTION_NTP (42) static uint8_t ntp_server[4]; @@ -87,6 +97,22 @@ int main(void) { LOG_INF("Run dhcpv4 client"); +#if defined(CONFIG_USB_HOST_STACK) + int err; + + err = usbh_init(&uhs_ctx); + if (err) { + LOG_ERR("Failed to initialize %s: %d", uhs_ctx.name, err); + return err; + } + + err = usbh_enable(&uhs_ctx); + if (err) { + LOG_ERR("Failed to enable %s: %d", uhs_ctx.name, err); + return err; + } +#endif + net_mgmt_init_event_callback(&mgmt_cb, handler, NET_EVENT_IPV4_ADDR_ADD); net_mgmt_add_event_callback(&mgmt_cb); From 7c3e00d10235011ea6fb9ae5811c3a40aec60449 Mon Sep 17 00:00:00 2001 From: Dv Alan Date: Mon, 1 Dec 2025 18:01:02 +0800 Subject: [PATCH 32/34] samples: net: dhcpv4_client: Add board configuration for rd_rw612_bga Add board-specific configuration and overlay files for the rd_rw612_bga board to enable USB host CDC-ECM Ethernet support in the DHCPv4 client sample. The overlay file defines a CDC-ECM host instance using the "zephyr,cdc-ecm-host" compatible string and enables the USB host controller (usbh) as zephyr_uhc0. The xtal32 node is disabled as it's not required for this configuration. The configuration file enables the USB host stack (CONFIG_USB_HOST_STACK) and the CDC-ECM host class driver (CONFIG_USBH_CDC_ECM_CLASS), allowing the sample to work with USB Ethernet adapters on the rd_rw612_bga board. Signed-off-by: Dv Alan --- .../dhcpv4_client/boards/rd_rw612_bga.conf | 2 ++ .../dhcpv4_client/boards/rd_rw612_bga.overlay | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 samples/net/dhcpv4_client/boards/rd_rw612_bga.conf create mode 100644 samples/net/dhcpv4_client/boards/rd_rw612_bga.overlay diff --git a/samples/net/dhcpv4_client/boards/rd_rw612_bga.conf b/samples/net/dhcpv4_client/boards/rd_rw612_bga.conf new file mode 100644 index 0000000000000..3cefeb555ef2c --- /dev/null +++ b/samples/net/dhcpv4_client/boards/rd_rw612_bga.conf @@ -0,0 +1,2 @@ +CONFIG_USB_HOST_STACK=y +CONFIG_USBH_CDC_ECM_CLASS=y diff --git a/samples/net/dhcpv4_client/boards/rd_rw612_bga.overlay b/samples/net/dhcpv4_client/boards/rd_rw612_bga.overlay new file mode 100644 index 0000000000000..ec51cd6f5913b --- /dev/null +++ b/samples/net/dhcpv4_client/boards/rd_rw612_bga.overlay @@ -0,0 +1,20 @@ +/* + * Copyright 2025 NXP + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/ { + usbh_cdc_ecm { + compatible = "zephyr,cdc-ecm-host"; + status = "okay"; + }; +}; + +&xtal32 { + status = "disabled"; +}; + +zephyr_uhc0: &usbh { + status = "okay"; +}; From 0d7f337151a4336ce77eb7594a6806c23168778a Mon Sep 17 00:00:00 2001 From: Dv Alan Date: Wed, 3 Dec 2025 16:13:31 +0800 Subject: [PATCH 33/34] usb: host: class: cdc_ecm: Fix incorrect Ethernet API calls Refactor the CDC-ECM host driver to improve MAC address handling and network interface lifecycle management: - Remove eth_usbh_cdc_ecm_get_capabilities() of eth_usbh_cdc_ecm_api, since it is static information about the ethernet capability, usb host does not match it - Remove manual net_if_up/down calls from probe and removed functions - Remove net_mgmt call for setting MAC address - Add upload_speed and download_speed fields to track connection speeds - Add interface and carrier state checks in RX callback to discard packets when interface is down Signed-off-by: Dv Alan --- subsys/usb/host/class/usbh_cdc_ecm.c | 94 +++++++++------------------- 1 file changed, 31 insertions(+), 63 deletions(-) diff --git a/subsys/usb/host/class/usbh_cdc_ecm.c b/subsys/usb/host/class/usbh_cdc_ecm.c index 8cd0f009d2617..0330676dab0ee 100644 --- a/subsys/usb/host/class/usbh_cdc_ecm.c +++ b/subsys/usb/host/class/usbh_cdc_ecm.c @@ -43,10 +43,12 @@ struct usbh_cdc_ecm_data { uint8_t data_out_ep_addr; uint16_t data_out_ep_mps; uint8_t mac_str_desc_idx; + struct net_eth_addr eth_mac; + uint32_t upload_speed; + uint32_t download_speed; uint16_t max_segment_size; atomic_t eth_pkt_filter_bitmap; struct net_if *iface; - enum ethernet_hw_caps caps; #if defined(CONFIG_NET_STATISTICS_ETHERNET) struct net_stats_eth stats; #endif @@ -296,7 +298,6 @@ static int usbh_cdc_ecm_comm_rx_cb(struct usb_device *const udev, struct uhc_tra if (sys_le16_to_cpu(notif->wValue)) { net_if_carrier_on(priv->iface); } else { - usbh_cdc_ecm_stop_auto_rx(priv); net_if_carrier_off(priv->iface); } break; @@ -310,18 +311,16 @@ static int usbh_cdc_ecm_comm_rx_cb(struct usb_device *const udev, struct uhc_tra for (size_t i = 0; i < 2; i++) { link_speeds[i] = sys_le32_to_cpu(link_speeds[i]); - switch (link_speeds[i]) { - case 2500 * 1000000U: - priv->caps |= ETHERNET_LINK_2500BASE; - case 1000 * 1000000U: - priv->caps |= ETHERNET_LINK_1000BASE; - case 100 * 1000000U: - priv->caps |= ETHERNET_LINK_100BASE; - case 10 * 1000000U: - priv->caps |= ETHERNET_LINK_10BASE; + switch (i) { + case 0: + priv->download_speed = link_speeds[i]; break; - default: + case 1: + priv->upload_speed = link_speeds[i]; break; + default: + ret = -EBADMSG; + goto cleanup; } } break; @@ -382,6 +381,10 @@ static int usbh_cdc_ecm_data_rx_cb(struct usb_device *const udev, struct uhc_tra goto cleanup; } + if (!net_if_is_up(priv->iface) || !net_if_is_carrier_ok(priv->iface)) { + goto cleanup; + } + if (!xfer->buf->len) { LOG_DBG("data rx xfer callback discard 0 length packet"); goto cleanup; @@ -729,7 +732,6 @@ static int usbh_cdc_ecm_get_mac_address(struct usbh_cdc_ecm_data *const data, uint8_t *mac_utf16le = NULL; char mac_str[NET_ETH_ADDR_LEN * 2 + 1] = {0}; bool found_mac = false; - struct ethernet_req_params eth_req_params; int ret; if (!data->mac_str_desc_idx) { @@ -780,9 +782,9 @@ static int usbh_cdc_ecm_get_mac_address(struct usbh_cdc_ecm_data *const data, mac_str[j] = (char)sys_get_le16(&mac_utf16le[j * 2]); } - if (hex2bin(mac_str, NET_ETH_ADDR_LEN * 2, eth_req_params.mac_address.addr, - NET_ETH_ADDR_LEN) == NET_ETH_ADDR_LEN) { - if (net_eth_is_addr_valid(ð_req_params.mac_address)) { + if (hex2bin(mac_str, NET_ETH_ADDR_LEN * 2, data->eth_mac.addr, NET_ETH_ADDR_LEN) == + NET_ETH_ADDR_LEN) { + if (net_eth_is_addr_valid(&data->eth_mac)) { found_mac = true; break; } @@ -794,12 +796,6 @@ static int usbh_cdc_ecm_get_mac_address(struct usbh_cdc_ecm_data *const data, goto cleanup; } - ret = net_mgmt(NET_REQUEST_ETHERNET_SET_MAC_ADDRESS, data->iface, ð_req_params, - sizeof(eth_req_params)); - if (ret) { - LOG_ERR("net management set mac address error (%d)", ret); - } - cleanup: if (zero_str_desc_allocated) { k_free(zero_str_desc); @@ -839,7 +835,6 @@ static int usbh_cdc_ecm_probe(struct usbh_class_data *const c_data, struct usb_d 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; - struct net_linkaddr *linkaddr; int ret; desc = usbh_desc_get_by_iface(desc_beg, desc_end, iface); @@ -877,6 +872,19 @@ static int usbh_cdc_ecm_probe(struct usbh_class_data *const c_data, struct usb_d priv->data_if_num, priv->data_in_ep_addr, priv->data_out_ep_addr); LOG_INF("device wMaxSegmentSize is %d", priv->max_segment_size); + ret = usbh_cdc_ecm_get_mac_address(priv, udev); + if (ret) { + LOG_ERR("get mac address error (%d)", ret); + return ret; + } + + LOG_INF("device mac address %02x:%02x:%02x:%02x:%02x:%02x", priv->eth_mac.addr[0], + priv->eth_mac.addr[1], priv->eth_mac.addr[2], priv->eth_mac.addr[3], + priv->eth_mac.addr[4], priv->eth_mac.addr[5]); + + net_if_set_link_addr(priv->iface, priv->eth_mac.addr, ARRAY_SIZE(priv->eth_mac.addr), + NET_LINK_ETHERNET); + if (priv->data_alt_num) { ret = usbh_device_interface_set(udev, priv->data_if_num, priv->data_alt_num, false); if (ret) { @@ -885,26 +893,8 @@ static int usbh_cdc_ecm_probe(struct usbh_class_data *const c_data, struct usb_d } } - priv->caps = 0; (void)atomic_clear(&priv->eth_pkt_filter_bitmap); - ret = net_if_down(priv->iface); - if (ret && ret != -EALREADY) { - LOG_ERR("down network interface error (%d)", ret); - return ret; - } - - ret = usbh_cdc_ecm_get_mac_address(priv, udev); - if (ret) { - LOG_ERR("get mac address error (%d)", ret); - return ret; - } - - linkaddr = net_if_get_link_addr(priv->iface); - LOG_INF("device mac address %02x:%02x:%02x:%02x:%02x:%02x", linkaddr->addr[0], - linkaddr->addr[1], linkaddr->addr[2], linkaddr->addr[3], linkaddr->addr[4], - linkaddr->addr[5]); - ret = usbh_cdc_ecm_set_pkt_filter(priv, udev, PACKET_TYPE_BROADCAST | PACKET_TYPE_DIRECTED | PACKET_TYPE_ALL_MULTICAST); @@ -913,12 +903,6 @@ static int usbh_cdc_ecm_probe(struct usbh_class_data *const c_data, struct usb_d return ret; } - ret = net_if_up(priv->iface); - if (ret) { - LOG_ERR("bring up network interface error (%d)", ret); - return ret; - } - usbh_cdc_ecm_start_auto_rx(priv); return 0; @@ -928,18 +912,11 @@ static int usbh_cdc_ecm_removed(struct usbh_class_data *const c_data) { struct device *dev = c_data->priv; struct usbh_cdc_ecm_data *priv = dev->data; - int ret; net_if_carrier_off(priv->iface); usbh_cdc_ecm_stop_auto_rx(priv); - ret = net_if_down(priv->iface); - if (ret && ret != -EALREADY) { - LOG_WRN("down network interface error (%d)", ret); - } - - priv->caps = 0; (void)atomic_clear(&priv->eth_pkt_filter_bitmap); return 0; @@ -975,17 +952,9 @@ static void eth_usbh_cdc_ecm_iface_init(struct net_if *iface) priv->iface = iface; ethernet_init(iface); - net_if_flag_clear(iface, NET_IF_UP); net_if_carrier_off(iface); } -static enum ethernet_hw_caps eth_usbh_cdc_ecm_get_capabilities(const struct device *dev) -{ - struct usbh_cdc_ecm_data *priv = dev->data; - - return priv->caps | ETHERNET_LINK_100BASE | ETHERNET_LINK_1000BASE; -} - #if defined(CONFIG_NET_STATISTICS_ETHERNET) struct net_stats_eth *eth_usbh_cdc_ecm_get_stats(const struct device *dev) { @@ -1043,7 +1012,6 @@ static int eth_usbh_cdc_ecm_send(const struct device *dev, struct net_pkt *pkt) static struct ethernet_api eth_usbh_cdc_ecm_api = { .iface_api.init = eth_usbh_cdc_ecm_iface_init, - .get_capabilities = eth_usbh_cdc_ecm_get_capabilities, #if defined(CONFIG_NET_STATISTICS_ETHERNET) .get_stats = eth_usbh_cdc_ecm_get_stats, #endif From 3b139434fe2f7e2450c330360a2e72c17c902157 Mon Sep 17 00:00:00 2001 From: Dv Alan Date: Wed, 3 Dec 2025 22:20:42 +0800 Subject: [PATCH 34/34] usb: host: class: cdc_ecm: Simplify connection speed notification Updates: - Remove unnecessary loop and switch statement in connection speed change notification - Add debug log in notification callback Signed-off-by: Dv Alan --- subsys/usb/host/class/usbh_cdc_ecm.c | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/subsys/usb/host/class/usbh_cdc_ecm.c b/subsys/usb/host/class/usbh_cdc_ecm.c index 0330676dab0ee..cb63bf7cf09b9 100644 --- a/subsys/usb/host/class/usbh_cdc_ecm.c +++ b/subsys/usb/host/class/usbh_cdc_ecm.c @@ -300,29 +300,19 @@ static int usbh_cdc_ecm_comm_rx_cb(struct usb_device *const udev, struct uhc_tra } else { net_if_carrier_off(priv->iface); } + LOG_DBG("comm rx xfer callback get network %s", + sys_le16_to_cpu(notif->wValue) ? "connected" : "disconnected"); break; case USB_CDC_CONNECTION_SPEED_CHANGE: if (xfer->buf->len != (sizeof(struct cdc_notification_packet) + 8)) { ret = -EBADMSG; goto cleanup; } - link_speeds = (uint32_t *)(notif + 1); - - for (size_t i = 0; i < 2; i++) { - link_speeds[i] = sys_le32_to_cpu(link_speeds[i]); - switch (i) { - case 0: - priv->download_speed = link_speeds[i]; - break; - case 1: - priv->upload_speed = link_speeds[i]; - break; - default: - ret = -EBADMSG; - goto cleanup; - } - } + priv->download_speed = sys_le32_to_cpu(link_speeds[0]); + priv->upload_speed = sys_le32_to_cpu(link_speeds[1]); + LOG_DBG("comm rx xfer callback get connection speed UL %d bps / DL %d bps", + priv->upload_speed, priv->download_speed); break; default: break;