diff --git a/dts/bindings/usb/zephyr,uvc-host.yaml b/dts/bindings/usb/zephyr,uvc-host.yaml new file mode 100644 index 0000000000000..fa720c5a64301 --- /dev/null +++ b/dts/bindings/usb/zephyr,uvc-host.yaml @@ -0,0 +1,15 @@ +# Copyright (c) 2025 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +description: | + USB Video Class (UVC) host instance. + + Each UVC instance added to the USB Host Controller (UHC) node will be visible + as a new camera from Zephyr point of view. + + as soon as a camera is connected to USB this device will be usable by the application as a + video device, following the video API. + +compatible: "zephyr,uvc-host" + +include: base.yaml diff --git a/include/zephyr/usb/class/usb_uvc.h b/include/zephyr/usb/class/usb_uvc.h new file mode 100644 index 0000000000000..b737efc5c7e2d --- /dev/null +++ b/include/zephyr/usb/class/usb_uvc.h @@ -0,0 +1,488 @@ +/* + * Copyright (c) 2025 tinyVision.ai Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file + * @brief USB Video Class private header + * + * Header follows below documentation: + * - USB Device Class Definition for Video Devices (Revision 1.5) + * + * Additional documentation considered a part of UVC 1.5: + * - USB Device Class Definition for Video Devices: Uncompressed Payload (Revision 1.5) + * - USB Device Class Definition for Video Devices: Motion-JPEG Payload (Revision 1.5) + */ + +#ifndef ZEPHYR_INCLUDE_USBD_CLASS_UVC_H_ +#define ZEPHYR_INCLUDE_USBD_CLASS_UVC_H_ + +#include + +/* Video Class-Specific Request Codes */ +#define UVC_SET_CUR 0x01 +#define UVC_GET_CUR 0x81 +#define UVC_GET_MIN 0x82 +#define UVC_GET_MAX 0x83 +#define UVC_GET_RES 0x84 +#define UVC_GET_LEN 0x85 +#define UVC_GET_INFO 0x86 +#define UVC_GET_DEF 0x87 + +/* Flags announcing which controls are supported */ +#define UVC_INFO_SUPPORTS_GET BIT(0) +#define UVC_INFO_SUPPORTS_SET BIT(1) + +/* Request Error Code Control */ +#define UVC_ERR_NOT_READY 0x01 +#define UVC_ERR_WRONG_STATE 0x02 +#define UVC_ERR_OUT_OF_RANGE 0x04 +#define UVC_ERR_INVALID_UNIT 0x05 +#define UVC_ERR_INVALID_CONTROL 0x06 +#define UVC_ERR_INVALID_REQUEST 0x07 +#define UVC_ERR_INVALID_VALUE_WITHIN_RANGE 0x08 +#define UVC_ERR_UNKNOWN 0xff + +/* Video and Still Image Payload Headers */ +#define UVC_BMHEADERINFO_FRAMEID BIT(0) +#define UVC_BMHEADERINFO_END_OF_FRAME BIT(1) +#define UVC_BMHEADERINFO_HAS_PRESENTATIONTIME BIT(2) +#define UVC_BMHEADERINFO_HAS_SOURCECLOCK BIT(3) +#define UVC_BMHEADERINFO_PAYLOAD_SPECIFIC_BIT BIT(4) +#define UVC_BMHEADERINFO_STILL_IMAGE BIT(5) +#define UVC_BMHEADERINFO_ERROR BIT(6) +#define UVC_BMHEADERINFO_END_OF_HEADER BIT(7) + +/* Video Interface Subclass Codes */ +#define UVC_SC_VIDEOCONTROL 0x01 +#define UVC_SC_VIDEOSTREAMING 0x02 +#define UVC_SC_VIDEO_INTERFACE_COLLECTION 0x03 + +/* Video Class-Specific Video Control Interface Descriptor Subtypes */ +#define UVC_VC_DESCRIPTOR_UNDEFINED 0x00 +#define UVC_VC_HEADER 0x01 +#define UVC_VC_INPUT_TERMINAL 0x02 +#define UVC_VC_OUTPUT_TERMINAL 0x03 +#define UVC_VC_SELECTOR_UNIT 0x04 +#define UVC_VC_PROCESSING_UNIT 0x05 +#define UVC_VC_EXTENSION_UNIT 0x06 +#define UVC_VC_ENCODING_UNIT 0x07 + +/* Video Class-Specific Video Stream Interface Descriptor Subtypes */ +#define UVC_VS_UNDEFINED 0x00 +#define UVC_VS_INPUT_HEADER 0x01 +#define UVC_VS_OUTPUT_HEADER 0x02 +#define UVC_VS_STILL_IMAGE_FRAME 0x03 +#define UVC_VS_FORMAT_UNCOMPRESSED 0x04 +#define UVC_VS_FRAME_UNCOMPRESSED 0x05 +#define UVC_VS_FORMAT_MJPEG 0x06 +#define UVC_VS_FRAME_MJPEG 0x07 +#define UVC_VS_FORMAT_MPEG2TS 0x0A +#define UVC_VS_FORMAT_DV 0x0C +#define UVC_VS_COLORFORMAT 0x0D +#define UVC_VS_FORMAT_FRAME_BASED 0x10 +#define UVC_VS_FRAME_FRAME_BASED 0x11 +#define UVC_VS_FORMAT_STREAM_BASED 0x12 +#define UVC_VS_FORMAT_H264 0x13 +#define UVC_VS_FRAME_H264 0x14 +#define UVC_VS_FORMAT_H264_SIMULCAST 0x15 +#define UVC_VS_FORMAT_VP8 0x16 +#define UVC_VS_FRAME_VP8 0x17 +#define UVC_VS_FORMAT_VP8_SIMULCAST 0x18 + +/* Video Class-Specific Endpoint Descriptor Subtypes */ +#define UVC_EP_UNDEFINED 0x00 +#define UVC_EP_GENERAL 0x01 +#define UVC_EP_ENDPOINT 0x02 +#define UVC_EP_INTERRUPT 0x03 + +/* USB Terminal Types */ +#define UVC_TT_VENDOR_SPECIFIC 0x0100 +#define UVC_TT_STREAMING 0x0101 + +/* Input Terminal Types */ +#define UVC_ITT_VENDOR_SPECIFIC 0x0200 +#define UVC_ITT_CAMERA 0x0201 +#define UVC_ITT_MEDIA_TRANSPORT_INPUT 0x0202 + +/* Output Terminal Types */ +#define UVC_OTT_VENDOR_SPECIFIC 0x0300 +#define UVC_OTT_DISPLAY 0x0301 +#define UVC_OTT_MEDIA_TRANSPORT_OUTPUT 0x0302 + +/* External Terminal Types */ +#define UVC_EXT_EXTERNAL_VENDOR_SPECIFIC 0x0400 +#define UVC_EXT_COMPOSITE_CONNECTOR 0x0401 +#define UVC_EXT_SVIDEO_CONNECTOR 0x0402 +#define UVC_EXT_COMPONENT_CONNECTOR 0x0403 + +/* VideoStreaming Interface Controls */ +#define UVC_VS_PROBE_CONTROL 0x01 +#define UVC_VS_COMMIT_CONTROL 0x02 +#define UVC_VS_STILL_PROBE_CONTROL 0x03 +#define UVC_VS_STILL_COMMIT_CONTROL 0x04 +#define UVC_VS_STILL_IMAGE_TRIGGER_CONTROL 0x05 +#define UVC_VS_STREAM_ERROR_CODE_CONTROL 0x06 +#define UVC_VS_GENERATE_KEY_FRAME_CONTROL 0x07 +#define UVC_VS_UPDATE_FRAME_SEGMENT_CONTROL 0x08 +#define UVC_VS_SYNCH_DELAY_CONTROL 0x09 + +/* VideoControl Interface Controls */ +#define UVC_VC_CONTROL_UNDEFINED 0x00 +#define UVC_VC_VIDEO_POWER_MODE_CONTROL 0x01 +#define UVC_VC_REQUEST_ERROR_CODE_CONTROL 0x02 + +/* Selector Unit Controls */ +#define UVC_SU_INPUT_SELECT_CONTROL 0x01 + +/* Camera Terminal Controls */ +#define UVC_CT_SCANNING_MODE_CONTROL 0x01 +#define UVC_CT_AE_MODE_CONTROL 0x02 +#define UVC_CT_AE_PRIORITY_CONTROL 0x03 +#define UVC_CT_EXPOSURE_TIME_ABS_CONTROL 0x04 +#define UVC_CT_EXPOSURE_TIME_REL_CONTROL 0x05 +#define UVC_CT_FOCUS_ABS_CONTROL 0x06 +#define UVC_CT_FOCUS_REL_CONTROL 0x07 +#define UVC_CT_FOCUS_AUTO_CONTROL 0x08 +#define UVC_CT_IRIS_ABS_CONTROL 0x09 +#define UVC_CT_IRIS_REL_CONTROL 0x0A +#define UVC_CT_ZOOM_ABS_CONTROL 0x0B +#define UVC_CT_ZOOM_REL_CONTROL 0x0C +#define UVC_CT_PANTILT_ABS_CONTROL 0x0D +#define UVC_CT_PANTILT_REL_CONTROL 0x0E +#define UVC_CT_ROLL_ABS_CONTROL 0x0F +#define UVC_CT_ROLL_REL_CONTROL 0x10 +#define UVC_CT_PRIVACY_CONTROL 0x11 +#define UVC_CT_FOCUS_SIMPLE_CONTROL 0x12 +#define UVC_CT_WINDOW_CONTROL 0x13 +#define UVC_CT_REGION_OF_INTEREST_CONTROL 0x14 + +/* Processing Unit Controls */ +#define UVC_PU_BACKLIGHT_COMPENSATION_CONTROL 0x01 +#define UVC_PU_BRIGHTNESS_CONTROL 0x02 +#define UVC_PU_CONTRAST_CONTROL 0x03 +#define UVC_PU_GAIN_CONTROL 0x04 +#define UVC_PU_POWER_LINE_FREQUENCY_CONTROL 0x05 +#define UVC_PU_HUE_CONTROL 0x06 +#define UVC_PU_SATURATION_CONTROL 0x07 +#define UVC_PU_SHARPNESS_CONTROL 0x08 +#define UVC_PU_GAMMA_CONTROL 0x09 +#define UVC_PU_WHITE_BALANCE_TEMP_CONTROL 0x0A +#define UVC_PU_WHITE_BALANCE_TEMP_AUTO_CONTROL 0x0B +#define UVC_PU_WHITE_BALANCE_COMPONENT_CONTROL 0x0C +#define UVC_PU_WHITE_BALANCE_COMPONENT_AUTO_CONTROL 0x0D +#define UVC_PU_DIGITAL_MULTIPLIER_CONTROL 0x0E +#define UVC_PU_DIGITAL_MULTIPLIER_LIMIT_CONTROL 0x0F +#define UVC_PU_HUE_AUTO_CONTROL 0x10 +#define UVC_PU_ANALOG_VIDEO_STANDARD_CONTROL 0x11 +#define UVC_PU_ANALOG_LOCK_STATUS_CONTROL 0x12 +#define UVC_PU_CONTRAST_AUTO_CONTROL 0x13 + +/* Encoding Unit Controls */ +#define UVC_EU_SELECT_LAYER_CONTROL 0x01 +#define UVC_EU_PROFILE_TOOLSET_CONTROL 0x02 +#define UVC_EU_VIDEO_RESOLUTION_CONTROL 0x03 +#define UVC_EU_MIN_FRAME_INTERVAL_CONTROL 0x04 +#define UVC_EU_SLICE_MODE_CONTROL 0x05 +#define UVC_EU_RATE_CONTROL_MODE_CONTROL 0x06 +#define UVC_EU_AVERAGE_BITRATE_CONTROL 0x07 +#define UVC_EU_CPB_SIZE_CONTROL 0x08 +#define UVC_EU_PEAK_BIT_RATE_CONTROL 0x09 +#define UVC_EU_QUANTIZATION_PARAMS_CONTROL 0x0A +#define UVC_EU_SYNC_REF_FRAME_CONTROL 0x0B +#define UVC_EU_LTR_BUFFER_CONTROL 0x0C +#define UVC_EU_LTR_PICTURE_CONTROL 0x0D +#define UVC_EU_LTR_VALIDATION_CONTROL 0x0E +#define UVC_EU_LEVEL_IDC_LIMIT_CONTROL 0x0F +#define UVC_EU_SEI_PAYLOADTYPE_CONTROL 0x10 +#define UVC_EU_QP_RANGE_CONTROL 0x11 +#define UVC_EU_PRIORITY_CONTROL 0x12 +#define UVC_EU_START_OR_STOP_LAYER_CONTROL 0x13 +#define UVC_EU_ERROR_RESILIENCY_CONTROL 0x14 + +/* Extension Unit Controls */ +#define UVC_XU_BASE_CONTROL 0x00 + +/* Base GUID string present at the end of most GUID formats, preceded by the FourCC code */ +#define UVC_FORMAT_GUID(fourcc) fourcc "\x00\x00\x10\x00\x80\x00\x00\xaa\x00\x38\x9b\x71" + +struct uvc_if_descriptor { + uint8_t bLength; + uint8_t bDescriptorType; + uint8_t bDescriptorSubtype; +} __packed; + +struct uvc_control_header_descriptor { + uint8_t bLength; + uint8_t bDescriptorType; + uint8_t bDescriptorSubtype; + uint16_t bcdUVC; + uint16_t wTotalLength; + uint32_t dwClockFrequency; + uint8_t bInCollection; + uint8_t baInterfaceNr[1]; +} __packed; + +struct uvc_unit_descriptor { + uint8_t bLength; + uint8_t bDescriptorType; + uint8_t bDescriptorSubtype; + uint8_t bUnitID; +}; + +struct uvc_output_terminal_descriptor { + uint8_t bLength; + uint8_t bDescriptorType; + uint8_t bDescriptorSubtype; + uint8_t bTerminalID; + uint16_t wTerminalType; + uint8_t bAssocTerminal; + uint8_t bSourceID; + uint8_t iTerminal; +} __packed; + +struct uvc_camera_terminal_descriptor { + uint8_t bLength; + uint8_t bDescriptorType; + uint8_t bDescriptorSubtype; + uint8_t bTerminalID; + uint16_t wTerminalType; + uint8_t bAssocTerminal; + uint8_t iTerminal; + uint16_t wObjectiveFocalLengthMin; + uint16_t wObjectiveFocalLengthMax; + uint16_t wOcularFocalLength; + uint8_t bControlSize; + uint8_t bmControls[3]; +} __packed; + +struct uvc_selector_unit_descriptor { + uint8_t bLength; + uint8_t bDescriptorType; + uint8_t bDescriptorSubtype; + uint8_t bUnitID; + uint8_t bNrInPins; + uint8_t baSourceID[1]; + uint8_t iSelector; +} __packed; + +struct uvc_processing_unit_descriptor { + uint8_t bLength; + uint8_t bDescriptorType; + uint8_t bDescriptorSubtype; + uint8_t bUnitID; + uint8_t bSourceID; + uint16_t wMaxMultiplier; + uint8_t bControlSize; + uint8_t bmControls[3]; + uint8_t iProcessing; + uint8_t bmVideoStandards; +} __packed; + +struct uvc_encoding_unit_descriptor { + uint8_t bLength; + uint8_t bDescriptorType; + uint8_t bDescriptorSubtype; + uint8_t bUnitID; + uint8_t bSourceID; + uint8_t iEncoding; + uint8_t bControlSize; + uint8_t bmControls[3]; + uint8_t bmControlsRuntime[3]; +} __packed; + +struct uvc_extension_unit_descriptor { + uint8_t bLength; + uint8_t bDescriptorType; + uint8_t bDescriptorSubtype; + uint8_t bUnitID; + uint8_t guidExtensionCode[16]; + uint8_t bNumControls; + uint8_t bNrInPins; + uint8_t baSourceID[1]; + uint8_t bControlSize; + uint8_t bmControls[4]; + uint8_t iExtension; +} __packed; + +struct uvc_stream_header_descriptor { + uint8_t bLength; + uint8_t bDescriptorType; + uint8_t bDescriptorSubtype; + uint8_t bNumFormats; + uint16_t wTotalLength; + uint8_t bEndpointAddress; + uint8_t bmInfo; + uint8_t bTerminalLink; + uint8_t bStillCaptureMethod; + uint8_t bTriggerSupport; + uint8_t bTriggerUsage; + uint8_t bControlSize; +} __packed; + +struct uvc_frame_still_image_descriptor { + uint8_t bLength; + uint8_t bDescriptorType; + uint8_t bDescriptorSubtype; + uint8_t bEndpointAddress; + uint8_t bNumImageSizePatterns; + struct { + uint16_t wWidth; + uint16_t wHeight; + } n[1] __packed; + uint8_t bNumCompressionPattern; + uint8_t bCompression[1]; +} __packed; + +struct uvc_format_descriptor { + uint8_t bLength; + uint8_t bDescriptorType; + uint8_t bDescriptorSubtype; + uint8_t bFormatIndex; + uint8_t bNumFrameDescriptors; + /* Other fields depending on bDescriptorSubtype value */ +} __packed; + +struct uvc_format_uncomp_descriptor { + uint8_t bLength; + uint8_t bDescriptorType; + uint8_t bDescriptorSubtype; + uint8_t bFormatIndex; + uint8_t bNumFrameDescriptors; + uint8_t guidFormat[16]; + uint8_t bBitsPerPixel; + uint8_t bDefaultFrameIndex; + uint8_t bAspectRatioX; + uint8_t bAspectRatioY; + uint8_t bmInterlaceFlags; + uint8_t bCopyProtect; +} __packed; + +struct uvc_format_mjpeg_descriptor { + uint8_t bLength; + uint8_t bDescriptorType; + uint8_t bDescriptorSubtype; + uint8_t bFormatIndex; + uint8_t bNumFrameDescriptors; + uint8_t bmFlags; +#define UVC_MJPEG_FLAGS_FIXEDSIZESAMPLES (1 << 0) + uint8_t bDefaultFrameIndex; + uint8_t bAspectRatioX; + uint8_t bAspectRatioY; + uint8_t bmInterlaceFlags; + uint8_t bCopyProtect; +} __packed; + +struct uvc_frame_descriptor { + uint8_t bLength; + uint8_t bDescriptorType; + uint8_t bDescriptorSubtype; + uint8_t bFrameIndex; + uint8_t bmCapabilities; + uint16_t wWidth; + uint16_t wHeight; + uint32_t dwMinBitRate; + uint32_t dwMaxBitRate; + uint32_t dwMaxVideoFrameBufferSize; + uint32_t dwDefaultFrameInterval; + uint8_t bFrameIntervalType; + /* Other fields depending on bFrameIntervalType value */ +} __packed; + +struct uvc_frame_continuous_descriptor { + uint8_t bLength; + uint8_t bDescriptorType; + uint8_t bDescriptorSubtype; + uint8_t bFrameIndex; + uint8_t bmCapabilities; + uint16_t wWidth; + uint16_t wHeight; + uint32_t dwMinBitRate; + uint32_t dwMaxBitRate; + uint32_t dwMaxVideoFrameBufferSize; + uint32_t dwDefaultFrameInterval; + uint8_t bFrameIntervalType; + uint32_t dwMinFrameInterval; + uint32_t dwMaxFrameInterval; + uint32_t dwFrameIntervalStep; +} __packed; + +struct uvc_frame_discrete_descriptor { + uint8_t bLength; + uint8_t bDescriptorType; + uint8_t bDescriptorSubtype; + uint8_t bFrameIndex; + uint8_t bmCapabilities; + uint16_t wWidth; + uint16_t wHeight; + uint32_t dwMinBitRate; + uint32_t dwMaxBitRate; + uint32_t dwMaxVideoFrameBufferSize; + uint32_t dwDefaultFrameInterval; + uint8_t bFrameIntervalType; +#ifdef CONFIG_USBD_VIDEO_MAX_FRMIVAL + uint32_t dwFrameInterval[CONFIG_USBD_VIDEO_MAX_FRMIVAL]; +#else + uint32_t dwFrameInterval[1]; +#endif +} __packed; + +struct uvc_color_descriptor { + uint8_t bLength; + uint8_t bDescriptorType; + uint8_t bDescriptorSubtype; + uint8_t bColorPrimaries; + uint8_t bTransferCharacteristics; + uint8_t bMatrixCoefficients; +#define UVC_COLOR_BT709 1 +#define UVC_COLOR_BT470M 2 +#define UVC_COLOR_BT470BG 3 +#define UVC_COLOR_BT601 4 +#define UVC_COLOR_SMPTE170M 4 +#define UVC_COLOR_SMPTE240M 5 +#define UVC_COLOR_LINEAR 6 +#define UVC_COLOR_SRGB 7 +} __packed; + +struct uvc_probe { + uint16_t bmHint; + uint8_t bFormatIndex; + uint8_t bFrameIndex; + uint32_t dwFrameInterval; + uint16_t wKeyFrameRate; + uint16_t wPFrameRate; + uint16_t wCompQuality; + uint16_t wCompWindowSize; + uint16_t wDelay; + uint32_t dwMaxVideoFrameSize; + uint32_t dwMaxPayloadTransferSize; + uint32_t dwClockFrequency; + uint8_t bmFramingInfo; +#define UVC_BMFRAMING_INFO_FID BIT(0) +#define UVC_BMFRAMING_INFO_EOF BIT(1) +#define UVC_BMFRAMING_INFO_EOS BIT(2) + uint8_t bPreferedVersion; + uint8_t bMinVersion; + uint8_t bMaxVersion; + uint8_t bUsage; + uint8_t bBitDepthLuma; + uint8_t bmSettings; + uint8_t bMaxNumberOfRefFramesPlus1; + uint16_t bmRateControlModes; + uint64_t bmLayoutPerStream; +} __packed; + +/* This is a particular variant of this struct that is used by the Zephyr implementation. Other + * organization of the fields are allowed by the standard. + */ +struct uvc_payload_header { + uint8_t bHeaderLength; + uint8_t bmHeaderInfo; + uint32_t dwPresentationTime; /* optional */ + uint32_t scrSourceClockSTC; /* optional */ + uint16_t scrSourceClockSOF; /* optional */ +} __packed; + +#endif /* ZEPHYR_INCLUDE_USBD_CLASS_UVC_H_ */ diff --git a/include/zephyr/usb/usbh.h b/include/zephyr/usb/usbh.h index e2e6f3074000e..9f885683e373a 100644 --- a/include/zephyr/usb/usbh.h +++ b/include/zephyr/usb/usbh.h @@ -1,5 +1,6 @@ /* - * Copyright (c) 2022 Nordic Semiconductor ASA + * Copyright (c) 2025 Nordic Semiconductor ASA + * Copyright 2025 NXP * * SPDX-License-Identifier: Apache-2.0 */ @@ -33,6 +34,16 @@ extern "C" { * @{ */ +/** + * USB host support status + */ +struct usbh_status { + /** USB host support is initialized */ + unsigned int initialized : 1; + /** USB host support is enabled */ + unsigned int enabled : 1; +}; + /** * USB host support runtime context */ @@ -43,6 +54,8 @@ struct usbh_context { struct k_mutex mutex; /** Pointer to UHC device struct */ const struct device *dev; + /** Status of the USB host support */ + struct usbh_status status; /** USB device list */ sys_dlist_t udevs; /** USB root device */ @@ -60,47 +73,79 @@ struct usbh_context { .addr_ba = &ba_##device_name, \ } +struct usbh_class_data; + /** - * @brief USB Class Code triple + * @brief USB host class instance API */ -struct usbh_code_triple { - /** Device Class Code */ - uint8_t dclass; - /** Class Subclass Code */ - uint8_t sub; - /** Class Protocol Code */ - uint8_t proto; +struct usbh_class_api { + /** Host init handler, before any device is connected */ + int (*init)(struct usbh_class_data *const c_data, + struct usbh_context *const uhs_ctx); + /** Request completion event handler */ + int (*completion_cb)(struct usbh_class_data *const c_data, + struct uhc_transfer *const xfer); + /** Device connection handler */ + int (*probe)(struct usbh_class_data *const c_data, + struct usb_device *const udev, + const uint8_t iface); + /** Device removal handler */ + int (*removed)(struct usbh_class_data *const c_data); + /** Bus suspended handler */ + int (*suspended)(struct usbh_class_data *const c_data); + /** Bus resumed handler */ + int (*resumed)(struct usbh_class_data *const c_data); }; /** - * @brief USB host class data and class instance API + * @brief USB host class instance data */ struct usbh_class_data { - /** Class code supported by this instance */ - struct usbh_code_triple code; - - /** Initialization of the class implementation */ - /* int (*init)(struct usbh_context *const uhs_ctx); */ - /** Request completion event handler */ - int (*request)(struct usbh_context *const uhs_ctx, - struct uhc_transfer *const xfer, int err); - /** Device connected handler */ - int (*connected)(struct usbh_context *const uhs_ctx); - /** Device removed handler */ - int (*removed)(struct usbh_context *const uhs_ctx); - /** Bus remote wakeup handler */ - int (*rwup)(struct usbh_context *const uhs_ctx); - /** Bus suspended handler */ - int (*suspended)(struct usbh_context *const uhs_ctx); - /** Bus resumed handler */ - int (*resumed)(struct usbh_context *const uhs_ctx); + /** Name of the USB host class instance */ + const char *name; + /** Pointer to USB host stack context structure */ + struct usbh_context *uhs_ctx; + /** Pointer to USB device this class is used for */ + struct usb_device *udev; + /** Interface number for which this class matched */ + uint8_t ifnum; + /** Pointer to host support class API */ + struct usbh_class_api *api; + /** Pointer to private data */ + void *priv; }; /** + * @cond INTERNAL_HIDDEN + * + * Variables used by the USB host stack but not exposed to the class + * through the class API. */ -#define USBH_DEFINE_CLASS(name) \ - static STRUCT_SECTION_ITERABLE(usbh_class_data, name) +struct usbh_class_node { + /** Class information exposed to host class implementations (drivers). */ + struct usbh_class_data *const c_data; +}; +/* @endcond */ +/** + * @brief Define USB host support class data + * + * Macro defines class (function) data, as well as corresponding node + * structures used internally by the stack. + * + * @param[in] class_name Class name + * @param[in] class_api Pointer to struct usbh_class_api + * @param[in] class_priv Class private data + */ +#define USBH_DEFINE_CLASS(class_name, class_api, class_priv) \ + static struct usbh_class_data class_data_##class_name = { \ + .name = STRINGIFY(class_name), \ + .api = class_api, \ + .priv = class_priv, \ + }; \ + static STRUCT_SECTION_ITERABLE(usbh_class_node, class_name) = { \ + .c_data = &class_data_##class_name, \ + }; /** * @brief Initialize the USB host support; diff --git a/subsys/Kconfig b/subsys/Kconfig index 748ae9981db0e..fd508721e9f9b 100644 --- a/subsys/Kconfig +++ b/subsys/Kconfig @@ -54,6 +54,7 @@ source "subsys/task_wdt/Kconfig" source "subsys/testsuite/Kconfig" source "subsys/timing/Kconfig" source "subsys/tracing/Kconfig" +source "subsys/usb/common/Kconfig" source "subsys/usb/device/Kconfig" source "subsys/usb/device_next/Kconfig" source "subsys/usb/host/Kconfig" diff --git a/subsys/usb/CMakeLists.txt b/subsys/usb/CMakeLists.txt index 6b968a4bf894e..b8367352d06de 100644 --- a/subsys/usb/CMakeLists.txt +++ b/subsys/usb/CMakeLists.txt @@ -5,3 +5,4 @@ add_subdirectory_ifdef(CONFIG_USB_DEVICE_STACK device) add_subdirectory_ifdef(CONFIG_USB_DEVICE_STACK_NEXT device_next) add_subdirectory_ifdef(CONFIG_USB_HOST_STACK host) add_subdirectory_ifdef(CONFIG_USBC_STACK usb_c) +add_subdirectory(common) diff --git a/subsys/usb/common/CMakeLists.txt b/subsys/usb/common/CMakeLists.txt new file mode 100644 index 0000000000000..5e3a9f72bdb36 --- /dev/null +++ b/subsys/usb/common/CMakeLists.txt @@ -0,0 +1,5 @@ +# Copyright (c) 2025 Nordic Semiconductor +# SPDX-License-Identifier: Apache-2.0 + +zephyr_include_directories(include) +zephyr_library_sources_ifdef(CONFIG_USB_COMMON_UVC usb_common_uvc.c) diff --git a/subsys/usb/common/Kconfig b/subsys/usb/common/Kconfig new file mode 100644 index 0000000000000..56756d0fa6984 --- /dev/null +++ b/subsys/usb/common/Kconfig @@ -0,0 +1,7 @@ +# Copyright (c) 2025 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +config USB_COMMON_UVC + bool + help + Enable resources used by both UVC device and host 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..afa43c67e2bab --- /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 + +#include "usb_common_uvc.h" + +static const struct uvc_guid_quirk uvc_guid_quirks[] = { + { + .fourcc = VIDEO_PIX_FMT_YUYV, + .guid = UVC_FORMAT_GUID("YUY2"), + }, + { + .fourcc = VIDEO_PIX_FMT_GREY, + .guid = UVC_FORMAT_GUID("Y800"), + }, +}; + +static const struct uvc_control_map uvc_control_map_ct[] = { + { + .size = 1, + .bit = 1, + .selector = UVC_CT_AE_MODE_CONTROL, + .cid = VIDEO_CID_EXPOSURE_AUTO, + .type = UVC_CONTROL_UNSIGNED, + }, + { + .size = 1, + .bit = 2, + .selector = UVC_CT_AE_PRIORITY_CONTROL, + .cid = VIDEO_CID_EXPOSURE_AUTO_PRIORITY, + .type = UVC_CONTROL_UNSIGNED, + }, + { + .size = 4, + .bit = 3, + .selector = UVC_CT_EXPOSURE_TIME_ABS_CONTROL, + .cid = VIDEO_CID_EXPOSURE, + .type = UVC_CONTROL_UNSIGNED, + }, + { + .size = 2, + .bit = 5, + .selector = UVC_CT_FOCUS_ABS_CONTROL, + .cid = VIDEO_CID_FOCUS_ABSOLUTE, + .type = UVC_CONTROL_UNSIGNED, + }, + { + .size = 2, + .bit = 6, + .selector = UVC_CT_FOCUS_REL_CONTROL, + .cid = VIDEO_CID_FOCUS_RELATIVE, + .type = UVC_CONTROL_SIGNED, + }, + { + .size = 2, + .bit = 7, + .selector = UVC_CT_IRIS_ABS_CONTROL, + .cid = VIDEO_CID_IRIS_ABSOLUTE, + .type = UVC_CONTROL_UNSIGNED, + }, + { + .size = 1, + .bit = 8, + .selector = UVC_CT_IRIS_REL_CONTROL, + .cid = VIDEO_CID_IRIS_RELATIVE, + .type = UVC_CONTROL_SIGNED, + }, + { + .size = 2, + .bit = 9, + .selector = UVC_CT_ZOOM_ABS_CONTROL, + .cid = VIDEO_CID_ZOOM_ABSOLUTE, + .type = UVC_CONTROL_UNSIGNED, + }, + { + .size = 3, + .bit = 10, + .selector = UVC_CT_ZOOM_REL_CONTROL, + .cid = VIDEO_CID_ZOOM_RELATIVE, + .type = UVC_CONTROL_SIGNED, + }, +}; + +static const struct uvc_control_map uvc_control_map_pu[] = { + { + .size = 2, + .bit = 0, + .selector = UVC_PU_BRIGHTNESS_CONTROL, + .cid = VIDEO_CID_BRIGHTNESS, + .type = UVC_CONTROL_SIGNED, + }, + { + .size = 1, + .bit = 1, + .selector = UVC_PU_CONTRAST_CONTROL, + .cid = VIDEO_CID_CONTRAST, + .type = UVC_CONTROL_UNSIGNED, + }, + { + .size = 2, + .bit = 9, + .selector = UVC_PU_GAIN_CONTROL, + .cid = VIDEO_CID_GAIN, + .type = UVC_CONTROL_UNSIGNED, + }, + { + .size = 2, + .bit = 3, + .selector = UVC_PU_SATURATION_CONTROL, + .cid = VIDEO_CID_SATURATION, + .type = UVC_CONTROL_UNSIGNED, + }, + { + .size = 2, + .bit = 6, + .selector = UVC_PU_WHITE_BALANCE_TEMP_CONTROL, + .cid = VIDEO_CID_WHITE_BALANCE_TEMPERATURE, + .type = UVC_CONTROL_UNSIGNED, + }, +}; + +static const struct uvc_control_map uvc_control_map_su[] = { + { + .size = 1, + .bit = 0, + .selector = UVC_SU_INPUT_SELECT_CONTROL, + .cid = VIDEO_CID_TEST_PATTERN, + .type = UVC_CONTROL_UNSIGNED, + }, +}; + +static const struct uvc_control_map uvc_control_map_xu[] = { + { + .size = 4, + .bit = 0, + .selector = UVC_XU_BASE_CONTROL + 0, + .cid = VIDEO_CID_PRIVATE_BASE + 0, + .type = UVC_CONTROL_UNSIGNED, + }, + { + .size = 4, + .bit = 1, + .selector = UVC_XU_BASE_CONTROL + 1, + .cid = VIDEO_CID_PRIVATE_BASE + 1, + .type = UVC_CONTROL_UNSIGNED, + }, + { + .size = 4, + .bit = 2, + .selector = UVC_XU_BASE_CONTROL + 2, + .cid = VIDEO_CID_PRIVATE_BASE + 2, + .type = UVC_CONTROL_UNSIGNED, + }, + { + .size = 4, + .bit = 3, + .selector = UVC_XU_BASE_CONTROL + 3, + .cid = VIDEO_CID_PRIVATE_BASE + 3, + .type = UVC_CONTROL_UNSIGNED, + }, +}; + +int uvc_get_control_map(uint8_t subtype, const struct uvc_control_map **map, size_t *length) +{ + switch (subtype) { + case UVC_VC_INPUT_TERMINAL: + *map = uvc_control_map_ct; + *length = ARRAY_SIZE(uvc_control_map_ct); + break; + case UVC_VC_SELECTOR_UNIT: + *map = uvc_control_map_su; + *length = ARRAY_SIZE(uvc_control_map_su); + break; + case UVC_VC_PROCESSING_UNIT: + *map = uvc_control_map_pu; + *length = ARRAY_SIZE(uvc_control_map_pu); + break; + case UVC_VC_EXTENSION_UNIT: + *map = uvc_control_map_xu; + *length = ARRAY_SIZE(uvc_control_map_xu); + break; + default: + return -EINVAL; + } + + return 0; +} + +void uvc_fourcc_to_guid(uint8_t guid[16], const uint32_t fourcc) +{ + uint32_t fourcc_le; + + /* Lookup in the "quirk table" if the UVC format GUID is custom */ + for (int i = 0; i < ARRAY_SIZE(uvc_guid_quirks); i++) { + if (uvc_guid_quirks[i].fourcc == fourcc) { + memcpy(guid, uvc_guid_quirks[i].guid, 16); + return; + } + } + + /* By default, UVC GUIDs are the four character code followed by a common suffix */ + fourcc_le = sys_cpu_to_le32(fourcc); + /* Copy the common suffix with the GUID set to 'XXXX' */ + memcpy(guid, UVC_FORMAT_GUID("XXXX"), 16); + /* Replace the 'XXXX' by the actual GUID of the format */ + memcpy(guid, &fourcc_le, 4); +} + +uint32_t uvc_guid_to_fourcc(const uint8_t guid[16]) +{ + uint32_t fourcc; + + /* Lookup in the "quirk table" if the UVC format GUID is custom */ + for (int i = 0; i < ARRAY_SIZE(uvc_guid_quirks); i++) { + if (memcmp(guid, uvc_guid_quirks[i].guid, 16) == 0) { + return uvc_guid_quirks[i].fourcc; + } + } + + /* Extract the four character code out of the leading 4 bytes of the GUID */ + memcpy(&fourcc, guid, 4); + fourcc = sys_le32_to_cpu(fourcc); + + return fourcc; +} diff --git a/subsys/usb/common/usb_common_uvc.h b/subsys/usb/common/usb_common_uvc.h new file mode 100644 index 0000000000000..aed1f23fde6dc --- /dev/null +++ b/subsys/usb/common/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/Kconfig.uvc b/subsys/usb/device_next/class/Kconfig.uvc index bab393e5d0e30..037c3c5ea4258 100644 --- a/subsys/usb/device_next/class/Kconfig.uvc +++ b/subsys/usb/device_next/class/Kconfig.uvc @@ -6,6 +6,7 @@ config USBD_VIDEO_CLASS bool "USB Video Class implementation [EXPERIMENTAL]" depends on DT_HAS_ZEPHYR_UVC_DEVICE_ENABLED select EXPERIMENTAL + select USB_COMMON_UVC help USB Device Video Class (UVC) implementation. diff --git a/subsys/usb/device_next/class/usbd_uvc.c b/subsys/usb/device_next/class/usbd_uvc.c index 9038a8e0d3f15..21277e1209b0f 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_uvc.h" + +#include "../../common/usb_common_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; @@ -149,29 +146,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, @@ -188,204 +162,6 @@ void uvc_set_video_dev(const struct device *const dev, const struct device *cons data->video_dev = video_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, @@ -1159,7 +935,7 @@ static int uvc_get_control_op(const struct device *dev, const struct usb_setup_p uint8_t ifnum = (setup->wIndex >> 0) & 0xff; uint8_t unit_id = setup->wIndex >> 8; uint8_t selector = setup->wValue >> 8; - uint8_t subtype = 0; + int ret; /* VideoStreaming operation */ @@ -1189,49 +965,27 @@ static int uvc_get_control_op(const struct device *dev, const struct usb_setup_p return UVC_OP_GET_ERRNO; } + ret = -ENOTSUP; 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; } } - - if (subtype == 0) { + if (ret != 0) { 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) { @@ -1622,6 +1376,8 @@ static int uvc_init(struct usbd_class_data *const c_data) struct uvc_data *data = dev->data; struct uvc_format_descriptor *format_desc = NULL; struct video_caps caps; + const struct uvc_control_map *map = NULL; + size_t map_sz = 0; uint32_t prev_pixfmt = 0; uint32_t mask = 0; int ret; @@ -1637,17 +1393,20 @@ static int uvc_init(struct usbd_class_data *const c_data) /* Generating VideoControl descriptors (interface 0) */ - 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 f48f5b8c71483..47f45352abaa1 100644 --- a/subsys/usb/host/CMakeLists.txt +++ b/subsys/usb/host/CMakeLists.txt @@ -5,9 +5,11 @@ zephyr_library() zephyr_library_include_directories(${CMAKE_CURRENT_SOURCE_DIR}) zephyr_library_sources( + usbh_api.c usbh_ch9.c + usbh_class.c usbh_core.c - usbh_api.c + usbh_desc.c usbh_device.c ) @@ -16,6 +18,11 @@ zephyr_library_sources_ifdef( usbh_shell.c ) +zephyr_library_sources_ifdef( + CONFIG_USBH_VIDEO_CLASS + class/usbh_uvc.c +) + zephyr_library_sources_ifdef( CONFIG_USBIP usbip.c diff --git a/subsys/usb/host/Kconfig b/subsys/usb/host/Kconfig index 8da3c95ebf7be..f77d0835be841 100644 --- a/subsys/usb/host/Kconfig +++ b/subsys/usb/host/Kconfig @@ -54,5 +54,6 @@ config USBH_MAX_UHC_MSG Maximum number of USB host controller events that can be queued. rsource "Kconfig.usbip" +rsource "class/Kconfig" endif # USB_HOST_STACK diff --git a/subsys/usb/host/class/Kconfig b/subsys/usb/host/class/Kconfig new file mode 100644 index 0000000000000..f9d957ad1a8ac --- /dev/null +++ b/subsys/usb/host/class/Kconfig @@ -0,0 +1,5 @@ +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: Apache-2.0 + +rsource "Kconfig.uvc" diff --git a/subsys/usb/host/class/Kconfig.uvc b/subsys/usb/host/class/Kconfig.uvc new file mode 100644 index 0000000000000..53516ab27453a --- /dev/null +++ b/subsys/usb/host/class/Kconfig.uvc @@ -0,0 +1,19 @@ +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: Apache-2.0 + +config USBH_VIDEO_CLASS + bool "USB Video Class implementation [EXPERIMENTAL]" + depends on DT_HAS_ZEPHYR_UVC_HOST_ENABLED + select EXPERIMENTAL + help + USB Host Video Class (UVC) implementation. + +if USBH_VIDEO_CLASS + +module = USBH_VIDEO +module-str = usbh uvc +default-count = 1 +source "subsys/logging/Kconfig.template.log_config" + +endif # USBH_VIDEO_CLASS diff --git a/subsys/usb/host/class/usbh_uvc.c b/subsys/usb/host/class/usbh_uvc.c new file mode 100644 index 0000000000000..610c23707517a --- /dev/null +++ b/subsys/usb/host/class/usbh_uvc.c @@ -0,0 +1,409 @@ +/* + * 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 + +#include "usbh_device.h" +#include "usbh_ch9.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; +}; + +static int usbh_uvc_completion_cb(struct usbh_class_data *const c_data, + struct uhc_transfer *const xfer) +{ + return 0; +} + +static int usbh_uvc_parse_control_header(struct usbh_class_data *const c_data, + struct usbh_uvc_data *const uvc, + struct uvc_control_header_descriptor *desc) +{ + return 0; +} + +static int usbh_uvc_parse_output_terminal(struct usbh_class_data *const c_data, + struct usbh_uvc_data *const uvc, + struct uvc_output_terminal_descriptor *desc) +{ + return 0; +} + +static int usbh_uvc_parse_input_terminal(struct usbh_class_data *const c_data, + struct usbh_uvc_data *const uvc, + struct uvc_camera_terminal_descriptor *desc) +{ + return 0; +} + +static int usbh_uvc_parse_selector_unit(struct usbh_class_data *const c_data, + struct usbh_uvc_data *const uvc, + struct uvc_selector_unit_descriptor *desc) +{ + return 0; +} + +static int usbh_uvc_parse_processing_unit(struct usbh_class_data *const c_data, + struct usbh_uvc_data *const uvc, + struct uvc_processing_unit_descriptor *desc) +{ + return 0; +} + +static int usbh_uvc_parse_extension_unit(struct usbh_class_data *const c_data, + struct usbh_uvc_data *const uvc, + struct uvc_extension_unit_descriptor *desc) +{ + return 0; +} + +static int usbh_uvc_parse_encoding_unit(struct usbh_class_data *const c_data, + struct usbh_uvc_data *const uvc, + struct uvc_encoding_unit_descriptor *desc) +{ + return 0; +} + +static int usbh_uvc_parse_control_desc(struct usbh_class_data *const c_data, + struct usbh_uvc_data *const uvc, + struct usb_if_descriptor *const if_control) +{ + struct uvc_if_descriptor *desc = (void *)if_control; + int ret = 0; + + while (desc->bLength != 0) { + /* Skip the interface descriptor or switch to the next descriptor */ + desc = (void *)((uint8_t *)desc + desc->bLength); + + if (desc->bDescriptorType == USB_DESC_INTERFACE || + desc->bDescriptorType == USB_DESC_INTERFACE_ASSOC || + desc->bDescriptorType == 0) { + break; + } else if (desc->bDescriptorType == USB_DESC_CS_INTERFACE) { + if (desc->bDescriptorSubtype == UVC_VC_HEADER) { + struct uvc_control_header_descriptor copy; + + memcpy(©, desc, MIN(sizeof(copy), desc->bLength)); + LOG_DBG("VideoControl interface: Header"); + ret = usbh_uvc_parse_control_header(c_data, uvc, ©); + } else if (desc->bDescriptorSubtype == UVC_VC_OUTPUT_TERMINAL) { + struct uvc_output_terminal_descriptor copy; + + memcpy(©, desc, MIN(sizeof(copy), desc->bLength)); + LOG_DBG("VideoControl interface: Output Terminal"); + ret = usbh_uvc_parse_output_terminal(c_data, uvc, ©); + } else if (desc->bDescriptorSubtype == UVC_VC_INPUT_TERMINAL) { + struct uvc_camera_terminal_descriptor copy; + + memcpy(©, desc, MIN(sizeof(copy), desc->bLength)); + LOG_DBG("VideoControl interface: Input/Camera Terminal"); + ret = usbh_uvc_parse_input_terminal(c_data, uvc, ©); + } else if (desc->bDescriptorSubtype == UVC_VC_SELECTOR_UNIT) { + struct uvc_selector_unit_descriptor copy; + + memcpy(©, desc, MIN(sizeof(copy), desc->bLength)); + LOG_DBG("VideoControl interface: Selector Unit"); + ret = usbh_uvc_parse_selector_unit(c_data, uvc, ©); + } else if (desc->bDescriptorSubtype == UVC_VC_PROCESSING_UNIT) { + struct uvc_processing_unit_descriptor copy; + + memcpy(©, desc, MIN(sizeof(copy), desc->bLength)); + LOG_DBG("VideoControl interface: Processing Unit"); + ret = usbh_uvc_parse_processing_unit(c_data, uvc, ©); + } else if (desc->bDescriptorSubtype == UVC_VC_EXTENSION_UNIT) { + struct uvc_extension_unit_descriptor copy; + + memcpy(©, desc, MIN(sizeof(copy), desc->bLength)); + LOG_DBG("VideoControl interface: Extension Unit"); + ret = usbh_uvc_parse_extension_unit(c_data, uvc, ©); + } else if (desc->bDescriptorSubtype == UVC_VC_ENCODING_UNIT) { + struct uvc_encoding_unit_descriptor copy; + + memcpy(©, desc, MIN(sizeof(copy), desc->bLength)); + LOG_DBG("VideoControl interface: Encoding Unit"); + ret = usbh_uvc_parse_encoding_unit(c_data, uvc, ©); + } else { + LOG_WRN("VideoControl interface: unknown subtype %u, skipping", + desc->bDescriptorSubtype); + } + } else { + LOG_DBG("VideoControl descriptor: unknown type %u, skipping", + desc->bDescriptorType); + } + + if (ret != 0) { + LOG_ERR("Error while parsing descriptor type %u"); + return ret; + } + } + + return 0; +} + +static int usbh_uvc_parse_input_header(struct usbh_class_data *const c_data, + struct usbh_uvc_data *const uvc, + struct uvc_stream_header_descriptor *const desc) +{ + return 0; +} + +static int usbh_uvc_parse_output_header(struct usbh_class_data *const c_data, + struct usbh_uvc_data *const uvc, + struct uvc_stream_header_descriptor *const desc) +{ + return 0; +} + +static int usbh_uvc_parse_format_uncomp(struct usbh_class_data *const c_data, + struct usbh_uvc_data *const uvc, + struct uvc_format_uncomp_descriptor *const desc) +{ + return 0; +} + +static int usbh_uvc_parse_format_mjpeg(struct usbh_class_data *const c_data, + struct usbh_uvc_data *const uvc, + struct uvc_format_mjpeg_descriptor *const desc) +{ + return 0; +} + +static int usbh_uvc_parse_frame(struct usbh_class_data *const c_data, + struct usbh_uvc_data *const uvc, + struct uvc_frame_descriptor *const desc) +{ + return 0; +} + +static int usbh_uvc_parse_color(struct usbh_class_data *const c_data, + struct usbh_uvc_data *const uvc, + struct uvc_color_descriptor *const desc) +{ + return 0; +} + +static int usbh_uvc_parse_streaming_desc(struct usbh_class_data *const c_data, + struct usbh_uvc_data *const uvc, + struct usb_if_descriptor *const if_streaming) +{ + struct uvc_if_descriptor *desc = (void *)if_streaming; + int ret; + + while (desc->bLength != 0) { + /* Skip the interface descriptor or switch to the next descriptor */ + desc = (void *)((uint8_t *)desc + desc->bLength); + + if (desc->bDescriptorType == USB_DESC_INTERFACE || + desc->bDescriptorType == USB_DESC_INTERFACE_ASSOC || + desc->bDescriptorType == 0) { + break; + } else if (desc->bDescriptorType == USB_DESC_CS_INTERFACE) { + if (desc->bDescriptorSubtype == UVC_VS_INPUT_HEADER) { + struct uvc_stream_header_descriptor copy; + + memcpy(©, desc, MIN(sizeof(copy), desc->bLength)); + LOG_DBG("VideoStreaming interface: Input header"); + ret = usbh_uvc_parse_input_header(c_data, uvc, ©); + } else if (desc->bDescriptorSubtype == UVC_VS_OUTPUT_HEADER) { + struct uvc_stream_header_descriptor copy; + + memcpy(©, desc, MIN(sizeof(copy), desc->bLength)); + LOG_DBG("VideoStreaming interface: Output header"); + ret = usbh_uvc_parse_output_header(c_data, uvc, ©); + } else if (desc->bDescriptorSubtype == UVC_VS_FORMAT_UNCOMPRESSED) { + struct uvc_format_uncomp_descriptor copy; + + memcpy(©, desc, MIN(sizeof(copy), desc->bLength)); + LOG_DBG("VideoStreaming interface: Uncompressed format"); + ret = usbh_uvc_parse_format_uncomp(c_data, uvc, ©); + } else if (desc->bDescriptorSubtype == UVC_VS_FORMAT_MJPEG) { + struct uvc_format_mjpeg_descriptor copy; + + memcpy(©, desc, MIN(sizeof(copy), desc->bLength)); + LOG_DBG("VideoStreaming interface: Uncompressed format"); + ret = usbh_uvc_parse_format_mjpeg(c_data, uvc, ©); + } else if (desc->bDescriptorSubtype == UVC_VS_FRAME_UNCOMPRESSED || + desc->bDescriptorSubtype == UVC_VS_FRAME_MJPEG) { + struct uvc_frame_descriptor copy; + + memcpy(©, desc, MIN(sizeof(copy), desc->bLength)); + LOG_DBG("VideoStreaming interface: Frame"); + ret = usbh_uvc_parse_frame(c_data, uvc, ©); + } else if (desc->bDescriptorSubtype == UVC_VS_COLORFORMAT) { + struct uvc_color_descriptor copy; + + memcpy(©, desc, MIN(sizeof(copy), desc->bLength)); + LOG_DBG("VideoStreaming interface: Color"); + ret = usbh_uvc_parse_color(c_data, uvc, ©); + } else { + LOG_DBG("VideoStreaming descriptor: unknown subtype %u, skipping", + desc->bDescriptorSubtype); + } + } else if (desc->bDescriptorType == USB_DESC_ENDPOINT) { + struct usb_ep_descriptor copy; + + memcpy(©, desc, MIN(sizeof(copy), desc->bLength)); + LOG_DBG("VideoStreaming Endpoint", copy.bEndpointAddress); + } else { + LOG_DBG("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) +{ + struct usbh_uvc_data *const uvc = NULL; + struct usb_if_descriptor *if_control = NULL; + struct usb_if_descriptor *if_streaming = NULL; + int ret; + + /* TODO only scan through the interfaces assigned to this class by usbh_class.c */ + for (unsigned int i = 0; udev->ifaces[i].dhp != NULL; i++) { + struct usb_if_descriptor *if_desc = (void *)udev->ifaces[i].dhp; + + if (if_desc->bInterfaceClass == USB_BCC_VIDEO && + if_desc->bInterfaceSubClass == UVC_SC_VIDEOCONTROL) { + if_control = if_desc; + } + + if (if_desc->bInterfaceClass == USB_BCC_VIDEO && + if_desc->bInterfaceSubClass == UVC_SC_VIDEOSTREAMING) { + if_streaming = if_desc; + } + } + + if (if_streaming == NULL || if_control == NULL) { + LOG_ERR("Video Streaming %p or Control %p interface missing", + if_streaming, if_control); + return -EINVAL; + } + + ret = usbh_uvc_parse_control_desc(c_data, uvc, if_control); + if (ret != 0) { + return ret; + } + + ret = usbh_uvc_parse_streaming_desc(c_data, uvc, if_streaming); + if (ret != 0) { + return ret; + } + + 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_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, +}; + +#define USBH_VIDEO_DT_DEVICE_DEFINE(n) \ + struct usbh_class_data uvc_class_data_##n = { \ + .api = &uvc_class_api, \ + }; \ + \ + DEVICE_DT_INST_DEFINE(n, usbh_uvc_preinit, NULL, \ + &uvc_class_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_class.c b/subsys/usb/host/usbh_class.c new file mode 100644 index 0000000000000..e569fdac8a9d1 --- /dev/null +++ b/subsys/usb/host/usbh_class.c @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * Copyright 2025 NXP + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include "usbh_class.h" +#include "usbh_class_api.h" +#include "usbh_desc.h" + +LOG_MODULE_REGISTER(usbh_class, CONFIG_USBH_LOG_LEVEL); + +int usbh_class_init_all(struct usbh_context *const uhs_ctx) +{ + int ret; + + STRUCT_SECTION_FOREACH(usbh_class_node, c_node) { + ret = usbh_class_init(c_node->c_data, uhs_ctx); + if (ret != 0) { + LOG_ERR("Failed to initialize all registered class instances"); + return ret; + } + } + + return 0; +} + +int usbh_class_remove_all(const struct usb_device *const udev) +{ + int ret; + + STRUCT_SECTION_FOREACH(usbh_class_node, c_node) { + if (c_node->c_data->udev == udev) { + ret = usbh_class_removed(c_node->c_data); + if (ret != 0) { + LOG_ERR("Failed to handle device removal for each classes"); + return ret; + } + + /* The class instance is now free to bind to a new device */ + c_node->c_data->udev = NULL; + } + } + + return 0; +} + +static int usbh_class_probe_one(struct usbh_class_data *const c_data, struct usb_device *const udev) +{ + const uint8_t *const desc_beg = usbh_desc_get_cfg_beg(udev); + const uint8_t *const desc_end = usbh_desc_get_cfg_end(udev); + const struct usb_desc_header *desc; + const struct usb_cfg_descriptor *cfg_desc; + int ret; + + desc = usbh_desc_get_by_type(desc_beg, desc_end, USB_DESC_CONFIGURATION); + if (desc == NULL) { + LOG_ERR("Unable to find a configuration descriptor"); + return -EBADMSG; + } + + cfg_desc = (struct usb_cfg_descriptor *)desc; + + for (uint8_t ifnum = 0; ifnum < cfg_desc->bNumInterfaces; ifnum++) { + ret = usbh_class_probe(c_data, udev, ifnum); + if (ret == -ENOTSUP) { + LOG_DBG("Class %s not supporting this device, skipping", c_data->name); + continue; + } + if (ret != 0) { + LOG_ERR("Failed to register the class %s for interface %u", + c_data->name, ifnum); + return ret; + } + if (ret == 0) { + LOG_INF("Class %s is matching this device for interface %u, assigning it", + c_data->name, ifnum); + c_data->udev = udev; + c_data->ifnum = ifnum; + return 0; + } + } + + return -ENOTSUP; +} + +int usbh_class_probe_all(struct usb_device *const udev) +{ + int ret; + + STRUCT_SECTION_FOREACH(usbh_class_node, c_node) { + struct usbh_class_data *const c_data = c_node->c_data; + + /* If the class is not already having a device */ + if (c_data->udev == NULL) { + ret = usbh_class_probe_one(c_data, udev); + if (ret == -ENOTSUP) { + continue; + } + if (ret != 0) { + return ret; + } + } + } + + LOG_WRN("Could not find any class for this device"); + return -ENODEV; +} + +bool usbh_class_is_matching(struct usbh_class_filter *const filters, size_t n_filters, + struct usb_device_descriptor *const desc) +{ + for (int i = 0; i < n_filters; i++) { + const struct usbh_class_filter *filt = &filters[i]; + + if (filt->flags & USBH_CLASS_MATCH_VID) { + if (desc->idVendor != filt->vid) { + continue; + } + } + + if (filt->flags & USBH_CLASS_MATCH_PID) { + if (desc->idProduct != filt->pid) { + continue; + } + } + + if (filt->flags & USBH_CLASS_MATCH_DCLASS) { + if (desc->bDeviceClass != filt->dclass) { + continue; + } + } + + if (filt->flags & USBH_CLASS_MATCH_SUB) { + if (desc->bDeviceSubClass != filt->sub) { + continue; + } + } + + if (filt->flags & USBH_CLASS_MATCH_PROTO) { + if (desc->bDeviceProtocol != filt->proto) { + continue; + } + } + + /* All the selected filters did match */ + return true; + } + + /* At the end of the filter table and still no match */ + return false; +} diff --git a/subsys/usb/host/usbh_class.h b/subsys/usb/host/usbh_class.h new file mode 100644 index 0000000000000..e6cc74d0342ea --- /dev/null +++ b/subsys/usb/host/usbh_class.h @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * Copyright 2025 NXP + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_INCLUDE_USBD_CLASS_H +#define ZEPHYR_INCLUDE_USBD_CLASS_H + +#include + +/** + * @brief Information about a device, which is relevant for matching a particular class. + */ +struct usbh_class_filter { + /** Vendor ID */ + uint16_t vid; + /** Product ID */ + uint16_t pid; + /** Device Class Code */ + uint8_t dclass; + /** Class Subclass Code */ + uint8_t sub; + /** Class Protocol Code */ + uint8_t proto; + /** Flags that tell which field to match */ + uint8_t flags; +}; + +/** Match a device's vendor ID */ +#define USBH_CLASS_MATCH_VID BIT(1) + +/** Match a device's product ID */ +#define USBH_CLASS_MATCH_PID BIT(2) + +/** Match a class code */ +#define USBH_CLASS_MATCH_DCLASS BIT(3) + +/** Match a subclass code */ +#define USBH_CLASS_MATCH_SUB BIT(4) + +/** Match a protocol code */ +#define USBH_CLASS_MATCH_PROTO BIT(5) + +/** Match a code triple */ +#define USBH_CLASS_MATCH_CODE_TRIPLE \ + (USBH_CLASS_MATCH_DCLASS | USBH_CLASS_MATCH_SUB | USBH_CLASS_MATCH_PROTO) + +/** + * @brief Match an USB host class (a driver) against a device descriptor. + * + * @param[in] filters Array of filter rules to match + * @param[in] n_filters Number of rules in the array. + * @param[in] desc USB Device descriptor to match against each rule + * @retval true if the USB Device descriptor matches at least one rule. + */ +bool usbh_class_is_matching(struct usbh_class_filter *const filters, size_t n_filters, + struct usb_device_descriptor *const desc); + +/** + * @brief Initialize every class instantiated on the system + * + * @param[in] uhs_ctx USB Host context to pass to the class. + * @retval 0 on success or negative error code on failure + */ +int usbh_class_init_all(struct usbh_context *const uhs_ctx); + +/** + * @brief Probe all classes to against a newly connected USB device. + * + * @param[in] udev USB device to probe. + * @retval 0 on success or negative error code on failure + */ +int usbh_class_probe_all(struct usb_device *const udev); + +/** + * @brief Call the device removal handler for every class configured with it + * + * @param[in] udev USB device that got removed. + * @retval 0 on success or negative error code on failure + */ +int usbh_class_remove_all(const struct usb_device *const udev); + +#endif /* ZEPHYR_INCLUDE_USBD_CLASS_H */ diff --git a/subsys/usb/host/usbh_class_api.h b/subsys/usb/host/usbh_class_api.h new file mode 100644 index 0000000000000..b2ec156ca2dd9 --- /dev/null +++ b/subsys/usb/host/usbh_class_api.h @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file + * @brief USB host stack class instances API + * + * This file contains the USB host stack class instances API. + */ + +#ifndef ZEPHYR_INCLUDE_USBH_CLASS_API_H +#define ZEPHYR_INCLUDE_USBH_CLASS_API_H + +#include + +/** + * @brief Initialization of the class implementation + * + * This is called for each instance during the initialization phase, + * for every registered class. + * It can be used to initialize underlying systems. + * + * @param[in] c_data Pointer to USB host class data + * @param[in] uhs_ctx USB host context to assign to this class + * + * @return 0 on success, negative error code on failure. + */ + +static inline int usbh_class_init(struct usbh_class_data *const c_data, + struct usbh_context *const uhs_ctx) +{ + const struct usbh_class_api *api = c_data->api; + + if (api->init != NULL) { + return api->init(c_data, uhs_ctx); + } + + return -ENOTSUP; +} + +/** + * @brief Request completion event handler + * + * Called upon completion of a request made by the host to this class. + * + * @param[in] c_data Pointer to USB host class data + * @param[in] udev USB device connected + * @param[in] xfer Completed transfer + * + * @return 0 on success, negative error code on failure. + */ +static inline int usbh_class_completion_cb(struct usbh_class_data *const c_data, + struct uhc_transfer *const xfer) +{ + const struct usbh_class_api *api = c_data->api; + + if (api->completion_cb != NULL) { + return api->completion_cb(c_data, xfer); + } + + return -ENOTSUP; +} + +/** + * @brief Device initialization handler + * + * Called when a device is connected to the bus for every device. + * + * @param[in] c_data Pointer to USB host class data + * @param[in] udev USB device connected + * @param[in] iface The @c bInterfaceNumber or @c bFirstInterface of this class + * + * @return 0 on success, negative error code on failure. + * @return -ENOTSUP if the class is not matching + */ +static inline int usbh_class_probe(struct usbh_class_data *const c_data, + struct usb_device *const udev, + const uint8_t iface) +{ + const struct usbh_class_api *api = c_data->api; + + if (api->probe != NULL) { + return api->probe(c_data, udev, iface); + } + + return -ENOTSUP; +} + +/** + * @brief Device removed handler + * + * Called when the device is removed from the bus + * and it matches the class filters of this instance. + * + * @param[in] c_data Pointer to USB host class data + * @param[in] udev USB device connected + * + * @return 0 on success, negative error code on failure. + */ +static inline int usbh_class_removed(struct usbh_class_data *const c_data) +{ + const struct usbh_class_api *api = c_data->api; + + if (api->removed != NULL) { + return api->removed(c_data); + } + + return -ENOTSUP; +} + +/** + * @brief Bus suspended handler + * + * Called when the host has suspending the bus. + * It can be used to suspend underlying systems. + * + * @param[in] c_data Pointer to USB host class data + * @param[in] udev USB device connected + * + * @return 0 on success, negative error code on failure. + */ +static inline int usbh_class_suspended(struct usbh_class_data *const c_data) +{ + const struct usbh_class_api *api = c_data->api; + + if (api->suspended != NULL) { + return api->suspended(c_data); + } + + return -ENOTSUP; +} + +/** + * @brief Bus resumed handler + * + * Called when the host resumes the activity on a bus. + * It can be used to wake-up underlying systems. + * + * @param[in] c_data Pointer to USB host class data + * @param[in] udev USB device connected + * + * @return 0 on success, negative error code on failure. + */ +static inline int usbh_class_resumed(struct usbh_class_data *const c_data) +{ + const struct usbh_class_api *api = c_data->api; + + if (api->resumed != NULL) { + return api->resumed(c_data); + } + + return -ENOTSUP; +} + +#endif /* ZEPHYR_INCLUDE_USBD_CLASS_API_H */ diff --git a/subsys/usb/host/usbh_core.c b/subsys/usb/host/usbh_core.c index 1b654b5041133..2b10ae3eab2a2 100644 --- a/subsys/usb/host/usbh_core.c +++ b/subsys/usb/host/usbh_core.c @@ -10,9 +10,12 @@ #include #include #include +#include -#include "usbh_internal.h" +#include "usbh_class.h" +#include "usbh_class_api.h" #include "usbh_device.h" +#include "usbh_internal.h" #include LOG_MODULE_REGISTER(uhs, CONFIG_USBH_LOG_LEVEL); @@ -46,6 +49,7 @@ static int usbh_event_carrier(const struct device *dev, static void dev_connected_handler(struct usbh_context *const ctx, const struct uhc_event *const event) { + int ret; LOG_DBG("Device connected event"); if (ctx->root != NULL) { @@ -71,6 +75,11 @@ static void dev_connected_handler(struct usbh_context *const ctx, if (usbh_device_init(ctx->root)) { LOG_ERR("Failed to reset new USB device"); } + + ret = usbh_class_probe_all(ctx->root); + if (ret != 0) { + LOG_ERR("Failed to probe all classes for this new USB device"); + } } static void dev_removed_handler(struct usbh_context *const ctx) @@ -194,12 +203,10 @@ int usbh_init_device_intl(struct usbh_context *const uhs_ctx) sys_dlist_init(&uhs_ctx->udevs); - STRUCT_SECTION_FOREACH(usbh_class_data, cdata) { - /* - * For now, we have not implemented any class drivers, - * so just keep it as placeholder. - */ - break; + ret = usbh_class_init_all(uhs_ctx); + if (ret != 0) { + LOG_ERR("Failed to initialize all classes"); + return ret; } return 0; diff --git a/subsys/usb/host/usbh_data.ld b/subsys/usb/host/usbh_data.ld index 4363154f29474..bc53c86119914 100644 --- a/subsys/usb/host/usbh_data.ld +++ b/subsys/usb/host/usbh_data.ld @@ -1,4 +1,4 @@ #include ITERABLE_SECTION_RAM(usbh_context, Z_LINK_ITERABLE_SUBALIGN) -ITERABLE_SECTION_RAM(usbh_class_data, Z_LINK_ITERABLE_SUBALIGN) +ITERABLE_SECTION_RAM(usbh_class_node, Z_LINK_ITERABLE_SUBALIGN) diff --git a/subsys/usb/host/usbh_desc.c b/subsys/usb/host/usbh_desc.c new file mode 100644 index 0000000000000..ca703328aa9cc --- /dev/null +++ b/subsys/usb/host/usbh_desc.c @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * Copyright NXP + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include "usbh_desc.h" + +const struct usb_desc_header *usbh_desc_get_next(const struct usb_desc_header *const desc, + const void *const desc_end) +{ + const struct usb_desc_header *next; + + if (desc == NULL) { + return NULL; + } + + next = (const struct usb_desc_header *)((uint8_t *)desc + desc->bLength); + + /* Validate the bLength field to make sure it does not point past the end */ + if ((uint8_t *)next >= (uint8_t *)desc_end || + (uint8_t *)next + next->bLength > (uint8_t *)desc_end || + desc->bLength == 0) { + return NULL; + } + + return next; +} + +const struct usb_desc_header *usbh_desc_get_by_type(const void *const desc_beg, + const void *const desc_end, + uint32_t type_mask) +{ + const struct usb_desc_header *desc; + + if (desc_beg == NULL) { + return NULL; + } + + for (desc = desc_beg; desc != NULL; desc = usbh_desc_get_next(desc, desc_end)) { + if ((BIT(desc->bDescriptorType) & type_mask) != 0) { + return desc; + } + } + + return NULL; +} + +const struct usb_desc_header *usbh_desc_get_by_ifnum(const void *const desc_beg, + const void *const desc_end, + const uint8_t ifnum) +{ + const struct usb_desc_header *desc; + + if (desc_beg == NULL) { + return NULL; + } + + for (desc = desc_beg; desc != NULL; desc = usbh_desc_get_next(desc, desc_end)) { + if (desc->bDescriptorType == USB_DESC_INTERFACE && + ((struct usb_if_descriptor *)desc)->bInterfaceNumber == ifnum) { + return desc; + } + if (desc->bDescriptorType == USB_DESC_INTERFACE_ASSOC && + ((struct usb_association_descriptor *)desc)->bFirstInterface == ifnum) { + return desc; + } + } + + return NULL; +} + +const void *usbh_desc_get_cfg_beg(const struct usb_device *udev) +{ + return udev->cfg_desc; +} + +const void *usbh_desc_get_cfg_end(const struct usb_device *udev) +{ + const struct usb_cfg_descriptor *cfg_desc; + + if (udev->cfg_desc == NULL) { + return NULL; + } + + cfg_desc = (struct usb_cfg_descriptor *)udev->cfg_desc; + + return (uint8_t *)cfg_desc + cfg_desc->wTotalLength; +} diff --git a/subsys/usb/host/usbh_desc.h b/subsys/usb/host/usbh_desc.h new file mode 100644 index 0000000000000..e78cf340f2ba5 --- /dev/null +++ b/subsys/usb/host/usbh_desc.h @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file + * @brief Descriptor matching and searching utilities + * + * This file contains utilities to browse USB descriptors returned by a device + * according to the USB Specification 2.0 Chapter 9. + */ + +#ifndef ZEPHYR_INCLUDE_USBH_DESC_H +#define ZEPHYR_INCLUDE_USBH_DESC_H + +#include + +#include + +/** + * @brief Get the next descriptor in an array of descriptors. + * + * This + * to search either an interface or interface association descriptor: + * + * @code{.c} + * BIT(USB_DESC_INTERFACE) | BIT(USB_DESC_INTERFACE_ASSOC) + * @endcode + * + * @param[in] desc_beg Pointer to the beginning of the descriptor array; to search. May be NULL. + * @param[in] desc_end Pointer after the end of the descriptor array to search + * @param[in] type_mask Bitmask of bDescriptorType values to match + * @return A pointer to the first descriptor matching + */ +const struct usb_desc_header *usbh_desc_get_next(const struct usb_desc_header *const desc, + const void *const desc_end); + +/** + * @brief Search the first descriptor matching the selected type(s). + * + * As an example, the @p type_mask argument can be constructed this way + * to search either an interface or interface association descriptor: + * + * @code{.c} + * BIT(USB_DESC_INTERFACE) | BIT(USB_DESC_INTERFACE_ASSOC) + * @endcode + * + * @param[in] desc_beg Pointer to the beginning of the descriptor array; to search. May be NULL. + * @param[in] desc_end Pointer after the end of the descriptor array to search + * @param[in] type_mask Bitmask of bDescriptorType values to match + * @return A pointer to the first descriptor matching + */ +const struct usb_desc_header *usbh_desc_get_by_type(const void *const desc_beg, + const void *const desc_end, + uint32_t type_mask); + +/** + * @brief Search a descriptor matching the interace number wanted. + * + * The descriptors are going to be scanned until either an interface association + * @c bFirstInterface field or an interface @c bInterfaceNumber field match. + * + * The descriptors following it can be browsed using @ref usbh_desc_get_next + * + * @param[in] desc_beg Pointer to the beginning of the descriptor array; to search. May be NULL. + * @param[in] desc_end Pointer after the end of the descriptor array to search + * @param[in] ifnum The interface number to search + * @return A pointer to the first descriptor matching + */ +const struct usb_desc_header *usbh_desc_get_by_ifnum(const void *const desc_beg, + const void *const desc_end, + const uint8_t ifnum); + +/** + * @brief Get the start of a device's configuration descriptor. + * + * @param[in] udev The device holding the confiugration descriptor + * @return A pointer to the beginning of the descriptor + */ +const void *usbh_desc_get_cfg_beg(const struct usb_device *udev); + +/** + * @brief Get the end of a device's configuration descriptor. + * + * @param[in] udev The device holding the confiugration descriptor + * @return A pointer to the beginning of the descriptor + */ +const void *usbh_desc_get_cfg_end(const struct usb_device *udev); + +#endif /* ZEPHYR_INCLUDE_USBH_DESC_H */ diff --git a/subsys/usb/host/usbh_device.c b/subsys/usb/host/usbh_device.c index 92ecf6ae7aee1..9c27419baf8dc 100644 --- a/subsys/usb/host/usbh_device.c +++ b/subsys/usb/host/usbh_device.c @@ -397,7 +397,7 @@ int usbh_device_set_configuration(struct usb_device *const udev, const uint8_t n } udev->cfg_desc = k_heap_alloc(&usb_device_heap, - cfg_desc.wTotalLength, + cfg_desc.wTotalLength + sizeof(struct usb_desc_header), K_NO_WAIT); if (udev->cfg_desc == NULL) { LOG_ERR("Failed to allocate memory for configuration descriptor"); diff --git a/tests/subsys/usb/host/class_api/CMakeLists.txt b/tests/subsys/usb/host/class_api/CMakeLists.txt new file mode 100644 index 0000000000000..d99885afb1d47 --- /dev/null +++ b/tests/subsys/usb/host/class_api/CMakeLists.txt @@ -0,0 +1,12 @@ +# Copyright (c) 2025 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(test_usb_host) + +target_include_directories(app PRIVATE ${ZEPHYR_BASE}/subsys/usb/host) +target_include_directories(app PRIVATE ${ZEPHYR_BASE}/tests/subsys/usb/host/common/include) + +target_sources(app PRIVATE src/main.c) diff --git a/tests/subsys/usb/host/class_api/boards/native_sim.conf b/tests/subsys/usb/host/class_api/boards/native_sim.conf new file mode 100644 index 0000000000000..68e22b2de6a0c --- /dev/null +++ b/tests/subsys/usb/host/class_api/boards/native_sim.conf @@ -0,0 +1 @@ +CONFIG_SYS_CLOCK_TICKS_PER_SEC=1000000 diff --git a/tests/subsys/usb/host/class_api/boards/native_sim.overlay b/tests/subsys/usb/host/class_api/boards/native_sim.overlay new file mode 100644 index 0000000000000..d352f555c8c89 --- /dev/null +++ b/tests/subsys/usb/host/class_api/boards/native_sim.overlay @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/delete-node/ &zephyr_udc0; + +/ { + zephyr_uhc0: uhc_vrt0 { + compatible = "zephyr,uhc-virtual"; + + zephyr_udc0: udc_vrt0 { + compatible = "zephyr,udc-virtual"; + num-bidir-endpoints = <8>; + maximum-speed = "high-speed"; + }; + }; +}; diff --git a/tests/subsys/usb/host/class_api/boards/native_sim_64.conf b/tests/subsys/usb/host/class_api/boards/native_sim_64.conf new file mode 100644 index 0000000000000..68e22b2de6a0c --- /dev/null +++ b/tests/subsys/usb/host/class_api/boards/native_sim_64.conf @@ -0,0 +1 @@ +CONFIG_SYS_CLOCK_TICKS_PER_SEC=1000000 diff --git a/tests/subsys/usb/host/class_api/boards/native_sim_64.overlay b/tests/subsys/usb/host/class_api/boards/native_sim_64.overlay new file mode 100644 index 0000000000000..e3ea1991fbdfe --- /dev/null +++ b/tests/subsys/usb/host/class_api/boards/native_sim_64.overlay @@ -0,0 +1,7 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "native_sim.overlay" diff --git a/tests/subsys/usb/host/class_api/boards/qemu_cortex_m3.overlay b/tests/subsys/usb/host/class_api/boards/qemu_cortex_m3.overlay new file mode 100644 index 0000000000000..4dd05c0fe9975 --- /dev/null +++ b/tests/subsys/usb/host/class_api/boards/qemu_cortex_m3.overlay @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/ { + zephyr_uhc0: uhc_vrt0 { + compatible = "zephyr,uhc-virtual"; + + zephyr_udc0: udc_vrt0 { + compatible = "zephyr,udc-virtual"; + num-bidir-endpoints = <8>; + maximum-speed = "high-speed"; + }; + }; +}; diff --git a/tests/subsys/usb/host/class_api/include/test_descriptor.h b/tests/subsys/usb/host/class_api/include/test_descriptor.h new file mode 100644 index 0000000000000..ac29e50aefee7 --- /dev/null +++ b/tests/subsys/usb/host/class_api/include/test_descriptor.h @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_TEST_USB_HOST_SAMPLE_DESC_H_ +#define ZEPHYR_TEST_USB_HOST_SAMPLE_DESC_H_ + +#include +#include + +#define _LE16(n) ((n) & 0xff), ((n) >> 8) + +/* + * Obtained with lsusb then verified against the + * USB 2.0 standard's sample HUB descriptor + */ + +#define TEST_HUB_DEVICE_DESCRIPTOR \ + 18, /* bLength */ \ + 1, /* bDescriptorType */ \ + _LE16(0x0200), /* bcdUSB */ \ + 0x09, /* bDeviceClass */ \ + 0x00, /* bDeviceSubClass */ \ + 0x02, /* bDeviceProtocol */ \ + 64, /* bMaxPacketSize0 */ \ + _LE16(0x0bda), /* idVendor */ \ + _LE16(0x5411), /* idProduct */ \ + _LE16(0x0001), /* bcdDevice */ \ + 0, /* iManufacturer */ \ + 0, /* iProduct */ \ + 0, /* iSerial */ \ + 1, /* bNumConfigurations */ + +#define TEST_HUB_CONFIG_DESCRIPTOR \ + 9, /* bLength */ \ + 2, /* bDescriptorType */ \ + _LE16(0x0029), /* wTotalLength */ \ + 1, /* bNumInterfaces */ \ + 1, /* bConfigurationValue */ \ + 0, /* iConfiguration */ \ + 0xe0, /* bmAttributes */ \ + 0, /* MaxPower */ + +#define TEST_HUB_INTERFACE_ALT0_DESCRIPTOR \ + 9, /* bLength */ \ + 4, /* bDescriptorType */ \ + 0, /* bInterfaceNumber */ \ + 0, /* bAlternateSetting */ \ + 1, /* bNumEndpoints */ \ + 9, /* bInterfaceClass */ \ + 0, /* bInterfaceSubClass */ \ + 1, /* bInterfaceProtocol */ \ + 0, /* iInterface */ + +#define TEST_HUB_INTERFACE_ALT1_DESCRIPTOR \ + 9, /* bLength */ \ + 4, /* bDescriptorType */ \ + 0, /* bInterfaceNumber */ \ + 1, /* bAlternateSetting */ \ + 1, /* bNumEndpoints */ \ + 9, /* bInterfaceClass */ \ + 0, /* bInterfaceSubClass */ \ + 2, /* bInterfaceProtocol */ \ + 0, /* iInterface */ + +#define TEST_HUB_ENDPOINT_DESCRIPTOR \ + 7, /* bLength */ \ + 5, /* bDescriptorType */ \ + 0x81, /* bEndpointAddress */ \ + 0x03, /* bmAttributes */ \ + _LE16(1), /* wMaxPacketSize */ \ + 12, /* bInterval */ + +#define TEST_HUB_DESCRIPTOR \ + TEST_HUB_DEVICE_DESCRIPTOR \ + TEST_HUB_CONFIG_DESCRIPTOR \ + TEST_HUB_INTERFACE_ALT0_DESCRIPTOR \ + TEST_HUB_ENDPOINT_DESCRIPTOR \ + TEST_HUB_INTERFACE_ALT1_DESCRIPTOR \ + TEST_HUB_ENDPOINT_DESCRIPTOR + +#endif /* ZEPHYR_TEST_USB_HOST_SAMPLE_DESC_H_ */ diff --git a/tests/subsys/usb/host/class_api/prj.conf b/tests/subsys/usb/host/class_api/prj.conf new file mode 100644 index 0000000000000..74f0aae233c58 --- /dev/null +++ b/tests/subsys/usb/host/class_api/prj.conf @@ -0,0 +1,8 @@ +# Copyright (c) 2025 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +CONFIG_LOG=y +CONFIG_ZTEST=y + +CONFIG_USB_HOST_STACK=y +CONFIG_UHC_DRIVER=y diff --git a/tests/subsys/usb/host/class_api/src/main.c b/tests/subsys/usb/host/class_api/src/main.c new file mode 100644 index 0000000000000..6fac39ccf7ad8 --- /dev/null +++ b/tests/subsys/usb/host/class_api/src/main.c @@ -0,0 +1,232 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include "usbh_ch9.h" +#include "usbh_class_api.h" +#include "usbh_desc.h" +#include "usbh_host.h" + +#include "test_descriptor.h" + +#include +LOG_MODULE_REGISTER(usb_test, LOG_LEVEL_DBG); + +USBH_CONTROLLER_DEFINE(uhs_ctx, DEVICE_DT_GET(DT_NODELABEL(zephyr_uhc0))); + +static const uint8_t test_hub_descriptor[] = {TEST_HUB_DESCRIPTOR}; +struct usb_device test_udev = {}; + +/* Private class data, here just an integer but usually a custom struct. */ +struct test_class_priv { + enum { + /* Test value stored before the class is initialized */ + TEST_CLASS_PRIV_INACTIVE, + /* Test value stored after the class is initialized */ + TEST_CLASS_PRIV_IDLE, + /* Test value stored after the class is probed */ + TEST_CLASS_PRIV_ENABLED, + /* Test value stored after the class is initialized */ + TEST_CLASS_PRIV_INITIALIZED, + /* Test value stored after the class is suspended */ + TEST_CLASS_PRIV_SUSPENDED, + } state; +}; + +static struct test_class_priv test_class_priv = { + .state = TEST_CLASS_PRIV_INACTIVE, +}; + +static int test_class_init(struct usbh_class_data *const c_data, + struct usbh_context *const uhs_ctx) +{ + struct test_class_priv *priv = c_data->priv; + + LOG_DBG("initializing %p, priv value 0x%x", c_data, *priv); + + zassert_equal(priv->state, TEST_CLASS_PRIV_INACTIVE, + "Class should be initialized only once"); + + priv->state = TEST_CLASS_PRIV_IDLE; + + return 0; +} + +static int test_class_completion_cb(struct usbh_class_data *const c_data, + struct uhc_transfer *const xfer) +{ + struct test_class_priv *priv = c_data->priv; + + LOG_DBG("completion callback for %p, transfer %p", c_data, xfer); + + zassert_equal(priv->state, TEST_CLASS_PRIV_ENABLED); + + return -ENOTSUP; +} + +static int test_class_probe(struct usbh_class_data *const c_data, + struct usb_device *const udev, + const uint8_t ifnum) +{ + struct test_class_priv *priv = c_data->priv; + const void *const desc_beg = test_hub_descriptor; + const void *const desc_end = test_hub_descriptor + sizeof(test_hub_descriptor); + const struct usb_desc_header *desc = desc_beg; + const struct usb_if_descriptor *if_desc; + + zassert_equal(priv->state, TEST_CLASS_PRIV_IDLE); + + desc = usbh_desc_get_by_ifnum(desc, desc_end, ifnum); + if (desc == NULL) { + return -ENOENT; + } + + if (desc->bDescriptorType != USB_DESC_INTERFACE) { + return -ENOTSUP; + } + + if_desc = (struct usb_if_descriptor *)desc; + if (if_desc->bInterfaceClass != USB_HUB_CLASSCODE || + if_desc->bInterfaceSubClass != 0x00) { + return -ENOTSUP; + } + + priv->state = TEST_CLASS_PRIV_ENABLED; + + return 0; +} + +static int test_class_removed(struct usbh_class_data *const c_data) +{ + struct test_class_priv *priv = c_data->priv; + + zassert_equal(priv->state, TEST_CLASS_PRIV_ENABLED); + + priv->state = TEST_CLASS_PRIV_IDLE; + + return 0; +} + +static int test_class_suspended(struct usbh_class_data *const c_data) +{ + struct test_class_priv *priv = c_data->priv; + + zassert_equal(priv->state, TEST_CLASS_PRIV_ENABLED); + + priv->state = TEST_CLASS_PRIV_SUSPENDED; + + return 0; +} + +static int test_class_resumed(struct usbh_class_data *const c_data) +{ + struct test_class_priv *priv = c_data->priv; + + zassert_equal(priv->state, TEST_CLASS_PRIV_SUSPENDED); + + priv->state = TEST_CLASS_PRIV_ENABLED; + + return 0; +} + +static struct usbh_class_api test_class_api = { + .init = &test_class_init, + .completion_cb = &test_class_completion_cb, + .probe = &test_class_probe, + .removed = &test_class_removed, + .suspended = &test_class_suspended, + .resumed = &test_class_resumed, +}; + +/* Define a class used in the tests */ +USBH_DEFINE_CLASS(test_class, &test_class_api, &test_class_priv); + +ZTEST(host_class, test_class_fake_device) +{ + struct usbh_class_data *c_data = test_class.c_data; + struct usb_device *udev = &test_udev; + struct test_class_priv *priv = c_data->priv; + int ret; + + zassert_equal(priv->state, TEST_CLASS_PRIV_IDLE, + "The class should have been initialized by usbh_init()"); + + ret = usbh_class_probe(c_data, udev, 1); + zassert_equal(ret, -ENOENT, "There is no interface 1 so should be rejected"); + zassert_equal(priv->state, TEST_CLASS_PRIV_IDLE, + "The class should not be enabled if probing failed"); + + ret = usbh_class_probe(c_data, udev, 0); + zassert_ok(ret, "The interface 0 should match in this example class (%s)", strerror(-ret)); + zassert_equal(priv->state, TEST_CLASS_PRIV_ENABLED, + "The class should be enabled if probing succeeded"); + + ret = usbh_class_suspended(c_data); + zassert_ok(ret, "Susupending the class while it is running should succeed"); + zassert_equal(priv->state, TEST_CLASS_PRIV_SUSPENDED, + "The class private state should have been updated"); + + ret = usbh_class_resumed(c_data); + zassert_ok(ret, "Resuming the class after suspending should succeed"); + zassert_equal(priv->state, TEST_CLASS_PRIV_ENABLED, + "The class private state should have been updated"); + + ret = usbh_class_removed(c_data); + zassert_ok(ret, "Removing the class after probing it should succeed"); + zassert_equal(priv->state, TEST_CLASS_PRIV_IDLE, + "The class should be back to inactive "); + + ret = usbh_class_probe(c_data, udev, 0); + zassert_ok(ret, "Probing the class again should succeed"); + zassert_equal(priv->state, TEST_CLASS_PRIV_ENABLED, + "The class should be back to active "); + +} + +static void *usb_test_enable(void) +{ + int ret; + + ret = usbh_init(&uhs_ctx); + zassert_ok(ret, "Failed to initialize USB host (%s)", strerror(-ret)); + + ret = usbh_enable(&uhs_ctx); + zassert_ok(ret, "Failed to enable USB host (%s)", strerror(-ret)); + + ret = uhc_bus_reset(uhs_ctx.dev); + zassert_ok(ret, "Failed to signal bus reset (%s)", strerror(-ret)); + + ret = uhc_bus_resume(uhs_ctx.dev); + zassert_ok(ret, "Failed to signal bus resume (%s)", strerror(-ret)); + + ret = uhc_sof_enable(uhs_ctx.dev); + zassert_ok(ret, "Failed to enable SoF generator (%s)", strerror(-ret)); + + LOG_INF("Host controller enabled"); + + /* Allow the host time to reset the host. */ + k_msleep(200); + + return NULL; +} + +static void usb_test_shutdown(void *f) +{ + int ret; + + ret = usbh_disable(&uhs_ctx); + zassert_ok(ret, "Failed to enable host support (%s)", strerror(-ret)); + + ret = usbh_shutdown(&uhs_ctx); + zassert_ok(ret, "Failed to shutdown host support (%s)", strerror(-ret)); + + LOG_INF("Host controller disabled"); +} + +ZTEST_SUITE(host_class, NULL, usb_test_enable, NULL, NULL, usb_test_shutdown); diff --git a/tests/subsys/usb/host/class_api/testcase.yaml b/tests/subsys/usb/host/class_api/testcase.yaml new file mode 100644 index 0000000000000..3ff951fc47a9f --- /dev/null +++ b/tests/subsys/usb/host/class_api/testcase.yaml @@ -0,0 +1,17 @@ +tests: + usb.host.class_api: + platform_allow: + - native_sim + - native_sim/native/64 + - qemu_cortex_m3 + integration_platforms: + - native_sim + tags: usb + usb.host.class_api.build_all: + platform_allow: + - native_sim + - native_sim/native/64 + integration_platforms: + - native_sim + tags: usb + build_only: true diff --git a/tests/subsys/usb/host/common/include/test_descriptor.h b/tests/subsys/usb/host/common/include/test_descriptor.h new file mode 100644 index 0000000000000..84a47d7d3924c --- /dev/null +++ b/tests/subsys/usb/host/common/include/test_descriptor.h @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_TEST_USB_HOST_SAMPLE_DESC_H_ +#define ZEPHYR_TEST_USB_HOST_SAMPLE_DESC_H_ + +#include +#include + +#define _LE16(n) ((n) & 0xff), ((n) >> 8) + +/* bInterfaceClass of these descriptors */ +#define USB_HUB_CLASSCODE 0x09 + +/* + * Obtained with lsusb then verified against the + * USB 2.0 standard's sample HUB descriptor + */ + +#define TEST_HUB_DEVICE_DESCRIPTOR \ + 18, /* bLength */ \ + 1, /* bDescriptorType */ \ + _LE16(0x0200), /* bcdUSB */ \ + 0x09, /* bDeviceClass */ \ + 0x00, /* bDeviceSubClass */ \ + 0x02, /* bDeviceProtocol */ \ + 64, /* bMaxPacketSize0 */ \ + _LE16(0x0bda), /* idVendor */ \ + _LE16(0x5411), /* idProduct */ \ + _LE16(0x0001), /* bcdDevice */ \ + 0, /* iManufacturer */ \ + 0, /* iProduct */ \ + 0, /* iSerial */ \ + 1, /* bNumConfigurations */ + +#define TEST_HUB_CONFIG_DESCRIPTOR \ + 9, /* bLength */ \ + 2, /* bDescriptorType */ \ + _LE16(0x0029), /* wTotalLength */ \ + 1, /* bNumInterfaces */ \ + 1, /* bConfigurationValue */ \ + 0, /* iConfiguration */ \ + 0xe0, /* bmAttributes */ \ + 0, /* MaxPower */ + +#define TEST_HUB_INTERFACE_ALT0_DESCRIPTOR \ + 9, /* bLength */ \ + 4, /* bDescriptorType */ \ + 0, /* bInterfaceNumber */ \ + 0, /* bAlternateSetting */ \ + 1, /* bNumEndpoints */ \ + 9, /* bInterfaceClass */ \ + 0, /* bInterfaceSubClass */ \ + 1, /* bInterfaceProtocol */ \ + 0, /* iInterface */ + +#define TEST_HUB_INTERFACE_ALT1_DESCRIPTOR \ + 9, /* bLength */ \ + 4, /* bDescriptorType */ \ + 0, /* bInterfaceNumber */ \ + 1, /* bAlternateSetting */ \ + 1, /* bNumEndpoints */ \ + 9, /* bInterfaceClass */ \ + 0, /* bInterfaceSubClass */ \ + 2, /* bInterfaceProtocol */ \ + 0, /* iInterface */ + +#define TEST_HUB_ENDPOINT_DESCRIPTOR \ + 7, /* bLength */ \ + 5, /* bDescriptorType */ \ + 0x81, /* bEndpointAddress */ \ + 0x03, /* bmAttributes */ \ + _LE16(1), /* wMaxPacketSize */ \ + 12, /* bInterval */ + +#define TEST_HUB_DESCRIPTOR \ + TEST_HUB_DEVICE_DESCRIPTOR \ + TEST_HUB_CONFIG_DESCRIPTOR \ + TEST_HUB_INTERFACE_ALT0_DESCRIPTOR \ + TEST_HUB_ENDPOINT_DESCRIPTOR \ + TEST_HUB_INTERFACE_ALT1_DESCRIPTOR \ + TEST_HUB_ENDPOINT_DESCRIPTOR + +#endif /* ZEPHYR_TEST_USB_HOST_SAMPLE_DESC_H_ */ diff --git a/tests/subsys/usb/host/desc/CMakeLists.txt b/tests/subsys/usb/host/desc/CMakeLists.txt new file mode 100644 index 0000000000000..d99885afb1d47 --- /dev/null +++ b/tests/subsys/usb/host/desc/CMakeLists.txt @@ -0,0 +1,12 @@ +# Copyright (c) 2025 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(test_usb_host) + +target_include_directories(app PRIVATE ${ZEPHYR_BASE}/subsys/usb/host) +target_include_directories(app PRIVATE ${ZEPHYR_BASE}/tests/subsys/usb/host/common/include) + +target_sources(app PRIVATE src/main.c) diff --git a/tests/subsys/usb/host/desc/boards/native_sim.conf b/tests/subsys/usb/host/desc/boards/native_sim.conf new file mode 100644 index 0000000000000..68e22b2de6a0c --- /dev/null +++ b/tests/subsys/usb/host/desc/boards/native_sim.conf @@ -0,0 +1 @@ +CONFIG_SYS_CLOCK_TICKS_PER_SEC=1000000 diff --git a/tests/subsys/usb/host/desc/boards/native_sim.overlay b/tests/subsys/usb/host/desc/boards/native_sim.overlay new file mode 100644 index 0000000000000..d352f555c8c89 --- /dev/null +++ b/tests/subsys/usb/host/desc/boards/native_sim.overlay @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/delete-node/ &zephyr_udc0; + +/ { + zephyr_uhc0: uhc_vrt0 { + compatible = "zephyr,uhc-virtual"; + + zephyr_udc0: udc_vrt0 { + compatible = "zephyr,udc-virtual"; + num-bidir-endpoints = <8>; + maximum-speed = "high-speed"; + }; + }; +}; diff --git a/tests/subsys/usb/host/desc/boards/native_sim_64.conf b/tests/subsys/usb/host/desc/boards/native_sim_64.conf new file mode 100644 index 0000000000000..68e22b2de6a0c --- /dev/null +++ b/tests/subsys/usb/host/desc/boards/native_sim_64.conf @@ -0,0 +1 @@ +CONFIG_SYS_CLOCK_TICKS_PER_SEC=1000000 diff --git a/tests/subsys/usb/host/desc/boards/native_sim_64.overlay b/tests/subsys/usb/host/desc/boards/native_sim_64.overlay new file mode 100644 index 0000000000000..e3ea1991fbdfe --- /dev/null +++ b/tests/subsys/usb/host/desc/boards/native_sim_64.overlay @@ -0,0 +1,7 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "native_sim.overlay" diff --git a/tests/subsys/usb/host/desc/boards/qemu_cortex_m3.overlay b/tests/subsys/usb/host/desc/boards/qemu_cortex_m3.overlay new file mode 100644 index 0000000000000..4dd05c0fe9975 --- /dev/null +++ b/tests/subsys/usb/host/desc/boards/qemu_cortex_m3.overlay @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/ { + zephyr_uhc0: uhc_vrt0 { + compatible = "zephyr,uhc-virtual"; + + zephyr_udc0: udc_vrt0 { + compatible = "zephyr,udc-virtual"; + num-bidir-endpoints = <8>; + maximum-speed = "high-speed"; + }; + }; +}; diff --git a/tests/subsys/usb/host/desc/prj.conf b/tests/subsys/usb/host/desc/prj.conf new file mode 100644 index 0000000000000..74f0aae233c58 --- /dev/null +++ b/tests/subsys/usb/host/desc/prj.conf @@ -0,0 +1,8 @@ +# Copyright (c) 2025 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +CONFIG_LOG=y +CONFIG_ZTEST=y + +CONFIG_USB_HOST_STACK=y +CONFIG_UHC_DRIVER=y diff --git a/tests/subsys/usb/host/desc/src/main.c b/tests/subsys/usb/host/desc/src/main.c new file mode 100644 index 0000000000000..b35d2dd0d7b39 --- /dev/null +++ b/tests/subsys/usb/host/desc/src/main.c @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include "usbh_desc.h" + +#include "test_descriptor.h" + +static const uint8_t test_hub_descriptor[] = {TEST_HUB_DESCRIPTOR}; + +static const uint8_t test_hub_device_descriptor[] = {TEST_HUB_DEVICE_DESCRIPTOR}; +static const uint8_t test_hub_interface_alt0_descriptor[] = {TEST_HUB_INTERFACE_ALT0_DESCRIPTOR}; +static const uint8_t test_hub_interface_alt1_descriptor[] = {TEST_HUB_INTERFACE_ALT1_DESCRIPTOR}; +static const uint8_t test_hub_endpoint_descriptor[] = {TEST_HUB_ENDPOINT_DESCRIPTOR}; + +ZTEST(host_desc, test_desc_get_by_type) +{ + const uint8_t *const start_addr = test_hub_descriptor; + const uint8_t *const end_addr = test_hub_descriptor + sizeof(test_hub_descriptor); + const struct usb_desc_header *desc; + uint32_t mask; + + zassert_is_null(usbh_desc_get_by_type(start_addr, end_addr, 0x00), + "requesting nothing needs to always return nothing"); + + /* From "test_descriptor.h": + * #0 TEST_HUB_DEVICE_DESCRIPTOR + * #1 TEST_HUB_CONFIG_DESCRIPTOR + * #2 TEST_HUB_INTERFACE_ALT0_DESCRIPTOR + * #3 TEST_HUB_ENDPOINT_DESCRIPTOR + * #4 TEST_HUB_INTERFACE_ALT1_DESCRIPTOR + * #5 TEST_HUB_ENDPOINT_DESCRIPTOR + */ + desc = (const struct usb_desc_header *)start_addr; + + /* desc at #0 TEST_HUB_DEVICE_DESCRIPTOR */ + + mask = BIT(USB_DESC_DEVICE); + + desc = usbh_desc_get_by_type(desc, end_addr, mask); + zassert_not_null(desc); + zassert_mem_equal(desc, test_hub_device_descriptor, + sizeof(test_hub_device_descriptor), + "needs to be able to reach the device descriptor"); + + /* desc at #0 TEST_HUB_DEVICE_DESCRIPTOR */ + + mask = BIT(USB_DESC_INTERFACE) | BIT(USB_DESC_ENDPOINT); + + desc = usbh_desc_get_by_type(desc, end_addr, mask); + zassert_not_null(desc); + zassert_mem_equal(desc, test_hub_interface_alt0_descriptor, + sizeof(test_hub_interface_alt0_descriptor), + "needs to return the first match (interface 0 alt 1) only"); + + /* desc at #2 TEST_HUB_INTERFACE_ALT0_DESCRIPTOR */ + + desc = usbh_desc_get_next(desc, end_addr); + zassert_not_null(desc); + zassert_mem_equal(desc, test_hub_endpoint_descriptor, + sizeof(test_hub_endpoint_descriptor), + "needs to properly shift to the next descriptor"); + + /* desc at #3 TEST_HUB_ENDPOINT_DESCRIPTOR */ + + mask = BIT(USB_DESC_INTERFACE); + + desc = usbh_desc_get_by_type((const uint8_t *)desc, end_addr, mask); + zassert_not_null(desc); + zassert_mem_equal(desc, test_hub_interface_alt1_descriptor, + sizeof(test_hub_interface_alt1_descriptor), + "needs to return the interface 0 alt 1 descriptor"); + + /* desc at #4 TEST_HUB_INTERFACE_ALT1_DESCRIPTOR */ + + desc = usbh_desc_get_next(desc, end_addr); + zassert_not_null(desc); + zassert_mem_equal(desc, test_hub_endpoint_descriptor, + sizeof(test_hub_endpoint_descriptor), + "needs to return the last descriptor (endpoint)"); + + /* desc at #5 TEST_HUB_ENDPOINT_DESCRIPTOR */ + + desc = usbh_desc_get_next(desc, end_addr); + zassert_is_null(desc, + "needs to return NULL when running past the end"); +} + +ZTEST_SUITE(host_desc, NULL, NULL, NULL, NULL, NULL); diff --git a/tests/subsys/usb/host/desc/testcase.yaml b/tests/subsys/usb/host/desc/testcase.yaml new file mode 100644 index 0000000000000..3eef037a273dc --- /dev/null +++ b/tests/subsys/usb/host/desc/testcase.yaml @@ -0,0 +1,9 @@ +tests: + usb.host.desc: + platform_allow: + - native_sim + - native_sim/native/64 + - qemu_cortex_m3 + integration_platforms: + - native_sim + tags: usb diff --git a/tests/subsys/usb/uvc/CMakeLists.txt b/tests/subsys/usb/uvc/CMakeLists.txt new file mode 100644 index 0000000000000..ff98cb58e5977 --- /dev/null +++ b/tests/subsys/usb/uvc/CMakeLists.txt @@ -0,0 +1,13 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(uvc_uvb) + +# This sample cannot use the common USB sample infrastructure as we do not want +# to initialize the board default `zephyr_udc0` but instead an unrelated `virtual_udc0`. +#include(${ZEPHYR_BASE}/samples/subsys/usb/common/common.cmake) + +FILE(GLOB app_sources src/*.c) +target_sources(app PRIVATE ${app_sources}) +zephyr_include_directories(${ZEPHYR_BASE}/subsys/usb/host) diff --git a/tests/subsys/usb/uvc/app.overlay b/tests/subsys/usb/uvc/app.overlay new file mode 100644 index 0000000000000..a0b35c939004c --- /dev/null +++ b/tests/subsys/usb/uvc/app.overlay @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/ { + virtual_uhc0: uhc-virtual { + compatible = "zephyr,uhc-virtual"; + maximum-speed = "full-speed"; + + virtual_udc0: udc-virtual { + compatible = "zephyr,udc-virtual"; + num-bidir-endpoints = <4>; + maximum-speed = "full-speed"; + }; + }; + + uvc_device: uvc-device { + compatible = "zephyr,uvc-device"; + }; + + uvc_host: uvc-host { + compatible = "zephyr,uvc-host"; + }; +}; diff --git a/tests/subsys/usb/uvc/prj.conf b/tests/subsys/usb/uvc/prj.conf new file mode 100644 index 0000000000000..ed2322f383032 --- /dev/null +++ b/tests/subsys/usb/uvc/prj.conf @@ -0,0 +1,15 @@ +CONFIG_LOG=y +CONFIG_UDC_BUF_POOL_SIZE=2048 +CONFIG_UDC_DRIVER_LOG_LEVEL_INF=y +CONFIG_UHC_DRIVER_LOG_LEVEL_INF=y +CONFIG_USBD_VIDEO_CLASS=y +CONFIG_USBD_VIDEO_LOG_LEVEL_INF=y +CONFIG_USBH_LOG_LEVEL_INF=y +CONFIG_USBH_VIDEO_CLASS=y +CONFIG_USBH_VIDEO_LOG_LEVEL_INF=y +CONFIG_USB_DEVICE_STACK_NEXT=y +CONFIG_USB_HOST_STACK=y +CONFIG_UVB_LOG_LEVEL_INF=y +CONFIG_VIDEO=y +CONFIG_VIDEO_LOG_LEVEL_INF=y +CONFIG_ZTEST=y diff --git a/tests/subsys/usb/uvc/src/main.c b/tests/subsys/usb/uvc/src/main.c new file mode 100644 index 0000000000000..4a0046ecade8e --- /dev/null +++ b/tests/subsys/usb/uvc/src/main.c @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include + +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 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); + + 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"