diff --git a/include/zephyr/usb/usbh.h b/include/zephyr/usb/usbh.h index e2e6f3074000e..9f885683e373a 100644 --- a/include/zephyr/usb/usbh.h +++ b/include/zephyr/usb/usbh.h @@ -1,5 +1,6 @@ /* - * Copyright (c) 2022 Nordic Semiconductor ASA + * Copyright (c) 2025 Nordic Semiconductor ASA + * Copyright 2025 NXP * * SPDX-License-Identifier: Apache-2.0 */ @@ -33,6 +34,16 @@ extern "C" { * @{ */ +/** + * USB host support status + */ +struct usbh_status { + /** USB host support is initialized */ + unsigned int initialized : 1; + /** USB host support is enabled */ + unsigned int enabled : 1; +}; + /** * USB host support runtime context */ @@ -43,6 +54,8 @@ struct usbh_context { struct k_mutex mutex; /** Pointer to UHC device struct */ const struct device *dev; + /** Status of the USB host support */ + struct usbh_status status; /** USB device list */ sys_dlist_t udevs; /** USB root device */ @@ -60,47 +73,79 @@ struct usbh_context { .addr_ba = &ba_##device_name, \ } +struct usbh_class_data; + /** - * @brief USB Class Code triple + * @brief USB host class instance API */ -struct usbh_code_triple { - /** Device Class Code */ - uint8_t dclass; - /** Class Subclass Code */ - uint8_t sub; - /** Class Protocol Code */ - uint8_t proto; +struct usbh_class_api { + /** Host init handler, before any device is connected */ + int (*init)(struct usbh_class_data *const c_data, + struct usbh_context *const uhs_ctx); + /** Request completion event handler */ + int (*completion_cb)(struct usbh_class_data *const c_data, + struct uhc_transfer *const xfer); + /** Device connection handler */ + int (*probe)(struct usbh_class_data *const c_data, + struct usb_device *const udev, + const uint8_t iface); + /** Device removal handler */ + int (*removed)(struct usbh_class_data *const c_data); + /** Bus suspended handler */ + int (*suspended)(struct usbh_class_data *const c_data); + /** Bus resumed handler */ + int (*resumed)(struct usbh_class_data *const c_data); }; /** - * @brief USB host class data and class instance API + * @brief USB host class instance data */ struct usbh_class_data { - /** Class code supported by this instance */ - struct usbh_code_triple code; - - /** Initialization of the class implementation */ - /* int (*init)(struct usbh_context *const uhs_ctx); */ - /** 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); + /** Name of the USB host class instance */ + const char *name; + /** Pointer to USB host stack context structure */ + struct usbh_context *uhs_ctx; + /** Pointer to USB device this class is used for */ + struct usb_device *udev; + /** Interface number for which this class matched */ + uint8_t ifnum; + /** Pointer to host support class API */ + struct usbh_class_api *api; + /** Pointer to private data */ + void *priv; }; /** + * @cond INTERNAL_HIDDEN + * + * Variables used by the USB host stack but not exposed to the class + * through the class API. */ -#define USBH_DEFINE_CLASS(name) \ - static STRUCT_SECTION_ITERABLE(usbh_class_data, name) +struct usbh_class_node { + /** Class information exposed to host class implementations (drivers). */ + struct usbh_class_data *const c_data; +}; +/* @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 + */ +#define USBH_DEFINE_CLASS(class_name, class_api, class_priv) \ + static struct usbh_class_data class_data_##class_name = { \ + .name = STRINGIFY(class_name), \ + .api = class_api, \ + .priv = class_priv, \ + }; \ + static STRUCT_SECTION_ITERABLE(usbh_class_node, class_name) = { \ + .c_data = &class_data_##class_name, \ + }; /** * @brief Initialize the USB host support; diff --git a/subsys/usb/CMakeLists.txt b/subsys/usb/CMakeLists.txt index 6b968a4bf894e..b8367352d06de 100644 --- a/subsys/usb/CMakeLists.txt +++ b/subsys/usb/CMakeLists.txt @@ -5,3 +5,4 @@ add_subdirectory_ifdef(CONFIG_USB_DEVICE_STACK device) add_subdirectory_ifdef(CONFIG_USB_DEVICE_STACK_NEXT device_next) add_subdirectory_ifdef(CONFIG_USB_HOST_STACK host) add_subdirectory_ifdef(CONFIG_USBC_STACK usb_c) +add_subdirectory(common) diff --git a/subsys/usb/common/CMakeLists.txt b/subsys/usb/common/CMakeLists.txt new file mode 100644 index 0000000000000..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 9038a8e0d3f15..1344e39441b26 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" diff --git a/subsys/usb/host/CMakeLists.txt b/subsys/usb/host/CMakeLists.txt index f48f5b8c71483..b9afd0a65801d 100644 --- a/subsys/usb/host/CMakeLists.txt +++ b/subsys/usb/host/CMakeLists.txt @@ -5,9 +5,11 @@ zephyr_library() zephyr_library_include_directories(${CMAKE_CURRENT_SOURCE_DIR}) zephyr_library_sources( + usbh_api.c usbh_ch9.c + usbh_class.c usbh_core.c - usbh_api.c + usbh_desc.c usbh_device.c ) diff --git a/subsys/usb/host/usbh_class.c b/subsys/usb/host/usbh_class.c new file mode 100644 index 0000000000000..e569fdac8a9d1 --- /dev/null +++ b/subsys/usb/host/usbh_class.c @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * Copyright 2025 NXP + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include "usbh_class.h" +#include "usbh_class_api.h" +#include "usbh_desc.h" + +LOG_MODULE_REGISTER(usbh_class, CONFIG_USBH_LOG_LEVEL); + +int usbh_class_init_all(struct usbh_context *const uhs_ctx) +{ + int ret; + + STRUCT_SECTION_FOREACH(usbh_class_node, c_node) { + ret = usbh_class_init(c_node->c_data, uhs_ctx); + if (ret != 0) { + LOG_ERR("Failed to initialize all registered class instances"); + return ret; + } + } + + return 0; +} + +int usbh_class_remove_all(const struct usb_device *const udev) +{ + int ret; + + STRUCT_SECTION_FOREACH(usbh_class_node, c_node) { + if (c_node->c_data->udev == udev) { + ret = usbh_class_removed(c_node->c_data); + if (ret != 0) { + LOG_ERR("Failed to handle device removal for each classes"); + return ret; + } + + /* The class instance is now free to bind to a new device */ + c_node->c_data->udev = NULL; + } + } + + return 0; +} + +static int usbh_class_probe_one(struct usbh_class_data *const c_data, struct usb_device *const udev) +{ + const uint8_t *const desc_beg = usbh_desc_get_cfg_beg(udev); + const uint8_t *const desc_end = usbh_desc_get_cfg_end(udev); + const struct usb_desc_header *desc; + const struct usb_cfg_descriptor *cfg_desc; + int ret; + + desc = usbh_desc_get_by_type(desc_beg, desc_end, USB_DESC_CONFIGURATION); + if (desc == NULL) { + LOG_ERR("Unable to find a configuration descriptor"); + return -EBADMSG; + } + + cfg_desc = (struct usb_cfg_descriptor *)desc; + + for (uint8_t ifnum = 0; ifnum < cfg_desc->bNumInterfaces; ifnum++) { + ret = usbh_class_probe(c_data, udev, ifnum); + if (ret == -ENOTSUP) { + LOG_DBG("Class %s not supporting this device, skipping", c_data->name); + continue; + } + if (ret != 0) { + LOG_ERR("Failed to register the class %s for interface %u", + c_data->name, ifnum); + return ret; + } + if (ret == 0) { + LOG_INF("Class %s is matching this device for interface %u, assigning it", + c_data->name, ifnum); + c_data->udev = udev; + c_data->ifnum = ifnum; + return 0; + } + } + + return -ENOTSUP; +} + +int usbh_class_probe_all(struct usb_device *const udev) +{ + int ret; + + STRUCT_SECTION_FOREACH(usbh_class_node, c_node) { + struct usbh_class_data *const c_data = c_node->c_data; + + /* If the class is not already having a device */ + if (c_data->udev == NULL) { + ret = usbh_class_probe_one(c_data, udev); + if (ret == -ENOTSUP) { + continue; + } + if (ret != 0) { + return ret; + } + } + } + + LOG_WRN("Could not find any class for this device"); + return -ENODEV; +} + +bool usbh_class_is_matching(struct usbh_class_filter *const filters, size_t n_filters, + struct usb_device_descriptor *const desc) +{ + for (int i = 0; i < n_filters; i++) { + const struct usbh_class_filter *filt = &filters[i]; + + if (filt->flags & USBH_CLASS_MATCH_VID) { + if (desc->idVendor != filt->vid) { + continue; + } + } + + if (filt->flags & USBH_CLASS_MATCH_PID) { + if (desc->idProduct != filt->pid) { + continue; + } + } + + if (filt->flags & USBH_CLASS_MATCH_DCLASS) { + if (desc->bDeviceClass != filt->dclass) { + continue; + } + } + + if (filt->flags & USBH_CLASS_MATCH_SUB) { + if (desc->bDeviceSubClass != filt->sub) { + continue; + } + } + + if (filt->flags & USBH_CLASS_MATCH_PROTO) { + if (desc->bDeviceProtocol != 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..e6cc74d0342ea --- /dev/null +++ b/subsys/usb/host/usbh_class.h @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * Copyright 2025 NXP + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_INCLUDE_USBD_CLASS_H +#define ZEPHYR_INCLUDE_USBD_CLASS_H + +#include + +/** + * @brief Information about a device, which is relevant for matching a particular class. + */ +struct usbh_class_filter { + /** Vendor ID */ + uint16_t vid; + /** Product ID */ + uint16_t pid; + /** Device Class Code */ + uint8_t dclass; + /** Class Subclass Code */ + uint8_t sub; + /** Class Protocol Code */ + uint8_t proto; + /** Flags that tell which field to match */ + uint8_t flags; +}; + +/** 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_DCLASS 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_DCLASS | USBH_CLASS_MATCH_SUB | USBH_CLASS_MATCH_PROTO) + +/** + * @brief Match an USB host class (a driver) against a device descriptor. + * + * @param[in] filters Array of filter rules to match + * @param[in] n_filters Number of rules in the array. + * @param[in] desc USB Device descriptor to match against each rule + * @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 n_filters, + struct usb_device_descriptor *const desc); + +/** + * @brief Initialize every class instantiated on the system + * + * @param[in] uhs_ctx USB Host context to pass to the class. + * @retval 0 on success or negative error code on failure + */ +int usbh_class_init_all(struct usbh_context *const uhs_ctx); + +/** + * @brief Probe all classes to against a newly connected USB device. + * + * @param[in] udev USB device to probe. + * @retval 0 on success or negative error code on failure + */ +int usbh_class_probe_all(struct usb_device *const udev); + +/** + * @brief Call the device removal handler for every class configured with it + * + * @param[in] udev USB device that got removed. + * @retval 0 on success or negative error code on failure + */ +int usbh_class_remove_all(const struct usb_device *const udev); + +#endif /* ZEPHYR_INCLUDE_USBD_CLASS_H */ diff --git a/subsys/usb/host/usbh_class_api.h b/subsys/usb/host/usbh_class_api.h new file mode 100644 index 0000000000000..b2ec156ca2dd9 --- /dev/null +++ b/subsys/usb/host/usbh_class_api.h @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file + * @brief USB host stack class instances API + * + * This file contains the USB host stack class instances API. + */ + +#ifndef ZEPHYR_INCLUDE_USBH_CLASS_API_H +#define ZEPHYR_INCLUDE_USBH_CLASS_API_H + +#include + +/** + * @brief Initialization of the class implementation + * + * This is called for each instance during the initialization phase, + * for every registered class. + * It can be used to initialize underlying systems. + * + * @param[in] c_data Pointer to USB host class data + * @param[in] uhs_ctx USB host context to assign to this class + * + * @return 0 on success, negative error code on failure. + */ + +static inline int usbh_class_init(struct usbh_class_data *const c_data, + struct usbh_context *const uhs_ctx) +{ + const struct usbh_class_api *api = c_data->api; + + if (api->init != NULL) { + return api->init(c_data, uhs_ctx); + } + + return -ENOTSUP; +} + +/** + * @brief Request completion event handler + * + * Called upon completion of a request made by the host to this class. + * + * @param[in] c_data Pointer to USB host class data + * @param[in] udev USB device connected + * @param[in] xfer Completed transfer + * + * @return 0 on success, negative error code on failure. + */ +static inline int usbh_class_completion_cb(struct usbh_class_data *const c_data, + struct uhc_transfer *const xfer) +{ + const struct usbh_class_api *api = c_data->api; + + if (api->completion_cb != NULL) { + return api->completion_cb(c_data, xfer); + } + + return -ENOTSUP; +} + +/** + * @brief Device initialization handler + * + * Called when a device is connected to the bus for every device. + * + * @param[in] c_data Pointer to USB host class data + * @param[in] udev USB device connected + * @param[in] iface The @c bInterfaceNumber or @c bFirstInterface of this class + * + * @return 0 on success, negative error code on failure. + * @return -ENOTSUP if the class is not matching + */ +static inline int usbh_class_probe(struct usbh_class_data *const c_data, + struct usb_device *const udev, + const uint8_t iface) +{ + const struct usbh_class_api *api = c_data->api; + + if (api->probe != NULL) { + return api->probe(c_data, udev, iface); + } + + return -ENOTSUP; +} + +/** + * @brief Device removed handler + * + * Called when the device is removed from the bus + * and it matches the class filters of this instance. + * + * @param[in] c_data Pointer to USB host class data + * @param[in] udev USB device connected + * + * @return 0 on success, negative error code on failure. + */ +static inline int usbh_class_removed(struct usbh_class_data *const c_data) +{ + const struct usbh_class_api *api = c_data->api; + + if (api->removed != NULL) { + return api->removed(c_data); + } + + return -ENOTSUP; +} + +/** + * @brief Bus suspended handler + * + * Called when the host has suspending the bus. + * It can be used to suspend underlying systems. + * + * @param[in] c_data Pointer to USB host class data + * @param[in] udev USB device connected + * + * @return 0 on success, negative error code on failure. + */ +static inline int usbh_class_suspended(struct usbh_class_data *const c_data) +{ + const struct usbh_class_api *api = c_data->api; + + if (api->suspended != NULL) { + return api->suspended(c_data); + } + + return -ENOTSUP; +} + +/** + * @brief Bus resumed handler + * + * Called when the host resumes the activity on a bus. + * It can be used to wake-up underlying systems. + * + * @param[in] c_data Pointer to USB host class data + * @param[in] udev USB device connected + * + * @return 0 on success, negative error code on failure. + */ +static inline int usbh_class_resumed(struct usbh_class_data *const c_data) +{ + const struct usbh_class_api *api = c_data->api; + + if (api->resumed != NULL) { + return api->resumed(c_data); + } + + return -ENOTSUP; +} + +#endif /* ZEPHYR_INCLUDE_USBD_CLASS_API_H */ diff --git a/subsys/usb/host/usbh_core.c b/subsys/usb/host/usbh_core.c index 1b654b5041133..2b10ae3eab2a2 100644 --- a/subsys/usb/host/usbh_core.c +++ b/subsys/usb/host/usbh_core.c @@ -10,9 +10,12 @@ #include #include #include +#include -#include "usbh_internal.h" +#include "usbh_class.h" +#include "usbh_class_api.h" #include "usbh_device.h" +#include "usbh_internal.h" #include LOG_MODULE_REGISTER(uhs, CONFIG_USBH_LOG_LEVEL); @@ -46,6 +49,7 @@ static int usbh_event_carrier(const struct device *dev, static void dev_connected_handler(struct usbh_context *const ctx, const struct uhc_event *const event) { + int ret; LOG_DBG("Device connected event"); if (ctx->root != NULL) { @@ -71,6 +75,11 @@ static void dev_connected_handler(struct usbh_context *const ctx, if (usbh_device_init(ctx->root)) { LOG_ERR("Failed to reset new USB device"); } + + ret = usbh_class_probe_all(ctx->root); + if (ret != 0) { + LOG_ERR("Failed to probe all classes for this new USB device"); + } } static void dev_removed_handler(struct usbh_context *const ctx) @@ -194,12 +203,10 @@ int usbh_init_device_intl(struct usbh_context *const uhs_ctx) sys_dlist_init(&uhs_ctx->udevs); - STRUCT_SECTION_FOREACH(usbh_class_data, cdata) { - /* - * For now, we have not implemented any class drivers, - * so just keep it as placeholder. - */ - break; + ret = usbh_class_init_all(uhs_ctx); + if (ret != 0) { + LOG_ERR("Failed to initialize all classes"); + return ret; } return 0; diff --git a/subsys/usb/host/usbh_data.ld b/subsys/usb/host/usbh_data.ld index 4363154f29474..bc53c86119914 100644 --- a/subsys/usb/host/usbh_data.ld +++ b/subsys/usb/host/usbh_data.ld @@ -1,4 +1,4 @@ #include ITERABLE_SECTION_RAM(usbh_context, Z_LINK_ITERABLE_SUBALIGN) -ITERABLE_SECTION_RAM(usbh_class_data, Z_LINK_ITERABLE_SUBALIGN) +ITERABLE_SECTION_RAM(usbh_class_node, Z_LINK_ITERABLE_SUBALIGN) diff --git a/subsys/usb/host/usbh_desc.c b/subsys/usb/host/usbh_desc.c new file mode 100644 index 0000000000000..ca703328aa9cc --- /dev/null +++ b/subsys/usb/host/usbh_desc.c @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * Copyright NXP + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include "usbh_desc.h" + +const struct usb_desc_header *usbh_desc_get_next(const struct usb_desc_header *const desc, + const void *const desc_end) +{ + const struct usb_desc_header *next; + + if (desc == NULL) { + return NULL; + } + + next = (const struct usb_desc_header *)((uint8_t *)desc + desc->bLength); + + /* Validate the bLength field to make sure it does not point past the end */ + if ((uint8_t *)next >= (uint8_t *)desc_end || + (uint8_t *)next + next->bLength > (uint8_t *)desc_end || + desc->bLength == 0) { + return NULL; + } + + return next; +} + +const struct usb_desc_header *usbh_desc_get_by_type(const void *const desc_beg, + const void *const desc_end, + uint32_t type_mask) +{ + const struct usb_desc_header *desc; + + if (desc_beg == NULL) { + return NULL; + } + + for (desc = desc_beg; desc != NULL; desc = usbh_desc_get_next(desc, desc_end)) { + if ((BIT(desc->bDescriptorType) & type_mask) != 0) { + return desc; + } + } + + return NULL; +} + +const struct usb_desc_header *usbh_desc_get_by_ifnum(const void *const desc_beg, + const void *const desc_end, + const uint8_t ifnum) +{ + const struct usb_desc_header *desc; + + if (desc_beg == NULL) { + return NULL; + } + + for (desc = desc_beg; desc != NULL; desc = usbh_desc_get_next(desc, desc_end)) { + if (desc->bDescriptorType == USB_DESC_INTERFACE && + ((struct usb_if_descriptor *)desc)->bInterfaceNumber == ifnum) { + return desc; + } + if (desc->bDescriptorType == USB_DESC_INTERFACE_ASSOC && + ((struct usb_association_descriptor *)desc)->bFirstInterface == ifnum) { + return desc; + } + } + + return NULL; +} + +const void *usbh_desc_get_cfg_beg(const struct usb_device *udev) +{ + return udev->cfg_desc; +} + +const void *usbh_desc_get_cfg_end(const struct usb_device *udev) +{ + const struct usb_cfg_descriptor *cfg_desc; + + if (udev->cfg_desc == NULL) { + return NULL; + } + + cfg_desc = (struct usb_cfg_descriptor *)udev->cfg_desc; + + return (uint8_t *)cfg_desc + cfg_desc->wTotalLength; +} diff --git a/subsys/usb/host/usbh_desc.h b/subsys/usb/host/usbh_desc.h new file mode 100644 index 0000000000000..e78cf340f2ba5 --- /dev/null +++ b/subsys/usb/host/usbh_desc.h @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file + * @brief Descriptor matching and searching utilities + * + * This file contains utilities to browse USB descriptors returned by a device + * according to the USB Specification 2.0 Chapter 9. + */ + +#ifndef ZEPHYR_INCLUDE_USBH_DESC_H +#define ZEPHYR_INCLUDE_USBH_DESC_H + +#include + +#include + +/** + * @brief Get the next descriptor in an array of descriptors. + * + * This + * to search either an interface or interface association descriptor: + * + * @code{.c} + * BIT(USB_DESC_INTERFACE) | BIT(USB_DESC_INTERFACE_ASSOC) + * @endcode + * + * @param[in] desc_beg Pointer to the beginning of the descriptor array; to search. May be NULL. + * @param[in] desc_end Pointer after the end of the descriptor array to search + * @param[in] type_mask Bitmask of bDescriptorType values to match + * @return A pointer to the first descriptor matching + */ +const struct usb_desc_header *usbh_desc_get_next(const struct usb_desc_header *const desc, + const void *const desc_end); + +/** + * @brief Search the first descriptor matching the selected type(s). + * + * As an example, the @p type_mask argument can be constructed this way + * to search either an interface or interface association descriptor: + * + * @code{.c} + * BIT(USB_DESC_INTERFACE) | BIT(USB_DESC_INTERFACE_ASSOC) + * @endcode + * + * @param[in] desc_beg Pointer to the beginning of the descriptor array; to search. May be NULL. + * @param[in] desc_end Pointer after the end of the descriptor array to search + * @param[in] type_mask Bitmask of bDescriptorType values to match + * @return A pointer to the first descriptor matching + */ +const struct usb_desc_header *usbh_desc_get_by_type(const void *const desc_beg, + const void *const desc_end, + uint32_t type_mask); + +/** + * @brief Search a descriptor matching the interace number wanted. + * + * The descriptors are going to be scanned until either an interface association + * @c bFirstInterface field or an interface @c bInterfaceNumber field match. + * + * The descriptors following it can be browsed using @ref usbh_desc_get_next + * + * @param[in] desc_beg Pointer to the beginning of the descriptor array; to search. May be NULL. + * @param[in] desc_end Pointer after the end of the descriptor array to search + * @param[in] ifnum The interface number to search + * @return A pointer to the first descriptor matching + */ +const struct usb_desc_header *usbh_desc_get_by_ifnum(const void *const desc_beg, + const void *const desc_end, + const uint8_t ifnum); + +/** + * @brief Get the start of a device's configuration descriptor. + * + * @param[in] udev The device holding the confiugration descriptor + * @return A pointer to the beginning of the descriptor + */ +const void *usbh_desc_get_cfg_beg(const struct usb_device *udev); + +/** + * @brief Get the end of a device's configuration descriptor. + * + * @param[in] udev The device holding the confiugration descriptor + * @return A pointer to the beginning of the descriptor + */ +const void *usbh_desc_get_cfg_end(const struct usb_device *udev); + +#endif /* ZEPHYR_INCLUDE_USBH_DESC_H */ 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/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_ */ 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..6fac39ccf7ad8 --- /dev/null +++ b/tests/subsys/usb/host/class_api/src/main.c @@ -0,0 +1,232 @@ +/* + * 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 "test_descriptor.h" + +#include +LOG_MODULE_REGISTER(usb_test, LOG_LEVEL_DBG); + +USBH_CONTROLLER_DEFINE(uhs_ctx, DEVICE_DT_GET(DT_NODELABEL(zephyr_uhc0))); + +static const uint8_t test_hub_descriptor[] = {TEST_HUB_DESCRIPTOR}; +struct usb_device test_udev = {}; + +/* 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 ifnum) +{ + 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_ifnum(desc, desc_end, ifnum); + 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, +}; + +/* Define a class used in the tests */ +USBH_DEFINE_CLASS(test_class, &test_class_api, &test_class_priv); + +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..84a47d7d3924c --- /dev/null +++ b/tests/subsys/usb/host/common/include/test_descriptor.h @@ -0,0 +1,87 @@ +/* + * 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_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_ */ 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..b35d2dd0d7b39 --- /dev/null +++ b/tests/subsys/usb/host/desc/src/main.c @@ -0,0 +1,95 @@ +/* + * 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_device_descriptor[] = {TEST_HUB_DEVICE_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; + uint32_t mask; + + zassert_is_null(usbh_desc_get_by_type(start_addr, end_addr, 0x00), + "requesting nothing needs to always return nothing"); + + /* From "test_descriptor.h": + * #0 TEST_HUB_DEVICE_DESCRIPTOR + * #1 TEST_HUB_CONFIG_DESCRIPTOR + * #2 TEST_HUB_INTERFACE_ALT0_DESCRIPTOR + * #3 TEST_HUB_ENDPOINT_DESCRIPTOR + * #4 TEST_HUB_INTERFACE_ALT1_DESCRIPTOR + * #5 TEST_HUB_ENDPOINT_DESCRIPTOR + */ + desc = (const struct usb_desc_header *)start_addr; + + /* desc at #0 TEST_HUB_DEVICE_DESCRIPTOR */ + + mask = BIT(USB_DESC_DEVICE); + + desc = usbh_desc_get_by_type(desc, end_addr, mask); + zassert_not_null(desc); + zassert_mem_equal(desc, test_hub_device_descriptor, + sizeof(test_hub_device_descriptor), + "needs to be able to reach the device descriptor"); + + /* desc at #0 TEST_HUB_DEVICE_DESCRIPTOR */ + + mask = BIT(USB_DESC_INTERFACE) | BIT(USB_DESC_ENDPOINT); + + desc = usbh_desc_get_by_type(desc, end_addr, mask); + 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 #2 TEST_HUB_INTERFACE_ALT0_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 properly shift to the next descriptor"); + + /* desc at #3 TEST_HUB_ENDPOINT_DESCRIPTOR */ + + mask = BIT(USB_DESC_INTERFACE); + + desc = usbh_desc_get_by_type((const uint8_t *)desc, end_addr, mask); + 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 #4 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 #5 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