diff --git a/doc/connectivity/usb/device/usb_device.rst b/doc/connectivity/usb/device/usb_device.rst index 1e4111f0eee5..ec513d19d8e0 100644 --- a/doc/connectivity/usb/device/usb_device.rst +++ b/doc/connectivity/usb/device/usb_device.rst @@ -604,6 +604,8 @@ The following Product IDs are currently used: +----------------------------------------------------+--------+ | :zephyr:code-sample:`uac2-implicit-feedback` | 0x000F | +----------------------------------------------------+--------+ +| :zephyr:code-sample:`uvc` | 0x0011 | ++----------------------------------------------------+--------+ | :zephyr:code-sample:`usb-dfu` (DFU Mode) | 0xFFFF | +----------------------------------------------------+--------+ diff --git a/doc/connectivity/usb/device_next/usb_device.rst b/doc/connectivity/usb/device_next/usb_device.rst index 7e7762997a01..7ab2805dc4fa 100644 --- a/doc/connectivity/usb/device_next/usb_device.rst +++ b/doc/connectivity/usb/device_next/usb_device.rst @@ -32,6 +32,8 @@ Samples * :zephyr:code-sample:`uac2-implicit-feedback` +* :zephyr:code-sample:`uvc` + Samples ported to new USB device support ---------------------------------------- @@ -223,6 +225,8 @@ instance (``n``) and is used as an argument to the :c:func:`usbd_register_class` +-----------------------------------+-------------------------+-------------------------+ | Bluetooth HCI USB transport layer | :ref:`bt_hci_raw` | :samp:`bt_hci_{n}` | +-----------------------------------+-------------------------+-------------------------+ +| USB Video Class (UVC) | Video device | :samp:`uvc_{n}` | ++-----------------------------------+-------------------------+-------------------------+ CDC ACM UART ============ diff --git a/samples/subsys/usb/common/sample_usbd_init.c b/samples/subsys/usb/common/sample_usbd_init.c index 2d3266b4c757..49ae2b12ba0c 100644 --- a/samples/subsys/usb/common/sample_usbd_init.c +++ b/samples/subsys/usb/common/sample_usbd_init.c @@ -80,7 +80,8 @@ static void sample_fix_code_triple(struct usbd_context *uds_ctx, IS_ENABLED(CONFIG_USBD_CDC_ECM_CLASS) || IS_ENABLED(CONFIG_USBD_CDC_NCM_CLASS) || IS_ENABLED(CONFIG_USBD_MIDI2_CLASS) || - IS_ENABLED(CONFIG_USBD_AUDIO2_CLASS)) { + IS_ENABLED(CONFIG_USBD_AUDIO2_CLASS) || + IS_ENABLED(CONFIG_USBD_VIDEO_CLASS)) { /* * Class with multiple interfaces have an Interface * Association Descriptor available, use an appropriate triple diff --git a/samples/subsys/usb/uvc/CMakeLists.txt b/samples/subsys/usb/uvc/CMakeLists.txt new file mode 100644 index 000000000000..62b0a45e66a4 --- /dev/null +++ b/samples/subsys/usb/uvc/CMakeLists.txt @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(usb_video) + +include(${ZEPHYR_BASE}/samples/subsys/usb/common/common.cmake) +target_sources(app PRIVATE src/main.c) diff --git a/samples/subsys/usb/uvc/Kconfig b/samples/subsys/usb/uvc/Kconfig new file mode 100644 index 000000000000..96c545589480 --- /dev/null +++ b/samples/subsys/usb/uvc/Kconfig @@ -0,0 +1,9 @@ +# Copyright (c) 2023 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +# Source common USB sample options used to initialize new experimental USB +# device stack. The scope of these options is limited to USB samples in project +# tree, you cannot use them in your own application. +source "samples/subsys/usb/common/Kconfig.sample_usbd" + +source "Kconfig.zephyr" diff --git a/samples/subsys/usb/uvc/README.rst b/samples/subsys/usb/uvc/README.rst new file mode 100644 index 000000000000..6539692f1377 --- /dev/null +++ b/samples/subsys/usb/uvc/README.rst @@ -0,0 +1,193 @@ +.. zephyr:code-sample:: uvc + :name: USB Video sample + :relevant-api: usbd_api video_interface + + Send video frames over USB. + +Overview +******** + +This sample demonstrates how to use a USB Video Class instance to +send video data over USB. + +Upon connection, a video interface would show-up in the operating system, +using the same protocol as most webcams, and be detected as such. + +Any host-side video processing software would then be able to access the +video streams. + +Requirements +************ + +The requirements for this sample is an USB driver using the latest +``device_next`` API. + +If a ``zephyr,camera`` node is chosen for the board, it will be used as source. +If not, this sample can be built using this command to use an emulated video source: + +.. code-block:: console + + west build -b -- -DEXTRA_DTC_OVERLAY_FILE=video-emul.overlay + +The USB descriptors are generated from the video API, and the user does not need +to configure them. + +Building and Running +******************** + +Build the sample application and flash the resulting binaries. + +.. zephyr-app-commands:: + :zephyr-app: samples/subsys/usb/uvc + :board: nrf52840dongle + :goals: build flash + :compact: + +.. zephyr-app-commands:: + :zephyr-app: samples/subsys/usb/uvc + :board: rpi_pico + :goals: build flash + :compact: + +Upon reboot, the device is expected to be detected as a webcam device: + +.. tabs:: + + .. group-tab:: Ubuntu + + The ``dmesg`` logs are expected to mention a ``generic UVC device``. + + The ``lsusb`` is expected to show an entry for a Zephyr device. + + Refers to `Ideas on board FAQ `_ + for how to get more debug information. + + .. group-tab:: MacOS + + The ``dmesg`` logs are expected to mention a video device. + + The ``ioreg -p IOUSB`` command list the USB devices including cameras. + + The ``system_profiler SPCameraDataType`` command list video input devices. + + .. group-tab:: Windows + + The Device Manager or USBView utilities permit to list the USB devices. + + The 3rd-party USB Tree View allows to review and debug the descriptors. + + In addition, the `USB3CV `_ tool + from USB-IF can check that the device is compliant with the UVC standard. + + +Playing the Stream +================== + +The device is recognized by the system as a native webcam and can be used by any video application. + +For instance with VLC: +``Media`` > ``Open Capture Device`` > ``Capture Device`` > ``Video device name``. + +Or with Gstreamer and FFmpeg: + +.. tabs:: + + .. group-tab:: Ubuntu + + Assuming ``/dev/video0`` is your Zephyr device. + + .. code-block:: console + + ffplay -i /dev/video0 + + .. code-block:: console + + gst-launch-1.0 v4l2src device=/dev/video0 ! videoconvert ! autovideosink + + .. group-tab:: MacOS + + Assuming ``0:0`` is your Zephyr device. + + .. code-block:: console + + ffplay -f avfoundation -i 0:0 + + .. code-block:: console + + gst-launch-1.0 avfvideosrc device-index=0 ! autovideosink + + .. group-tab:: Windows + + Assuming ``UVC sample`` is your Zephyr device. + + .. code-block:: console + + ffplay.exe -f dshow -i video="UVC sample" + + .. code-block:: console + + gst-launch-1.0.exe ksvideosrc device-name="UVC sample" ! videoconvert ! autovideosink + +The video device can also be used by web and video call applications systems. + +Android and iPad (but not yet iOS) are also expected to work via dedicated applications. + +Accessing the Video Controls +============================ + +On the host system, the controls would be available as video source +control through various applications, like any webcam. + +.. tabs:: + + .. group-tab:: Ubuntu + + Assuming ``/dev/video0`` is your Zephyr device. + + .. code-block:: console + + $ v4l2-ctl --device /dev/video0 --list-ctrls + + Camera Controls + + auto_exposure 0x009a0901 (menu) : min=0 max=3 default=1 value=1 (Manual Mode) + exposure_dynamic_framerate 0x009a0903 (bool) : default=0 value=0 + exposure_time_absolute 0x009a0902 (int) : min=10 max=2047 step=1 default=384 value=384 flags=inactive + + $ v4l2-ctl --device /dev/video0 --set-ctrl auto_exposure=1 + $ v4l2-ctl --device /dev/video0 --set-ctrl exposure_time_absolute=1500 + + .. group-tab:: Windows + + The `VLC `_ client and `Pot Player `_ + client permit to further access the video controls. + + .. group-tab:: MacOS + + The `VLC `_ client and the system Webcam Settings panel + allows adjustment of the supported video controls. + + +Software Processing +=================== + +Software processing tools can also use the video interface directly. + +Here is an example with OpenCV: + +.. code-block:: python + + import cv2 + + # Number of the /dev/video# interface + devnum = 2 + + cv2.namedWindow("preview") + vc = cv2.VideoCapture(devnum) + + while (val := vc.read())[0]: + cv2.waitKey(20) + cv2.imshow("preview", val[1]) + + cv2.destroyWindow("preview") + vc.release() diff --git a/samples/subsys/usb/uvc/app.overlay b/samples/subsys/usb/uvc/app.overlay new file mode 100644 index 000000000000..8f7cc121413c --- /dev/null +++ b/samples/subsys/usb/uvc/app.overlay @@ -0,0 +1,11 @@ +/* + * Copyright (c) 2025 tinyVision.ai Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/ { + uvc: uvc { + compatible = "zephyr,uvc-device"; + }; +}; diff --git a/samples/subsys/usb/uvc/boards/arduino_nicla_vision_stm32h747xx_m7.overlay b/samples/subsys/usb/uvc/boards/arduino_nicla_vision_stm32h747xx_m7.overlay new file mode 100644 index 000000000000..01d37cca53a3 --- /dev/null +++ b/samples/subsys/usb/uvc/boards/arduino_nicla_vision_stm32h747xx_m7.overlay @@ -0,0 +1,8 @@ +/* + * Copyright (c) 2025 tinyVision.ai Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "../app.overlay" +#include "../video-dcmi.overlay" diff --git a/samples/subsys/usb/uvc/prj.conf b/samples/subsys/usb/uvc/prj.conf new file mode 100644 index 000000000000..6659c35213d2 --- /dev/null +++ b/samples/subsys/usb/uvc/prj.conf @@ -0,0 +1,12 @@ +CONFIG_LOG=y +CONFIG_VIDEO=y +CONFIG_VIDEO_BUFFER_POOL_NUM_MAX=4 +CONFIG_VIDEO_BUFFER_POOL_SZ_MAX=7300 +CONFIG_VIDEO_LOG_LEVEL_WRN=y +CONFIG_USB_DEVICE_STACK_NEXT=y +CONFIG_USBD_LOG_LEVEL_WRN=y +CONFIG_USBD_VIDEO_CLASS=y +CONFIG_USBD_VIDEO_LOG_LEVEL_DBG=y +CONFIG_UDC_DRIVER_LOG_LEVEL_WRN=y +CONFIG_SAMPLE_USBD_PID=0x0011 +CONFIG_SAMPLE_USBD_PRODUCT="UVC sample" diff --git a/samples/subsys/usb/uvc/sample.yaml b/samples/subsys/usb/uvc/sample.yaml new file mode 100644 index 000000000000..84c4c5962b7f --- /dev/null +++ b/samples/subsys/usb/uvc/sample.yaml @@ -0,0 +1,17 @@ +sample: + name: USB Video sample +tests: + sample.subsys.usb.uvc: + depends_on: + - usbd + tags: usb + integration_platforms: + - nrf52840dk/nrf52840 + - nrf54h20dk/nrf54h20/cpuapp + - frdm_k64f + - stm32f723e_disco + - nucleo_f413zh + - mimxrt685_evk/mimxrt685s/cm33 + - mimxrt1060_evk/mimxrt1062/qspi + extra_args: + - EXTRA_DTC_OVERLAY_FILE="video-emul.overlay" diff --git a/samples/subsys/usb/uvc/src/main.c b/samples/subsys/usb/uvc/src/main.c new file mode 100644 index 000000000000..5b9a72d01405 --- /dev/null +++ b/samples/subsys/usb/uvc/src/main.c @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2025 tinyVision.ai Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include + +#include +#include +#include +#include + +LOG_MODULE_REGISTER(uvc_sample, LOG_LEVEL_INF); + +#define VIDEO_DEV_SW "VIDEO_SW_GENERATOR" + +int main(void) +{ + const struct device *uvc_dev = DEVICE_DT_GET(DT_NODELABEL(uvc)); + const struct device *video_dev; + struct usbd_context *sample_usbd; + struct video_buffer *vbuf; + struct video_format fmt = {0}; + int ret; + +#if DT_HAS_CHOSEN(zephyr_camera) + video_dev = DEVICE_DT_GET(DT_CHOSEN(zephyr_camera)); +#else + video_dev = device_get_binding(VIDEO_DEV_SW); +#endif + if (video_dev == NULL || !device_is_ready(video_dev)) { + LOG_ERR("%s: video source is not ready or failed to initialize", + video_dev->name); + return -ENODEV; + } + + sample_usbd = sample_usbd_init_device(NULL); + if (sample_usbd == NULL) { + return -ENODEV; + } + + ret = usbd_enable(sample_usbd); + if (ret != 0) { + return ret; + } + + /* Get the video format once it is selected by the host */ + while (true) { + ret = video_get_format(uvc_dev, VIDEO_EP_IN, &fmt); + if (ret == 0) { + break; + } + if (ret != -EAGAIN) { + LOG_ERR("Failed to get the video format"); + return ret; + } + + k_sleep(K_MSEC(10)); + } + + ret = video_set_format(video_dev, VIDEO_EP_OUT, &fmt); + if (ret != 0) { + LOG_ERR("Failed to set %s format", video_dev->name); + return ret; + } + + LOG_INF("Preparing %u buffers of %u bytes, %ux%u frame", + CONFIG_VIDEO_BUFFER_POOL_NUM_MAX, fmt.pitch * fmt.height, fmt.pitch, fmt.height); + + /* If this gets blocked, increase CONFIG_VIDEO_BUFFER_POOL_SZ_MAX */ + vbuf = video_buffer_alloc(fmt.pitch * fmt.height, K_NO_WAIT); + if (vbuf == NULL) { + LOG_ERR("Could not allocate the video buffer"); + return -ENOMEM; + } + + ret = video_stream_start(video_dev); + if (ret != 0) { + LOG_ERR("Failed to start %s", video_dev->name); + return ret; + } + + LOG_INF("Starting video transfer"); + while (true) { + LOG_DBG("enqueuing to %s", video_dev->name); + ret = video_enqueue(video_dev, VIDEO_EP_OUT, vbuf); + if (ret != 0) { + LOG_ERR("Could not enqueue video buffer"); + return ret; + } + + LOG_DBG("dequeueing from %s", video_dev->name); + ret = video_dequeue(video_dev, VIDEO_EP_OUT, &vbuf, K_FOREVER); + if (ret != 0) { + LOG_ERR("Could not dequeue video buffer"); + return ret; + } + + LOG_DBG("enqueuing to %s", uvc_dev->name); + ret = video_enqueue(uvc_dev, VIDEO_EP_IN, vbuf); + if (ret != 0) { + LOG_ERR("Could not dequeue video buffer"); + return ret; + } + + LOG_DBG("dequeueing from %s", uvc_dev->name); + ret = video_dequeue(uvc_dev, VIDEO_EP_IN, &vbuf, K_FOREVER); + if (ret != 0) { + LOG_ERR("Could not enqueue video buffer"); + return ret; + } + LOG_DBG("dequeued vbuf %p", vbuf); + + LOG_INF("Transferred one more %ux%u frame", fmt.width, fmt.height); + } + + return 0; +} diff --git a/samples/subsys/usb/uvc/video-dcmi.overlay b/samples/subsys/usb/uvc/video-dcmi.overlay new file mode 100644 index 000000000000..94ba2eae1658 --- /dev/null +++ b/samples/subsys/usb/uvc/video-dcmi.overlay @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2025 tinyVision.ai Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +&dcmi { + port { + #address-cells = <1>; + #size-cells = <0>; + + dcmi_ep_out: endpoint@1 { + reg = <0x1>; + remote-endpoint-label = "uvc_ep_in"; + }; + }; +}; + +&uvc { + port { + uvc_ep_in: endpoint { + remote-endpoint-label = "dcmi_ep_out"; + }; + }; +}; diff --git a/samples/subsys/usb/uvc/video-emul.overlay b/samples/subsys/usb/uvc/video-emul.overlay new file mode 100644 index 000000000000..b051c843a18c --- /dev/null +++ b/samples/subsys/usb/uvc/video-emul.overlay @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2025 tinyVision.ai Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/ { + chosen { + zephyr,camera = &rx0; + }; + + imager0: emul_imager_0 { + compatible = "zephyr,video-emul-imager"; + + port { + imager0_ep_out: endpoint { + remote-endpoint-label = "rx0_ep_in"; + }; + }; + }; + + rx0: video_emul_rx_0 { + compatible = "zephyr,video-emul-rx"; + + port { + #address-cells = <1>; + #size-cells = <0>; + + rx0_ep_in: endpoint@0 { + reg = <0x0>; + remote-endpoint-label = "imager0_ep_out"; + }; + + rx0_ep_out: zephyr_camera_ep_out: endpoint@1 { + reg = <0x1>; + remote-endpoint-label = "uvc_ep_in"; + }; + }; + }; +}; + +&uvc { + port { + uvc_ep_in: endpoint { + remote-endpoint-label = "rx0_ep_out"; + }; + }; +};