diff --git a/drivers/usb/uhc/uhc_common.c b/drivers/usb/uhc/uhc_common.c index 097d4a01457e0..e06048c4e926a 100644 --- a/drivers/usb/uhc/uhc_common.c +++ b/drivers/usb/uhc/uhc_common.c @@ -101,6 +101,8 @@ struct uhc_transfer *uhc_xfer_alloc(const struct device *dev, const struct uhc_api *api = dev->api; struct uhc_transfer *xfer = NULL; uint16_t mps; + uint16_t interval; + uint8_t type; api->lock(dev); @@ -109,6 +111,8 @@ struct uhc_transfer *uhc_xfer_alloc(const struct device *dev, } if (ep_idx == 0) { + interval = 0; + type = USB_EP_TYPE_CONTROL; mps = udev->dev_desc.bMaxPacketSize0; } else { struct usb_ep_descriptor *ep_desc; @@ -125,6 +129,8 @@ struct uhc_transfer *uhc_xfer_alloc(const struct device *dev, } mps = ep_desc->wMaxPacketSize; + interval = ep_desc->bInterval; + type = ep_desc->bmAttributes & USB_EP_TRANSFER_TYPE_MASK; } LOG_DBG("Allocate xfer, ep 0x%02x mps %u cb %p", ep, mps, cb); @@ -137,6 +143,8 @@ struct uhc_transfer *uhc_xfer_alloc(const struct device *dev, memset(xfer, 0, sizeof(struct uhc_transfer)); xfer->ep = ep; xfer->mps = mps; + xfer->interval = interval; + xfer->type = type; xfer->udev = udev; xfer->cb = cb; xfer->priv = cb_priv; diff --git a/drivers/usb/uhc/uhc_mcux_common.c b/drivers/usb/uhc/uhc_mcux_common.c index 7ed7aa169e8f5..d8e96d42d580d 100644 --- a/drivers/usb/uhc/uhc_mcux_common.c +++ b/drivers/usb/uhc/uhc_mcux_common.c @@ -243,10 +243,9 @@ static usb_host_pipe_handle uhc_mcux_check_hal_ep(const struct device *dev, } } - /* TODO: need to check endpoint type too */ - if (mcux_ep != NULL && + if (mcux_ep != NULL && mcux_ep->pipeType == xfer->type && (mcux_ep->maxPacketSize != xfer->mps || - mcux_ep->interval != xfer->interval)) { + priv->mcux_eps_interval[i] != xfer->interval)) { /* re-initialize the ep */ status = priv->mcux_if->controllerClosePipe(priv->mcux_host.controllerHandle, mcux_ep); @@ -256,6 +255,7 @@ static usb_host_pipe_handle uhc_mcux_check_hal_ep(const struct device *dev, uhc_mcux_lock(dev); priv->mcux_eps[i] = NULL; + priv->mcux_eps_interval[i] = 0; uhc_mcux_unlock(dev); mcux_ep = NULL; } @@ -280,21 +280,16 @@ usb_host_pipe_t *uhc_mcux_init_hal_ep(const struct device *dev, struct uhc_trans /* USB_HostHelperGetPeripheralInformation uses this value as first parameter */ pipe_init.devInstance = xfer->udev; pipe_init.nakCount = USB_HOST_CONFIG_MAX_NAK; - pipe_init.maxPacketSize = xfer->mps; + pipe_init.maxPacketSize = USB_MPS_EP_SIZE(xfer->mps); pipe_init.endpointAddress = USB_EP_GET_IDX(xfer->ep); pipe_init.direction = USB_EP_GET_IDX(xfer->ep) == 0 ? USB_OUT : USB_EP_GET_DIR(xfer->ep) ? USB_IN : USB_OUT; /* Current Zephyr Host stack is experimental, the endpoint's interval, * 'number per uframe' and the endpoint type cannot be got yet. */ - pipe_init.numberPerUframe = 0; /* TODO: need right way to implement it. */ + pipe_init.numberPerUframe = USB_MPS_ADDITIONAL_TRANSACTIONS(xfer->mps); pipe_init.interval = xfer->interval; - /* TODO: need right way to implement it. */ - if (pipe_init.endpointAddress == 0) { - pipe_init.pipeType = USB_ENDPOINT_CONTROL; - } else { - pipe_init.pipeType = USB_ENDPOINT_BULK; - } + pipe_init.pipeType = xfer->type; status = priv->mcux_if->controllerOpenPipe(priv->mcux_host.controllerHandle, (usb_host_pipe_handle *)&mcux_ep, &pipe_init); @@ -314,6 +309,7 @@ usb_host_pipe_t *uhc_mcux_init_hal_ep(const struct device *dev, struct uhc_trans for (i = 0; i < USB_HOST_CONFIG_MAX_PIPES; i++) { if (priv->mcux_eps[i] == NULL) { priv->mcux_eps[i] = mcux_ep; + priv->mcux_eps_interval[i] = xfer->interval; break; } } diff --git a/drivers/usb/uhc/uhc_mcux_common.h b/drivers/usb/uhc/uhc_mcux_common.h index d80ccdadcfcf0..3e9014af85a6a 100644 --- a/drivers/usb/uhc/uhc_mcux_common.h +++ b/drivers/usb/uhc/uhc_mcux_common.h @@ -12,6 +12,7 @@ struct uhc_mcux_data { const usb_host_controller_interface_t *mcux_if; /* TODO: Maybe make it to link with udev->ep_in and udev->ep_out */ usb_host_pipe_t *mcux_eps[USB_HOST_CONFIG_MAX_PIPES]; + uint16_t mcux_eps_interval[USB_HOST_CONFIG_MAX_PIPES]; usb_host_instance_t mcux_host; struct k_thread drv_stack_data; uint8_t controller_id; /* MCUX hal controller id, 0xFF is invalid value */ diff --git a/dts/bindings/usb/zephyr,uvc-host.yaml b/dts/bindings/usb/zephyr,uvc-host.yaml new file mode 100644 index 0000000000000..99941a38c7f96 --- /dev/null +++ b/dts/bindings/usb/zephyr,uvc-host.yaml @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +description: | + USB Video Class (UVC) host instance. + + Each UVC instance added to the USB Host Controller (UHC) node will be visible + as a new camera from Zephyr point of view. + + as soon as a camera is connected to USB this device will be usable by the application as a + video device, following the video API. + +compatible: "zephyr,uvc-host" + +include: base.yaml diff --git a/include/zephyr/drivers/usb/uhc.h b/include/zephyr/drivers/usb/uhc.h index 1e0acd71aad1f..5bacfdc19556f 100644 --- a/include/zephyr/drivers/usb/uhc.h +++ b/include/zephyr/drivers/usb/uhc.h @@ -120,6 +120,8 @@ struct uhc_transfer { struct net_buf *buf; /** Endpoint to which request is associated */ uint8_t ep; + /** Endpoint type */ + uint8_t type; /** Maximum packet size */ uint16_t mps; /** Interval, used for periodic transfers only */ diff --git a/include/zephyr/usb/class/usbd_uvc.h b/include/zephyr/usb/class/usbd_uvc.h index 9cd3ad1d1931b..43f62b88b9942 100644 --- a/include/zephyr/usb/class/usbd_uvc.h +++ b/include/zephyr/usb/class/usbd_uvc.h @@ -13,6 +13,7 @@ #define ZEPHYR_INCLUDE_USB_CLASS_USBD_UVC_H #include +#include /** * @brief USB Video Class (UVC) device API diff --git a/include/zephyr/usb/usbh.h b/include/zephyr/usb/usbh.h index e2e6f3074000e..4f7499ba8065c 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,10 +54,10 @@ 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 */ - struct usb_device *root; /** Allocated device addresses bit array */ struct sys_bitarray *addr_ba; }; @@ -60,47 +71,116 @@ 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 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); +}; + +/** + * @brief USB host class instance data */ struct usbh_class_data { - /** Class code supported by this instance */ - struct usbh_code_triple code; + /** Name of the USB host class instance */ + const char *name; + /** Pointer to USB host stack context structure */ + struct usbh_context *uhs_ctx; + /** Pointer to USB device this class is used for */ + struct usb_device *udev; + /** Interface number for which this class matched */ + uint8_t iface; + /** Pointer to host support class API */ + struct usbh_class_api *api; + /** Pointer to private data */ + void *priv; +}; - /** 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); +/** + * @cond INTERNAL_HIDDEN + * + * Internal state of an USB class. Not corresponding to an USB protocol state, + * but instead software life cycle. + */ +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. */ -#define USBH_DEFINE_CLASS(name) \ - static STRUCT_SECTION_ITERABLE(usbh_class_data, name) +struct usbh_class_node { + /** Class information exposed to host class implementations (drivers). */ + struct usbh_class_data *const c_data; + /** Filter rules to match this USB host class instance against a device class **/ + struct usbh_class_filter *filters; + /** 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, the 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..df270ed0b4b8e 100644 --- a/subsys/usb/CMakeLists.txt +++ b/subsys/usb/CMakeLists.txt @@ -1,7 +1,8 @@ -# Copyright (c) 2022 Nordic Semiconductor +# SPDX-FileCopyrightText: Copyright Nordic Semiconductor ASA # SPDX-License-Identifier: Apache-2.0 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..5015d5c438030 --- /dev/null +++ b/subsys/usb/common/CMakeLists.txt @@ -0,0 +1,7 @@ +# Copyright (c) 2025 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +if(CONFIG_USBD_VIDEO_CLASS OR CONFIG_USBH_VIDEO_CLASS) + zephyr_include_directories(include) + zephyr_sources(usb_common_uvc.c) +endif() diff --git a/subsys/usb/common/include/usb_common_uvc.h b/subsys/usb/common/include/usb_common_uvc.h new file mode 100644 index 0000000000000..aed1f23fde6dc --- /dev/null +++ b/subsys/usb/common/include/usb_common_uvc.h @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_INCLUDE_USB_COMMON_UVC_H +#define ZEPHYR_INCLUDE_USB_COMMON_UVC_H + +#include + +/** + * @brief Type of value used by the USB protocol for this control. + */ +enum uvc_control_type { + /** Signed integer control type */ + UVC_CONTROL_SIGNED, + /** Unsigned integer control type */ + UVC_CONTROL_UNSIGNED, +}; + +/** + * @brief Mapping between UVC controls and Video controls + */ +struct uvc_control_map { + /* Video CID to use for this control */ + uint32_t cid; + /* Size to write out */ + uint8_t size; + /* Bit position in the UVC control */ + uint8_t bit; + /* UVC selector identifying this control */ + uint8_t selector; + /* Whether the UVC value is signed, always false for bitmaps and boolean */ + enum uvc_control_type type; +}; + +/** + * @brief Mapping between UVC GUIDs and standard FourCC. + */ +struct uvc_guid_quirk { + /* A Video API format identifier, for which the UVC format GUID is not standard. */ + uint32_t fourcc; + /* GUIDs are 16-bytes long, with the first four bytes being the Four Character Code of the + * format and the rest constant, except for some exceptions listed in this table. + */ + uint8_t guid[16]; +}; + +/** + * @brief Get a conversion table for a given control unit type + * + * The mappings contains information about how UVC control structures are related to + * video control structures. + * + * @param subtype The field bDescriptorSubType of a descriptor of type USB_DESC_CS_INTERFACE. + * @param map Filled with a pointer to the conversion table + * @param length Filled with the number of elements in that conversion table. + * + * @return 0 on success, negative code on error. + */ +int uvc_get_control_map(uint8_t subtype, const struct uvc_control_map **map, size_t *length); + +/** + * @brief Convert a standard FourCC to an equivalent UVC GUID. + * + * @param guid Array to a GUID, filled in binary format + * @param fourcc Four character code + */ +void uvc_fourcc_to_guid(uint8_t guid[16], const uint32_t fourcc); + +/** + * @brief Convert an UVC GUID to a standard FourCC + * + * @param guid GUID, to convert + * + * @return Four Character Code + */ +uint32_t uvc_guid_to_fourcc(const uint8_t guid[16]); + +#endif /* ZEPHYR_INCLUDE_USB_COMMON_UVC_H */ diff --git a/subsys/usb/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/common/usb_common_uvc.c b/subsys/usb/common/usb_common_uvc.c new file mode 100644 index 0000000000000..15f09f19a4d5f --- /dev/null +++ b/subsys/usb/common/usb_common_uvc.c @@ -0,0 +1,234 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include +#include +#include + +#include "usb_common_uvc.h" +#include "usb_uvc.h" + +static const struct uvc_guid_quirk uvc_guid_quirks[] = { + { + .fourcc = VIDEO_PIX_FMT_YUYV, + .guid = UVC_FORMAT_GUID("YUY2"), + }, + { + .fourcc = VIDEO_PIX_FMT_GREY, + .guid = UVC_FORMAT_GUID("Y800"), + }, +}; + +static const struct uvc_control_map uvc_control_map_ct[] = { + { + .size = 1, + .bit = 1, + .selector = UVC_CT_AE_MODE_CONTROL, + .cid = VIDEO_CID_EXPOSURE_AUTO, + .type = UVC_CONTROL_UNSIGNED, + }, + { + .size = 1, + .bit = 2, + .selector = UVC_CT_AE_PRIORITY_CONTROL, + .cid = VIDEO_CID_EXPOSURE_AUTO_PRIORITY, + .type = UVC_CONTROL_UNSIGNED, + }, + { + .size = 4, + .bit = 3, + .selector = UVC_CT_EXPOSURE_TIME_ABS_CONTROL, + .cid = VIDEO_CID_EXPOSURE, + .type = UVC_CONTROL_UNSIGNED, + }, + { + .size = 2, + .bit = 5, + .selector = UVC_CT_FOCUS_ABS_CONTROL, + .cid = VIDEO_CID_FOCUS_ABSOLUTE, + .type = UVC_CONTROL_UNSIGNED, + }, + { + .size = 2, + .bit = 6, + .selector = UVC_CT_FOCUS_REL_CONTROL, + .cid = VIDEO_CID_FOCUS_RELATIVE, + .type = UVC_CONTROL_SIGNED, + }, + { + .size = 2, + .bit = 7, + .selector = UVC_CT_IRIS_ABS_CONTROL, + .cid = VIDEO_CID_IRIS_ABSOLUTE, + .type = UVC_CONTROL_UNSIGNED, + }, + { + .size = 1, + .bit = 8, + .selector = UVC_CT_IRIS_REL_CONTROL, + .cid = VIDEO_CID_IRIS_RELATIVE, + .type = UVC_CONTROL_SIGNED, + }, + { + .size = 2, + .bit = 9, + .selector = UVC_CT_ZOOM_ABS_CONTROL, + .cid = VIDEO_CID_ZOOM_ABSOLUTE, + .type = UVC_CONTROL_UNSIGNED, + }, + { + .size = 3, + .bit = 10, + .selector = UVC_CT_ZOOM_REL_CONTROL, + .cid = VIDEO_CID_ZOOM_RELATIVE, + .type = UVC_CONTROL_SIGNED, + }, +}; + +static const struct uvc_control_map uvc_control_map_pu[] = { + { + .size = 2, + .bit = 0, + .selector = UVC_PU_BRIGHTNESS_CONTROL, + .cid = VIDEO_CID_BRIGHTNESS, + .type = UVC_CONTROL_SIGNED, + }, + { + .size = 2, + .bit = 1, + .selector = UVC_PU_CONTRAST_CONTROL, + .cid = VIDEO_CID_CONTRAST, + .type = UVC_CONTROL_UNSIGNED, + }, + { + .size = 2, + .bit = 9, + .selector = UVC_PU_GAIN_CONTROL, + .cid = VIDEO_CID_GAIN, + .type = UVC_CONTROL_UNSIGNED, + }, + { + .size = 2, + .bit = 3, + .selector = UVC_PU_SATURATION_CONTROL, + .cid = VIDEO_CID_SATURATION, + .type = UVC_CONTROL_UNSIGNED, + }, + { + .size = 2, + .bit = 6, + .selector = UVC_PU_WHITE_BALANCE_TEMP_CONTROL, + .cid = VIDEO_CID_WHITE_BALANCE_TEMPERATURE, + .type = UVC_CONTROL_UNSIGNED, + }, +}; + +static const struct uvc_control_map uvc_control_map_su[] = { + { + .size = 1, + .bit = 0, + .selector = UVC_SU_INPUT_SELECT_CONTROL, + .cid = VIDEO_CID_TEST_PATTERN, + .type = UVC_CONTROL_UNSIGNED, + }, +}; + +static const struct uvc_control_map uvc_control_map_xu[] = { + { + .size = 4, + .bit = 0, + .selector = UVC_XU_BASE_CONTROL + 0, + .cid = VIDEO_CID_PRIVATE_BASE + 0, + .type = UVC_CONTROL_UNSIGNED, + }, + { + .size = 4, + .bit = 1, + .selector = UVC_XU_BASE_CONTROL + 1, + .cid = VIDEO_CID_PRIVATE_BASE + 1, + .type = UVC_CONTROL_UNSIGNED, + }, + { + .size = 4, + .bit = 2, + .selector = UVC_XU_BASE_CONTROL + 2, + .cid = VIDEO_CID_PRIVATE_BASE + 2, + .type = UVC_CONTROL_UNSIGNED, + }, + { + .size = 4, + .bit = 3, + .selector = UVC_XU_BASE_CONTROL + 3, + .cid = VIDEO_CID_PRIVATE_BASE + 3, + .type = UVC_CONTROL_UNSIGNED, + }, +}; + +int uvc_get_control_map(uint8_t subtype, const struct uvc_control_map **map, size_t *length) +{ + switch (subtype) { + case UVC_VC_INPUT_TERMINAL: + *map = uvc_control_map_ct; + *length = ARRAY_SIZE(uvc_control_map_ct); + break; + case UVC_VC_SELECTOR_UNIT: + *map = uvc_control_map_su; + *length = ARRAY_SIZE(uvc_control_map_su); + break; + case UVC_VC_PROCESSING_UNIT: + *map = uvc_control_map_pu; + *length = ARRAY_SIZE(uvc_control_map_pu); + break; + case UVC_VC_EXTENSION_UNIT: + *map = uvc_control_map_xu; + *length = ARRAY_SIZE(uvc_control_map_xu); + break; + default: + return -EINVAL; + } + + return 0; +} + +void uvc_fourcc_to_guid(uint8_t guid[16], const uint32_t fourcc) +{ + uint32_t fourcc_le; + + /* Lookup in the "quirk table" if the UVC format GUID is custom */ + for (int i = 0; i < ARRAY_SIZE(uvc_guid_quirks); i++) { + if (uvc_guid_quirks[i].fourcc == fourcc) { + memcpy(guid, uvc_guid_quirks[i].guid, 16); + return; + } + } + + /* By default, UVC GUIDs are the four character code followed by a common suffix */ + fourcc_le = sys_cpu_to_le32(fourcc); + /* Copy the common suffix with the GUID set to 'XXXX' */ + memcpy(guid, UVC_FORMAT_GUID("XXXX"), 16); + /* Replace the 'XXXX' by the actual GUID of the format */ + memcpy(guid, &fourcc_le, 4); +} + +uint32_t uvc_guid_to_fourcc(const uint8_t guid[16]) +{ + uint32_t fourcc; + + /* Lookup in the "quirk table" if the UVC format GUID is custom */ + for (int i = 0; i < ARRAY_SIZE(uvc_guid_quirks); i++) { + if (memcmp(guid, uvc_guid_quirks[i].guid, 16) == 0) { + return uvc_guid_quirks[i].fourcc; + } + } + + /* Extract the four character code out of the leading 4 bytes of the GUID */ + memcpy(&fourcc, guid, 4); + fourcc = sys_le32_to_cpu(fourcc); + + return fourcc; +} diff --git a/subsys/usb/device_next/class/usbd_uvc.c b/subsys/usb/device_next/class/usbd_uvc.c index 2b80084079e37..a2f9fb78d2b39 100644 --- a/subsys/usb/device_next/class/usbd_uvc.c +++ b/subsys/usb/device_next/class/usbd_uvc.c @@ -25,7 +25,9 @@ #include #include -#include "usbd_uvc.h" +#include "usb_common_uvc.h" +#include "usb_uvc.h" + #include "../../../drivers/video/video_ctrls.h" #include "../../../drivers/video/video_device.h" @@ -62,11 +64,6 @@ enum uvc_unit_id { UVC_UNIT_ID_OT, }; -enum uvc_control_type { - UVC_CONTROL_SIGNED, - UVC_CONTROL_UNSIGNED, -}; - union uvc_fmt_desc { struct usb_desc_header hdr; struct uvc_format_descriptor fmt; @@ -154,29 +151,6 @@ struct uvc_buf_info { struct video_buffer *vbuf; } __packed; -/* Mapping between UVC controls and Video controls */ -struct uvc_control_map { - /* Video CID to use for this control */ - uint32_t cid; - /* Size to write out */ - uint8_t size; - /* Bit position in the UVC control */ - uint8_t bit; - /* UVC selector identifying this control */ - uint8_t selector; - /* Whether the UVC value is signed, always false for bitmaps and boolean */ - enum uvc_control_type type; -}; - -struct uvc_guid_quirk { - /* A Video API format identifier, for which the UVC format GUID is not standard. */ - uint32_t fourcc; - /* GUIDs are 16-bytes long, with the first four bytes being the Four Character Code of the - * format and the rest constant, except for some exceptions listed in this table. - */ - uint8_t guid[16]; -}; - #define UVC_TOTAL_BUFS (DT_NUM_INST_STATUS_OKAY(DT_DRV_COMPAT) * CONFIG_USBD_VIDEO_NUM_BUFS) UDC_BUF_POOL_VAR_DEFINE(uvc_buf_pool, UVC_TOTAL_BUFS, UVC_TOTAL_BUFS * USBD_MAX_BULK_MPS, @@ -186,202 +160,6 @@ static void uvc_flush_queue(const struct device *dev); /* UVC helper functions */ -static const struct uvc_guid_quirk uvc_guid_quirks[] = { - { - .fourcc = VIDEO_PIX_FMT_YUYV, - .guid = UVC_FORMAT_GUID("YUY2"), - }, - { - .fourcc = VIDEO_PIX_FMT_GREY, - .guid = UVC_FORMAT_GUID("Y800"), - }, -}; - -static void uvc_fourcc_to_guid(uint8_t guid[16], const uint32_t fourcc) -{ - uint32_t fourcc_le; - - /* Lookup in the "quirk table" if the UVC format GUID is custom */ - for (int i = 0; i < ARRAY_SIZE(uvc_guid_quirks); i++) { - if (uvc_guid_quirks[i].fourcc == fourcc) { - memcpy(guid, uvc_guid_quirks[i].guid, 16); - return; - } - } - - /* By default, UVC GUIDs are the four character code followed by a common suffix */ - fourcc_le = sys_cpu_to_le32(fourcc); - /* Copy the common suffix with the GUID set to 'XXXX' */ - memcpy(guid, UVC_FORMAT_GUID("XXXX"), 16); - /* Replace the 'XXXX' by the actual GUID of the format */ - memcpy(guid, &fourcc_le, 4); -} - -static uint32_t uvc_guid_to_fourcc(const uint8_t guid[16]) -{ - uint32_t fourcc; - - /* Lookup in the "quirk table" if the UVC format GUID is custom */ - for (int i = 0; i < ARRAY_SIZE(uvc_guid_quirks); i++) { - if (memcmp(guid, uvc_guid_quirks[i].guid, 16) == 0) { - return uvc_guid_quirks[i].fourcc; - } - } - - /* Extract the four character code out of the leading 4 bytes of the GUID */ - memcpy(&fourcc, guid, 4); - fourcc = sys_le32_to_cpu(fourcc); - - return fourcc; -} - -/* UVC control handling */ - -static const struct uvc_control_map uvc_control_map_ct[] = { - { - .size = 1, - .bit = 1, - .selector = UVC_CT_AE_MODE_CONTROL, - .cid = VIDEO_CID_EXPOSURE_AUTO, - .type = UVC_CONTROL_UNSIGNED, - }, - { - .size = 1, - .bit = 2, - .selector = UVC_CT_AE_PRIORITY_CONTROL, - .cid = VIDEO_CID_EXPOSURE_AUTO_PRIORITY, - .type = UVC_CONTROL_UNSIGNED, - }, - { - .size = 4, - .bit = 3, - .selector = UVC_CT_EXPOSURE_TIME_ABS_CONTROL, - .cid = VIDEO_CID_EXPOSURE, - .type = UVC_CONTROL_UNSIGNED, - }, - { - .size = 2, - .bit = 5, - .selector = UVC_CT_FOCUS_ABS_CONTROL, - .cid = VIDEO_CID_FOCUS_ABSOLUTE, - .type = UVC_CONTROL_UNSIGNED, - }, - { - .size = 2, - .bit = 6, - .selector = UVC_CT_FOCUS_REL_CONTROL, - .cid = VIDEO_CID_FOCUS_RELATIVE, - .type = UVC_CONTROL_SIGNED, - }, - { - .size = 2, - .bit = 7, - .selector = UVC_CT_IRIS_ABS_CONTROL, - .cid = VIDEO_CID_IRIS_ABSOLUTE, - .type = UVC_CONTROL_UNSIGNED, - }, - { - .size = 1, - .bit = 8, - .selector = UVC_CT_IRIS_REL_CONTROL, - .cid = VIDEO_CID_IRIS_RELATIVE, - .type = UVC_CONTROL_SIGNED, - }, - { - .size = 2, - .bit = 9, - .selector = UVC_CT_ZOOM_ABS_CONTROL, - .cid = VIDEO_CID_ZOOM_ABSOLUTE, - .type = UVC_CONTROL_UNSIGNED, - }, - { - .size = 3, - .bit = 10, - .selector = UVC_CT_ZOOM_REL_CONTROL, - .cid = VIDEO_CID_ZOOM_RELATIVE, - .type = UVC_CONTROL_SIGNED, - }, -}; - -static const struct uvc_control_map uvc_control_map_pu[] = { - { - .size = 2, - .bit = 0, - .selector = UVC_PU_BRIGHTNESS_CONTROL, - .cid = VIDEO_CID_BRIGHTNESS, - .type = UVC_CONTROL_SIGNED, - }, - { - .size = 1, - .bit = 1, - .selector = UVC_PU_CONTRAST_CONTROL, - .cid = VIDEO_CID_CONTRAST, - .type = UVC_CONTROL_UNSIGNED, - }, - { - .size = 2, - .bit = 9, - .selector = UVC_PU_GAIN_CONTROL, - .cid = VIDEO_CID_GAIN, - .type = UVC_CONTROL_UNSIGNED, - }, - { - .size = 2, - .bit = 3, - .selector = UVC_PU_SATURATION_CONTROL, - .cid = VIDEO_CID_SATURATION, - .type = UVC_CONTROL_UNSIGNED, - }, - { - .size = 2, - .bit = 6, - .selector = UVC_PU_WHITE_BALANCE_TEMP_CONTROL, - .cid = VIDEO_CID_WHITE_BALANCE_TEMPERATURE, - .type = UVC_CONTROL_UNSIGNED, - }, -}; - -static const struct uvc_control_map uvc_control_map_su[] = { - { - .size = 1, - .bit = 0, - .selector = UVC_SU_INPUT_SELECT_CONTROL, - .cid = VIDEO_CID_TEST_PATTERN, - .type = UVC_CONTROL_UNSIGNED, - }, -}; - -static const struct uvc_control_map uvc_control_map_xu[] = { - { - .size = 4, - .bit = 0, - .selector = UVC_XU_BASE_CONTROL + 0, - .cid = VIDEO_CID_PRIVATE_BASE + 0, - .type = UVC_CONTROL_UNSIGNED, - }, - { - .size = 4, - .bit = 1, - .selector = UVC_XU_BASE_CONTROL + 1, - .cid = VIDEO_CID_PRIVATE_BASE + 1, - .type = UVC_CONTROL_UNSIGNED, - }, - { - .size = 4, - .bit = 2, - .selector = UVC_XU_BASE_CONTROL + 2, - .cid = VIDEO_CID_PRIVATE_BASE + 2, - .type = UVC_CONTROL_UNSIGNED, - }, - { - .size = 4, - .bit = 3, - .selector = UVC_XU_BASE_CONTROL + 3, - .cid = VIDEO_CID_PRIVATE_BASE + 3, - .type = UVC_CONTROL_UNSIGNED, - }, -}; - /* Get the format and frame descriptors selected for the given VideoStreaming interface. */ static void uvc_get_vs_fmtfrm_desc(const struct device *dev, struct uvc_format_descriptor **const format_desc, @@ -1167,6 +945,7 @@ static int uvc_get_control_op(const struct device *dev, const struct usb_setup_p uint8_t unit_id = setup->wIndex >> 8; uint8_t selector = setup->wValue >> 8; uint8_t subtype = 0; + int ret; /* VideoStreaming operation */ @@ -1199,17 +978,16 @@ static int uvc_get_control_op(const struct device *dev, const struct usb_setup_p for (int i = UVC_IDX_VC_UNIT;; i++) { struct uvc_unit_descriptor *desc = (void *)cfg->fs_desc[i]; - if (desc->bDescriptorType != USB_DESC_CS_INTERFACE || - (desc->bDescriptorSubtype != UVC_VC_INPUT_TERMINAL && - desc->bDescriptorSubtype != UVC_VC_ENCODING_UNIT && - desc->bDescriptorSubtype != UVC_VC_SELECTOR_UNIT && - desc->bDescriptorSubtype != UVC_VC_EXTENSION_UNIT && - desc->bDescriptorSubtype != UVC_VC_PROCESSING_UNIT)) { + if (desc->bDescriptorType != USB_DESC_CS_INTERFACE) { break; } + ret = uvc_get_control_map(desc->bDescriptorSubtype, &list, &list_sz); + if (ret != 0) { + goto err; + } + if (unit_id == desc->bUnitID) { - subtype = desc->bDescriptorSubtype; break; } } @@ -1218,27 +996,6 @@ static int uvc_get_control_op(const struct device *dev, const struct usb_setup_p goto err; } - switch (subtype) { - case UVC_VC_INPUT_TERMINAL: - list = uvc_control_map_ct; - list_sz = ARRAY_SIZE(uvc_control_map_ct); - break; - case UVC_VC_SELECTOR_UNIT: - list = uvc_control_map_su; - list_sz = ARRAY_SIZE(uvc_control_map_su); - break; - case UVC_VC_PROCESSING_UNIT: - list = uvc_control_map_pu; - list_sz = ARRAY_SIZE(uvc_control_map_pu); - break; - case UVC_VC_EXTENSION_UNIT: - list = uvc_control_map_xu; - list_sz = ARRAY_SIZE(uvc_control_map_xu); - break; - default: - CODE_UNREACHABLE; - } - *map = NULL; for (int i = 0; i < list_sz; i++) { if (list[i].selector == selector) { @@ -1762,7 +1519,9 @@ void uvc_set_video_dev(const struct device *const dev, const struct device *cons { struct uvc_data *data = dev->data; const struct uvc_config *cfg = dev->config; + const struct uvc_control_map *map = NULL; uint32_t mask = 0; + size_t map_sz = 0; data->video_dev = video_dev; @@ -1770,17 +1529,20 @@ void uvc_set_video_dev(const struct device *const dev, const struct device *cons cfg->desc->if0_hdr.baInterfaceNr[0] = cfg->desc->if1.bInterfaceNumber; - mask = uvc_get_mask(data->video_dev, uvc_control_map_ct, ARRAY_SIZE(uvc_control_map_ct)); + uvc_get_control_map(UVC_VC_INPUT_TERMINAL, &map, &map_sz); + mask = uvc_get_mask(data->video_dev, map, map_sz); cfg->desc->if0_ct.bmControls[0] = mask >> 0; cfg->desc->if0_ct.bmControls[1] = mask >> 8; cfg->desc->if0_ct.bmControls[2] = mask >> 16; - mask = uvc_get_mask(data->video_dev, uvc_control_map_pu, ARRAY_SIZE(uvc_control_map_pu)); + uvc_get_control_map(UVC_VC_PROCESSING_UNIT, &map, &map_sz); + mask = uvc_get_mask(data->video_dev, map, map_sz); cfg->desc->if0_pu.bmControls[0] = mask >> 0; cfg->desc->if0_pu.bmControls[1] = mask >> 8; cfg->desc->if0_pu.bmControls[2] = mask >> 16; - mask = uvc_get_mask(data->video_dev, uvc_control_map_xu, ARRAY_SIZE(uvc_control_map_xu)); + uvc_get_control_map(UVC_VC_EXTENSION_UNIT, &map, &map_sz); + mask = uvc_get_mask(data->video_dev, map, map_sz); cfg->desc->if0_xu.bmControls[0] = mask >> 0; cfg->desc->if0_xu.bmControls[1] = mask >> 8; cfg->desc->if0_xu.bmControls[2] = mask >> 16; diff --git a/subsys/usb/host/CMakeLists.txt b/subsys/usb/host/CMakeLists.txt index 4e8f7b9d28dc8..2759c9a37b0b4 100644 --- a/subsys/usb/host/CMakeLists.txt +++ b/subsys/usb/host/CMakeLists.txt @@ -1,13 +1,15 @@ -# Copyright (c) 2022 Nordic Semiconductor ASA +# SPDX-FileCopyrightText: Copyright Nordic Semiconductor ASA # SPDX-License-Identifier: Apache-2.0 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 ) @@ -16,6 +18,17 @@ zephyr_library_sources_ifdef( usbh_shell.c ) +zephyr_library_sources_ifdef( + CONFIG_USBH_VIDEO_CLASS + class/usbh_uvc.c +) + +zephyr_library_sources_ifdef( + CONFIG_USBH_HUB_CLASS + class/usbh_hub.c + class/usbh_hub_mgr.c +) + zephyr_library_sources_ifdef( CONFIG_USBIP usbip.c diff --git a/subsys/usb/host/Kconfig b/subsys/usb/host/Kconfig index 8da3c95ebf7be..aae840cf3b81c 100644 --- a/subsys/usb/host/Kconfig +++ b/subsys/usb/host/Kconfig @@ -1,5 +1,4 @@ -# Copyright (c) 2022 Nordic Semiconductor ASA -# +# SPDX-FileCopyrightText: Copyright Nordic Semiconductor ASA # SPDX-License-Identifier: Apache-2.0 menuconfig USB_HOST_STACK @@ -54,5 +53,6 @@ config USBH_MAX_UHC_MSG Maximum number of USB host controller events that can be queued. rsource "Kconfig.usbip" +rsource "class/Kconfig" endif # USB_HOST_STACK diff --git a/subsys/usb/host/Kconfig.usbip b/subsys/usb/host/Kconfig.usbip index a880fdb48f2af..aa9dacbd61ba6 100644 --- a/subsys/usb/host/Kconfig.usbip +++ b/subsys/usb/host/Kconfig.usbip @@ -1,5 +1,4 @@ -# Copyright (c) 2024 Nordic Semiconductor ASA -# +# SPDX-FileCopyrightText: Copyright Nordic Semiconductor ASA # SPDX-License-Identifier: Apache-2.0 menuconfig USBIP diff --git a/subsys/usb/host/class/Kconfig b/subsys/usb/host/class/Kconfig new file mode 100644 index 0000000000000..4e961913aab9a --- /dev/null +++ b/subsys/usb/host/class/Kconfig @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: Copyright Nordic Semiconductor ASA +# +# Copyright 2025 NXP +# +# SPDX-License-Identifier: Apache-2.0 + +rsource "Kconfig.uvc" +rsource "Kconfig.hub_host" diff --git a/subsys/usb/host/class/Kconfig.hub_host b/subsys/usb/host/class/Kconfig.hub_host new file mode 100644 index 0000000000000..a93ff6702f209 --- /dev/null +++ b/subsys/usb/host/class/Kconfig.hub_host @@ -0,0 +1,51 @@ +# +# Copyright 2025 NXP +# +# SPDX-License-Identifier: Apache-2.0 +# + +config USBH_HUB_CLASS + bool "USB Host Hub Class Driver" + default n + help + Enable USB Host Hub Class driver support. + This allows connecting USB hubs and managing downstream devices. + +if USBH_HUB_CLASS + +config USBH_HUB_MAX_LEVELS + int "Maximum Hub chain depth" + default 5 + range 1 5 + help + Maximum number of non-root hubs allowed in a single communication path + between the host and any device. USB specification allows up to five + non-root hubs per path, which means the deepest path can include 5 hubs. + +config USBH_HUB_MAX_COUNT + int "Maximum Hub count" + default 7 + range 1 16 + help + Maximum number of hub devices supported in the entire USB topology. + +config USBH_HUB_PORT_RESET_TIMES + int "Hub port reset retry count" + default 2 + range 1 10 + help + Maximum number of port reset retries before giving up. + +config USBH_HUB_INIT_PRIORITY + int "Hub manager initialization priority" + default 85 + help + Initialization priority for the Hub manager. Should be + higher than the USB host stack but lower than device drivers + +module = USBH_HUB +module-str = usbh hub +default-count = 1 +source "subsys/logging/Kconfig.template.log_config" + +endif # USBH_HUB_CLASS diff --git a/subsys/usb/host/class/Kconfig.uvc b/subsys/usb/host/class/Kconfig.uvc new file mode 100644 index 0000000000000..39e652f7a0484 --- /dev/null +++ b/subsys/usb/host/class/Kconfig.uvc @@ -0,0 +1,18 @@ +# SPDX-FileCopyrightText: Copyright Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +config USBH_VIDEO_CLASS + bool "USB Video Class implementation [EXPERIMENTAL]" + depends on DT_HAS_ZEPHYR_UVC_HOST_ENABLED + select EXPERIMENTAL + help + USB Host Video Class (UVC) implementation. + +if USBH_VIDEO_CLASS + +module = USBH_VIDEO +module-str = usbh uvc +default-count = 1 +source "subsys/logging/Kconfig.template.log_config" + +endif # USBH_VIDEO_CLASS diff --git a/subsys/usb/host/class/usbh_hub.c b/subsys/usb/host/class/usbh_hub.c new file mode 100644 index 0000000000000..9c467968ecbc4 --- /dev/null +++ b/subsys/usb/host/class/usbh_hub.c @@ -0,0 +1,282 @@ +/* + * Copyright 2025 NXP + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include "usbh_device.h" +#include "usbh_ch9.h" +#include "usbh_hub.h" + +LOG_MODULE_REGISTER(usbh_hub, CONFIG_USBH_LOG_LEVEL); + +/** + * @brief Common hub control request function + */ +static int usbh_hub_class_request_common(struct usbh_hub_instance *hub_instance, + uint8_t bmRequestType, uint8_t bRequest, + uint16_t wValue, uint16_t wIndex, + struct net_buf *buf) +{ + int ret; + + if (!hub_instance || !hub_instance->hub_udev || !buf) { + return -EINVAL; + } + + k_mutex_lock(&hub_instance->ctrl_lock, K_FOREVER); + + /* Execute synchronous control transfer */ + ret = usbh_req_setup(hub_instance->hub_udev, + bmRequestType, + bRequest, + wValue, + wIndex, + buf->size, + buf); + + if (ret < 0) { + LOG_ERR("Hub control request failed: %d", ret); + } else { + LOG_DBG("Hub control request completed: type=0x%02x, req=0x%02x, len=%d", + bmRequestType, bRequest, buf->len); + } + + k_mutex_unlock(&hub_instance->ctrl_lock); + return ret; +} + +int usbh_hub_get_descriptor(struct usbh_hub_instance *hub_instance, + uint8_t *buffer, uint16_t wLength) +{ + struct net_buf *buf; + int ret; + + if (!hub_instance || !buffer || wLength == 0) { + return -EINVAL; + } + + buf = usbh_xfer_buf_alloc(hub_instance->hub_udev, wLength); + if (NULL == buf) { + LOG_ERR("Failed to allocate buffer for hub descriptor"); + return -ENOMEM; + } + + ret = usbh_hub_class_request_common(hub_instance, + (USB_REQTYPE_DIR_TO_HOST << 7) | + (USB_REQTYPE_TYPE_CLASS << 5) | + (USB_REQTYPE_RECIPIENT_DEVICE << 0), + USB_HUB_REQ_GET_DESCRIPTOR, + (USB_HUB_DESCRIPTOR_TYPE << 8), + 0, + buf); + + if (ret == 0 && buf->len > 0) { + memcpy(buffer, buf->data, MIN(wLength, buf->len)); + } + + usbh_xfer_buf_free(hub_instance->hub_udev, buf); + return ret; +} + +int usbh_hub_set_port_feature(struct usbh_hub_instance *hub_instance, + uint8_t port_number, uint8_t feature) +{ + struct net_buf *buf; + int ret; + + if (!hub_instance) { + return -EINVAL; + } + + buf = usbh_xfer_buf_alloc(hub_instance->hub_udev, 0); + if (NULL == buf) { + LOG_ERR("Failed to allocate buffer for set port feature"); + return -ENOMEM; + } + + ret = usbh_hub_class_request_common(hub_instance, + (USB_REQTYPE_DIR_TO_DEVICE << 7) | + (USB_REQTYPE_TYPE_CLASS << 5) | + (USB_REQTYPE_RECIPIENT_OTHER << 0), + USB_HUB_REQ_SET_FEATURE, + feature, + port_number, + buf); + + usbh_xfer_buf_free(hub_instance->hub_udev, buf); + return ret; +} + +int usbh_hub_clear_port_feature(struct usbh_hub_instance *hub_instance, + uint8_t port_number, uint8_t feature) +{ + struct net_buf *buf; + int ret; + + if (!hub_instance) { + return -EINVAL; + } + + buf = usbh_xfer_buf_alloc(hub_instance->hub_udev, 0); + if (NULL == buf) { + LOG_ERR("Failed to allocate buffer for clear port feature"); + return -ENOMEM; + } + + ret = usbh_hub_class_request_common(hub_instance, + (USB_REQTYPE_DIR_TO_DEVICE << 7) | + (USB_REQTYPE_TYPE_CLASS << 5) | + (USB_REQTYPE_RECIPIENT_OTHER << 0), + USB_HUB_REQ_CLEAR_FEATURE, + feature, + port_number, + buf); + + usbh_xfer_buf_free(hub_instance->hub_udev, buf); + return ret; +} + +int usbh_hub_get_port_status(struct usbh_hub_instance *hub_instance, + uint8_t port_number, + uint16_t *const wPortStatus, + uint16_t *const wPortChange) +{ + struct net_buf *buf; + int ret; + + if (!hub_instance || !wPortStatus || !wPortChange) { + return -EINVAL; + } + + buf = usbh_xfer_buf_alloc(hub_instance->hub_udev, 4); + if (NULL == buf) { + LOG_ERR("Failed to allocate buffer for port status"); + return -ENOMEM; + } + + ret = usbh_hub_class_request_common(hub_instance, + (USB_REQTYPE_DIR_TO_HOST << 7) | + (USB_REQTYPE_TYPE_CLASS << 5) | + (USB_REQTYPE_RECIPIENT_OTHER << 0), + USB_HUB_REQ_GET_STATUS, + 0, + port_number, + buf); + + if (ret == 0 && buf->len >= 4) { + *wPortStatus = net_buf_pull_le16(buf); + *wPortChange = net_buf_pull_le16(buf); + + LOG_DBG("Port %d status: wPortStatus=0x%04x, wPortChange=0x%04x", + port_number, *wPortStatus, *wPortChange); + } else { + LOG_ERR("Failed to get port status or insufficient data (ret=%d, len=%d)", + ret, buf ? buf->len : 0); + *wPortStatus = 0; + *wPortChange = 0; + } + + usbh_xfer_buf_free(hub_instance->hub_udev, buf); + return ret; +} + +int usbh_hub_get_hub_status(struct usbh_hub_instance *hub_instance, + uint16_t *const wHubStatus, + uint16_t *const wHubChange) +{ + struct net_buf *buf; + int ret; + + if (!hub_instance || !wHubStatus || !wHubChange) { + return -EINVAL; + } + + /* Hub status response is always 4 bytes (2 bytes status + 2 bytes change) */ + buf = usbh_xfer_buf_alloc(hub_instance->hub_udev, 4); + if (NULL == buf) { + LOG_ERR("Failed to allocate buffer for hub status"); + return -ENOMEM; + } + + ret = usbh_hub_class_request_common(hub_instance, + (USB_REQTYPE_DIR_TO_HOST << 7) | + (USB_REQTYPE_TYPE_CLASS << 5) | + (USB_REQTYPE_RECIPIENT_DEVICE << 0), + USB_HUB_REQ_GET_STATUS, + 0, + 0, + buf); + + if (ret == 0 && buf->len >= 4) { + *wHubStatus = net_buf_remove_le16(buf); + *wHubChange = net_buf_remove_le16(buf); + + LOG_DBG("Hub status: wHubStatus=0x%04x, wHubChange=0x%04x", + *wHubStatus, *wHubChange); + } else { + LOG_ERR("Failed to get hub status or insufficient data (ret=%d, len=%d)", + ret, buf ? buf->len : 0); + *wHubStatus = 0; + *wHubChange = 0; + } + + usbh_xfer_buf_free(hub_instance->hub_udev, buf); + return ret; +} + +int usbh_hub_clear_hub_feature(struct usbh_hub_instance *hub_instance, uint8_t feature) +{ + struct net_buf *buf; + int ret; + + if (!hub_instance) { + return -EINVAL; + } + + buf = usbh_xfer_buf_alloc(hub_instance->hub_udev, 0); + if (NULL == buf) { + LOG_ERR("Failed to allocate buffer for clear hub feature"); + return -ENOMEM; + } + + ret = usbh_hub_class_request_common(hub_instance, + (USB_REQTYPE_DIR_TO_DEVICE << 7) | + (USB_REQTYPE_TYPE_CLASS << 5) | + (USB_REQTYPE_RECIPIENT_DEVICE << 0), + USB_HUB_REQ_CLEAR_FEATURE, + feature, + 0, + buf); + + usbh_xfer_buf_free(hub_instance->hub_udev, buf); + return ret; +} + +int usbh_hub_init_instance(struct usbh_hub_instance *hub_instance, + struct usb_device *udev) +{ + if (!hub_instance || !udev) { + return -EINVAL; + } + + memset(hub_instance, 0, sizeof(*hub_instance)); + hub_instance->hub_udev = udev; + + k_sem_init(&hub_instance->ctrl_sem, 0, 1); + k_mutex_init(&hub_instance->ctrl_lock); + + return 0; +} + +void usbh_hub_cleanup_instance(struct usbh_hub_instance *hub_instance) +{ + if (hub_instance) { + /* Clean up any pending operations */ + memset(hub_instance, 0, sizeof(*hub_instance)); + } +} diff --git a/subsys/usb/host/class/usbh_hub.h b/subsys/usb/host/class/usbh_hub.h new file mode 100644 index 0000000000000..cb60a1e2254ef --- /dev/null +++ b/subsys/usb/host/class/usbh_hub.h @@ -0,0 +1,162 @@ +/* + * Copyright 2025 NXP + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef _USBH_HUB_H_ +#define _USBH_HUB_H_ + +#include + +/* USB HUB Class-specific requests */ +#define USB_HUB_REQ_GET_STATUS 0x00 +#define USB_HUB_REQ_CLEAR_FEATURE 0x01 +#define USB_HUB_REQ_SET_FEATURE 0x03 +#define USB_HUB_REQ_GET_DESCRIPTOR 0x06 + +/* USB HUB Descriptor Type */ +#define USB_HUB_DESCRIPTOR_TYPE 0x29 + +/* USB HUB Port Features */ +#define USB_HUB_FEATURE_PORT_CONNECTION 0x00 +#define USB_HUB_FEATURE_PORT_ENABLE 0x01 +#define USB_HUB_FEATURE_PORT_SUSPEND 0x02 +#define USB_HUB_FEATURE_PORT_OVER_CURRENT 0x03 +#define USB_HUB_FEATURE_PORT_RESET 0x04 +#define USB_HUB_FEATURE_PORT_POWER 0x08 +#define USB_HUB_FEATURE_PORT_LOW_SPEED 0x09 +#define USB_HUB_FEATURE_PORT_HIGH_SPEED 0x0A + +/* USB Hub Status Change */ +#define USB_HUB_FEATURE_C_HUB_LOCAL_POWER 0 +#define USB_HUB_FEATURE_C_HUB_OVER_CURRENT 1 + +/* USB HUB Port Change Features */ +#define USB_HUB_FEATURE_C_PORT_CONNECTION 0x10 +#define USB_HUB_FEATURE_C_PORT_ENABLE 0x11 +#define USB_HUB_FEATURE_C_PORT_SUSPEND 0x12 +#define USB_HUB_FEATURE_C_PORT_OVER_CURRENT 0x13 +#define USB_HUB_FEATURE_C_PORT_RESET 0x14 + +/* USB HUB Class codes */ +#define USB_HUB_CLASS_CODE 0x09 +#define USB_HUB_SUBCLASS_CODE 0x00 +#define USB_HUB_PROTOCOL_CODE 0x00 + +/* Maximum ports per hub */ +#define USB_HUB_MAX_PORTS 7 + +/* Maximum hub descriptor size. + * 7 bytes (fixed) + max 32 bytes (DeviceRemovable) + max 32 bytes (PortPwrCtrlMask) + */ +#define USBH_HUB_DESC_BUF_SIZE 71 + +/* USB HUB descriptor structure */ +struct usb_hub_descriptor { + uint8_t bDescLength; + uint8_t bDescriptorType; + uint8_t bNbrPorts; + uint16_t wHubCharacteristics; + uint8_t bPwrOn2PwrGood; + uint8_t bHubContrCurrent; + /** uint8_t DeviceRemovable[]; */ + /** uint8_t PortPwrCtrlMask[]; */ +} __packed; + +/* USB HUB status structure */ +struct usb_hub_status { + uint16_t wHubStatus; + uint16_t wHubChange; +} __packed; + +/* USB HUB port status structure */ +struct usb_hub_port_status { + uint16_t wPortStatus; + uint16_t wPortChange; +} __packed; + +/* Hub state enumeration */ +enum usbh_hub_state { + HUB_STATE_INIT, + HUB_STATE_GET_DESCRIPTOR, + HUB_STATE_POWER_PORTS, + HUB_STATE_OPERATIONAL, + HUB_STATE_ERROR, +}; + +/* Port state enumeration */ +enum usbh_port_state { + PORT_STATE_DISCONNECTED, + PORT_STATE_CONNECTED, + PORT_STATE_RESETTING, + PORT_STATE_ENABLED, + PORT_STATE_SUSPENDED, + PORT_STATE_ERROR, +}; + +/** + * @brief Hub statistics information + */ +struct usbh_hub_stats { + uint16_t total_hubs; + uint16_t operational_hubs; + uint16_t total_ports; + uint16_t active_ports; + uint8_t max_level; + uint8_t hubs_by_level[8]; +}; + +/* Hub instance structure */ +struct usbh_hub_instance { + struct usb_device *hub_udev; + struct usbh_context *uhs_ctx; + + /* Hub properties */ + uint8_t num_ports; + uint8_t hub_level; + uint16_t think_time; + + /* Control transfer handling */ + struct k_sem ctrl_sem; + struct k_mutex ctrl_lock; + int ctrl_status; + + /* Hub descriptor buffer */ + uint8_t hub_desc_buf[USBH_HUB_DESC_BUF_SIZE]; + + /* Status buffers */ + struct usb_hub_status hub_status; + struct usb_hub_port_status port_status; +}; + +/* Hub transfer callback function type */ +typedef void (*usbh_hub_callback_t)(void *param, uint8_t *data, + uint32_t data_len, int status); + +int usbh_hub_init_instance(struct usbh_hub_instance *hub_instance, + struct usb_device *udev); + +int usbh_hub_get_descriptor(struct usbh_hub_instance *hub_instance, + uint8_t *buffer, uint16_t buffer_length); + +int usbh_hub_set_port_feature(struct usbh_hub_instance *hub_instance, + uint8_t port_number, uint8_t feature); + +int usbh_hub_clear_port_feature(struct usbh_hub_instance *hub_instance, + uint8_t port_number, uint8_t feature); + +int usbh_hub_get_port_status(struct usbh_hub_instance *hub_instance, + uint8_t port_number, + uint16_t *const wPortStatus, + uint16_t *const wPortChange); + +int usbh_hub_get_hub_status(struct usbh_hub_instance *hub_instance, + uint16_t *const wHubStatus, + uint16_t *const wHubChange); + +int usbh_hub_clear_hub_feature(struct usbh_hub_instance *hub_instance, uint8_t feature); + +void usbh_hub_cleanup_instance(struct usbh_hub_instance *hub_instance); + +#endif /* _USBH_HUB_H_ */ diff --git a/subsys/usb/host/class/usbh_hub_mgr.c b/subsys/usb/host/class/usbh_hub_mgr.c new file mode 100644 index 0000000000000..aa3621a101c88 --- /dev/null +++ b/subsys/usb/host/class/usbh_hub_mgr.c @@ -0,0 +1,1200 @@ +/* + * Copyright 2025 NXP + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include +#include "usbh_class.h" +#include "usbh_device.h" +#include "usbh_desc.h" +#include "usbh_ch9.h" +#include "usbh_hub.h" +#include "usbh_hub_mgr.h" + +LOG_MODULE_REGISTER(usbh_hub_mgr, CONFIG_USBH_HUB_LOG_LEVEL); + +static struct { + uint8_t total_hubs; + sys_slist_t hub_list; + struct k_mutex lock; + struct usbh_context *uhs_ctx; + struct usbh_hub_mgr_data *processing_hub; /* Current processing hub */ +} hub_mgr; + +static void usbh_hub_mgr_process(struct k_work *work); +static void usbh_hub_port_process(struct k_work *work); +static int usbh_hub_mgr_start_interrupt(struct usbh_hub_mgr_data *hub_mgr_data); +static int usbh_hub_mgr_interrupt_in_cb(struct usb_device *const dev, + struct uhc_transfer *const xfer); + +/** + * @brief Resubmit interrupt IN transfer + */ +static int usbh_hub_mgr_resubmit_interrupt_in(struct usbh_hub_mgr_data *hub_mgr_data, + struct uhc_transfer *xfer) +{ + struct net_buf *buf; + int ret; + + if (!hub_mgr_data->connected || !hub_mgr_data->int_ep || hub_mgr_data->being_removed) { + usbh_xfer_free(hub_mgr_data->hub_udev, xfer); + return -ENODEV; + } + + /* Allocate buffer for next transfer */ + buf = usbh_xfer_buf_alloc(hub_mgr_data->hub_udev, + sys_le16_to_cpu(hub_mgr_data->int_ep->wMaxPacketSize)); + if (!buf) { + LOG_ERR("Failed to allocate interrupt IN buffer"); + usbh_xfer_free(hub_mgr_data->hub_udev, xfer); + return -ENOMEM; + } + + /* Reuse transfer with new buffer */ + xfer->buf = buf; + + ret = usbh_xfer_enqueue(hub_mgr_data->hub_udev, xfer); + if (ret != 0) { + LOG_ERR("Failed to resubmit interrupt IN transfer: %d", ret); + usbh_xfer_buf_free(hub_mgr_data->hub_udev, buf); + usbh_xfer_free(hub_mgr_data->hub_udev, xfer); + return ret; + } + + hub_mgr_data->int_active = true; + return 0; +} + +/** + * @brief Start hub interrupt monitoring + */ +static int usbh_hub_mgr_start_interrupt(struct usbh_hub_mgr_data *hub_mgr_data) +{ + struct uhc_transfer *xfer; + struct net_buf *buf; + int ret; + + if (!hub_mgr_data || hub_mgr_data->being_removed || hub_mgr_data->int_active) { + return -EINVAL; + } + + /* Start interrupt only when operational */ + if (hub_mgr_data->state != HUB_STATE_OPERATIONAL) { + return -ENOENT; + } + + /* Check interrupt endpoint */ + if (!hub_mgr_data->int_ep) { + LOG_ERR("No interrupt endpoint available"); + return -ENODEV; + } + + /* Allocate interrupt transfer */ + xfer = usbh_xfer_alloc(hub_mgr_data->hub_udev, hub_mgr_data->int_ep->bEndpointAddress, + usbh_hub_mgr_interrupt_in_cb, (void *)hub_mgr_data); + if (!xfer) { + LOG_ERR("Failed to allocate interrupt transfer"); + return -ENOMEM; + } + + hub_mgr_data->interrupt_transfer = xfer; + + /* Allocate receive buffer */ + buf = usbh_xfer_buf_alloc(hub_mgr_data->hub_udev, + sys_le16_to_cpu(hub_mgr_data->int_ep->wMaxPacketSize)); + if (!buf) { + LOG_ERR("Failed to allocate interrupt buffer"); + usbh_xfer_free(hub_mgr_data->hub_udev, xfer); + return -ENOMEM; + } + + xfer->buf = buf; + + ret = usbh_xfer_enqueue(hub_mgr_data->hub_udev, xfer); + if (ret != 0) { + LOG_ERR("Failed to enqueue interrupt transfer: %d", ret); + usbh_xfer_buf_free(hub_mgr_data->hub_udev, buf); + usbh_xfer_free(hub_mgr_data->hub_udev, xfer); + return ret; + } + + hub_mgr_data->interrupt_transfer = xfer; + hub_mgr_data->int_active = true; + hub_mgr_data->prime_status = HUB_PRIME_INTERRUPT; + + LOG_DBG("Hub level %d interrupt monitoring started", hub_mgr_data->hub_level); + return 0; +} + +/** + * @brief Find hub manager data by USB device + */ +static struct usbh_hub_mgr_data *find_hub_mgr_by_udev(struct usb_device *udev) +{ + struct usbh_hub_mgr_data *hub_mgr_data; + + k_mutex_lock(&hub_mgr.lock, K_FOREVER); + + SYS_SLIST_FOR_EACH_CONTAINER(&hub_mgr.hub_list, hub_mgr_data, node) { + if (hub_mgr_data->hub_udev == udev) { + k_mutex_unlock(&hub_mgr.lock); + return hub_mgr_data; + } + } + + k_mutex_unlock(&hub_mgr.lock); + + return NULL; +} + +/** + * @brief Process hub interrupt data + */ +static void usbh_hub_mgr_process_data(struct usbh_hub_mgr_data *hub_mgr_data) +{ + uint8_t port_index; + int ret; + + k_mutex_lock(&hub_mgr_data->lock, K_FOREVER); + + if (hub_mgr_data->being_removed) { + k_mutex_unlock(&hub_mgr_data->lock); + return; + } + + if (hub_mgr_data->state != HUB_STATE_OPERATIONAL) { + LOG_DBG("Hub level %d not operational yet, deferring interrupt processing", + hub_mgr_data->hub_level); + k_mutex_unlock(&hub_mgr_data->lock); + /* Resubmit interrupt to process later */ + if (!hub_mgr_data->int_active && !hub_mgr_data->being_removed) { + usbh_hub_mgr_start_interrupt(hub_mgr_data); + } + return; + } + + for (port_index = 0; port_index <= hub_mgr_data->num_ports; ++port_index) { + + if (0 == ((0x01U << (port_index & 0x07U)) & + (hub_mgr_data->int_buffer[port_index >> 3U]))) { + continue; + } + + if (port_index == 0) { + /* Hub status change */ + LOG_INF("Hub level %d status changed, processing", hub_mgr_data->hub_level); + ret = usbh_hub_get_hub_status(&hub_mgr_data->hub_instance, + &hub_mgr_data->hub_instance.hub_status.wHubStatus, + &hub_mgr_data->hub_instance.hub_status.wHubChange); + if (ret != 0) { + LOG_ERR("Failed to get hub status: %d", ret); + continue; + } + + LOG_INF("Hub status: 0x%04x, change: 0x%04x", + hub_mgr_data->hub_instance.hub_status.wHubStatus, + hub_mgr_data->hub_instance.hub_status.wHubChange); + + if (hub_mgr_data->hub_instance.hub_status.wHubChange & + (1UL << USB_HUB_FEATURE_C_HUB_LOCAL_POWER)) { + LOG_WRN("Hub local power status changed"); + ret = usbh_hub_clear_hub_feature(&hub_mgr_data->hub_instance, + USB_HUB_FEATURE_C_HUB_LOCAL_POWER); + if (ret != 0) { + LOG_ERR("Failed to clear hub local power feature: %d", ret); + } + } + + if (hub_mgr_data->hub_instance.hub_status.wHubChange & + (1UL << USB_HUB_FEATURE_C_HUB_OVER_CURRENT)) { + LOG_ERR("Hub over-current detected!"); + ret = usbh_hub_clear_hub_feature(&hub_mgr_data->hub_instance, + USB_HUB_FEATURE_C_HUB_OVER_CURRENT); + if (ret != 0) { + LOG_ERR("Failed to clear hub over-current feature: %d", ret); + } + } + } else { + if (hub_mgr.processing_hub != NULL && hub_mgr.processing_hub != hub_mgr_data) { + continue; + } + + hub_mgr.processing_hub = hub_mgr_data; + hub_mgr_data->current_port = port_index; + + LOG_INF("Hub level %d port %d status changed, starting processing", + hub_mgr_data->hub_level, port_index); + + k_mutex_unlock(&hub_mgr_data->lock); + + k_work_submit(&hub_mgr_data->port_work.work); + return; + } + } + + k_mutex_unlock(&hub_mgr_data->lock); + + if (!hub_mgr_data->int_active && !hub_mgr_data->being_removed) { + usbh_hub_mgr_start_interrupt(hub_mgr_data); + } +} + +/** + * @brief Hub interrupt IN callback + */ +static int usbh_hub_mgr_interrupt_in_cb(struct usb_device *const dev, + struct uhc_transfer *const xfer) +{ + struct usbh_hub_mgr_data *hub_mgr_data = (struct usbh_hub_mgr_data *)xfer->priv; + struct net_buf *buf = xfer->buf; + + if (!hub_mgr_data || hub_mgr_data->being_removed) { + goto cleanup_and_exit; + } + + hub_mgr_data->int_active = false; + + hub_mgr_data->prime_status = HUB_PRIME_NONE; + + if (!buf || buf->len == 0) { + LOG_ERR("Hub level %d interrupt transfer failed or no data", + hub_mgr_data->hub_level); + hub_mgr.processing_hub = NULL; + hub_mgr_data->current_port = 0; + goto resubmit; + } + + memcpy(hub_mgr_data->int_buffer, buf->data, + MIN(buf->len, sizeof(hub_mgr_data->int_buffer))); + + LOG_DBG("Hub level %d interrupt data received: length=%d", + hub_mgr_data->hub_level, buf->len); + + usbh_hub_mgr_process_data(hub_mgr_data); + + net_buf_unref(buf); + return 0; + +resubmit: + /* Resubmit transfer */ + if (hub_mgr_data->connected && hub_mgr_data->state == HUB_STATE_OPERATIONAL) { + int ret = usbh_hub_mgr_resubmit_interrupt_in(hub_mgr_data, xfer); + if (ret) { + LOG_ERR("Failed to resubmit interrupt transfer: %d", ret); + } + } else { + usbh_xfer_free(hub_mgr_data->hub_udev, xfer); + } + return 0; + +cleanup_and_exit: + /* Cleanup resources */ + if (buf) { + net_buf_unref(buf); + } + usbh_xfer_free(dev, xfer); + return 0; +} + +/** + * @brief Hub process state machine + */ +static void usbh_hub_mgr_process(struct k_work *work) +{ + struct k_work_delayable *dwork = k_work_delayable_from_work(work); + struct usbh_hub_mgr_data *hub_mgr_data = + CONTAINER_OF(dwork, struct usbh_hub_mgr_data, hub_work); + struct usb_hub_descriptor *hub_desc; + uint8_t need_prime_interrupt = 0; + uint8_t process_success = 0; + uint16_t port_num; + int ret; + + k_mutex_lock(&hub_mgr_data->lock, K_FOREVER); + + if (hub_mgr_data->being_removed) { + k_mutex_unlock(&hub_mgr_data->lock); + return; + } + + switch (hub_mgr_data->hub_status) { + case HUB_RUN_IDLE: + /* Hub idle, process hub status change */ + if (hub_mgr_data->prime_status == HUB_PRIME_CONTROL) { + LOG_DBG("Processing Hub status change"); + hub_mgr_data->hub_status = HUB_RUN_CLEAR_DONE; + process_success = 1; + need_prime_interrupt = 1; + } + break; + + case HUB_RUN_INVALID: + LOG_ERR("Hub in invalid state"); + hub_mgr_data->state = HUB_STATE_ERROR; + break; + + case HUB_RUN_WAIT_SET_INTERFACE: + hub_mgr_data->hub_status = HUB_RUN_GET_DESCRIPTOR_7; + /* Get basic hub descriptor */ + ret = usbh_hub_get_descriptor(&hub_mgr_data->hub_instance, + hub_mgr_data->hub_instance.hub_desc_buf, sizeof(struct usb_hub_descriptor)); + if (ret == 0) { + process_success = 1; + LOG_DBG("Getting 7-byte hub descriptor"); + k_work_submit(&hub_mgr_data->hub_work.work); + } else { + LOG_ERR("Failed to get hub descriptor: %d", ret); + hub_mgr_data->hub_status = HUB_RUN_INVALID; + } + break; + + case HUB_RUN_GET_DESCRIPTOR_7: + hub_desc = (struct usb_hub_descriptor *)hub_mgr_data->hub_instance.hub_desc_buf; + + /* Save hub properties */ + hub_mgr_data->hub_instance.num_ports = hub_desc->bNbrPorts; + hub_mgr_data->num_ports = hub_desc->bNbrPorts; + + if (hub_mgr_data->num_ports > USB_HUB_MAX_PORTS) { + LOG_ERR("Too many ports: %d", hub_mgr_data->num_ports); + hub_mgr_data->hub_status = HUB_RUN_INVALID; + break; + } + + LOG_INF("Hub has %d ports", hub_mgr_data->num_ports); + + hub_mgr_data->hub_status = HUB_RUN_SET_PORT_POWER; + /* Get full hub descriptor */ + port_num = (hub_mgr_data->num_ports + 7) / 8; + ret = usbh_hub_get_descriptor(&hub_mgr_data->hub_instance, + hub_mgr_data->hub_instance.hub_desc_buf, + 7 + port_num); + if (ret == 0) { + process_success = 1; + LOG_DBG("Getting full hub descriptor"); + k_work_submit(&hub_mgr_data->hub_work.work); + } else { + LOG_ERR("Failed to get full hub descriptor: %d", ret); + hub_mgr_data->hub_status = HUB_RUN_INVALID; + } + break; + + case HUB_RUN_SET_PORT_POWER: + /* Allocate port list if not already done */ + if (hub_mgr_data->port_list == NULL) { + hub_mgr_data->port_list = k_malloc(hub_mgr_data->num_ports * + sizeof(struct usbh_hub_port_instance)); + if (hub_mgr_data->port_list == NULL) { + LOG_ERR("Failed to allocate port list"); + hub_mgr_data->hub_status = HUB_RUN_INVALID; + break; + } + hub_mgr_data->port_index = 0; + } + + /* Power on all ports */ + if (hub_mgr_data->port_index < hub_mgr_data->num_ports) { + hub_mgr_data->port_index++; + ret = usbh_hub_set_port_feature(&hub_mgr_data->hub_instance, + hub_mgr_data->port_index, + USB_HUB_FEATURE_PORT_POWER); + if (ret == 0) { + process_success = 1; + LOG_DBG("Setting port %d power", hub_mgr_data->port_index); + k_work_submit(&hub_mgr_data->hub_work.work); + } else { + LOG_ERR("Failed to set port %d power: %d", + hub_mgr_data->port_index, ret); + need_prime_interrupt = 1; + } + break; + } + + /* All ports powered, initialize port states */ + for (int i = 0; i < hub_mgr_data->num_ports; i++) { + /* Initialize port instance */ + hub_mgr_data->port_list[i].udev = NULL; + hub_mgr_data->port_list[i].reset_count = CONFIG_USBH_HUB_PORT_RESET_TIMES; + hub_mgr_data->port_list[i].port_status = HUB_PORT_RUN_WAIT_PORT_CHANGE; + hub_mgr_data->port_list[i].state = PORT_STATE_DISCONNECTED; + hub_mgr_data->port_list[i].port_num = i + 1; + hub_mgr_data->port_list[i].speed = USB_SPEED_SPEED_FS; /* Default full speed */ + } + + hub_mgr_data->hub_status = HUB_RUN_IDLE; + hub_mgr_data->state = HUB_STATE_OPERATIONAL; + need_prime_interrupt = 1; + LOG_INF("Hub initialization completed, starting interrupt monitoring"); + break; + + default: + LOG_ERR("Unknown hub status: %d", hub_mgr_data->hub_status); + hub_mgr_data->hub_status = HUB_RUN_INVALID; + hub_mgr_data->state = HUB_STATE_ERROR; + break; + } + + k_mutex_unlock(&hub_mgr_data->lock); + + if (need_prime_interrupt) { + hub_mgr_data->hub_status = HUB_RUN_IDLE; + if (!hub_mgr_data->int_active && !hub_mgr_data->being_removed) { + ret = usbh_hub_mgr_start_interrupt(hub_mgr_data); + if (ret) { + LOG_ERR("Failed to start interrupt monitoring: %d", ret); + } + } + } else if (!process_success && hub_mgr_data->hub_status != HUB_RUN_INVALID) { + hub_mgr_data->hub_status = HUB_RUN_INVALID; + hub_mgr_data->state = HUB_STATE_ERROR; + } +} + +/** + * @brief Recursively disconnect hub and all children + */ +static void usbh_hub_mgr_recursive_disconnect(struct usbh_hub_mgr_data *hub_mgr_data) +{ + if (!hub_mgr_data) { + return; + } + + LOG_DBG("Recursively disconnecting Hub level %d and all children", + hub_mgr_data->hub_level); + + k_work_cancel_delayable(&hub_mgr_data->port_work); + k_work_cancel_delayable(&hub_mgr_data->hub_work); + hub_mgr_data->int_active = false; + + if (hub_mgr.processing_hub == hub_mgr_data) { + hub_mgr.processing_hub = NULL; + hub_mgr_data->current_port = 0; + } + + for (int i = 0; i < hub_mgr_data->num_ports; i++) { + if (hub_mgr_data->port_list && hub_mgr_data->port_list[i].udev) { + struct usb_device *port_udev = hub_mgr_data->port_list[i].udev; + struct usbh_hub_mgr_data *child_hub = find_hub_mgr_by_udev(port_udev); + + if (child_hub) { + LOG_DBG("Found child Hub on port %d, recursing", i + 1); + usbh_hub_mgr_recursive_disconnect(child_hub); + + } else { + LOG_DBG("Disconnecting device on port %d", i + 1); + usbh_disconnect_device(hub_mgr_data->uhs_ctx, port_udev); + } + + /* Clear port state */ + hub_mgr_data->port_list[i].udev = NULL; + hub_mgr_data->port_list[i].state = PORT_STATE_DISCONNECTED; + } + } + + if (!hub_mgr_data->being_removed) { + LOG_DBG("Triggering Hub level %d removal", hub_mgr_data->hub_level); + usbh_disconnect_device(hub_mgr_data->uhs_ctx, hub_mgr_data->hub_udev); + } +} + +/** + * @brief Print hub information + */ +static void usbh_hub_print_info(struct usbh_hub_mgr_data *hub_mgr_data) +{ + struct usb_device_descriptor *dev_desc; + + if (!hub_mgr_data || !hub_mgr_data->hub_udev) { + return; + } + + dev_desc = &hub_mgr_data->hub_udev->dev_desc; + + LOG_INF("=== USB Hub Information ==="); + LOG_INF("Hub Level: %d", hub_mgr_data->hub_level); + LOG_INF("Vendor ID: 0x%04x", sys_le16_to_cpu(dev_desc->idVendor)); + LOG_INF("Product ID: 0x%04x", sys_le16_to_cpu(dev_desc->idProduct)); + LOG_INF("Device Address: %d", hub_mgr_data->hub_udev->addr); + if (hub_mgr_data->parent_hub) { + LOG_INF("Parent Hub Level: %d, Port: %d", + hub_mgr_data->parent_hub->hub_level, + hub_mgr_data->parent_port); + } else { + LOG_INF("Root Hub (no parent)"); + } + LOG_INF("==========================="); +} + +/** + * @brief Establish parent-child hub relationship + */ +static int usbh_hub_establish_parent_child_relationship(struct usbh_hub_mgr_data *parent_hub, + uint8_t port_num, + struct usb_device *child_udev) +{ + struct usbh_hub_mgr_data *child_hub; + uint8_t new_level; + + if (!parent_hub || !child_udev) { + return -EINVAL; + } + + child_hub = find_hub_mgr_by_udev(child_udev); + if (!child_hub || child_hub->parent_hub) { + return 0; + } + + new_level = parent_hub->hub_level + 1; + + /* Check maximum hub chain depth */ + if (new_level > CONFIG_USBH_HUB_MAX_LEVELS) { + LOG_ERR("Hub chain depth limit exceeded (%d > %d), removing hub", + new_level, CONFIG_USBH_HUB_MAX_LEVELS); + + k_mutex_lock(&child_hub->lock, K_FOREVER); + child_hub->being_removed = true; + child_hub->state = HUB_STATE_ERROR; + k_mutex_unlock(&child_hub->lock); + + usbh_hub_mgr_recursive_disconnect(child_hub); + + return -ENOSPC; + } + + k_mutex_lock(&child_hub->lock, K_FOREVER); + child_hub->parent_hub = parent_hub; + child_hub->parent_port = port_num; + child_hub->hub_level = new_level; + sys_slist_append(&parent_hub->child_hubs, &child_hub->child_node); + k_mutex_unlock(&child_hub->lock); + + usbh_hub_print_info(child_hub); + + return 0; +} + +/** + * @brief Port processing state machine + */ +static void usbh_hub_port_process(struct k_work *work) +{ + struct k_work_delayable *dwork = k_work_delayable_from_work(work); + struct usbh_hub_mgr_data *hub_mgr_data = + CONTAINER_OF(dwork, struct usbh_hub_mgr_data, port_work); + struct usbh_hub_port_instance *port_instance; + uint8_t port_num = hub_mgr_data->current_port; + uint32_t spec_status; + uint8_t feature = 0; + int ret; + bool need_restart_interrupt = false; + bool process_complete = false; + + if (port_num == 0 || port_num > hub_mgr_data->num_ports || !hub_mgr_data->port_list) { + LOG_ERR("Invalid port state for processing"); + goto exit_processing; + } + + port_instance = &hub_mgr_data->port_list[port_num - 1]; + + k_mutex_lock(&hub_mgr_data->lock, K_FOREVER); + + if (hub_mgr_data->being_removed) { + k_mutex_unlock(&hub_mgr_data->lock); + return; + } + + if (!hub_mgr_data->hub_udev || hub_mgr_data->hub_udev->state != USB_STATE_CONFIGURED) { + LOG_ERR("Hub device not ready"); + k_work_reschedule(&hub_mgr_data->port_work, K_MSEC(5000)); + k_mutex_unlock(&hub_mgr_data->lock); + return; + } + + k_mutex_unlock(&hub_mgr_data->lock); + + LOG_DBG("Processing port %d, status=%d", port_num, port_instance->port_status); + + /* Port state machine main logic */ + switch (port_instance->port_status) { + case HUB_PORT_RUN_WAIT_PORT_CHANGE: + LOG_DBG("Port %d: Getting port status", port_num); + port_instance->port_status = HUB_PORT_RUN_CHECK_C_PORT_CONNECTION; + ret = usbh_hub_get_port_status(&hub_mgr_data->hub_instance, port_num, + &hub_mgr_data->hub_instance.port_status.wPortStatus, + &hub_mgr_data->hub_instance.port_status.wPortChange); + if (ret != 0) { + LOG_ERR("Failed to get port status: %d", ret); + goto error_recovery; + } + k_work_submit(&hub_mgr_data->port_work.work); + return; + + case HUB_PORT_RUN_CHECK_C_PORT_CONNECTION: + spec_status = ((uint32_t)hub_mgr_data->hub_instance.port_status.wPortChange << 16) | + hub_mgr_data->hub_instance.port_status.wPortStatus; + + LOG_DBG("Port %d status: wPortStatus=0x%04x, wPortChange=0x%04x", + port_num, + hub_mgr_data->hub_instance.port_status.wPortStatus, + hub_mgr_data->hub_instance.port_status.wPortChange); + + if (spec_status & (1UL << USB_HUB_FEATURE_C_PORT_CONNECTION)) { + /* Connection status change */ + port_instance->port_status = HUB_PORT_RUN_GET_PORT_CONNECTION; + ret = usbh_hub_clear_port_feature(&hub_mgr_data->hub_instance, port_num, + USB_HUB_FEATURE_C_PORT_CONNECTION); + if (ret != 0) { + LOG_ERR("Failed to clear port connection change: %d", ret); + goto error_recovery; + } + LOG_DBG("Port %d: Cleared connection change bit", port_num); + k_work_submit(&hub_mgr_data->port_work.work); + return; + + } else if (spec_status & (1UL << USB_HUB_FEATURE_PORT_CONNECTION)) { + /* Device connected, need reset */ + LOG_ERR("Device connected to port %d, starting reset", port_num); + port_instance->port_status = HUB_PORT_RUN_WAIT_PORT_RESET_DONE; + ret = usbh_hub_set_port_feature(&hub_mgr_data->hub_instance, port_num, + USB_HUB_FEATURE_PORT_RESET); + if (ret != 0) { + LOG_ERR("Failed to reset port: %d", ret); + goto error_recovery; + } + k_work_submit(&hub_mgr_data->port_work.work); + return; + + } else if (spec_status & (1UL << (USB_HUB_FEATURE_C_PORT_RESET))) { + /* Reset completed */ + feature = USB_HUB_FEATURE_C_PORT_RESET; + port_instance->port_status = HUB_PORT_RUN_CHECK_C_PORT_RESET; + + } else if (spec_status & (1UL << (USB_HUB_FEATURE_C_PORT_ENABLE))) { + /* Enable status change */ + feature = USB_HUB_FEATURE_C_PORT_ENABLE; + port_instance->port_status = HUB_PORT_RUN_WAIT_PORT_CHANGE; + + } else if (spec_status & (1UL << (USB_HUB_FEATURE_C_PORT_OVER_CURRENT))) { + /* Over-current detection */ + feature = USB_HUB_FEATURE_C_PORT_OVER_CURRENT; + port_instance->port_status = HUB_PORT_RUN_WAIT_PORT_CHANGE; + LOG_WRN("Port %d over-current detected", port_num); + + } else if (!(spec_status & (1UL << USB_HUB_FEATURE_PORT_CONNECTION))) { + /* Device disconnected */ + goto process_disconnection; + } + + if (feature != 0) { + ret = usbh_hub_clear_port_feature(&hub_mgr_data->hub_instance, port_num, feature); + if (ret != 0) { + LOG_ERR("Failed to clear feature %d: %d", feature, ret); + goto error_recovery; + } + LOG_DBG("Port %d: Cleared feature %d", port_num, feature); + k_work_submit(&hub_mgr_data->port_work.work); + return; + } + break; + + case HUB_PORT_RUN_GET_PORT_CONNECTION: + port_instance->port_status = HUB_PORT_RUN_CHECK_PORT_CONNECTION; + ret = usbh_hub_get_port_status(&hub_mgr_data->hub_instance, port_num, + &hub_mgr_data->hub_instance.port_status.wPortStatus, + &hub_mgr_data->hub_instance.port_status.wPortChange); + if (ret != 0) { + LOG_ERR("Failed to get port connection status: %d", ret); + goto error_recovery; + } + k_work_submit(&hub_mgr_data->port_work.work); + return; + + case HUB_PORT_RUN_CHECK_PORT_CONNECTION: + spec_status = ((uint32_t)hub_mgr_data->hub_instance.port_status.wPortChange << 16) | + hub_mgr_data->hub_instance.port_status.wPortStatus; + + if (spec_status & (1UL << USB_HUB_FEATURE_PORT_CONNECTION)) { + /* Confirm connection, start reset */ + LOG_INF("Port %d connection confirmed, resetting", port_num); + port_instance->port_status = HUB_PORT_RUN_WAIT_PORT_RESET_DONE; + ret = usbh_hub_set_port_feature(&hub_mgr_data->hub_instance, port_num, + USB_HUB_FEATURE_PORT_RESET); + if (ret != 0) { + LOG_ERR("Failed to reset port: %d", ret); + goto error_recovery; + } + if (port_instance->reset_count > 0) { + port_instance->reset_count--; + } + k_work_submit(&hub_mgr_data->port_work.work); + return; + } else { + /* Device disconnected */ + goto process_disconnection; + } + break; + + case HUB_PORT_RUN_WAIT_PORT_RESET_DONE: + port_instance->port_status = HUB_PORT_RUN_WAIT_C_PORT_RESET; + /* Wait for interrupt notification of reset completion, restart interrupt listening */ + if (!hub_mgr_data->int_active && !hub_mgr_data->being_removed) { + ret = usbh_hub_mgr_start_interrupt(hub_mgr_data); + if (ret) { + LOG_ERR("Failed to restart interrupt for reset wait: %d", ret); + } + } + LOG_DBG("Port %d waiting for reset completion interrupt", port_num); + return; + + case HUB_PORT_RUN_WAIT_C_PORT_RESET: + port_instance->port_status = HUB_PORT_RUN_CHECK_C_PORT_RESET; + ret = usbh_hub_get_port_status(&hub_mgr_data->hub_instance, port_num, + &hub_mgr_data->hub_instance.port_status.wPortStatus, + &hub_mgr_data->hub_instance.port_status.wPortChange); + if (ret != 0) { + LOG_ERR("Failed to get port status for reset check: %d", ret); + goto error_recovery; + } + k_work_submit(&hub_mgr_data->port_work.work); + return; + + case HUB_PORT_RUN_CHECK_C_PORT_RESET: + spec_status = ((uint32_t)hub_mgr_data->hub_instance.port_status.wPortChange << 16) | + hub_mgr_data->hub_instance.port_status.wPortStatus; + + if (spec_status & (1UL << USB_HUB_FEATURE_C_PORT_RESET)) { + if (port_instance->reset_count == 0) { + /* Reset completed, device connected */ + port_instance->port_status = HUB_PORT_RUN_PORT_ATTACHED; + + /* Detect device speed */ + if (spec_status & (1UL << USB_HUB_FEATURE_PORT_HIGH_SPEED)) { + port_instance->speed = USB_SPEED_SPEED_HS; + } else if (spec_status & (1UL << USB_HUB_FEATURE_PORT_LOW_SPEED)) { + port_instance->speed = USB_SPEED_SPEED_LS; + } else { + port_instance->speed = USB_SPEED_SPEED_FS; + } + + LOG_INF("Device ready on port %d (speed: %s)", port_num, + port_instance->speed == USB_SPEED_SPEED_HS ? "HIGH" : + port_instance->speed == USB_SPEED_SPEED_LS ? "LOW" : "FULL"); + } else { + /* Need another reset */ + port_instance->port_status = HUB_PORT_RUN_RESET_AGAIN; + } + + /* Clear C_PORT_RESET */ + ret = usbh_hub_clear_port_feature(&hub_mgr_data->hub_instance, port_num, + USB_HUB_FEATURE_C_PORT_RESET); + if (ret != 0) { + LOG_ERR("Failed to clear port reset feature: %d", ret); + goto error_recovery; + } + k_work_submit(&hub_mgr_data->port_work.work); + return; + } else { + /* Reset not completed, check again */ + LOG_DBG("Port %d reset not completed, checking again", port_num); + port_instance->port_status = HUB_PORT_RUN_WAIT_C_PORT_RESET; + k_work_reschedule(&hub_mgr_data->port_work, K_MSEC(100)); + return; + } + break; + + case HUB_PORT_RUN_RESET_AGAIN: + LOG_INF("Port %d retrying reset (%d attempts left)", + port_num, port_instance->reset_count); + port_instance->port_status = HUB_PORT_RUN_CHECK_PORT_CONNECTION; + ret = usbh_hub_get_port_status(&hub_mgr_data->hub_instance, port_num, + &hub_mgr_data->hub_instance.port_status.wPortStatus, + &hub_mgr_data->hub_instance.port_status.wPortChange); + if (ret != 0) { + LOG_ERR("Failed to get port status for reset again: %d", ret); + goto error_recovery; + } + k_work_submit(&hub_mgr_data->port_work.work); + return; + + case HUB_PORT_RUN_PORT_ATTACHED: + LOG_INF("Device attached to port %d", port_num); + + /* Notify USB host stack to start device enumeration */ + struct usb_device *new_udev = usbh_connect_device(hub_mgr_data->uhs_ctx, port_instance->speed); + if (new_udev != NULL) { + /* Save device to port instance */ + port_instance->udev = new_udev; + + LOG_INF("Device enumeration completed for port %d, addr=%d", + port_num, new_udev->addr); + + /* Delay check for child hub relationship */ + port_instance->port_status = HUB_PORT_RUN_CHECK_CHILD_HUB; + k_work_reschedule(&hub_mgr_data->port_work, K_MSEC(50)); + return; + + } else { + LOG_ERR("Device enumeration failed for port %d", port_num); + + /* Retry enumeration */ + if (port_instance->reset_count > 0) { + port_instance->reset_count--; + port_instance->port_status = HUB_PORT_RUN_WAIT_PORT_CHANGE; + LOG_WRN("Port %d enumeration failed, %d retries left", + port_num, port_instance->reset_count); + k_work_reschedule(&hub_mgr_data->port_work, K_MSEC(1000)); + return; + } else { + LOG_ERR("Port %d enumeration max retries exceeded", port_num); + } + } + + process_complete = true; + hub_mgr_data->current_port = 0; + hub_mgr.processing_hub = NULL; + port_instance->reset_count = CONFIG_USBH_HUB_PORT_RESET_TIMES; + need_restart_interrupt = true; + goto exit_processing; + + case HUB_PORT_RUN_CHECK_CHILD_HUB: + /* Check and establish parent-child hub relationship */ + if (port_instance->udev) { + ret = usbh_hub_establish_parent_child_relationship(hub_mgr_data, port_num, port_instance->udev); + if (ret != 0) { + port_instance->udev = NULL; + port_instance->state = PORT_STATE_DISCONNECTED; + } + } + + /* Complete port processing */ + process_complete = true; + hub_mgr_data->current_port = 0; + hub_mgr.processing_hub = NULL; + port_instance->reset_count = CONFIG_USBH_HUB_PORT_RESET_TIMES; + need_restart_interrupt = true; + + goto exit_processing; + + default: + LOG_ERR("Unknown port status: %d", port_instance->port_status); + goto error_recovery; + } + + /* Process device disconnection */ +process_disconnection: + if (port_instance->udev) { + struct usb_device *disconnected_udev = port_instance->udev; + + LOG_INF("Device disconnected from Hub level %d port %d", + hub_mgr_data->hub_level, port_num); + + /* Clear port state immediately */ + port_instance->udev = NULL; + port_instance->state = PORT_STATE_DISCONNECTED; + + if (disconnected_udev) { + struct usbh_hub_mgr_data *child_hub = find_hub_mgr_by_udev(disconnected_udev); + if (child_hub) { + LOG_INF("Child Hub disconnected, triggering recursive removal"); + + usbh_hub_mgr_recursive_disconnect(child_hub); + } else { + /* Normal device disconnect */ + usbh_disconnect_device(hub_mgr_data->uhs_ctx, disconnected_udev); + } + } + } + process_complete = true; + +exit_processing: + /* Processing completed, cleanup state */ + if (process_complete || port_instance->port_status == HUB_PORT_RUN_INVALID) { + hub_mgr_data->current_port = 0; + hub_mgr.processing_hub = NULL; + port_instance->port_status = HUB_PORT_RUN_WAIT_PORT_CHANGE; + port_instance->reset_count = CONFIG_USBH_HUB_PORT_RESET_TIMES; + need_restart_interrupt = true; + + LOG_DBG("Port %d processing completed", port_num); + } + + if (need_restart_interrupt && !hub_mgr_data->int_active && !hub_mgr_data->being_removed) { + ret = usbh_hub_mgr_start_interrupt(hub_mgr_data); + if (ret) { + LOG_ERR("Failed to restart interrupt monitoring: %d", ret); + } + } + + return; + +error_recovery: + if (port_instance->reset_count > 0) { + port_instance->reset_count--; + port_instance->port_status = HUB_PORT_RUN_WAIT_PORT_CHANGE; + + LOG_WRN("Port %d error recovery, %d retries left", port_num, port_instance->reset_count); + k_work_reschedule(&hub_mgr_data->port_work, K_MSEC(3000)); + } else { + LOG_ERR("Port %d max retries exceeded, disabling", port_num); + port_instance->port_status = HUB_PORT_RUN_INVALID; + goto exit_processing; + } +} + +/** + * @brief USBH class implementation for HUB devices + */ +static int usbh_hub_mgr_probe(struct usbh_class_data *const c_data, + struct usb_device *const udev, + const uint8_t iface) +{ + struct usbh_hub_mgr_data *hub_mgr_data; + const void *desc_start; + const void *desc_end; + const void *cfg_end; + int ret; + + if (CONFIG_USBH_HUB_MAX_COUNT == hub_mgr.total_hubs) + { + LOG_ERR("Maximum number of hubs reached (%d)", CONFIG_USBH_HUB_MAX_COUNT); + return -ENOMEM; + } + + LOG_DBG("USB HUB device probe at interface %u", iface); + + /* Get descriptor range for current interface */ + cfg_end = usbh_desc_get_cfg_end(udev); + desc_start = usbh_desc_get_by_iface(usbh_desc_get_cfg_beg(udev), cfg_end, iface); + if (desc_start == NULL) { + LOG_ERR("Failed to find interface %u descriptor", iface); + return -ENODEV; + } + + /* Get the start of next function as the end of current function */ + desc_end = usbh_desc_get_next_function(desc_start, cfg_end); + if (desc_end == NULL) { + /* If no next function exists, use configuration descriptor end */ + desc_end = cfg_end; + } + + hub_mgr_data = k_malloc(sizeof(*hub_mgr_data)); + if (!hub_mgr_data) { + LOG_ERR("Failed to allocate HUB management data"); + return -ENOMEM; + } + + memset(hub_mgr_data, 0, sizeof(*hub_mgr_data)); + + ret = usbh_hub_init_instance(&hub_mgr_data->hub_instance, udev); + if (ret) { + LOG_ERR("Failed to initialize HUB instance: %d", ret); + k_free(hub_mgr_data); + return ret; + } + + hub_mgr_data->hub_udev = udev; + hub_mgr_data->uhs_ctx = c_data->uhs_ctx; + hub_mgr_data->state = HUB_STATE_INIT; + hub_mgr_data->hub_status = HUB_RUN_WAIT_SET_INTERFACE; + hub_mgr_data->port_index = 0; + hub_mgr_data->port_list = NULL; + hub_mgr_data->being_removed = false; + + hub_mgr_data->prime_status = HUB_PRIME_NONE; + hub_mgr_data->interrupt_transfer = NULL; + hub_mgr_data->int_active = false; + + sys_slist_init(&hub_mgr_data->child_hubs); + + /* Parse interrupt endpoint within the correct descriptor range */ + uint8_t *desc_buf = (uint8_t *)desc_start; + uint8_t *desc_end_buf = (uint8_t *)desc_end; + + while (desc_buf < desc_end_buf) { + struct usb_desc_header *header = (struct usb_desc_header *)desc_buf; + + if (header->bLength == 0) { + break; + } + + if (header->bDescriptorType == USB_DESC_ENDPOINT) { + struct usb_ep_descriptor *ep_desc = (struct usb_ep_descriptor *)desc_buf; + + if ((ep_desc->bEndpointAddress & USB_EP_DIR_MASK) == USB_EP_DIR_IN && + (ep_desc->bmAttributes & USB_EP_TRANSFER_TYPE_MASK) == USB_EP_TYPE_INTERRUPT) { + hub_mgr_data->int_ep = ep_desc; + LOG_DBG("Found hub interrupt IN endpoint 0x%02x", + ep_desc->bEndpointAddress); + break; + } + } + + desc_buf += header->bLength; + } + + hub_mgr_data->connected = true; + hub_mgr_data->int_active = false; + + k_mutex_init(&hub_mgr_data->lock); + k_work_init_delayable(&hub_mgr_data->hub_work, usbh_hub_mgr_process); + k_work_init_delayable(&hub_mgr_data->port_work, usbh_hub_port_process); + + if (0U == hub_mgr.total_hubs) { + hub_mgr_data->hub_level = 1; + hub_mgr_data->parent_hub = NULL; + hub_mgr_data->parent_port = 0; + usbh_hub_print_info(hub_mgr_data); + } + + c_data->priv = hub_mgr_data; + + k_mutex_lock(&hub_mgr.lock, K_FOREVER); + sys_slist_append(&hub_mgr.hub_list, &hub_mgr_data->node); + hub_mgr.total_hubs++; + k_mutex_unlock(&hub_mgr.lock); + + k_work_submit(&hub_mgr_data->hub_work.work); + + return 0; +} + +/** + * @brief USBH class implementation for HUB device removal + */ +static int usbh_hub_mgr_removed(struct usbh_class_data *const cdata) +{ + struct usbh_hub_mgr_data *hub_mgr_data; + + int ret; + + if (!cdata || !cdata->priv) { + LOG_ERR("Invalid cdata or priv data for device"); + return -EINVAL; + } + + hub_mgr_data = cdata->priv; + + if (hub_mgr.processing_hub == hub_mgr_data) { + hub_mgr.processing_hub = NULL; + } + + k_mutex_lock(&hub_mgr_data->lock, K_FOREVER); + hub_mgr_data->being_removed = true; + k_mutex_unlock(&hub_mgr_data->lock); + + /* Recursively disconnect all child hubs and devices */ + usbh_hub_mgr_recursive_disconnect(hub_mgr_data); + + k_work_cancel_delayable(&hub_mgr_data->hub_work); + k_work_cancel_delayable(&hub_mgr_data->port_work); + + /* Cancel interrupt transfer */ + k_mutex_lock(&hub_mgr_data->lock, K_FOREVER); + if (hub_mgr_data->interrupt_transfer && hub_mgr_data->int_active) { + ret = usbh_xfer_dequeue(hub_mgr_data->hub_udev, hub_mgr_data->interrupt_transfer); + if (ret != 0) { + LOG_ERR("Failed to dequeue interrupt transfer: %d", ret); + } + hub_mgr_data->interrupt_transfer = NULL; + hub_mgr_data->int_active = false; + LOG_DBG("Interrupt transfer cancelled"); + } + + /* Remove all connected devices */ + for (int i = 0; i < hub_mgr_data->num_ports; i++) { + if (hub_mgr_data->port_list) { + if (hub_mgr_data->port_list[i].udev) { + hub_mgr_data->port_list[i].udev = NULL; + hub_mgr_data->port_list[i].state = PORT_STATE_DISCONNECTED; + } + } + } + k_mutex_unlock(&hub_mgr_data->lock); + + /* Remove from parent hub's child hub list */ + if (hub_mgr_data->parent_hub) { + k_mutex_lock(&hub_mgr_data->parent_hub->lock, K_FOREVER); + sys_slist_find_and_remove(&hub_mgr_data->parent_hub->child_hubs, &hub_mgr_data->child_node); + k_mutex_unlock(&hub_mgr_data->parent_hub->lock); + } + + /* Remove from global list */ + k_mutex_lock(&hub_mgr.lock, K_FOREVER); + sys_slist_find_and_remove(&hub_mgr.hub_list, &hub_mgr_data->node); + if (hub_mgr.total_hubs > 0) { + hub_mgr.total_hubs--; + } + k_mutex_unlock(&hub_mgr.lock); + + /* Cleanup hub instance */ + usbh_hub_cleanup_instance(&hub_mgr_data->hub_instance); + + /* Free port list */ + if (hub_mgr_data->port_list) { + k_free(hub_mgr_data->port_list); + hub_mgr_data->port_list = NULL; + } + + LOG_INF("Hub (level %d, Vendor ID: 0x%04x, Product ID: 0x%04x) removal completed", + hub_mgr_data->hub_level, + sys_le16_to_cpu(hub_mgr_data->hub_udev->dev_desc.idVendor), + sys_le16_to_cpu(hub_mgr_data->hub_udev->dev_desc.idProduct)); + + k_free(hub_mgr_data); + + return 0; +} + +/** + * @brief Hub class initialization function + */ +static int usbh_hub_mgr_class_init(struct usbh_class_data *const c_data, struct usbh_context *const uhs_ctx) +{ + c_data->uhs_ctx = uhs_ctx; + return 0; +} + +static const struct usbh_class_filter hub_filters[] = {{ + .flags = USBH_CLASS_MATCH_CLASS | USBH_CLASS_MATCH_SUB, + .class = USB_HUB_CLASS_CODE, + .sub = USB_HUB_SUBCLASS_CODE, +}}; + +/* Hub class API structure */ +static struct usbh_class_api usbh_hub_class_api = { + .init = usbh_hub_mgr_class_init, + .completion_cb = NULL, + .probe = usbh_hub_mgr_probe, + .removed = usbh_hub_mgr_removed, + .suspended = NULL, + .resumed = NULL, +}; + +/** + * @brief Initialize HUB manager + */ +static int usbh_hub_mgr_init(void) +{ + sys_slist_init(&hub_mgr.hub_list); + k_mutex_init(&hub_mgr.lock); + hub_mgr.total_hubs = 0; + hub_mgr.processing_hub = NULL; + hub_mgr.uhs_ctx = NULL; + + LOG_INF("USB HUB manager initialized"); + return 0; +} + +SYS_INIT(usbh_hub_mgr_init, POST_KERNEL, CONFIG_USBH_INIT_PRIO); + +#define USBH_DEFINE_HUB_CLASS(i, _) \ +USBH_DEFINE_CLASS(UTIL_CAT(usbh_hub_class_, i), &usbh_hub_class_api, \ + NULL, hub_filters, ARRAY_SIZE(hub_filters)) + +LISTIFY(CONFIG_USBH_HUB_MAX_COUNT, USBH_DEFINE_HUB_CLASS, (;), _) diff --git a/subsys/usb/host/class/usbh_hub_mgr.h b/subsys/usb/host/class/usbh_hub_mgr.h new file mode 100644 index 0000000000000..a96b9486f45dd --- /dev/null +++ b/subsys/usb/host/class/usbh_hub_mgr.h @@ -0,0 +1,111 @@ +/* + * Copyright 2025 NXP + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef _USBH_HUB_MGR_H_ +#define _USBH_HUB_MGR_H_ + +#include +#include "usbh_hub.h" + +enum usbh_hub_app_status { + HUB_RUN_IDLE = 0, + HUB_RUN_INVALID, + HUB_RUN_WAIT_SET_INTERFACE, + HUB_RUN_GET_DESCRIPTOR_7, + HUB_RUN_GET_DESCRIPTOR, + HUB_RUN_SET_PORT_POWER, + HUB_RUN_GET_STATUS_DONE, + HUB_RUN_CLEAR_DONE, +}; + +enum usbh_hub_port_app_status { + HUB_PORT_RUN_IDLE = 0, + HUB_PORT_RUN_INVALID, + HUB_PORT_RUN_WAIT_PORT_CHANGE, + HUB_PORT_RUN_CHECK_C_PORT_CONNECTION, + HUB_PORT_RUN_GET_PORT_CONNECTION, + HUB_PORT_RUN_CHECK_PORT_CONNECTION, + HUB_PORT_RUN_WAIT_PORT_RESET_DONE, + HUB_PORT_RUN_WAIT_C_PORT_RESET, + HUB_PORT_RUN_CHECK_C_PORT_RESET, + HUB_PORT_RUN_RESET_AGAIN, + HUB_PORT_RUN_PORT_ATTACHED, + HUB_PORT_RUN_CHECK_PORT_DETACH, + HUB_PORT_RUN_CHECK_CHILD_HUB, + HUB_PORT_RUN_GET_CONNECTION_BIT, + HUB_PORT_RUN_CHECK_CONNECTION_BIT, +}; + +enum usbh_hub_prime_status { + HUB_PRIME_NONE = 0, + HUB_PRIME_CONTROL, + HUB_PRIME_PORT_CONTROL, + HUB_PRIME_INTERRUPT, +}; + +/* Port instance structure (unified structure with necessary fields) */ +struct usbh_hub_port_instance { + struct usb_device *udev; /* Connected USB device */ + uint8_t port_status; /* Port application status */ + enum usbh_port_state state; /* Port overall state */ + uint8_t reset_count; /* Reset retry count */ + uint8_t speed; /* Device speed */ + uint8_t port_num; /* Port number */ +}; + +/** + * @brief HUB management data structure + */ +struct usbh_hub_mgr_data { + struct usb_device *hub_udev; + struct usbh_context *uhs_ctx; + + /* Hub instance data */ + struct usbh_hub_instance hub_instance; + + /* Hub state management */ + enum usbh_hub_state state; + enum usbh_hub_app_status hub_status; + + /* Port management - unified using port_list */ + struct usbh_hub_port_instance *port_list; /* Port instance list */ + uint8_t num_ports; /* Total number of ports */ + uint8_t current_port; /* Currently processing port */ + uint8_t port_index; /* Port index */ + + /* Work items for state machine processing */ + struct k_work_delayable hub_work; + struct k_work_delayable port_work; + + /** Interrupt endpoint descriptor */ + struct usb_ep_descriptor *int_ep; + /** Device connection status */ + bool connected; + /** Interrupt transfer active flag */ + bool int_active; + + /* Hierarchy structure management fields */ + struct usbh_hub_mgr_data *parent_hub; + uint8_t parent_port; + sys_slist_t child_hubs; + sys_snode_t child_node; + uint8_t hub_level; + + /* Global Hub chain management list node */ + sys_snode_t node; + + /* Synchronization and state management */ + struct k_mutex lock; + bool being_removed; + uint32_t last_error_time; + uint8_t error_count; + uint8_t prime_status; + + struct uhc_transfer *interrupt_transfer; + uint8_t int_buffer[8]; +}; + +#endif /* _USBH_HUB_MGR_H_ */ diff --git a/subsys/usb/host/class/usbh_uvc.c b/subsys/usb/host/class/usbh_uvc.c new file mode 100644 index 0000000000000..ddd7955f70c42 --- /dev/null +++ b/subsys/usb/host/class/usbh_uvc.c @@ -0,0 +1,411 @@ +/* + * Copyright (c) 2025 tinyVision.ai Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#define DT_DRV_COMPAT zephyr_uvc_host + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "usbh_ch9.h" +#include "usbh_class.h" +#include "usbh_desc.h" +#include "usbh_device.h" +#include "usb_uvc.h" + +#include "../../../drivers/video/video_ctrls.h" +#include "../../../drivers/video/video_device.h" + +LOG_MODULE_REGISTER(usbh_uvc, CONFIG_USBH_VIDEO_LOG_LEVEL); + +struct usbh_uvc_data { + int todo; +}; + +/* + * Descriptor parsing utilities + * Validate and parse the video streaming and video control descriptors. + */ + +static bool usbh_uvc_desc_is_valid_vs_header(const void *const desc, + const void *const desc_end) +{ + const struct uvc_stream_header_descriptor *const header_desc = desc; + + return usbh_desc_is_valid(desc, desc_end, sizeof(struct uvc_stream_header_descriptor)) && + header_desc->bDescriptorType == USB_DESC_CS_INTERFACE && + (header_desc->bDescriptorSubtype == UVC_VS_OUTPUT_HEADER || + header_desc->bDescriptorSubtype == UVC_VS_INPUT_HEADER); +} + +static bool usbh_uvc_desc_is_valid_vc_header(const void *const desc, + const void *const desc_end) +{ + const struct uvc_control_header_descriptor *const header_desc = desc; + + return usbh_desc_is_valid(desc, desc_end, sizeof(struct uvc_control_header_descriptor)) && + header_desc->bDescriptorType == USB_DESC_CS_INTERFACE && + header_desc->bDescriptorSubtype == UVC_VC_HEADER; +} + +const void *usbh_uvc_desc_get_vs_end(const struct usb_if_descriptor *if_desc, + const void *const desc_end) +{ + const struct uvc_stream_header_descriptor *const header_desc = + usbh_desc_get_next(if_desc, desc_end); + const void *vs_end; + + if (!usbh_uvc_desc_is_valid_vs_header(header_desc, desc_end)) { + return NULL; + } + + vs_end = (uint8_t *)header_desc + header_desc->wTotalLength; + if (vs_end > desc_end) { + return NULL; + } + + return vs_end; +} + +const void *usbh_uvc_desc_get_vc_end(const struct usb_if_descriptor *if_desc, + const void *const desc_end) +{ + const struct uvc_control_header_descriptor *const header_desc = + usbh_desc_get_next(if_desc, desc_end); + const void *vc_end; + + if (!usbh_uvc_desc_is_valid_vc_header(header_desc, desc_end)) { + return NULL; + } + + vc_end = (uint8_t *)header_desc + header_desc->wTotalLength; + if (vc_end > desc_end) { + LOG_WRN("vc_end %p > desc_end %p", vc_end, desc_end); + return NULL; + } + + return vc_end; +} + +static int usbh_uvc_parse_vc_desc(struct usbh_class_data *const c_data, + const void *const desc_beg, + const void *const desc_end) +{ + /* Skip the interface descriptor itself */ + const struct usb_desc_header *desc = usbh_desc_get_next(desc_beg, desc_end); + + for (; desc != NULL; desc = usbh_desc_get_next(desc, desc_end)) { + if (desc->bDescriptorType == USB_DESC_INTERFACE || + desc->bDescriptorType == USB_DESC_INTERFACE_ASSOC || + desc->bDescriptorType == 0) { + break; + } else if (desc->bDescriptorType == USB_DESC_CS_INTERFACE) { + const struct uvc_if_descriptor *const if_desc = (const void *)desc; + + switch (if_desc->bDescriptorSubtype) { + case UVC_VC_HEADER: + LOG_DBG("VideoControl interface: Header"); + break; + case UVC_VC_OUTPUT_TERMINAL: + LOG_DBG("VideoControl interface: Output Terminal"); + break; + case UVC_VC_INPUT_TERMINAL: + LOG_DBG("VideoControl interface: Input/Camera Terminal"); + break; + case UVC_VC_SELECTOR_UNIT: + LOG_DBG("VideoControl interface: Selector Unit"); + break; + case UVC_VC_PROCESSING_UNIT: + LOG_DBG("VideoControl interface: Processing Unit"); + break; + case UVC_VC_EXTENSION_UNIT: + LOG_DBG("VideoControl interface: Extension Unit"); + break; + case UVC_VC_ENCODING_UNIT: + LOG_DBG("VideoControl interface: Encoding Unit"); + break; + default: + LOG_WRN("VideoControl interface: unknown subtype %u, skipping", + if_desc->bDescriptorSubtype); + } + } else { + LOG_WRN("VideoControl descriptor: unknown type %u, skipping", + desc->bDescriptorType); + } + } + + return 0; +} + +static int usbh_uvc_parse_vs_desc(struct usbh_class_data *const c_data, + const void *const desc_beg, + const void *const desc_end) +{ + /* Skip the interface descriptor itself */ + const struct usb_desc_header *desc = usbh_desc_get_next(desc_beg, desc_end); + + for (; desc != NULL; desc = usbh_desc_get_next(desc, desc_end)) { + if (desc->bDescriptorType == USB_DESC_INTERFACE || + desc->bDescriptorType == USB_DESC_INTERFACE_ASSOC || + desc->bDescriptorType == 0) { + break; + } else if (desc->bDescriptorType == USB_DESC_CS_INTERFACE) { + const struct uvc_if_descriptor *const if_desc = (const void *)desc; + + switch (if_desc->bDescriptorSubtype) { + case UVC_VS_INPUT_HEADER: + LOG_DBG("VideoStreaming interface: Input header"); + break; + case UVC_VS_OUTPUT_HEADER: + LOG_DBG("VideoStreaming interface: Output header"); + break; + case UVC_VS_FORMAT_UNCOMPRESSED: + LOG_DBG("VideoStreaming interface: Uncompressed format"); + break; + case UVC_VS_FORMAT_MJPEG: + LOG_DBG("VideoStreaming interface: MJPEG format"); + break; + case UVC_VS_FRAME_UNCOMPRESSED: + LOG_DBG("VideoStreaming interface: Uncompressed Frame"); + break; + case UVC_VS_FRAME_MJPEG: + LOG_DBG("VideoStreaming interface: MJPEG Frame"); + break; + case UVC_VS_COLORFORMAT: + LOG_DBG("VideoStreaming interface: Color"); + break; + default: + LOG_DBG("VideoStreaming descriptor: unknown subtype %u, skipping", + if_desc->bDescriptorSubtype); + } + } else if (desc->bDescriptorType == USB_DESC_ENDPOINT) { + const struct usb_ep_descriptor *const ep_desc = (const void *)desc; + + LOG_DBG("VideoStreaming Endpoint", ep_desc->bEndpointAddress); + } else { + LOG_WRN("VideoStreaming descriptor: unknown type %u, skipping", + desc->bDescriptorType); + } + } + + return 0; +} + +static int usbh_uvc_probe(struct usbh_class_data *const c_data, + struct usb_device *const udev, uint8_t iface) +{ + const void *const desc_beg = usbh_desc_get_cfg_beg(udev); + const void *const desc_end = usbh_desc_get_cfg_end(udev); + const struct usb_association_descriptor *const iad_desc = + usbh_desc_get_by_iface(desc_beg, desc_end, iface); + const struct usb_desc_header *desc; + bool has_vc_if = false; + bool has_vs_if = false; + int ret; + + if (iad_desc == NULL) { + LOG_ERR("failed to find interface or interface association number %u", iface); + return -ENOSYS; + } + + if (iad_desc->bDescriptorType != USB_DESC_INTERFACE_ASSOC) { + LOG_WRN("Interface %u is not a valid %s, skipping", iface, "interface assoc"); + return -ENOTSUP; + } + + desc = (struct usb_desc_header *)usbh_desc_get_next(iad_desc, desc_end); + if (desc == NULL) { + return -EBADMSG; + } + + for (int i = 0; i < iad_desc->bInterfaceCount; i++) { + const struct usb_if_descriptor *if_desc; + + if_desc = (const void *)usbh_desc_get_by_iface(desc, desc_end, c_data->iface + i); + if (if_desc == NULL) { + LOG_ERR("Not as many interfaces (%u) as announced (%u)", + i, iad_desc->bInterfaceCount); + return -EBADMSG; + } + + if (if_desc->bInterfaceClass == USB_BCC_VIDEO && + if_desc->bInterfaceSubClass == UVC_SC_VIDEOCONTROL) { + const void *vc_end; + + if (has_vc_if) { + LOG_WRN("Skipping extra VideoControl interface"); + continue; + } + + vc_end = usbh_uvc_desc_get_vc_end(if_desc, desc_end); + if (vc_end == NULL) { + LOG_ERR("Invalid VideoControl interface descriptor"); + return -EBADMSG; + } + + ret = usbh_uvc_parse_vc_desc(c_data, if_desc, vc_end); + if (ret != 0) { + LOG_ERR("Failed to parse VC descriptor"); + return ret; + } + + has_vc_if = true; + } + + if (if_desc->bInterfaceClass == USB_BCC_VIDEO && + if_desc->bInterfaceSubClass == UVC_SC_VIDEOSTREAMING) { + const void *vs_end; + + if (has_vs_if) { + LOG_WRN("Skipping extra VideoStreaming interface"); + continue; + } + + vs_end = usbh_uvc_desc_get_vs_end(if_desc, desc_end); + if (vs_end == NULL) { + LOG_ERR("Invalid VideoStreaming interface descriptor"); + return -EBADMSG; + } + + ret = usbh_uvc_parse_vs_desc(c_data, if_desc, vs_end); + if (ret != 0) { + return ret; + } + + has_vs_if = true; + } + } + + if (!has_vs_if) { + LOG_ERR("No VideoStreaming interface found"); + return -EINVAL; + } + + if (!has_vc_if) { + LOG_DBG("No VideoControl interface found"); + return -EINVAL; + } + + LOG_INF("Interface %u associated with UVC class", iface); + + return 0; +} + +static int usbh_uvc_removed(struct usbh_class_data *const c_data) +{ + return 0; +} + +static int usbh_uvc_init(struct usbh_class_data *const c_data, + struct usbh_context *const uhs_ctx) +{ + return 0; +} + +static int usbh_uvc_completion_cb(struct usbh_class_data *const c_data, + struct uhc_transfer *const xfer) +{ + return 0; +} + +static int usbh_uvc_suspended(struct usbh_class_data *const c_data) +{ + return 0; +} + +static int usbh_uvc_resumed(struct usbh_class_data *const c_data) +{ + return 0; +} + +static int usbh_uvc_preinit(const struct device *dev) +{ + LOG_DBG("%s", dev->name); + + return 0; +} + +static struct usbh_class_api uvc_class_api = { + .init = usbh_uvc_init, + .completion_cb = usbh_uvc_completion_cb, + .probe = usbh_uvc_probe, + .removed = usbh_uvc_removed, + .suspended = usbh_uvc_suspended, + .resumed = usbh_uvc_resumed, +}; + +int usbh_uvc_get_caps(const struct device *const dev, struct video_caps *const caps) +{ + return 0; +} + +int usbh_uvc_get_format(const struct device *const dev, struct video_format *const fmt) +{ + return 0; +} + +int usbh_uvc_set_stream(const struct device *const dev, bool enable, enum video_buf_type type) +{ + return 0; +} + +int usbh_uvc_enqueue(const struct device *const dev, struct video_buffer *const vbuf) +{ + return 0; +} + +int usbh_uvc_dequeue(const struct device *const dev, struct video_buffer **const vbuf, + k_timeout_t timeout) +{ + return 0; +} + +static DEVICE_API(video, uvc_video_api) = { + .get_caps = usbh_uvc_get_caps, + .get_format = usbh_uvc_get_format, + .set_stream = usbh_uvc_set_stream, + .enqueue = usbh_uvc_enqueue, + .dequeue = usbh_uvc_dequeue, +}; + +static struct usbh_class_filter usbh_uvc_filters[] = { + { + .flags = USBH_CLASS_MATCH_CLASS | USBH_CLASS_MATCH_SUB, + .class = USB_BCC_VIDEO, + .sub = UVC_SC_VIDEO_INTERFACE_COLLECTION, + }, +}; + +#define USBH_VIDEO_DT_DEVICE_DEFINE(n) \ + struct usbh_uvc_data usbh_uvc_data_##n = { \ + }; \ + \ + USBH_DEFINE_CLASS(uvc_c_data_##n, &uvc_class_api, \ + (void *)DEVICE_DT_INST_GET(n), \ + usbh_uvc_filters, ARRAY_SIZE(usbh_uvc_filters)); \ + \ + DEVICE_DT_INST_DEFINE(n, usbh_uvc_preinit, NULL, \ + &usbh_uvc_data_##n, NULL, \ + POST_KERNEL, CONFIG_VIDEO_INIT_PRIORITY, \ + &uvc_video_api); \ + \ + VIDEO_DEVICE_DEFINE(uvc_host_##n, DEVICE_DT_INST_GET(n), NULL); + +DT_INST_FOREACH_STATUS_OKAY(USBH_VIDEO_DT_DEVICE_DEFINE) diff --git a/subsys/usb/host/usbh_api.c b/subsys/usb/host/usbh_api.c index 8501ee044b2ad..4f24cd598943a 100644 --- a/subsys/usb/host/usbh_api.c +++ b/subsys/usb/host/usbh_api.c @@ -1,6 +1,5 @@ /* - * Copyright (c) 2022 Nordic Semiconductor ASA - * + * SPDX-FileCopyrightText: Copyright Nordic Semiconductor ASA * SPDX-License-Identifier: Apache-2.0 */ diff --git a/subsys/usb/host/usbh_ch9.c b/subsys/usb/host/usbh_ch9.c index 80783e430559b..e724472843be4 100644 --- a/subsys/usb/host/usbh_ch9.c +++ b/subsys/usb/host/usbh_ch9.c @@ -1,6 +1,5 @@ /* - * Copyright (c) 2022 Nordic Semiconductor ASA - * + * SPDX-FileCopyrightText: Copyright Nordic Semiconductor ASA * SPDX-License-Identifier: Apache-2.0 */ diff --git a/subsys/usb/host/usbh_ch9.h b/subsys/usb/host/usbh_ch9.h index 19879dae6e2df..54f7812afac25 100644 --- a/subsys/usb/host/usbh_ch9.h +++ b/subsys/usb/host/usbh_ch9.h @@ -1,6 +1,5 @@ /* - * Copyright (c) 2022 Nordic Semiconductor ASA - * + * SPDX-FileCopyrightText: Copyright Nordic Semiconductor ASA * SPDX-License-Identifier: Apache-2.0 */ diff --git a/subsys/usb/host/usbh_class.c b/subsys/usb/host/usbh_class.c new file mode 100644 index 0000000000000..e9546e5b5243b --- /dev/null +++ b/subsys/usb/host/usbh_class.c @@ -0,0 +1,187 @@ +/* + * 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) { + ret = usbh_class_removed(c_data); + if (ret != 0) { + LOG_ERR("Failed to handle device removal for each class (%d)", ret); + c_node->state = USBH_CLASS_STATE_ERROR; + continue; + } + + /* The class instance is now free to bind to a new device */ + c_data->udev = NULL; + c_node->state = USBH_CLASS_STATE_IDLE; + } + } + + k_mutex_unlock(&udev->mutex); +} + +/* + * Probe an USB device function against all available classes of the system. + * + * Try to match a class from the global list of all system classes, using their filter rules + * and return status to tell if a class matches or not. + * + * The first matched class will stop the loop, and the status will be updated so that classes + * are only matched for a single USB function at a time. + * + * USB functions will only have one class matching, and calling usbh_class_probe_function() + * multiple times consequently has no effect. + */ +static void usbh_class_probe_function(struct usb_device *const udev, + struct usbh_class_filter *const info, 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, info)) { + LOG_DBG("Class %s not matching interface %u", c_data->name, iface); + continue; + } + + ret = usbh_class_probe(c_data, udev, iface); + if (ret == -ENOTSUP) { + LOG_DBG("Class %s not supporting this device, skipping", c_data->name); + continue; + } + + LOG_INF("Class '%s' matches interface %u", c_data->name, iface); + c_node->state = USBH_CLASS_STATE_BOUND; + c_data->udev = udev; + c_data->iface = iface; + break; + } +} + +void usbh_class_probe_device(struct usb_device *const udev) +{ + const void *desc_end; + const struct usb_desc_header *desc; + struct usbh_class_filter info; + uint8_t iface; + int ret; + + k_mutex_lock(&udev->mutex, K_FOREVER); + + desc = usbh_desc_get_cfg(udev); + desc_end = usbh_desc_get_cfg_end(udev); + info.vid = udev->dev_desc.idVendor; + info.pid = udev->dev_desc.idProduct; + + while (true) { + desc = usbh_desc_get_next_function(desc, desc_end); + if (desc == NULL) { + break; + } + + ret = usbh_desc_get_iface_info(desc, &info, &iface); + if (ret < 0) { + LOG_ERR("Failed to collect class codes for matching interface %u", iface); + continue; + } + + usbh_class_probe_function(udev, &info, iface); + } + + k_mutex_unlock(&udev->mutex); +} + +bool usbh_class_is_matching(struct usbh_class_filter *const filters, + struct usbh_class_filter *const info) +{ + /* Make empty filter set match everything (use class_api->probe() only) */ + if (filters == NULL) { + return true; + } + + /* Try to find a rule that matches completely */ + for (size_t i = 0; filters[i].flags != 0; i++) { + const struct usbh_class_filter *filt = &filters[i]; + + if (filt->flags & USBH_CLASS_MATCH_VID_PID && + (info->vid != filt->vid || info->pid != filt->pid)) { + continue; + } + + if (filt->flags & USBH_CLASS_MATCH_CODE_TRIPLE && + (info->class != filt->class || info->sub != filt->sub || + info->proto != filt->proto)) { + continue; + } + + /* All the selected filters did match */ + return true; + } + + /* At the end of the filter table and still no match */ + return false; +} diff --git a/subsys/usb/host/usbh_class.h b/subsys/usb/host/usbh_class.h new file mode 100644 index 0000000000000..015c5bf23ab3d --- /dev/null +++ b/subsys/usb/host/usbh_class.h @@ -0,0 +1,58 @@ +/* + * 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) + +/** + * @brief Match an USB host class (a driver) against a device descriptor. + * + * An empty filter set matches everything. + * This can be used to only rely on @c class_api->probe() return value. + * + * @param[in] filters Array of filter rules to match + * @param[in] device_info Device information filled by this function + * + * @retval true if the USB Device descriptor matches at least one rule. + */ +bool usbh_class_is_matching(struct usbh_class_filter *const filters, + struct usbh_class_filter *const device_info); + +/** + * @brief Initialize every class instantiated on the system + * + * @param[in] uhs_ctx USB Host context to pass to the class. + */ +void usbh_class_init_all(struct usbh_context *const uhs_ctx); + +/** + * @brief Probe an USB device function against all available classes. + * + * Try to match a class from the global list of all system classes using their filter rules + * and return status to update the state of each matched class. + * + * The first matched class + * + * @param[in] udev USB device to probe. + */ +void usbh_class_probe_device(struct usb_device *const udev); + +/** + * @brief Call the device removal handler for every class configured with it + * + * @param[in] udev USB device that got removed. + */ +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..906ef8bb1d159 --- /dev/null +++ b/subsys/usb/host/usbh_class_api.h @@ -0,0 +1,107 @@ +/* + * 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; +} + +#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..4b023bfe0d9ce 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,10 @@ #include #include #include +#include -#include "usbh_internal.h" #include "usbh_device.h" +#include "usbh_internal.h" #include LOG_MODULE_REGISTER(uhs, CONFIG_USBH_LOG_LEVEL); @@ -46,39 +46,28 @@ 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"); - usbh_device_free(ctx->root); - ctx->root = NULL; - } - - ctx->root = usbh_device_alloc(ctx); - if (ctx->root == NULL) { - LOG_ERR("Failed allocate new device"); - return; - } - - ctx->root->state = USB_STATE_DEFAULT; + struct usb_device *udev; + uint8_t speed; if (event->type == UHC_EVT_DEV_CONNECTED_HS) { - ctx->root->speed = USB_SPEED_SPEED_HS; + speed = USB_SPEED_SPEED_HS; } else { - ctx->root->speed = USB_SPEED_SPEED_FS; + speed = USB_SPEED_SPEED_FS; } - if (usbh_device_init(ctx->root)) { - LOG_ERR("Failed to reset new USB device"); + udev = usbh_connect_device(ctx, speed); + if (udev == NULL) { + return; } } static void dev_removed_handler(struct usbh_context *const ctx) { - if (ctx->root != NULL) { - usbh_device_free(ctx->root); - ctx->root = NULL; - LOG_DBG("Device removed"); + struct usb_device *udev = NULL; + + udev = usbh_device_get_root(ctx); + if (udev) { + usbh_disconnect_device(ctx, udev); } else { LOG_DBG("Spurious device removed event"); } @@ -194,13 +183,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..d45aae6868d20 --- /dev/null +++ b/subsys/usb/host/usbh_desc.c @@ -0,0 +1,213 @@ +/* + * 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_desc.h" + +LOG_MODULE_REGISTER(usbh_desc, CONFIG_USBH_LOG_LEVEL); + +bool usbh_desc_is_valid(const void *const desc_ptr, const void *const desc_end, + const size_t expected_size, const uint8_t type) +{ + const struct usb_desc_header *desc = desc_ptr; + + /* Block invalid input */ + if (desc == NULL || desc_end == NULL || expected_size < sizeof(struct usb_desc_header)) { + return false; + } + + /* Avoid out of order pointers */ + if ((const uint8_t *)desc > (const uint8_t *)desc_end) { + return false; + } + + /* Avoid too short descriptors */ + if ((const uint8_t *)desc + expected_size > (const uint8_t *)desc_end) { + return false; + } + + /* Avoid too short bLength field */ + if (desc->bLength < expected_size) { + return false; + } + + /* Expect the correct type */ + if (type != 0 && type != desc->bDescriptorType) { + return false; + } + + return true; +} + +bool usbh_desc_is_valid_interface(const void *const desc, const void *const desc_end) +{ + return usbh_desc_is_valid(desc, desc_end, sizeof(struct usb_if_descriptor), + USB_DESC_INTERFACE); +} + +bool usbh_desc_is_valid_association(const void *const desc, const void *const desc_end) +{ + return usbh_desc_is_valid(desc, desc_end, sizeof(struct usb_association_descriptor), + USB_DESC_INTERFACE_ASSOC); +} + +bool usbh_desc_is_valid_configuration(const void *const desc, const void *const desc_end) +{ + return usbh_desc_is_valid(desc, desc_end, sizeof(struct usb_cfg_descriptor), + USB_DESC_CONFIGURATION); +} + +const void *usbh_desc_get_next(const void *const desc, const void *const desc_end) +{ + const void *next; + + if (!usbh_desc_is_valid(desc, desc_end, sizeof(const struct usb_desc_header), 0)) { + return NULL; + } + + next = (const uint8_t *)desc + ((const struct usb_desc_header *)desc)->bLength; + + if (!usbh_desc_is_valid(next, desc_end, sizeof(const struct usb_desc_header), 0)) { + return NULL; + } + + return next; +} + +const void *usbh_desc_get_next_alt_setting(const void *const desc_beg, const void *const desc_end) +{ + const struct usb_desc_header *desc = desc_beg; + + /* Expect desc to already be pointing at an interface descriptor */ + if (desc_beg == NULL || desc_end == NULL || desc->bDescriptorType != USB_DESC_INTERFACE) { + return NULL; + } + + /* Skip the current interface descriptor */ + desc = usbh_desc_get_next(desc, desc_end); + + /* Seek to the nextAlternate Setting for this interface */ + for (; desc != NULL; desc = usbh_desc_get_next(desc, desc_end)) { + if (desc->bDescriptorType == USB_DESC_INTERFACE) { + /* Non-zero Alternate Setting */ + if (usbh_desc_is_valid_interface(desc, desc_end) && + ((const struct usb_if_descriptor *)desc)->bAlternateSetting != 0) { + return desc; + } + + /* Do not continue to the next interface */ + return NULL; + } + } + + return NULL; +} + +const void *usbh_desc_get_by_iface(const void *const desc_ptr, const void *const desc_end, + const uint8_t iface) +{ + if (desc_ptr == NULL || desc_end == NULL) { + return NULL; + } + + for (const struct usb_desc_header *desc = desc_ptr; + desc != NULL; + desc = usbh_desc_get_next(desc, desc_end)) { + if (usbh_desc_is_valid_interface(desc, desc_end) && + ((const struct usb_if_descriptor *)desc)->bInterfaceNumber == iface) { + return desc; + } + if (usbh_desc_is_valid_association(desc, desc_end) && + ((const struct usb_association_descriptor *)desc)->bFirstInterface == iface) { + return desc; + } + } + + return NULL; +} + +const void *usbh_desc_get_cfg(const struct usb_device *const udev) +{ + return udev->cfg_desc; +} + +const void *usbh_desc_get_cfg_end(const struct usb_device *const udev) +{ + const struct usb_cfg_descriptor *const cfg_desc = udev->cfg_desc; + + __ASSERT_NO_MSG(cfg_desc != NULL); + + /* Relies on wTotalLength being validated in usbh_device_set_configuration() */ + return (const uint8_t *)cfg_desc + cfg_desc->wTotalLength; +} + +int usbh_desc_get_iface_info(const struct usb_desc_header *const desc, + struct usbh_class_filter *const iface_code, uint8_t *const iface_num) +{ + if (desc->bDescriptorType == USB_DESC_INTERFACE_ASSOC) { + const struct usb_association_descriptor *iad_desc = + (const struct usb_association_descriptor *)desc; + + iface_code->class = iad_desc->bFunctionClass; + iface_code->sub = iad_desc->bFunctionSubClass; + iface_code->proto = iad_desc->bFunctionProtocol; + if (iface_num != NULL) { + *iface_num = iad_desc->bFirstInterface; + } + return 0; + } + + if (desc->bDescriptorType == USB_DESC_INTERFACE) { + const struct usb_if_descriptor *if_desc = + (const struct usb_if_descriptor *)desc; + + iface_code->class = if_desc->bInterfaceClass; + iface_code->sub = if_desc->bInterfaceSubClass; + iface_code->proto = if_desc->bInterfaceProtocol; + if (iface_num != NULL) { + *iface_num = if_desc->bInterfaceNumber; + } + return 0; + } + + return -EINVAL; +} + +const void *usbh_desc_get_next_function(const void *const desc_ptr, const void *const desc_end) +{ + const void *desc = desc_ptr; + uint8_t skip_num = 1; + + if (desc == NULL || desc_end == NULL) { + return NULL; + } + + /* Skip all interfaces the Association descriptor contains */ + if (usbh_desc_is_valid_association(desc, desc_end)) { + skip_num = ((const struct usb_association_descriptor *)desc)->bInterfaceCount; + } + + while (desc != NULL && skip_num > 0) { + /* If already on an Interface Association or Interface, this will skip it */ + desc = usbh_desc_get_next(desc, desc_end); + + /* Association descriptor: this is always a new function */ + if (usbh_desc_is_valid_association(desc, desc_end)) { + return desc; + } + + /* Only count the first Alternate Setting of an Interface */ + if (usbh_desc_is_valid_interface(desc, desc_end) && + ((const struct usb_if_descriptor *)desc)->bAlternateSetting == 0) { + skip_num--; + } + } + + return desc; +} diff --git a/subsys/usb/host/usbh_desc.h b/subsys/usb/host/usbh_desc.h new file mode 100644 index 0000000000000..8dcdb717ae4e7 --- /dev/null +++ b/subsys/usb/host/usbh_desc.h @@ -0,0 +1,148 @@ +/* + * 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. May be NULL. + * @param[in] desc_end Pointer after the end of the descriptor array + * + * @return A pointer to the first descriptor matching + */ +const void *usbh_desc_get_next(const void *const desc, const void *const desc_end); + +/** + * @brief Search a descriptor matching the interace number wanted. + * + * The descriptors are going to be scanned until either an interface association + * @c bFirstInterface field or an interface @c bInterfaceNumber field match. + * + * The descriptors following it can be browsed using @ref usbh_desc_get_next + * + * @param[in] desc 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 + * @param[in] iface The interface number to search for + * + * @return A pointer to the first descriptor matching + */ +const void *usbh_desc_get_by_iface(const void *const desc, const void *const desc_end, + const uint8_t iface); + +/** + * @brief Get the start of a device's configuration descriptor. + * + * @param[in] udev The device holding the configuration descriptor + * + * @return A pointer to the beginning of the descriptor + */ +const void *usbh_desc_get_cfg(const struct usb_device *const udev); + +/** + * @brief Get the end of a device's configuration descriptor. + * + * @param[in] udev The device holding the configuration descriptor + * + * @return A pointer right after the end of the descriptor (should not be accessed) + */ +const void *usbh_desc_get_cfg_end(const struct usb_device *const udev); + +/** + * @brief Extract information from an interface or interface association descriptors + * + * @param[in] desc The descriptor to use + * @param[out] iface_code Device information filled by this function + * @param[out] iface_num Pointer filled with the interface number, or NULL + * + * @return 0 on success or negative error code on failure. + */ +int usbh_desc_get_iface_info(const struct usb_desc_header *desc, + struct usbh_class_filter *const iface_code, + uint8_t *const iface_num); + +/** + * @brief Checks that the pointed descriptor is not truncated and of the expected type. + * + * @param[in] desc The descriptor to validate + * @param[in] desc_end Pointer after the end of the descriptor array. + * @param[in] expected_size The size of the descriptor. + * @param[in] expected_type The type of the descriptor, 0 for any. + * + * @return true if the descriptor size and type are valid + */ +const bool usbh_desc_is_valid(const void *const desc, const void *const desc_end, + const size_t expected_size, const uint8_t expected_type); + +/** + * @brief Checks that the pointed descriptor is a valid interface descriptor. + * + * @param[in] desc The descriptor to validate + * @param[in] desc_end Pointer after the end of the descriptor array. + * + * @return true if the descriptor size is the expected type and size + */ +bool usbh_desc_is_valid_interface(const void *const desc, const void *const desc_end); + +/** + * @brief Checks that the pointed descriptor is a valid interface association descriptor. + * + * @param[in] desc The descriptor to validate + * @param[in] desc_end Pointer after the end of the descriptor array. + * + * @return true if the descriptor size is the expected type and size + */ +bool usbh_desc_is_valid_association(const void *const desc, const void *const desc_end); + +/** + * @brief Get the next function in the descriptor list. + * + * This 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. + * + * If @p desc is an interface association descriptor, it will return a pointer to the interface + * after all those related to the association descriptor. + * + * If @p desc is an interface descriptor, it will skip all the interface alternate settings + * and return a pointer to the next interface with bAlternateSetting of 0. + * + * If @p desc is another type of descriptors, it will seek to a matching descriptor type and + * return it, without skipping to the next one after it. + * + * @param[in] desc 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. + * + * @return Pointer to the next matching descriptor or NULL. + */ +const void *usbh_desc_get_next_function(const void *const desc, const void *const desc_end); + +/** + * @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. May be NULL. + * @param[in] desc_end Pointer after the end of the descriptor array. + * + * @return Pointer to the next matching descriptor or NULL. + */ +const void *usbh_desc_get_next_alt_setting(const void *const desc, const void *const desc_end); + +#endif /* ZEPHYR_INCLUDE_USBH_DESC_H */ diff --git a/subsys/usb/host/usbh_device.c b/subsys/usb/host/usbh_device.c index 92ecf6ae7aee1..977e1542f35ce 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 */ @@ -9,6 +8,8 @@ #include "usbh_device.h" #include "usbh_ch9.h" +#include "usbh_class.h" +#include "usbh_class_api.h" #include LOG_MODULE_REGISTER(usbh_dev, CONFIG_USBH_LOG_LEVEL); @@ -447,11 +448,81 @@ int usbh_device_set_configuration(struct usb_device *const udev, const uint8_t n return err; } +struct usb_device *usbh_device_get_root(struct usbh_context *const ctx) +{ + sys_dnode_t *node; + + if (ctx == NULL) { + return NULL; + } + + node = sys_dlist_peek_head(&ctx->udevs); + if (node == NULL) { + /* No devices in the list */ + return NULL; + } + + /* Get the usb_device structure from the node */ + return CONTAINER_OF(node, struct usb_device, node); +} + +bool usbh_device_is_root(struct usbh_context *const ctx, + struct usb_device *const udev) +{ + if (ctx == NULL || udev == NULL) { + return false; + } + + return sys_dlist_peek_head(&ctx->udevs) == &udev->node; +} + +struct usb_device *usbh_connect_device(struct usbh_context *const ctx, + uint8_t speed) +{ + struct usb_device *udev; + + LOG_DBG("Device connected event"); + + /* Allocate new device */ + udev = usbh_device_alloc(ctx); + if (udev == NULL) { + LOG_ERR("Failed allocate new device"); + return NULL; + } + + udev->state = USB_STATE_DEFAULT; + udev->speed = speed; + + if (usbh_device_init(udev)) { + LOG_ERR("Failed to init new USB device"); + usbh_device_free(udev); + return NULL; + } + + usbh_class_probe_device(udev); + + return udev; +} + +void usbh_disconnect_device(struct usbh_context *ctx, struct usb_device *udev) +{ + if (!ctx || !udev) { + return; + } + + usbh_class_remove_all(udev); + + usbh_device_free(udev); + + LOG_DBG("Device removed"); +} + int usbh_device_init(struct usb_device *const udev) { struct usbh_context *const uhs_ctx = udev->ctx; uint8_t new_addr; int err; + uint8_t device_count = 0; if (udev->state != USB_STATE_DEFAULT) { LOG_ERR("USB device is not in default state"); @@ -464,11 +535,17 @@ int usbh_device_init(struct usb_device *const udev) return err; } - /* FIXME: The port to which the device is connected should be reset. */ - err = uhc_bus_reset(uhs_ctx->dev); - if (err) { - LOG_ERR("Failed to signal bus reset"); - return err; + k_mutex_lock(&uhs_ctx->mutex, K_FOREVER); + device_count = sys_dlist_len(&uhs_ctx->udevs); + k_mutex_unlock(&uhs_ctx->mutex); + + /* Only reset bus if this is the root device. */ + if (device_count == 1U) { + err = uhc_bus_reset(uhs_ctx->dev); + if (err) { + LOG_ERR("Failed to signal bus reset"); + return err; + } } /* @@ -487,18 +564,6 @@ int usbh_device_init(struct usb_device *const udev) goto error; } - err = usbh_req_desc_dev(udev, sizeof(udev->dev_desc), &udev->dev_desc); - if (err) { - LOG_ERR("Failed to read device descriptor"); - goto error; - } - - if (!udev->dev_desc.bNumConfigurations) { - LOG_ERR("Device has no configurations, bNumConfigurations %d", - udev->dev_desc.bNumConfigurations); - goto error; - } - err = alloc_device_address(udev, &new_addr); if (err) { LOG_ERR("Failed to allocate device address"); @@ -518,6 +583,18 @@ int usbh_device_init(struct usb_device *const udev) LOG_INF("New device with address %u state %u", udev->addr, udev->state); + err = usbh_req_desc_dev(udev, sizeof(udev->dev_desc), &udev->dev_desc); + if (err) { + LOG_ERR("Failed to read device descriptor"); + goto error; + } + + if (!udev->dev_desc.bNumConfigurations) { + LOG_ERR("Device has no configurations, bNumConfigurations %d", + udev->dev_desc.bNumConfigurations); + goto error; + } + err = usbh_device_set_configuration(udev, 1); if (err) { LOG_ERR("Failed to configure new device with address %u", udev->addr); diff --git a/subsys/usb/host/usbh_device.h b/subsys/usb/host/usbh_device.h index 5c5637fb0bee8..224bd2674d8f3 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 */ @@ -36,6 +35,19 @@ int usbh_device_interface_set(struct usb_device *const udev, const uint8_t iface, const uint8_t alt, const bool dry); +/* Get root USB device */ +struct usb_device *usbh_device_get_root(struct usbh_context *const ctx); + +/* Check if USB device is root */ +bool usbh_device_is_root(struct usbh_context *const ctx, + struct usb_device *const udev); + +/* Connect a new USB device */ +struct usb_device *usbh_connect_device(struct usbh_context *const ctx, + uint8_t device_speed); +/* Disconnect USB device */ +void usbh_disconnect_device(struct usbh_context *ctx, struct usb_device *udev); + /* Wrappers around to avoid glue UHC calls. */ static inline struct uhc_transfer *usbh_xfer_alloc(struct usb_device *udev, const uint8_t ep, diff --git a/subsys/usb/host/usbh_host.h b/subsys/usb/host/usbh_host.h index 1f04a72cdd019..50fd4af08745e 100644 --- a/subsys/usb/host/usbh_host.h +++ b/subsys/usb/host/usbh_host.h @@ -1,6 +1,5 @@ /* - * Copyright (c) 2025 Nordic Semiconductor ASA - * + * SPDX-FileCopyrightText: Copyright Nordic Semiconductor ASA * SPDX-License-Identifier: Apache-2.0 */ diff --git a/subsys/usb/host/usbh_internal.h b/subsys/usb/host/usbh_internal.h index 8fa8e7d48e19c..21c99fc1d0e08 100644 --- a/subsys/usb/host/usbh_internal.h +++ b/subsys/usb/host/usbh_internal.h @@ -1,6 +1,5 @@ /* - * Copyright (c) 2022 Nordic Semiconductor ASA - * + * SPDX-FileCopyrightText: Copyright Nordic Semiconductor ASA * SPDX-License-Identifier: Apache-2.0 */ diff --git a/subsys/usb/host/usbh_shell.c b/subsys/usb/host/usbh_shell.c index 2832e644d081c..d4990add5f5cc 100644 --- a/subsys/usb/host/usbh_shell.c +++ b/subsys/usb/host/usbh_shell.c @@ -1,6 +1,5 @@ /* - * Copyright (c) 2022 Nordic Semiconductor ASA - * + * SPDX-FileCopyrightText: Copyright Nordic Semiconductor ASA * SPDX-License-Identifier: Apache-2.0 */ diff --git a/subsys/usb/host/usbip.c b/subsys/usb/host/usbip.c index fc6072c65bada..69fe6fbe96d14 100644 --- a/subsys/usb/host/usbip.c +++ b/subsys/usb/host/usbip.c @@ -1,6 +1,5 @@ /* - * Copyright (c) 2024 Nordic Semiconductor ASA - * + * SPDX-FileCopyrightText: Copyright Nordic Semiconductor ASA * SPDX-License-Identifier: Apache-2.0 */ diff --git a/subsys/usb/host/usbip.h b/subsys/usb/host/usbip.h index 5334b3f469150..f337b1bfd4673 100644 --- a/subsys/usb/host/usbip.h +++ b/subsys/usb/host/usbip.h @@ -1,6 +1,5 @@ /* - * Copyright (c) 2021 Nordic Semiconductor ASA - * + * SPDX-FileCopyrightText: Copyright Nordic Semiconductor ASA * SPDX-License-Identifier: Apache-2.0 */ 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..2a0ba4108fd1d --- /dev/null +++ b/tests/subsys/usb/host/class_api/CMakeLists.txt @@ -0,0 +1,12 @@ +# 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_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..273319f67befb --- /dev/null +++ b/tests/subsys/usb/host/class_api/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/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..95557a41be572 --- /dev/null +++ b/tests/subsys/usb/host/class_api/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/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..1b0f07ead4b33 --- /dev/null +++ b/tests/subsys/usb/host/class_api/boards/qemu_cortex_m3.overlay @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: Copyright 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..aa47a0cc3d3b1 --- /dev/null +++ b/tests/subsys/usb/host/class_api/prj.conf @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: Copyright 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..23774e88f0280 --- /dev/null +++ b/tests/subsys/usb/host/class_api/src/main.c @@ -0,0 +1,277 @@ +/* + * SPDX-FileCopyrightText: Copyright Nordic Semiconductor ASA + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include "usbh_ch9.h" +#include "usbh_class_api.h" +#include "usbh_desc.h" +#include "usbh_host.h" +#include "usbh_class.h" + +#include "test_descriptor.h" + +#include +LOG_MODULE_REGISTER(usb_test, LOG_LEVEL_DBG); + +USBH_CONTROLLER_DEFINE(uhs_ctx, DEVICE_DT_GET(DT_NODELABEL(zephyr_uhc0))); + +#define TEST_VID 0x2FE3 +#define TEST_PID 0x0002 + +static const uint8_t test_hub_descriptor[] = { + TEST_HUB_DESCRIPTOR +}; + +struct usb_device test_udev = { + .cfg_desc = (const void *)test_hub_descriptor, +}; + +/* Private class data, here just an integer but usually a custom struct. */ +struct test_class_priv { + enum { + /* Test value stored before the class is initialized */ + TEST_CLASS_PRIV_INACTIVE, + /* Test value stored after the class is initialized */ + TEST_CLASS_PRIV_IDLE, + /* Test value stored after the class is probed */ + TEST_CLASS_PRIV_ENABLED, + /* Test value stored after the class is initialized */ + TEST_CLASS_PRIV_INITIALIZED, + } state; +}; + +static struct test_class_priv test_class_priv = { + .state = TEST_CLASS_PRIV_INACTIVE, +}; + +static int test_class_init(struct usbh_class_data *const c_data, + struct usbh_context *const uhs_ctx) +{ + struct test_class_priv *priv = c_data->priv; + + LOG_DBG("initializing %p, priv value 0x%x", c_data, *priv); + + zassert_equal(priv->state, TEST_CLASS_PRIV_INACTIVE, + "Class should be initialized only once"); + + priv->state = TEST_CLASS_PRIV_IDLE; + + return 0; +} + +static int test_class_completion_cb(struct usbh_class_data *const c_data, + struct uhc_transfer *const xfer) +{ + struct test_class_priv *priv = c_data->priv; + + LOG_DBG("completion callback for %p, transfer %p", c_data, xfer); + + zassert_equal(priv->state, TEST_CLASS_PRIV_ENABLED); + + return -ENOTSUP; +} + +static int test_class_probe(struct usbh_class_data *const c_data, + struct usb_device *const udev, + const uint8_t iface) +{ + struct test_class_priv *priv = c_data->priv; + const struct usb_desc_header *desc = (const struct usb_desc_header *)test_hub_descriptor; + const void *const desc_end = test_hub_descriptor + sizeof(test_hub_descriptor); + const struct usb_if_descriptor *if_desc; + + zassert_equal(priv->state, TEST_CLASS_PRIV_IDLE); + + desc = usbh_desc_get_by_iface(desc, desc_end, iface); + if (desc == NULL) { + return -ENOENT; + } + + if (desc->bDescriptorType != USB_DESC_INTERFACE) { + return -ENOTSUP; + } + + if_desc = (const 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 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, +}; + +struct usbh_class_filter test_filters[] = { + { + .class = 9, + .sub = 0, + .proto = 1, + .flags = USBH_CLASS_MATCH_CODE_TRIPLE, + }, + { + .vid = TEST_VID, + .pid = TEST_PID, + .flags = USBH_CLASS_MATCH_VID_PID, + }, + {0}, +}; + +struct usbh_class_filter test_empty_filters[] = { + {0}, +}; + +/* Define a class used in the tests */ +USBH_DEFINE_CLASS(test_class, &test_class_api, &test_class_priv, test_filters); + +ZTEST(host_class, test_class_matchiing) +{ + struct usb_device *udev = &test_udev; + const struct usb_desc_header *desc = usbh_desc_get_cfg(udev); + const void *const desc_end = usbh_desc_get_cfg_end(udev); + struct usbh_class_filter info_ok = { + .vid = TEST_VID, + .pid = TEST_PID, + }; + struct usbh_class_filter info_wrong_vid = { + .vid = TEST_VID + 1, + .pid = TEST_PID, + }; + struct usbh_class_filter info_wrong_pid = { + .vid = TEST_VID, + .pid = TEST_PID + 1, + }; + int ret; + + zassert(usbh_class_is_matching(test_filters, &info_ok), + "Filtering on valid VID:PID should match"); + + zassert(usbh_class_is_matching(NULL, &info_ok), + "Filtering on NULL rules should match"); + + zassert(!usbh_class_is_matching(test_empty_filters, &info_ok), + "Filtering on NULL rules should not match"); + + zassert(!usbh_class_is_matching(test_filters, &info_wrong_vid), + "Filtering on invalid VID only should not match"); + + zassert(!usbh_class_is_matching(test_filters, &info_wrong_pid), + "Filtering on invalid PID only should not match"); + + desc = usbh_desc_get_next_function(desc, desc_end); + zassert_not_null(desc, "There should be at least a function descriptor"); + + ret = usbh_desc_get_iface_info(desc, &info_ok, 0); + zassert_ok(ret, "Expecting the class info to be found"); + + ret = usbh_desc_get_iface_info(desc, &info_wrong_pid, 0); + zassert_ok(ret, "Expecting the class info to be found"); + + ret = usbh_desc_get_iface_info(desc, &info_wrong_vid, 0); + zassert_ok(ret, "Expecting the class info to be found"); + + zassert(usbh_class_is_matching(test_filters, &info_ok), + "Filteringon valid VID:PID and code triple should match"); + + zassert(usbh_class_is_matching(test_filters, &info_wrong_vid), + "Filtering on code triple only should match"); + + zassert(usbh_class_is_matching(test_filters, &info_wrong_pid), + "Filtering on code triple only should match"); +} + +ZTEST(host_class, test_class_fake_device) +{ + struct usbh_class_data *c_data = test_class.c_data; + struct usb_device *udev = &test_udev; + struct test_class_priv *priv = c_data->priv; + int ret; + + zassert_equal(priv->state, TEST_CLASS_PRIV_IDLE, + "The class should have been initialized by usbh_init()"); + + ret = usbh_class_probe(c_data, udev, 1); + zassert_equal(ret, -ENOENT, "There is no interface 1 so should be rejected"); + zassert_equal(priv->state, TEST_CLASS_PRIV_IDLE, + "The class should not be enabled if probing failed"); + + ret = usbh_class_probe(c_data, udev, 0); + zassert_ok(ret, "The interface 0 should match in this example class (%s)", strerror(-ret)); + zassert_equal(priv->state, TEST_CLASS_PRIV_ENABLED, + "The class should be enabled if probing succeeded"); + + ret = usbh_class_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..dc378b07dd152 --- /dev/null +++ b/tests/subsys/usb/host/common/include/test_descriptor.h @@ -0,0 +1,85 @@ +/* + * SPDX-FileCopyrightText: Copyright Nordic Semiconductor ASA + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_TEST_USB_HOST_SAMPLE_DESC_H_ +#define ZEPHYR_TEST_USB_HOST_SAMPLE_DESC_H_ + +#include +#include + +#define _LE16(n) ((n) & 0xff), ((n) >> 8) + +/* bInterfaceClass of these descriptors */ +#define USB_HUB_CLASSCODE 0x09 + +/* + * Obtained with lsusb then verified against the + * USB 2.0 standard's sample HUB descriptor + */ + +#define TEST_HUB_DEVICE_DESCRIPTOR \ + 18, /* bLength */ \ + 1, /* bDescriptorType */ \ + _LE16(0x0200), /* bcdUSB */ \ + 0x09, /* bDeviceClass */ \ + 0x00, /* bDeviceSubClass */ \ + 0x02, /* bDeviceProtocol */ \ + 64, /* bMaxPacketSize0 */ \ + _LE16(0x0bda), /* idVendor */ \ + _LE16(0x5411), /* idProduct */ \ + _LE16(0x0001), /* bcdDevice */ \ + 0, /* iManufacturer */ \ + 0, /* iProduct */ \ + 0, /* iSerial */ \ + 1, /* bNumConfigurations */ + +#define TEST_HUB_CONFIG_DESCRIPTOR \ + 9, /* bLength */ \ + 2, /* bDescriptorType */ \ + _LE16(0x0029), /* wTotalLength */ \ + 1, /* bNumInterfaces */ \ + 1, /* bConfigurationValue */ \ + 0, /* iConfiguration */ \ + 0xe0, /* bmAttributes */ \ + 0, /* MaxPower */ + +#define TEST_HUB_INTERFACE_ALT0_DESCRIPTOR \ + 9, /* bLength */ \ + 4, /* bDescriptorType */ \ + 0, /* bInterfaceNumber */ \ + 0, /* bAlternateSetting */ \ + 1, /* bNumEndpoints */ \ + 9, /* bInterfaceClass */ \ + 0, /* bInterfaceSubClass */ \ + 1, /* bInterfaceProtocol */ \ + 0, /* iInterface */ + +#define TEST_HUB_INTERFACE_ALT1_DESCRIPTOR \ + 9, /* bLength */ \ + 4, /* bDescriptorType */ \ + 0, /* bInterfaceNumber */ \ + 1, /* bAlternateSetting */ \ + 1, /* bNumEndpoints */ \ + 9, /* bInterfaceClass */ \ + 0, /* bInterfaceSubClass */ \ + 2, /* bInterfaceProtocol */ \ + 0, /* iInterface */ + +#define TEST_HUB_ENDPOINT_DESCRIPTOR \ + 7, /* bLength */ \ + 5, /* bDescriptorType */ \ + 0x81, /* bEndpointAddress */ \ + 0x03, /* bmAttributes */ \ + _LE16(1), /* wMaxPacketSize */ \ + 12, /* bInterval */ + +#define TEST_HUB_DESCRIPTOR \ + TEST_HUB_CONFIG_DESCRIPTOR \ + TEST_HUB_INTERFACE_ALT0_DESCRIPTOR \ + TEST_HUB_ENDPOINT_DESCRIPTOR \ + TEST_HUB_INTERFACE_ALT1_DESCRIPTOR \ + TEST_HUB_ENDPOINT_DESCRIPTOR + +#endif /* ZEPHYR_TEST_USB_HOST_SAMPLE_DESC_H_ */ diff --git a/tests/subsys/usb/host/desc/CMakeLists.txt b/tests/subsys/usb/host/desc/CMakeLists.txt new file mode 100644 index 0000000000000..2a0ba4108fd1d --- /dev/null +++ b/tests/subsys/usb/host/desc/CMakeLists.txt @@ -0,0 +1,12 @@ +# 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_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..273319f67befb --- /dev/null +++ b/tests/subsys/usb/host/desc/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/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..95557a41be572 --- /dev/null +++ b/tests/subsys/usb/host/desc/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/desc/boards/qemu_cortex_m3.overlay b/tests/subsys/usb/host/desc/boards/qemu_cortex_m3.overlay new file mode 100644 index 0000000000000..1b0f07ead4b33 --- /dev/null +++ b/tests/subsys/usb/host/desc/boards/qemu_cortex_m3.overlay @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: Copyright 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..aa47a0cc3d3b1 --- /dev/null +++ b/tests/subsys/usb/host/desc/prj.conf @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: Copyright 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..c5fdeae0dc6ae --- /dev/null +++ b/tests/subsys/usb/host/desc/src/main.c @@ -0,0 +1,73 @@ +/* + * SPDX-FileCopyrightText: Copyright Nordic Semiconductor ASA + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include "usbh_desc.h" + +#include "test_descriptor.h" + +static const uint8_t test_hub_descriptor[] = {TEST_HUB_DESCRIPTOR}; + +static const uint8_t test_hub_config_descriptor[] = {TEST_HUB_CONFIG_DESCRIPTOR}; +static const uint8_t test_hub_interface_alt0_descriptor[] = {TEST_HUB_INTERFACE_ALT0_DESCRIPTOR}; +static const uint8_t test_hub_interface_alt1_descriptor[] = {TEST_HUB_INTERFACE_ALT1_DESCRIPTOR}; +static const uint8_t test_hub_endpoint_descriptor[] = {TEST_HUB_ENDPOINT_DESCRIPTOR}; + +ZTEST(host_desc, test_desc_get_by_type) +{ + const uint8_t *const start_addr = test_hub_descriptor; + const uint8_t *const end_addr = test_hub_descriptor + sizeof(test_hub_descriptor); + const struct usb_desc_header *desc; + + /* From "test_descriptor.h": + * #0 TEST_HUB_CONFIG_DESCRIPTOR + * #1 TEST_HUB_INTERFACE_ALT0_DESCRIPTOR + * #2 TEST_HUB_ENDPOINT_DESCRIPTOR + * #3 TEST_HUB_INTERFACE_ALT1_DESCRIPTOR + * #4 TEST_HUB_ENDPOINT_DESCRIPTOR + */ + desc = (const struct usb_desc_header *)start_addr; + + /* desc at #0 TEST_HUB_CONFIG_DESCRIPTOR */ + + zassert_mem_equal(desc, test_hub_config_descriptor, + sizeof(test_hub_config_descriptor), + "needs to be able to reach the config descriptor"); + + /* desc at #0 TEST_HUB_CONFIG_DESCRIPTOR */ + + desc = usbh_desc_get_next(desc, end_addr); + zassert_not_null(desc); + zassert_mem_equal(desc, test_hub_interface_alt0_descriptor, + sizeof(test_hub_interface_alt0_descriptor), + "needs to return the first match (interface 0 alt 1) only"); + + /* desc at #1 TEST_HUB_INTERFACE_ALT0_DESCRIPTOR */ + + desc = usbh_desc_get_next_alt_setting(desc, end_addr); + zassert_not_null(desc); + zassert_mem_equal(desc, test_hub_interface_alt1_descriptor, + sizeof(test_hub_interface_alt1_descriptor), + "needs to return the interface 0 alt 1 descriptor"); + + /* desc at #3 TEST_HUB_INTERFACE_ALT1_DESCRIPTOR */ + + desc = usbh_desc_get_next(desc, end_addr); + zassert_not_null(desc); + zassert_mem_equal(desc, test_hub_endpoint_descriptor, + sizeof(test_hub_endpoint_descriptor), + "needs to return the last descriptor (endpoint)"); + + /* desc at #4 TEST_HUB_ENDPOINT_DESCRIPTOR */ + + desc = usbh_desc_get_next(desc, end_addr); + zassert_is_null(desc, + "needs to return NULL when running past the end"); +} + +ZTEST_SUITE(host_desc, NULL, NULL, NULL, NULL, NULL); diff --git a/tests/subsys/usb/host/desc/testcase.yaml b/tests/subsys/usb/host/desc/testcase.yaml new file mode 100644 index 0000000000000..3eef037a273dc --- /dev/null +++ b/tests/subsys/usb/host/desc/testcase.yaml @@ -0,0 +1,9 @@ +tests: + usb.host.desc: + platform_allow: + - native_sim + - native_sim/native/64 + - qemu_cortex_m3 + integration_platforms: + - native_sim + tags: usb diff --git a/tests/subsys/usb/uvc/CMakeLists.txt b/tests/subsys/usb/uvc/CMakeLists.txt new file mode 100644 index 0000000000000..ff98cb58e5977 --- /dev/null +++ b/tests/subsys/usb/uvc/CMakeLists.txt @@ -0,0 +1,13 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(uvc_uvb) + +# This sample cannot use the common USB sample infrastructure as we do not want +# to initialize the board default `zephyr_udc0` but instead an unrelated `virtual_udc0`. +#include(${ZEPHYR_BASE}/samples/subsys/usb/common/common.cmake) + +FILE(GLOB app_sources src/*.c) +target_sources(app PRIVATE ${app_sources}) +zephyr_include_directories(${ZEPHYR_BASE}/subsys/usb/host) diff --git a/tests/subsys/usb/uvc/app.overlay b/tests/subsys/usb/uvc/app.overlay new file mode 100644 index 0000000000000..8be25b7834e28 --- /dev/null +++ b/tests/subsys/usb/uvc/app.overlay @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: Copyright Nordic Semiconductor ASA + * SPDX-License-Identifier: Apache-2.0 + */ + +/ { + virtual_uhc0: uhc-virtual { + compatible = "zephyr,uhc-virtual"; + maximum-speed = "full-speed"; + + virtual_udc0: udc-virtual { + compatible = "zephyr,udc-virtual"; + num-bidir-endpoints = <4>; + maximum-speed = "full-speed"; + }; + }; + + uvc_device: uvc-device { + compatible = "zephyr,uvc-device"; + }; + + uvc_host: uvc-host { + compatible = "zephyr,uvc-host"; + }; +}; diff --git a/tests/subsys/usb/uvc/prj.conf b/tests/subsys/usb/uvc/prj.conf new file mode 100644 index 0000000000000..ed2322f383032 --- /dev/null +++ b/tests/subsys/usb/uvc/prj.conf @@ -0,0 +1,15 @@ +CONFIG_LOG=y +CONFIG_UDC_BUF_POOL_SIZE=2048 +CONFIG_UDC_DRIVER_LOG_LEVEL_INF=y +CONFIG_UHC_DRIVER_LOG_LEVEL_INF=y +CONFIG_USBD_VIDEO_CLASS=y +CONFIG_USBD_VIDEO_LOG_LEVEL_INF=y +CONFIG_USBH_LOG_LEVEL_INF=y +CONFIG_USBH_VIDEO_CLASS=y +CONFIG_USBH_VIDEO_LOG_LEVEL_INF=y +CONFIG_USB_DEVICE_STACK_NEXT=y +CONFIG_USB_HOST_STACK=y +CONFIG_UVB_LOG_LEVEL_INF=y +CONFIG_VIDEO=y +CONFIG_VIDEO_LOG_LEVEL_INF=y +CONFIG_ZTEST=y diff --git a/tests/subsys/usb/uvc/src/main.c b/tests/subsys/usb/uvc/src/main.c new file mode 100644 index 0000000000000..f97cd8f88d582 --- /dev/null +++ b/tests/subsys/usb/uvc/src/main.c @@ -0,0 +1,155 @@ +/* + * SPDX-FileCopyrightText: Copyright Nordic Semiconductor ASA + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include + +#include "../../../../../drivers/video/video_common.h" + +LOG_MODULE_REGISTER(app, LOG_LEVEL_INF); + +/* + * Minimal USB Host initialization. + */ + +USBH_CONTROLLER_DEFINE(app_usbh, + DEVICE_DT_GET(DT_NODELABEL(virtual_uhc0))); + +struct usbh_context *app_usbh_init_controller(void) +{ + int err; + + err = usbh_init(&app_usbh); + if (err) { + LOG_ERR("Failed to initialize host support"); + return NULL; + } + + return &app_usbh; +} + +USBD_DEVICE_DEFINE(app_usbd, DEVICE_DT_GET(DT_NODELABEL(virtual_udc0)), 0x2fe3, 0x9999); +USBD_DESC_LANG_DEFINE(app_lang); +USBD_DESC_MANUFACTURER_DEFINE(app_mfr, "Nordic"); +USBD_DESC_PRODUCT_DEFINE(app_product, "Virtual UVC device"); +USBD_DESC_CONFIG_DEFINE(fs_cfg_desc, "FS Configuration"); +USBD_CONFIGURATION_DEFINE(app_fs_config, 0, 200, &fs_cfg_desc); + +/* + * Minimal USB Device initialization. + */ + +struct usbd_context *app_usbd_init_device(void) +{ + int ret; + + ret = usbd_add_descriptor(&app_usbd, &app_lang); + if (ret != 0) { + LOG_ERR("Failed to initialize language descriptor (%d)", ret); + return NULL; + } + + ret = usbd_add_descriptor(&app_usbd, &app_mfr); + if (ret != 0) { + LOG_ERR("Failed to initialize manufacturer descriptor (%d)", ret); + return NULL; + } + + ret = usbd_add_descriptor(&app_usbd, &app_product); + if (ret != 0) { + LOG_ERR("Failed to initialize product descriptor (%d)", ret); + return NULL; + } + + ret = usbd_add_configuration(&app_usbd, USBD_SPEED_FS, &app_fs_config); + if (ret != 0) { + LOG_ERR("Failed to add Full-Speed configuration"); + return NULL; + } + + ret = usbd_register_all_classes(&app_usbd, USBD_SPEED_FS, 1, NULL); + if (ret != 0) { + LOG_ERR("Failed to add register classes"); + return NULL; + } + + usbd_device_set_code_triple(&app_usbd, USBD_SPEED_FS, USB_BCC_MISCELLANEOUS, 0x02, 0x01); + + usbd_self_powered(&app_usbd, USB_SCD_SELF_POWERED); + + ret = usbd_init(&app_usbd); + if (ret != 0) { + LOG_ERR("Failed to initialize device support"); + return NULL; + } + + return &app_usbd; +} + +/* + * Test using this host connected to this device via UVB + */ + +const struct video_format test_formats[] = { + {.pixelformat = VIDEO_PIX_FMT_YUYV, .width = 640, .height = 480}, + {.pixelformat = VIDEO_PIX_FMT_YUYV, .width = 320, .height = 240}, + {.pixelformat = VIDEO_PIX_FMT_YUYV, .width = 160, .height = 120}, +}; + +const struct device *const uvc_dev = DEVICE_DT_GET(DT_NODELABEL(uvc_device)); +const struct device *const video_dev = DEVICE_DT_GET(DT_CHOSEN(zephyr_camera)); + +ZTEST(usb_uvc, test_virtual_device_virtual_host) +{ + struct usbd_context *usbd_ctx; + struct usbh_context *usbh_ctx; + int ret; + + uvc_set_video_dev(uvc_dev, video_dev); + + for (size_t i = 0; i < ARRAY_SIZE(test_formats); i++) { + struct video_format fmt = test_formats[i]; + + ret = video_estimate_fmt_size(&fmt); + zassert_ok(ret); + + ret = uvc_add_format(uvc_dev, &fmt); + zassert_ok(ret); + } + + usbd_ctx = app_usbd_init_device(); + zassert_not_null(usbd_ctx, "USB device initialization must succeed"); + + usbh_ctx = app_usbh_init_controller(); + zassert_not_null(usbh_ctx, "USB host initialization must succeed"); + + ret = usbd_enable(usbd_ctx); + zassert_ok(ret, "USB device error code %d must be 0", ret); + + ret = usbh_enable(usbh_ctx); + zassert_ok(ret, "USB host enable error code %d must be 0", ret); + + k_sleep(K_MSEC(500)); + + /* TODO: test the video devices here. */ + + ret = usbh_disable(usbh_ctx); + zassert_ok(ret, "USB host disable error code %d must be 0", ret); + + ret = usbd_disable(usbd_ctx); + zassert_ok(ret, "USB device disable error code %d must be 0", ret); + + ret = usbh_shutdown(usbh_ctx); + zassert_ok(ret, "USB host shutdown error code %d must be 0", ret); + + ret = usbd_shutdown(usbd_ctx); + zassert_ok(ret, "USB device shutdown error code %d must be 0", ret); +} + +ZTEST_SUITE(usb_uvc, NULL, NULL, NULL, NULL, NULL); diff --git a/tests/subsys/usb/uvc/testcase.yaml b/tests/subsys/usb/uvc/testcase.yaml new file mode 100644 index 0000000000000..a6ab1f92033ad --- /dev/null +++ b/tests/subsys/usb/uvc/testcase.yaml @@ -0,0 +1,11 @@ +tests: + usb.uvc: + tags: + - usb + - video + platform_allow: + - native_sim/native + integration_platforms: + - native_sim/native + extra_args: + - platform:native_sim/native:SNIPPET="video-sw-generator"