diff --git a/include/zephyr/drivers/usb/uhc.h b/include/zephyr/drivers/usb/uhc.h index 5bacfdc19556f..dad0515903184 100644 --- a/include/zephyr/drivers/usb/uhc.h +++ b/include/zephyr/drivers/usb/uhc.h @@ -54,11 +54,16 @@ enum usb_device_speed { #define UHC_INTERFACES_MAX 32 struct usb_host_interface { + /** Pointer to the interface descriptor */ struct usb_desc_header *dhp; + /** Pointer to the association interface descriptor, if any */ + struct usb_association_descriptor *iad; + /** Alternate setting selected for this interface */ uint8_t alternate; }; struct usb_host_ep { + /** Pointer to the endpoint descriptor */ struct usb_ep_descriptor *desc; }; diff --git a/include/zephyr/usb/usbh.h b/include/zephyr/usb/usbh.h index e2e6f3074000e..c9cbdf8fd7c86 100644 --- a/include/zephyr/usb/usbh.h +++ b/include/zephyr/usb/usbh.h @@ -1,6 +1,6 @@ /* - * Copyright (c) 2022 Nordic Semiconductor ASA - * + * SPDX-FileCopyrightText: Copyright Nordic Semiconductor ASA + * SPDX-FileCopyrightText: Copyright 2025 NXP * SPDX-License-Identifier: Apache-2.0 */ @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -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,120 @@ struct usbh_context { .addr_ba = &ba_##device_name, \ } +struct usbh_class_data; + /** - * @brief USB Class Code triple + * @brief Information about a device, which is relevant for matching a particular class. */ -struct usbh_code_triple { - /** Device Class Code */ - uint8_t dclass; - /** Class Subclass Code */ +struct usbh_class_filter { + /** Vendor ID */ + uint16_t vid; + /** Product ID */ + uint16_t pid; + /** Class Code */ + uint8_t class; + /** Subclass Code */ uint8_t sub; - /** Class Protocol Code */ + /** Protocol Code */ uint8_t proto; + /** Flags that tell which field to match */ + uint8_t flags; }; /** - * @brief USB host class data and class instance API + * @brief USB host class instance API + */ +struct usbh_class_api { + /** Host initialization handler, before any device is connected */ + int (*init)(struct usbh_class_data *const c_data, + struct usbh_context *const uhs_ctx); + /** Request completion 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 (optional) */ + int (*suspended)(struct usbh_class_data *const c_data); + /** Bus resumed handler (optional) */ + int (*resumed)(struct usbh_class_data *const c_data); +}; + +/** + * @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; + /** First interface number or claimed function */ + uint8_t iface; + /** Pointer to host support class API */ + struct usbh_class_api *api; + /** Pointer to private data */ + void *priv; }; /** + * @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 **/ + const struct usbh_class_filter *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. + * When non-NULL, then it has to be terminated by an entry with @c flags set to 0. + */ +#define USBH_DEFINE_CLASS(class_name, class_api, class_priv, 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, \ + }; /** * @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..459a72df30ae0 --- /dev/null +++ b/subsys/usb/common/CMakeLists.txt @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: Copyright Nordic Semiconductor ASA +# 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 a6772f6aafad5..703a262fb2692 100644 --- a/subsys/usb/device_next/class/usbd_uvc.c +++ b/subsys/usb/device_next/class/usbd_uvc.c @@ -23,7 +23,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 4e8f7b9d28dc8..769275e637298 100644 --- a/subsys/usb/host/CMakeLists.txt +++ b/subsys/usb/host/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright (c) 2022 Nordic Semiconductor ASA +# SPDX-FileCopyrightText: Copyright Nordic Semiconductor ASA # SPDX-License-Identifier: Apache-2.0 zephyr_library() @@ -6,9 +6,12 @@ zephyr_library_include_directories(${CMAKE_CURRENT_SOURCE_DIR}) zephyr_library_sources( usbh_ch9.c + usbh_class.c usbh_core.c usbh_api.c usbh_device.c + usbh_desc.c + usbh_class.c ) zephyr_library_sources_ifdef( diff --git a/subsys/usb/host/usbh_class.c b/subsys/usb/host/usbh_class.c new file mode 100644 index 0000000000000..c2c60ea245113 --- /dev/null +++ b/subsys/usb/host/usbh_class.c @@ -0,0 +1,200 @@ +/* + * SPDX-FileCopyrightText: Copyright Nordic Semiconductor ASA + * SPDX-FileCopyrightText: Copyright 2025 NXP + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include "usbh_class.h" +#include "usbh_class_api.h" +#include "usbh_desc.h" +#include "usbh_host.h" + +LOG_MODULE_REGISTER(usbh_class, CONFIG_USBH_LOG_LEVEL); + +void usbh_class_init_all(struct usbh_context *const uhs_ctx) +{ + int ret; + + usbh_host_lock(uhs_ctx); + + 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; + } + } + + usbh_host_unlock(uhs_ctx); +} + +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) { + continue; + } + + 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; + c_data->udev = NULL; + continue; + } + + /* The class instance is now free to bind to a new device */ + c_node->state = USBH_CLASS_STATE_IDLE; + c_data->udev = NULL; + } + + k_mutex_unlock(&udev->mutex); +} + +/* + * Probe an USB device function against each host class instantiated. + * + * 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 match will stop the loop, and the status will be updated + * so that classes only match one function at a time. + * + * USB functions will have at most 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 filter_data, + const 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, filter_data)) { + 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 function, 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 struct usb_desc_header *desc = udev->cfg_desc; + struct usbh_class_filter filter_data; + uint8_t iface; + int ret; + + /* To support single-function devices, match against the entire device */ + + filter_data.vid = udev->dev_desc.idVendor; + filter_data.pid = udev->dev_desc.idProduct; + filter_data.class = udev->dev_desc.bDeviceClass; + filter_data.sub = udev->dev_desc.bDeviceSubClass; + filter_data.proto = udev->dev_desc.bDeviceProtocol; + + usbh_class_probe_function(udev, &filter_data, USBH_CLASS_IFNUM_DEVICE); + + /* To support multi-function devices, match against each function */ + + while (true) { + desc = usbh_desc_get_next_function(desc); + if (desc == NULL) { + break; + } + + ret = usbh_desc_fill_filter(desc, &filter_data, &iface); + if (ret != 0) { + LOG_ERR("Failed to collect class codes for matching interface %u", + iface); + continue; + } + + usbh_class_probe_function(udev, &filter_data, iface); + } +} + +bool usbh_class_is_matching(const struct usbh_class_filter *const filter_rules, + const struct usbh_class_filter *const filter_data) +{ + /* Make empty filter set match everything (use class_api->probe() only) */ + if (filter_rules == NULL) { + return true; + } + + /* Try to find a rule that matches completely */ + for (size_t i = 0; filter_rules[i].flags != 0; i++) { + const struct usbh_class_filter *rule = &filter_rules[i]; + + if (rule->flags & USBH_CLASS_MATCH_VID_PID && + (filter_data->vid != rule->vid || filter_data->pid != rule->pid)) { + continue; + } + + if (rule->flags & USBH_CLASS_MATCH_CODE_TRIPLE && + (filter_data->class != rule->class || filter_data->sub != rule->sub || + filter_data->proto != rule->proto)) { + continue; + } + + /* All the selected filter_rules 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..0b3d673c05840 --- /dev/null +++ b/subsys/usb/host/usbh_class.h @@ -0,0 +1,63 @@ +/* + * SPDX-FileCopyrightText: Copyright Nordic Semiconductor ASA + * SPDX-FileCopyrightText: Copyright 2025 NXP + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_INCLUDE_USBH_CLASS_H +#define ZEPHYR_INCLUDE_USBH_CLASS_H + +#include + +/** Match both the device's vendor ID and product ID */ +#define USBH_CLASS_MATCH_VID_PID BIT(1) + +/** Match a class/subclass/protocol code triple */ +#define USBH_CLASS_MATCH_CODE_TRIPLE BIT(2) + +/** Interface number referring to the entire device instead of a particular interface */ +#define USBH_CLASS_IFNUM_DEVICE 0xff + +/** + * @brief Match an USB host class (a driver) against a device descriptor. + * + * A filter set to NULL always matches. + * This can be used to only rely on @c class_api->probe() (typically happening next) return value + * for the matching. + * + * @param[in] filter_rules Array of filter rules to match, or NULL + * @param[in] filtered_data Data against which match the filter + * + * @retval true if the USB Device descriptor matches at least one rule. + * @retval false if all the rules failed to match. + */ +bool usbh_class_is_matching(const struct usbh_class_filter *const filter_rules, + const struct usbh_class_filter *const filtered_data); + +/** + * @brief Initialize all available host class instances. + * + * @param[in] uhs_ctx USB Host context to pass to the class. + */ +void usbh_class_init_all(struct usbh_context *const uhs_ctx); + +/** + * @brief Probe an USB device function against all available host class instances. + * + * 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 matching host class driver is going to stop the scanning, and become the one in use. + * + * @param[in] udev USB device to probe. + */ +void usbh_class_probe_device(struct usb_device *const udev); + +/** + * @brief Call the device removal handler for every class configured with this device. + * + * @param[in] udev USB device that got removed. + */ +void usbh_class_remove_all(struct usb_device *const udev); + +#endif /* ZEPHYR_INCLUDE_USBH_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..1e2b1432d1038 --- /dev/null +++ b/subsys/usb/host/usbh_class_api.h @@ -0,0 +1,147 @@ +/* + * SPDX-FileCopyrightText: Copyright 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. + * It is called once for every USB function of that 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 0; +} + +/** + * @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 0; +} + +#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..5cd7dbc4006c6 100644 --- a/subsys/usb/host/usbh_core.c +++ b/subsys/usb/host/usbh_core.c @@ -1,6 +1,5 @@ /* - * Copyright (c) 2022,2025 Nordic Semiconductor ASA - * + * SPDX-FileCopyrightText: Copyright Nordic Semiconductor ASA * SPDX-License-Identifier: Apache-2.0 */ @@ -10,9 +9,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 +48,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 +72,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 +198,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 4363154f29474..98f470a080436 100644 --- a/subsys/usb/host/usbh_data.ld +++ b/subsys/usb/host/usbh_data.ld @@ -1,4 +1,9 @@ +/* + * SPDX-FileCopyrightText: Copyright The Zephyr Project Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + #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..cf82b1bba1b89 --- /dev/null +++ b/subsys/usb/host/usbh_desc.c @@ -0,0 +1,220 @@ +/* + * SPDX-FileCopyrightText: Copyright Nordic Semiconductor ASA + * SPDX-FileCopyrightText: Copyright 2025 NXP + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include "usbh_class.h" +#include "usbh_desc.h" + +LOG_MODULE_REGISTER(usbh_desc, CONFIG_USBH_LOG_LEVEL); + +bool usbh_desc_is_valid(const void *const desc, + const size_t size, const uint8_t type) +{ + const struct usb_desc_header *const head = desc; + + if (size < sizeof(struct usb_desc_header)) { + return false; + } + + /* Avoid too short bLength field or nil descriptor */ + if (head->bLength < size) { + return false; + } + + /* Expect the correct type */ + if (type != 0 && type != head->bDescriptorType) { + return false; + } + + return true; +} + +bool usbh_desc_is_valid_interface(const void *const desc) +{ + return usbh_desc_is_valid(desc, sizeof(struct usb_if_descriptor), + USB_DESC_INTERFACE); +} + +bool usbh_desc_is_valid_association(const void *const desc) +{ + return usbh_desc_is_valid(desc, sizeof(struct usb_association_descriptor), + USB_DESC_INTERFACE_ASSOC); +} + +bool usbh_desc_is_valid_configuration(const void *const desc) +{ + return usbh_desc_is_valid(desc, sizeof(struct usb_cfg_descriptor), + USB_DESC_CONFIGURATION); +} + +bool usbh_desc_is_valid_endpoint(const void *const desc) +{ + return usbh_desc_is_valid(desc, sizeof(struct usb_ep_descriptor), + USB_DESC_ENDPOINT); +} + +const void *usbh_desc_get_next(const void *const desc) +{ + const struct usb_desc_header *const head = desc; + const void *next; + + if (!usbh_desc_is_valid(desc, sizeof(const struct usb_desc_header), 0)) { + return NULL; + } + + next = (const uint8_t *)desc + head->bLength; + + if (!usbh_desc_is_valid(next, sizeof(const struct usb_desc_header), 0)) { + return NULL; + } + + return next; +} + +const void *usbh_desc_get_next_alt_setting(const void *const desc) +{ + const struct usb_desc_header *head = desc; + + /* Skip the current interface descriptor */ + head = usbh_desc_get_next(desc); + + /* Seek to the next alternate setting for this interface */ + for (; head != NULL; head = usbh_desc_get_next(head)) { + struct usb_if_descriptor *if_d = (void *)head; + + if (head->bDescriptorType != USB_DESC_INTERFACE) { + continue; + } + + /* Non-zero Alternate Setting */ + if (usbh_desc_is_valid_interface(desc) && if_d->bAlternateSetting != 0) { + return head; + } + + /* Do not continue to the next interface */ + return NULL; + } + + return NULL; +} + +const void *usbh_desc_get_iface(const struct usb_device *const udev, const uint8_t iface) +{ + const struct usb_cfg_descriptor *const c_desc = udev->cfg_desc; + + for (unsigned int i = 0; i < c_desc->bNumInterfaces; i++) { + const struct usb_host_interface *const host_iface = &udev->ifaces[i]; + const struct usb_if_descriptor *const if_d = (void *)host_iface->dhp; + + if (if_d->bInterfaceNumber == iface) { + return host_iface->dhp; + } + } + + return NULL; +} + +const void *usbh_desc_get_iad(const struct usb_device *const udev, const uint8_t iface) +{ + const struct usb_cfg_descriptor *const c_desc = udev->cfg_desc; + + for (unsigned int i = 0; i < c_desc->bNumInterfaces; i++) { + const struct usb_host_interface *const host_iface = &udev->ifaces[i]; + const struct usb_if_descriptor *const if_d = (void *)host_iface->dhp; + + if (if_d->bInterfaceNumber == iface && host_iface->iad != NULL) { + return host_iface->iad; + } + } + + return NULL; +} + +const void *usbh_desc_get_endpoint(const struct usb_device *const udev, const uint8_t ep) +{ + uint8_t idx = USB_EP_GET_IDX(ep) & 0xf; + + return USB_EP_DIR_IS_IN(ep) ? udev->ep_in[idx].desc : udev->ep_out[idx].desc; +} + +int usbh_desc_fill_filter(const struct usb_desc_header *const desc, + struct usbh_class_filter *const filter, + uint8_t *const iface) +{ + if (desc->bDescriptorType == USB_DESC_INTERFACE_ASSOC) { + const struct usb_association_descriptor *ia_desc = (const void *)desc; + + filter->class = ia_desc->bFunctionClass; + filter->sub = ia_desc->bFunctionSubClass; + filter->proto = ia_desc->bFunctionProtocol; + if (iface != NULL) { + *iface = ia_desc->bFirstInterface; + } + return 0; + } + + if (desc->bDescriptorType == USB_DESC_INTERFACE) { + const struct usb_if_descriptor *if_desc = (const void *)desc; + + filter->class = if_desc->bInterfaceClass; + filter->sub = if_desc->bInterfaceSubClass; + filter->proto = if_desc->bInterfaceProtocol; + if (iface != NULL) { + *iface = if_desc->bInterfaceNumber; + } + return 0; + } + + return -EINVAL; +} + +const void *usbh_desc_get_next_function(const void *const desc) +{ + const struct usb_desc_header *head = desc; + const struct usb_association_descriptor *const ass_d = desc; + const struct usb_if_descriptor *if_d; + uint8_t skip_num = 0; + + /* Skip all interfaces the Association descriptor contains */ + if (usbh_desc_is_valid_association(head)) { + skip_num = ass_d->bInterfaceCount; + } + + /* Skip the interface if the head is interface */ + if (usbh_desc_is_valid_interface(head)) { + skip_num = 1; + } + + while (true) { + /* If already on an Interface Association or Interface, this will skip it */ + head = usbh_desc_get_next(head); + if (head == NULL) { + break; + } + + if_d = (const void *)head; + + /* Association descriptor: this is always a new function */ + if (usbh_desc_is_valid_association(head)) { + return head; + } + + /* Only count the first Alternate Setting of an Interface */ + if (usbh_desc_is_valid_interface(head) && + if_d->bAlternateSetting == 0) { + if (skip_num == 0) { + return head; + } + + skip_num--; + } + } + + return NULL; +} diff --git a/subsys/usb/host/usbh_desc.h b/subsys/usb/host/usbh_desc.h new file mode 100644 index 0000000000000..dffac653083df --- /dev/null +++ b/subsys/usb/host/usbh_desc.h @@ -0,0 +1,155 @@ +/* + * SPDX-FileCopyrightText: Copyright 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 Pointer to the beginning of the descriptor array to search. + * + * @return A pointer to the descriptor + */ +const void *usbh_desc_get_next(const void *const desc); + +/** + * @brief Search an interface descriptor matching the interace number wanted. + * + * The descriptors following it can be browsed using @ref usbh_desc_get_next + * + * @param[in] udev USB device that contains the list of interfaces + * @param[in] iface The interface number to search + * + * @return A pointer to the descriptor + */ +const void *usbh_desc_get_iface(const struct usb_device *const udev, const uint8_t iface); + +/** + * @brief Search an endpoint descriptor matching the endpoint number wanted. + * + * The descriptors following it can be browsed using @ref usbh_desc_get_next + * + * @param[in] udev USB device that contains the list of endpoints + * @param[in] ep The endpoint number to search + * + * @return A pointer to the descriptor + */ +const void *usbh_desc_get_endpoint(const struct usb_device *const udev, const uint8_t ep); + +/** + * @brief Search an interface association 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] udev USB device that contains the list of interface associations + * @param[in] iface The interface number to search + * + * @return A pointer to the descriptor + */ +const void *usbh_desc_get_iad(const struct usb_device *const udev, const uint8_t iface); + +/** + * @brief Extract information from an interface or interface association descriptors + * + * @param[in] desc The descriptor to use + * @param[out] filter 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_fill_filter(const struct usb_desc_header *desc, + struct usbh_class_filter *const filter, + uint8_t *const iface_num); + +/** + * @brief Checks that the pointed descriptor is not truncated and of the expected type. + * + * @param[in] desc The descriptor to validate + * @param[in] size The size of the descriptor. + * @param[in] type The type of the descriptor, 0 for any. + * + * @return true if the descriptor size and type are correct + * @return false if the descriptor size or type is wrong + */ +const bool usbh_desc_is_valid(const void *const desc, + const size_t size, const uint8_t type); + +/** + * @brief Checks that the pointed descriptor is an interface descriptor. + * + * @param[in] desc The descriptor to validate + * + * @return true if the descriptor size and type are correct + * @return false if the descriptor size or type is wrong + */ +bool usbh_desc_is_valid_interface(const void *const desc); + +/** + * @brief Checks that the pointed descriptor is an interface association descriptor. + * + * @param[in] desc The descriptor to validate + * + * @return true if the descriptor size and type are correct + * @return false if the descriptor size or type is wrong + */ +bool usbh_desc_is_valid_association(const void *const desc); + +/** + * @brief Checks that the pointed descriptor is an endpoint descriptor. + * + * @param[in] desc The descriptor to validate + * + * @return true if the descriptor size and type are correct + * @return false if the descriptor size or type is wrong + */ +bool usbh_desc_is_valid_endpoint(const void *const desc); + +/** + * @brief Get the next function in the descriptor list. + * + * This searches the interface or interface association of the next USB + * function. This can be used to walk through the list of USB functions + * to associate a class to each. + * + * It will seek a pointer to the next interface with bAlternateSetting of 0 or next interface + * association descriptor (IAD), and return it. + * + * @param[in] desc Pointer to the descriptor array to search. + * + * @retval Pointer to the next matching descriptor. + * @retval NULL if no matching descriptor was found + */ +const void *usbh_desc_get_next_function(const void *const desc); + +/** + * @brief Get the next alternate setting in the current interface. + * + * The @p desc 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 Pointer to the beginning 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); + +#endif /* ZEPHYR_INCLUDE_USBH_DESC_H */ diff --git a/subsys/usb/host/usbh_device.c b/subsys/usb/host/usbh_device.c index 92ecf6ae7aee1..28d2cd174b9f1 100644 --- a/subsys/usb/host/usbh_device.c +++ b/subsys/usb/host/usbh_device.c @@ -1,6 +1,5 @@ /* - * Copyright (c) 2023,2025 Nordic Semiconductor ASA - * + * SPDX-FileCopyrightText: Copyright Nordic Semiconductor ASA * SPDX-License-Identifier: Apache-2.0 */ @@ -285,7 +284,14 @@ static int parse_configuration_descriptor(struct usb_device *const udev) dhp = (void *)((uint8_t *)udev->cfg_desc + cfg_desc->bLength); desc_end = (void *)((uint8_t *)udev->cfg_desc + cfg_desc->wTotalLength); - while ((void *)dhp < desc_end && (dhp->bDescriptorType != 0 || dhp->bLength != 0)) { + while ((void *)dhp < desc_end) { + if ((uint8_t *)dhp + sizeof(struct usb_desc_header) > (uint8_t *)desc_end || + (uint8_t *)dhp + dhp->bLength > (uint8_t *)desc_end || + dhp->bLength <= sizeof(struct usb_desc_header)) { + LOG_ERR("Invalid descriptor size %d.", dhp->bLength); + return -EINVAL; + } + if (dhp->bDescriptorType == USB_DESC_INTERFACE_ASSOC) { iad = (struct usb_association_descriptor *)dhp; LOG_DBG("bFirstInterface %u", iad->bFirstInterface); @@ -303,6 +309,11 @@ static int parse_configuration_descriptor(struct usb_device *const udev) } udev->ifaces[tmp_nif].dhp = dhp; + if (iad != NULL && + iad->bFirstInterface == if_desc->bInterfaceNumber) { + udev->ifaces[tmp_nif].iad = iad; + } + tmp_nif++; } } @@ -397,7 +408,7 @@ int usbh_device_set_configuration(struct usb_device *const udev, const uint8_t n } udev->cfg_desc = k_heap_alloc(&usb_device_heap, - cfg_desc.wTotalLength, + cfg_desc.wTotalLength + sizeof(struct usb_desc_header), K_NO_WAIT); if (udev->cfg_desc == NULL) { LOG_ERR("Failed to allocate memory for configuration descriptor"); @@ -411,14 +422,15 @@ int usbh_device_set_configuration(struct usb_device *const udev, const uint8_t n goto error; } - memset(udev->cfg_desc, 0, cfg_desc.wTotalLength); + memset(udev->cfg_desc, 0, cfg_desc.wTotalLength + sizeof(struct usb_desc_header)); if (udev->state == USB_STATE_CONFIGURED) { reset_configuration(udev); } err = usbh_req_desc_cfg(udev, idx, cfg_desc.wTotalLength, udev->cfg_desc); if (err) { - LOG_ERR("Failed to read configuration descriptor"); + LOG_ERR("Failed to read configuration descriptor of %u bytes: %d", + cfg_desc.wTotalLength, err); k_heap_free(&usb_device_heap, udev->cfg_desc); goto error; } diff --git a/subsys/usb/host/usbh_device.h b/subsys/usb/host/usbh_device.h index 5c5637fb0bee8..e5f9be92d15c7 100644 --- a/subsys/usb/host/usbh_device.h +++ b/subsys/usb/host/usbh_device.h @@ -1,6 +1,5 @@ /* - * Copyright (c) 2023 Nordic Semiconductor ASA - * + * SPDX-FileCopyrightText: Copyright Nordic Semiconductor ASA * SPDX-License-Identifier: Apache-2.0 */ diff --git a/tests/subsys/usb/host/CMakeLists.txt b/tests/subsys/usb/host/CMakeLists.txt new file mode 100644 index 0000000000000..d932ad7c6ba6b --- /dev/null +++ b/tests/subsys/usb/host/CMakeLists.txt @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright 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_usbh) + +include(${ZEPHYR_BASE}/samples/subsys/usb/common/common.cmake) + +target_include_directories(app PRIVATE include) +target_include_directories(app PRIVATE ${ZEPHYR_BASE}/tests/subsys/usb/host/common) +target_include_directories(app PRIVATE ${ZEPHYR_BASE}/subsys/usb/host) + +target_sources(app PRIVATE src/main.c src/class.c) diff --git a/tests/subsys/usb/host/Kconfig b/tests/subsys/usb/host/Kconfig new file mode 100644 index 0000000000000..2e0b55a5818d6 --- /dev/null +++ b/tests/subsys/usb/host/Kconfig @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: Copyright The Zephyr Project Contributors +# SPDX-License-Identifier: Apache-2.0 + +config SYS_CLOCK_TICKS_PER_SEC + default 1000000 if BOARD_NATIVE_SIM + +# Source common USB sample options used to initialize new experimental USB +# device stack. The scope of these options is limited to USB samples in project +# tree, you cannot use them in your own application. +source "samples/subsys/usb/common/Kconfig.sample_usbd" + +source "Kconfig.zephyr" diff --git a/tests/subsys/usb/host/boards/native_sim.overlay b/tests/subsys/usb/host/boards/native_sim.overlay new file mode 100644 index 0000000000000..273319f67befb --- /dev/null +++ b/tests/subsys/usb/host/boards/native_sim.overlay @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: Copyright 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/boards/native_sim_64.overlay b/tests/subsys/usb/host/boards/native_sim_64.overlay new file mode 100644 index 0000000000000..95557a41be572 --- /dev/null +++ b/tests/subsys/usb/host/boards/native_sim_64.overlay @@ -0,0 +1,6 @@ +/* + * SPDX-FileCopyrightText: Copyright Nordic Semiconductor ASA + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "native_sim.overlay" diff --git a/tests/subsys/usb/host/include/usbh_test_common.h b/tests/subsys/usb/host/include/usbh_test_common.h new file mode 100644 index 0000000000000..bce0afa7ba911 --- /dev/null +++ b/tests/subsys/usb/host/include/usbh_test_common.h @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: Copyright Nordic Semiconductor ASA + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef USBH_TEST_COMMON_H +#define USBH_TEST_COMMON_H + +#define FOO_TEST_VID 0x2FE3 +#define FOO_TEST_PID 0x0000 +#define FOO_TEST_CLASS USB_BCC_VENDOR +#define FOO_TEST_SUB 0x0 +#define FOO_TEST_PROTO 0x0 + +/* USB host context for using it from the tests */ +extern struct usbh_context *const uhs_ctx; + +#endif /* USBH_TEST_COMMON_H */ diff --git a/tests/subsys/usb/host/prj.conf b/tests/subsys/usb/host/prj.conf new file mode 100644 index 0000000000000..526318a1a783f --- /dev/null +++ b/tests/subsys/usb/host/prj.conf @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +CONFIG_LOG=y +CONFIG_ZTEST=y + +CONFIG_USB_DEVICE_STACK_NEXT=y +CONFIG_USBD_LOOPBACK_CLASS=y +CONFIG_UDC_BUF_POOL_SIZE=4096 +CONFIG_SAMPLE_USBD_PID=0xFFFF +CONFIG_SAMPLE_USBD_REMOTE_WAKEUP=y +CONFIG_SAMPLE_USBD_PRODUCT="Zephyr USB Test" + +CONFIG_UHC_DRIVER=y +CONFIG_USB_HOST_STACK=y diff --git a/tests/subsys/usb/host/src/class.c b/tests/subsys/usb/host/src/class.c new file mode 100644 index 0000000000000..2ede4ab595cb9 --- /dev/null +++ b/tests/subsys/usb/host/src/class.c @@ -0,0 +1,185 @@ +/* + * SPDX-FileCopyrightText: Copyright Nordic Semiconductor ASA + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include "usbh_ch9.h" +#include "usbh_class.h" +#include "usbh_class_api.h" +#include "usbh_desc.h" +#include "usbh_device.h" +#include "usbh_host.h" + +#include "usbh_test_common.h" + +LOG_MODULE_REGISTER(test_class, LOG_LEVEL_DBG); + +/* Private class data, here just an integer but usually a custom struct. */ +struct usbh_foo_priv { + enum { + /* Test value stored before the class is initialized */ + FOO_CLASS_PRIV_INACTIVE, + /* Test value stored after the class is initialized */ + FOO_CLASS_PRIV_IDLE, + /* Test value stored after the class is probed */ + FOO_CLASS_PRIV_ENABLED, + /* Test value stored after the class is initialized */ + FOO_CLASS_PRIV_INITIALIZED, + /* Test value stored after the class is suspended */ + FOO_CLASS_PRIV_SUSPENDED, + } state; +}; + +static struct usbh_foo_priv usbh_foo_priv = { + .state = FOO_CLASS_PRIV_INACTIVE, +}; + +static int usbh_foo_init(struct usbh_class_data *const c_data, + struct usbh_context *const uhs_ctx) +{ + struct usbh_foo_priv *priv = c_data->priv; + + LOG_DBG("initializing %p, priv value 0x%x", c_data, *priv); + + zassert_equal(priv->state, FOO_CLASS_PRIV_INACTIVE, + "Class should be initialized only once"); + + priv->state = FOO_CLASS_PRIV_IDLE; + + return 0; +} + +static int usbh_foo_completion_cb(struct usbh_class_data *const c_data, + struct uhc_transfer *const xfer) +{ + struct usbh_foo_priv *priv = c_data->priv; + + LOG_DBG("completion callback for %p, transfer %p", c_data, xfer); + + zassert_equal(priv->state, FOO_CLASS_PRIV_ENABLED); + + return -ENOTSUP; +} + +static int usbh_foo_probe(struct usbh_class_data *const c_data, + struct usb_device *const udev, + const uint8_t iface) +{ + struct usbh_foo_priv *const priv = c_data->priv; + const struct usb_desc_header *desc; + const struct usb_if_descriptor *if_desc; + + LOG_DBG("Probing class %s", c_data->name); + + zassert_equal(priv->state, FOO_CLASS_PRIV_IDLE); + + desc = usbh_desc_get_iface(udev, iface); + if (desc == NULL) { + LOG_WRN("Could not get interface %d", iface); + return -ENOENT; + } + + if (desc->bDescriptorType != USB_DESC_INTERFACE) { + LOG_ERR("Not an interface descriptor"); + return -ENOTSUP; + } + + if_desc = (const struct usb_if_descriptor *)desc; + if (if_desc->bInterfaceClass != USB_BCC_VENDOR) { + LOG_ERR("Unexpected class code"); + return -ENOTSUP; + } + + priv->state = FOO_CLASS_PRIV_ENABLED; + + return 0; +} + +static int usbh_foo_removed(struct usbh_class_data *const c_data) +{ + struct usbh_foo_priv *const priv = c_data->priv; + + LOG_INF("Removed class %s", c_data->name); + + zassert_equal(priv->state, FOO_CLASS_PRIV_ENABLED); + + priv->state = FOO_CLASS_PRIV_IDLE; + + return 0; +} + +static int usbh_foo_suspended(struct usbh_class_data *const c_data) +{ + struct usbh_foo_priv *const priv = c_data->priv; + + zassert_equal(priv->state, FOO_CLASS_PRIV_ENABLED); + + priv->state = FOO_CLASS_PRIV_SUSPENDED; + + return 0; +} + +static int usbh_foo_resumed(struct usbh_class_data *const c_data) +{ + struct usbh_foo_priv *const priv = c_data->priv; + + zassert_equal(priv->state, FOO_CLASS_PRIV_SUSPENDED); + + priv->state = FOO_CLASS_PRIV_ENABLED; + + return 0; +} + +static struct usbh_class_api usbh_foo_api = { + .init = &usbh_foo_init, + .completion_cb = &usbh_foo_completion_cb, + .probe = &usbh_foo_probe, + .removed = &usbh_foo_removed, + .suspended = &usbh_foo_suspended, + .resumed = &usbh_foo_resumed, +}; + +const struct usbh_class_filter filter_rules_vid_pid[] = { + { + .vid = FOO_TEST_VID, + .pid = FOO_TEST_PID, + .flags = USBH_CLASS_MATCH_VID_PID, + }, + {0}, +}; + +const struct usbh_class_filter filter_rules_triple[] = { + { + .class = FOO_TEST_CLASS, + .sub = FOO_TEST_SUB, + .proto = FOO_TEST_PROTO, + .flags = USBH_CLASS_MATCH_CODE_TRIPLE, + }, + {0}, +}; + +const struct usbh_class_filter filter_rules_either[] = { + { + .class = FOO_TEST_CLASS, + .sub = FOO_TEST_SUB, + .proto = FOO_TEST_PROTO, + .flags = USBH_CLASS_MATCH_CODE_TRIPLE, + }, + { + .vid = FOO_TEST_VID, + .pid = FOO_TEST_PID, + .flags = USBH_CLASS_MATCH_VID_PID, + }, + {0}, +}; + +const struct usbh_class_filter filter_rules_empty[] = { + {0}, +}; + +/* Define a class used in the tests */ +USBH_DEFINE_CLASS(foo, &usbh_foo_api, &usbh_foo_priv, filter_rules_triple); diff --git a/tests/subsys/usb/host/src/main.c b/tests/subsys/usb/host/src/main.c new file mode 100644 index 0000000000000..458b5ab159467 --- /dev/null +++ b/tests/subsys/usb/host/src/main.c @@ -0,0 +1,363 @@ +/* + * SPDX-FileCopyrightText: Copyright Nordic Semiconductor ASA + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include + +#include "usbh_ch9.h" +#include "usbh_class.h" +#include "usbh_class_api.h" +#include "usbh_desc.h" +#include "usbh_device.h" +#include "usbh_device.h" +#include "usbh_host.h" + +#include "usbh_test_common.h" + +LOG_MODULE_REGISTER(usbh_test, LOG_LEVEL_INF); + +static const struct usbh_class_filter filter_rules_vid_pid[] = { + { + .vid = FOO_TEST_VID, + .pid = FOO_TEST_PID, + .flags = USBH_CLASS_MATCH_VID_PID, + }, + {0}, +}; + +static const struct usbh_class_filter filter_rules_triple[] = { + { + .class = FOO_TEST_CLASS, + .sub = FOO_TEST_SUB, + .proto = FOO_TEST_PROTO, + .flags = USBH_CLASS_MATCH_CODE_TRIPLE, + }, + {0}, +}; + +static const struct usbh_class_filter filter_rules_either[] = { + { + .class = FOO_TEST_CLASS, + .sub = FOO_TEST_SUB, + .proto = FOO_TEST_PROTO, + .flags = USBH_CLASS_MATCH_CODE_TRIPLE, + }, + { + .vid = FOO_TEST_VID, + .pid = FOO_TEST_PID, + .flags = USBH_CLASS_MATCH_VID_PID, + }, + {0}, +}; + +static const struct usbh_class_filter filter_rules_empty[] = { + {0}, +}; + +static const struct usbh_class_filter filter_invalid_triple = { + .vid = FOO_TEST_VID, + .pid = FOO_TEST_PID, +}; + +static const struct usbh_class_filter filter_invalid_vid_triple = { + .vid = FOO_TEST_VID + 1, + .pid = FOO_TEST_PID, +}; + +static const struct usbh_class_filter filter_invalid_pid_triple = { + .vid = FOO_TEST_VID, + .pid = FOO_TEST_PID + 1, +}; + +static const struct usbh_class_filter filter_valid = { + .vid = FOO_TEST_VID, + .pid = FOO_TEST_PID, + .class = FOO_TEST_CLASS, + .sub = FOO_TEST_SUB, + .proto = FOO_TEST_PROTO, +}; + +static const struct usbh_class_filter filter_invalid_vid = { + .vid = FOO_TEST_VID + 1, + .pid = FOO_TEST_PID, + .class = FOO_TEST_CLASS, + .sub = FOO_TEST_SUB, + .proto = FOO_TEST_PROTO, +}; + +static const struct usbh_class_filter filter_invalid_pid = { + .vid = FOO_TEST_VID, + .pid = FOO_TEST_PID + 1, + .class = FOO_TEST_CLASS, + .sub = FOO_TEST_SUB, + .proto = FOO_TEST_PROTO, +}; + +ZTEST(usbh_test, test_class_matching) +{ + /* Invalid code triple */ + + zassert(usbh_class_is_matching(NULL, &filter_invalid_triple), + "Filtering on NULL rules should match"); + + zassert(!usbh_class_is_matching(filter_rules_empty, &filter_invalid_triple), + "Filtering on empty rules should not match"); + + zassert(!usbh_class_is_matching(filter_rules_vid_pid, &filter_invalid_vid_triple), + "Filtering on invalid VID + invalid code triple should not match"); + + zassert(!usbh_class_is_matching(filter_rules_vid_pid, &filter_invalid_pid_triple), + "Filtering on invalid PID + invalid code triple should not match"); + + zassert(usbh_class_is_matching(filter_rules_vid_pid, &filter_invalid_triple), + "Filtering on valid VID:PID + invalid code triple (ignored) should match"); + + zassert(!usbh_class_is_matching(filter_rules_triple, &filter_invalid_triple), + "Filtering on valid VID:PID (ignored) + invalid code triple should not match"); + + zassert(usbh_class_is_matching(filter_rules_either, &filter_invalid_triple), + "Filtering on valid VID:PID + invalid code triple should match"); + + zassert(!usbh_class_is_matching(filter_rules_either, &filter_invalid_pid_triple), + "Filtering on invalid VID:PID + invalid code triple should not match"); + + zassert(!usbh_class_is_matching(filter_rules_either, &filter_invalid_pid_triple), + "Filtering on invalid VID:PID + invalid code triple should not match"); + + /* Valid code triple */ + + zassert(usbh_class_is_matching(filter_rules_vid_pid, &filter_valid), + "Filtering on valid VID:PID + valid code triple (ignored) should match"); + + zassert(!usbh_class_is_matching(filter_rules_vid_pid, &filter_invalid_vid), + "Filtering on invalid VID + valid code triple (ignored) should not match"); + + zassert(!usbh_class_is_matching(filter_rules_vid_pid, &filter_invalid_pid), + "Filtering on invalid PID + valid code triple (ignored) should not match"); + + zassert(usbh_class_is_matching(filter_rules_triple, &filter_invalid_pid), + "Filtering on invalid PID (ignored) + valid code triple should match"); + + zassert(usbh_class_is_matching(filter_rules_triple, &filter_invalid_vid), + "Filtering on invalid VID (ignored) + valid code triple should match"); + + zassert(usbh_class_is_matching(filter_rules_either, &filter_invalid_pid), + "Filtering on invalid PID + valid code triple should match"); + + zassert(usbh_class_is_matching(filter_rules_either, &filter_valid), + "Filtering on valid VID:PID + valid code triple should match"); +} + +ZTEST(usbh_test, test_get_next_desc) +{ + const struct usb_device *udev; + const struct usb_desc_header *desc; + + udev = usbh_device_get_any(uhs_ctx); + zassert_not_null(udev); + + desc = udev->cfg_desc; + zassert_not_null(desc); + + /* #0 cfg */ + zassert_not_null(desc); + zassert_equal(desc->bDescriptorType, USB_DESC_CONFIGURATION); + desc = usbh_desc_get_next(desc); + + /* #1 iad */ + zassert_not_null(desc); + zassert_equal(desc->bDescriptorType, USB_DESC_INTERFACE_ASSOC); + desc = usbh_desc_get_next(desc); + + /* #2 if0 */ + zassert_not_null(desc); + zassert_equal(desc->bDescriptorType, USB_DESC_INTERFACE); + desc = usbh_desc_get_next(desc); + + /* #3 if0_out_ep */ + zassert_not_null(desc); + zassert_equal(desc->bDescriptorType, USB_DESC_ENDPOINT); + desc = usbh_desc_get_next(desc); + + /* #4 if0_in_ep */ + zassert_not_null(desc); + zassert_equal(desc->bDescriptorType, USB_DESC_ENDPOINT); + desc = usbh_desc_get_next(desc); + + /* #5 if1 */ + zassert_not_null(desc); + zassert_equal(desc->bDescriptorType, USB_DESC_INTERFACE); + desc = usbh_desc_get_next(desc); + + /* #6 if1_int_out_ep */ + zassert_not_null(desc); + zassert_equal(desc->bDescriptorType, USB_DESC_ENDPOINT); + desc = usbh_desc_get_next(desc); + + /* #7 if1_int_in_ep */ + zassert_not_null(desc); + zassert_equal(desc->bDescriptorType, USB_DESC_ENDPOINT); + desc = usbh_desc_get_next(desc); + + /* #8 if2_0 */ + zassert_not_null(desc); + zassert_equal(desc->bDescriptorType, USB_DESC_INTERFACE); + desc = usbh_desc_get_next(desc); + + /* #9 if2_0_iso_in_ep */ + zassert_not_null(desc); + zassert_equal(desc->bDescriptorType, USB_DESC_ENDPOINT); + desc = usbh_desc_get_next(desc); + + /* #10 if2_0_iso_out_ep */ + zassert_not_null(desc); + zassert_equal(desc->bDescriptorType, USB_DESC_ENDPOINT); + desc = usbh_desc_get_next(desc); + + /* #11 if2_1 */ + zassert_not_null(desc); + zassert_equal(desc->bDescriptorType, USB_DESC_INTERFACE); + desc = usbh_desc_get_next(desc); + + /* #12 if2_1_iso_in_ep */ + zassert_not_null(desc); + zassert_equal(desc->bDescriptorType, USB_DESC_ENDPOINT); + desc = usbh_desc_get_next(desc); + + /* #13 if2_1_iso_out_ep */ + zassert_not_null(desc); + zassert_equal(desc->bDescriptorType, USB_DESC_ENDPOINT); + desc = usbh_desc_get_next(desc); + + /* #14 nil_desc */ + zassert_is_null(desc); +} + +ZTEST(usbh_test, test_get_types) +{ + const struct usb_device *udev; + const struct usb_desc_header *desc; + + udev = usbh_device_get_any(uhs_ctx); + zassert_not_null(udev); + + /* #2 if0 */ + desc = usbh_desc_get_iface(udev, 0); + zassert_not_null(desc); + zassert_equal(desc->bDescriptorType, USB_DESC_INTERFACE); + zassert_equal(((struct usb_if_descriptor *)desc)->bInterfaceNumber, 0); + + /* #3 if0_out_ep */ + desc = usbh_desc_get_endpoint(udev, 0x01); + zassert_not_null(desc); + zassert_equal(desc->bDescriptorType, USB_DESC_ENDPOINT); + zassert_equal(((struct usb_ep_descriptor *)desc)->bEndpointAddress, 0x01); + + /* #4 if0_in_ep */ + desc = usbh_desc_get_endpoint(udev, 0x81); + zassert_not_null(desc); + zassert_equal(desc->bDescriptorType, USB_DESC_ENDPOINT); + zassert_equal(((struct usb_ep_descriptor *)desc)->bEndpointAddress, 0x81); + + /* #5 if1 */ + desc = usbh_desc_get_iface(udev, 1); + zassert_not_null(desc); + zassert_equal(desc->bDescriptorType, USB_DESC_INTERFACE); + zassert_equal(((struct usb_if_descriptor *)desc)->bInterfaceNumber, 1); + zassert_equal(((struct usb_if_descriptor *)desc)->bAlternateSetting, 0); + desc = usbh_desc_get_next_alt_setting(desc); + zassert_is_null(desc); +} + +ZTEST(usbh_test, test_get_next_function) +{ + const struct usb_device *udev; + const struct usb_desc_header *desc; + + udev = usbh_device_get_any(uhs_ctx); + zassert_not_null(udev); + + desc = udev->cfg_desc; + zassert_not_null(desc); + + /* #1 iad */ + desc = usbh_desc_get_next_function(desc); + zassert_not_null(desc); + zassert_equal(desc->bDescriptorType, USB_DESC_INTERFACE_ASSOC); + + /* end */ + desc = usbh_desc_get_next_function(desc); + zassert_is_null(desc); +} + +static struct usbd_context *test_usbd; + +USBH_CONTROLLER_DEFINE(test_uhs_ctx, DEVICE_DT_GET(DT_NODELABEL(zephyr_uhc0))); + +struct usbh_context *const uhs_ctx = &test_uhs_ctx; + +void *usbh_test_enable(void) +{ + int ret; + + ret = usbh_init(uhs_ctx); + zassert_ok(ret, "Failed to initialize USB host"); + + ret = usbh_enable(uhs_ctx); + zassert_ok(ret, "Failed to enable USB host"); + + ret = uhc_bus_reset(uhs_ctx->dev); + zassert_ok(ret, "Failed to signal bus reset"); + + ret = uhc_bus_resume(uhs_ctx->dev); + zassert_ok(ret, "Failed to signal bus resume"); + + ret = uhc_sof_enable(uhs_ctx->dev); + zassert_ok(ret, "Failed to enable SoF generator"); + + LOG_INF("Host controller enabled"); + + test_usbd = sample_usbd_setup_device(NULL); + zassert_not_null(test_usbd, "Failed to setup USB device"); + + ret = usbd_init(test_usbd); + zassert_ok(ret, "Failed to initialize device support"); + + ret = usbd_enable(test_usbd); + zassert_ok(ret, "Failed to enable device support"); + + LOG_INF("Device support enabled"); + + /* Allow the host time to reset the device. */ + k_msleep(200); + + return NULL; +} + +void usbh_test_shutdown(void *f) +{ + int ret; + + ret = usbd_disable(test_usbd); + zassert_ok(ret, "Failed to disable device support"); + + ret = usbd_shutdown(test_usbd); + zassert_ok(ret, "Failed to shutdown device support"); + + LOG_INF("Device support disabled"); + + ret = usbh_disable(uhs_ctx); + zassert_ok(ret, "Failed to disable USB host"); + + ret = usbh_shutdown(uhs_ctx); + zassert_ok(ret, "Failed to shutdown host support"); + + LOG_INF("Host controller disabled"); +} + +ZTEST_SUITE(usbh_test, NULL, usbh_test_enable, NULL, NULL, usbh_test_shutdown); diff --git a/tests/subsys/usb/host/testcase.yaml b/tests/subsys/usb/host/testcase.yaml new file mode 100644 index 0000000000000..61fe9bdc714ba --- /dev/null +++ b/tests/subsys/usb/host/testcase.yaml @@ -0,0 +1,8 @@ +tests: + usb.host: + platform_allow: + - native_sim + - native_sim/native/64 + integration_platforms: + - native_sim + tags: usb