From fa73ab29cf2b434f22a2a689cd4f7296f373d4ec Mon Sep 17 00:00:00 2001 From: wangjianyu3 Date: Sat, 21 Mar 2026 10:00:00 +0800 Subject: [PATCH 1/4] arch/xtensa/esp32s3: fix CAM DMA race and heap-allocate descriptors - Remove debug polling loop from esp32s3_cam_start_capture() that busy-waited on DMA status register. - Fix DMA ISR race: stop DMA before invoking capture callback to prevent ISR re-triggering while callback processes the buffer. Check priv->capturing before restarting DMA in ISR. - Move DMA descriptors from struct to heap allocation, avoiding stack/BSS alignment issues with cache-line-aligned descriptors. Signed-off-by: wangjianyu3 --- arch/xtensa/src/esp32s3/esp32s3_cam.c | 136 +++++++++++++++----------- 1 file changed, 80 insertions(+), 56 deletions(-) diff --git a/arch/xtensa/src/esp32s3/esp32s3_cam.c b/arch/xtensa/src/esp32s3/esp32s3_cam.c index f89fbb19de815..81caa2c6011c5 100644 --- a/arch/xtensa/src/esp32s3/esp32s3_cam.c +++ b/arch/xtensa/src/esp32s3/esp32s3_cam.c @@ -90,7 +90,7 @@ struct esp32s3_cam_s uint8_t cpu; int32_t dma_channel; - struct esp32s3_dmadesc_s dmadesc[ESP32S3_CAM_DMADESC_NUM]; + struct esp32s3_dmadesc_s *dmadesc; /* Heap-allocated DMA descriptors */ uint8_t *fb; /* Frame buffer */ uint32_t fb_size; /* Frame buffer size */ @@ -196,7 +196,13 @@ static int IRAM_ATTR cam_interrupt(int irq, void *context, void *arg) struct timeval ts; uint32_t regval; - /* Stop capture */ + /* Stop capture and DMA before invoking callback. + * The callback may call set_buf() which rewrites DMA + * descriptors. If DMA is still draining the CAM AFIFO + * it could read half-written descriptors and follow a + * corrupted pbuf pointer, writing pixel data over + * unrelated memory (e.g. g_cam_priv.data.ops). + */ regval = getreg32(LCD_CAM_CAM_CTRL1_REG); regval &= ~LCD_CAM_CAM_START_M; @@ -206,54 +212,72 @@ static int IRAM_ATTR cam_interrupt(int irq, void *context, void *arg) regval |= LCD_CAM_CAM_UPDATE_REG_M; putreg32(regval, LCD_CAM_CAM_CTRL_REG); + /* Stop DMA channel before callback to prevent race */ + + esp32s3_dma_reset_channel(priv->dma_channel, false); + gettimeofday(&ts, NULL); /* Notify frame complete */ priv->cb(0, priv->fb_size, &ts, priv->cb_arg); - /* Restart capture for next frame */ + /* Check if callback called stop_capture. With a + * single-buffer FIFO the V4L2 layer stops capture + * inside the callback; restarting DMA after that + * would run unsynchronized and corrupt memory. + */ - priv->vsync_cnt = 0; + if (!priv->capturing) + { + priv->vsync_cnt = 0; + } + else + { + /* Restart capture for next frame */ - /* Reset DMA */ + priv->vsync_cnt = 0; - esp32s3_dma_reset_channel(priv->dma_channel, false); + /* DMA channel was already reset before the + * callback above. Reset CAM + AFIFO now. + */ - /* Reset CAM + AFIFO */ + /* Reset CAM + AFIFO */ - regval = getreg32(LCD_CAM_CAM_CTRL1_REG); - regval |= LCD_CAM_CAM_RESET_M; - putreg32(regval, LCD_CAM_CAM_CTRL1_REG); - regval &= ~LCD_CAM_CAM_RESET_M; - putreg32(regval, LCD_CAM_CAM_CTRL1_REG); + regval = getreg32(LCD_CAM_CAM_CTRL1_REG); + regval |= LCD_CAM_CAM_RESET_M; + putreg32(regval, LCD_CAM_CAM_CTRL1_REG); + regval &= ~LCD_CAM_CAM_RESET_M; + putreg32(regval, LCD_CAM_CAM_CTRL1_REG); - regval |= LCD_CAM_CAM_AFIFO_RESET_M; - putreg32(regval, LCD_CAM_CAM_CTRL1_REG); - regval &= ~LCD_CAM_CAM_AFIFO_RESET_M; - putreg32(regval, LCD_CAM_CAM_CTRL1_REG); + regval |= LCD_CAM_CAM_AFIFO_RESET_M; + putreg32(regval, LCD_CAM_CAM_CTRL1_REG); + regval &= ~LCD_CAM_CAM_AFIFO_RESET_M; + putreg32(regval, LCD_CAM_CAM_CTRL1_REG); - /* Re-set REC_DATA_BYTELEN after reset */ + /* Re-set REC_DATA_BYTELEN after reset */ - regval &= ~LCD_CAM_CAM_REC_DATA_BYTELEN_M; - regval |= ((ESP32S3_CAM_DMA_BUFLEN - 1) - << LCD_CAM_CAM_REC_DATA_BYTELEN_S); - putreg32(regval, LCD_CAM_CAM_CTRL1_REG); + regval &= ~LCD_CAM_CAM_REC_DATA_BYTELEN_M; + regval |= ((ESP32S3_CAM_DMA_BUFLEN - 1) + << LCD_CAM_CAM_REC_DATA_BYTELEN_S); + putreg32(regval, LCD_CAM_CAM_CTRL1_REG); - /* Reload DMA descriptors */ + /* Reload DMA descriptors */ - esp32s3_dma_load(priv->dmadesc, priv->dma_channel, false); - esp32s3_dma_enable(priv->dma_channel, false); + esp32s3_dma_load(priv->dmadesc, priv->dma_channel, + false); + esp32s3_dma_enable(priv->dma_channel, false); - /* Restart */ + /* Restart */ - regval = getreg32(LCD_CAM_CAM_CTRL1_REG); - regval |= LCD_CAM_CAM_START_M; - putreg32(regval, LCD_CAM_CAM_CTRL1_REG); + regval = getreg32(LCD_CAM_CAM_CTRL1_REG); + regval |= LCD_CAM_CAM_START_M; + putreg32(regval, LCD_CAM_CAM_CTRL1_REG); - regval = getreg32(LCD_CAM_CAM_CTRL_REG); - regval |= LCD_CAM_CAM_UPDATE_REG_M; - putreg32(regval, LCD_CAM_CAM_CTRL_REG); + regval = getreg32(LCD_CAM_CAM_CTRL_REG); + regval |= LCD_CAM_CAM_UPDATE_REG_M; + putreg32(regval, LCD_CAM_CAM_CTRL_REG); + } } } } @@ -512,6 +536,23 @@ static int esp32s3_cam_init(struct imgdata_s *data) spin_lock_init(&priv->lock); + /* Allocate DMA descriptors from heap so they are isolated from + * the driver struct. If GDMA ever follows a stale/corrupted + * descriptor it will scribble on heap, not on g_cam_priv. + */ + + if (priv->dmadesc == NULL) + { + priv->dmadesc = kmm_memalign(4, + sizeof(struct esp32s3_dmadesc_s) * + ESP32S3_CAM_DMADESC_NUM); + if (priv->dmadesc == NULL) + { + snerr("ERROR: Failed to allocate DMA descriptors\n"); + return -ENOMEM; + } + } + /* Configure GPIO pins */ esp32s3_cam_gpio_config(); @@ -554,6 +595,14 @@ static int esp32s3_cam_uninit(struct imgdata_s *data) priv->dma_channel = -1; } + /* Free DMA descriptors */ + + if (priv->dmadesc) + { + kmm_free(priv->dmadesc); + priv->dmadesc = NULL; + } + /* Free frame buffer */ if (priv->fb) @@ -723,31 +772,6 @@ static int esp32s3_cam_start_capture(struct imgdata_s *data, priv->capturing = true; - syslog(LOG_INFO, "CAM start: CTRL=0x%08lx CTRL1=0x%08lx ENA=0x%08lx\n", - (unsigned long)getreg32(LCD_CAM_CAM_CTRL_REG), - (unsigned long)getreg32(LCD_CAM_CAM_CTRL1_REG), - (unsigned long)getreg32(LCD_CAM_LC_DMA_INT_ENA_REG)); - - /* Debug: poll RAW register to see if VSYNC appears */ - - for (int i = 0; i < 50; i++) - { - up_mdelay(20); - uint32_t raw = getreg32(LCD_CAM_LC_DMA_INT_RAW_REG); - uint32_t st = getreg32(LCD_CAM_LC_DMA_INT_ST_REG); - if (raw || st) - { - syslog(LOG_INFO, "CAM poll[%d]: raw=0x%08lx st=0x%08lx\n", - i, (unsigned long)raw, (unsigned long)st); - break; - } - - if (i == 49) - { - syslog(LOG_INFO, "CAM poll: no VSYNC after 1s\n"); - } - } - return OK; } From e2ef473c0a3c68498311d7ac27c18386b81afc3d Mon Sep 17 00:00:00 2001 From: wangjianyu3 Date: Sat, 21 Mar 2026 14:00:00 +0800 Subject: [PATCH 2/4] drivers/video/gc0308: add YUYV output format support GC0308 sensor supports multiple output formats via register 0x24. Add YUYV (YCbCr422) alongside existing RGB565 in fmtdescs and configure the output format register dynamically in start_capture() based on the requested pixel format. Signed-off-by: wangjianyu3 --- drivers/video/gc0308.c | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/drivers/video/gc0308.c b/drivers/video/gc0308.c index 339940e5a69bc..e6a98753c6def 100644 --- a/drivers/video/gc0308.c +++ b/drivers/video/gc0308.c @@ -154,6 +154,7 @@ struct gc0308_dev_s struct i2c_master_s *i2c; uint16_t width; uint16_t height; + uint32_t pixelformat; struct v4l2_frmsizeenum frmsizes; bool streaming; }; @@ -455,6 +456,10 @@ static const struct imgsensor_ops_s g_gc0308_ops = static const struct v4l2_fmtdesc g_gc0308_fmtdescs[] = { + { + .pixelformat = V4L2_PIX_FMT_YUYV, + .description = "YUV 4:2:2 (YUYV)", + }, { .pixelformat = V4L2_PIX_FMT_RGB565, .description = "RGB565", @@ -648,7 +653,7 @@ static int gc0308_init(struct imgsensor_s *sensor) up_mdelay(80); - /* Set RGB565 output format: reg 0x24 bits[3:0] = 6 */ + /* Set default RGB565 output format: reg 0x24 bits[3:0] = 6 */ ret = gc0308_putreg(priv->i2c, GC0308_REG_RESET, 0x00); if (ret < 0) @@ -657,6 +662,7 @@ static int gc0308_init(struct imgsensor_s *sensor) } ret = gc0308_modreg(priv->i2c, GC0308_REG_OUTPUT_FMT, 0x0f, 0x06); + priv->pixelformat = IMGSENSOR_PIX_FMT_RGB565; if (ret < 0) { return ret; @@ -752,6 +758,8 @@ static int gc0308_validate_frame_setting(struct imgsensor_s *sensor, } if (datafmts[IMGSENSOR_FMT_MAIN].pixelformat != + IMGSENSOR_PIX_FMT_YUYV && + datafmts[IMGSENSOR_FMT_MAIN].pixelformat != IMGSENSOR_PIX_FMT_RGB565) { return -EINVAL; @@ -777,12 +785,32 @@ static int gc0308_start_capture(struct imgsensor_s *sensor, imgsensor_interval_t *interval) { struct gc0308_dev_s *priv = (struct gc0308_dev_s *)sensor; + uint8_t fmtval; + int ret; if (priv->streaming) { return -EBUSY; } + /* Configure output format register based on requested pixel format */ + + if (datafmts[IMGSENSOR_FMT_MAIN].pixelformat == IMGSENSOR_PIX_FMT_RGB565) + { + fmtval = 0x06; /* RGB565 */ + } + else + { + fmtval = 0x02; /* YCbCr422 */ + } + + ret = gc0308_modreg(priv->i2c, GC0308_REG_OUTPUT_FMT, 0x0f, fmtval); + if (ret < 0) + { + return ret; + } + + priv->pixelformat = datafmts[IMGSENSOR_FMT_MAIN].pixelformat; priv->streaming = true; return OK; } From 5e854c948d88c0aa10466702c21ecbe243902e1a Mon Sep 17 00:00:00 2001 From: wangjianyu3 Date: Sun, 22 Mar 2026 10:00:00 +0800 Subject: [PATCH 3/4] drivers/usbdev: add UVC gadget class driver Add USB Video Class 1.1 gadget driver supporting Bulk transport with uncompressed YUY2 video streaming. Resolution and frame interval are negotiated dynamically via PROBE/COMMIT control. - uvc.h: protocol constants, streaming control struct, public API - uvc.c: class driver with PROBE/COMMIT, bulk EP, /dev/uvc0 chardev - Kconfig/Make.defs: USBUVC config and build rules - boardctl.c: BOARDIOC_USBDEV_UVC standalone init path Hardened against host disconnect: - Removed nxmutex_lock from USB interrupt context paths - Added 30s semaphore timeout in uvc_write with EP_CANCEL fallback - Drain stale wrsem counts in VS_COMMIT before new stream - Guard uvc_streaming_stop() against double EP_CANCEL race - Handle EP_SUBMIT returning -ESHUTDOWN gracefully Signed-off-by: wangjianyu3 --- .../components/drivers/special/index.rst | 1 + .../components/drivers/special/uvc.rst | 127 ++ boards/boardctl.c | 39 + drivers/usbdev/CMakeLists.txt | 4 + drivers/usbdev/Kconfig | 45 + drivers/usbdev/Make.defs | 4 + drivers/usbdev/uvc.c | 1665 +++++++++++++++++ include/nuttx/usb/uvc.h | 282 +++ include/sys/boardctl.h | 3 + 9 files changed, 2170 insertions(+) create mode 100644 Documentation/components/drivers/special/uvc.rst create mode 100644 drivers/usbdev/uvc.c create mode 100644 include/nuttx/usb/uvc.h diff --git a/Documentation/components/drivers/special/index.rst b/Documentation/components/drivers/special/index.rst index 919362b895033..1444d74eb9b04 100644 --- a/Documentation/components/drivers/special/index.rst +++ b/Documentation/components/drivers/special/index.rst @@ -44,6 +44,7 @@ following section. syslog.rst sdio.rst usbdev.rst + uvc.rst usbhost.rst usbmisc.rst usbmonitor.rst diff --git a/Documentation/components/drivers/special/uvc.rst b/Documentation/components/drivers/special/uvc.rst new file mode 100644 index 0000000000000..9c39a1aad9d39 --- /dev/null +++ b/Documentation/components/drivers/special/uvc.rst @@ -0,0 +1,127 @@ +====================================== +USB Video Class (UVC) Gadget Driver +====================================== + +Overview +======== + +The UVC gadget driver (``drivers/usbdev/uvc.c``) implements a USB Video Class +1.1 device that makes NuttX appear as a USB webcam to the host. The driver +exposes a character device (``/dev/uvc0``) that applications write video frames +to. It handles all UVC class-specific control requests (PROBE / COMMIT) +internally and uses bulk transfers for video data. + +The driver supports two modes: + +- **Standalone** – the UVC function is the only USB class on the bus. +- **Composite** – the UVC function is combined with other USB class drivers + (e.g. CDC/ACM) via the NuttX composite device framework. + +Features +======== + +- UVC 1.1 compliant (uncompressed YUY2 format) +- Bulk transfer mode for video data +- Automatic PROBE / COMMIT negotiation with the host +- ``poll()`` support – applications can wait for the host to start streaming + (``POLLOUT``) and detect disconnection (``POLLHUP``) +- Runtime video parameters – resolution and frame rate are passed at + initialization time, so USB descriptors always match the actual sensor +- ``boardctl()`` integration for easy application-level bring-up + +Configuration +============= + +The driver is enabled through the following Kconfig options: + +.. code-block:: kconfig + + CONFIG_USBUVC=y # Enable UVC gadget support + CONFIG_USBUVC_COMPOSITE=n # Set y for composite device mode + CONFIG_USBUVC_EPBULKIN=1 # Bulk IN endpoint number (standalone) + CONFIG_USBUVC_EP0MAXPACKET=64 # EP0 max packet size (standalone) + CONFIG_USBUVC_EPBULKIN_FSSIZE=64 # Bulk IN full-speed max packet size + CONFIG_USBUVC_NWRREQS=4 # Number of pre-allocated write requests + CONFIG_USBUVC_NPOLLWAITERS=2 # Number of poll waiters + +Header Files +============ + +- ``include/nuttx/usb/uvc.h`` – Public API, UVC descriptor constants, and + data structures. + +Data Structures +=============== + +``struct uvc_params_s`` +----------------------- + +Passed to the initialization function so that USB descriptors reflect the +actual sensor capabilities:: + + struct uvc_params_s + { + uint16_t width; /* Frame width in pixels */ + uint16_t height; /* Frame height in pixels */ + uint8_t fps; /* Frames per second */ + }; + +Public Interfaces +================= + +Standalone Mode +--------------- + +``usbdev_uvc_initialize()`` + Initialize the UVC gadget and register ``/dev/uvc0``. *params* may be + ``NULL`` to use defaults (320 × 240 @ 5 fps). Returns a handle for later + ``usbdev_uvc_uninitialize()``. + +``usbdev_uvc_uninitialize()`` + Tear down the UVC gadget and unregister the character device. + +Composite Mode +-------------- + +``usbdev_uvc_classobject()`` + Create a UVC class driver instance for use inside a composite device. + +``usbdev_uvc_classuninitialize()`` + Destroy a class driver instance created by ``usbdev_uvc_classobject()``. + +``usbdev_uvc_get_composite_devdesc()`` + Fill a ``composite_devdesc_s`` for the composite framework. + +boardctl Integration +-------------------- + +Applications can manage the UVC gadget through ``boardctl()``:: + + struct boardioc_usbdev_ctrl_s ctrl; + struct uvc_params_s params = { .width = 320, .height = 240, .fps = 15 }; + FAR void *handle = (FAR void *)¶ms; + + ctrl.usbdev = BOARDIOC_USBDEV_UVC; + ctrl.action = BOARDIOC_USBDEV_CONNECT; + ctrl.instance = 0; + ctrl.config = 0; + ctrl.handle = &handle; + + boardctl(BOARDIOC_USBDEV_CONTROL, (uintptr_t)&ctrl); + + /* ... use /dev/uvc0 ... */ + + ctrl.action = BOARDIOC_USBDEV_DISCONNECT; + boardctl(BOARDIOC_USBDEV_CONTROL, (uintptr_t)&ctrl); + +Device Operation +================ + +1. The application opens ``/dev/uvc0`` for writing. +2. Use ``poll()`` with ``POLLOUT`` to wait for the USB host to start streaming + (i.e. the host sends a VS_COMMIT_CONTROL SET_CUR request). +3. Write complete video frames via ``write()``. The driver prepends a 2-byte + UVC payload header (with FID / EOF bits) automatically. +4. When the host stops streaming, ``write()`` returns ``-EAGAIN``. The + application can ``poll()`` again to wait for the host to restart. +5. ``POLLHUP`` is reported when the host explicitly stops streaming. diff --git a/boards/boardctl.c b/boards/boardctl.c index 12743473edbeb..258ee99aa22d2 100644 --- a/boards/boardctl.c +++ b/boards/boardctl.c @@ -56,6 +56,7 @@ # include # include # include +# include # include #endif @@ -206,6 +207,44 @@ static inline int break; #endif +#if defined(CONFIG_USBUVC) && !defined(CONFIG_USBUVC_COMPOSITE) + case BOARDIOC_USBDEV_UVC: /* UVC class */ + switch (ctrl->action) + { + case BOARDIOC_USBDEV_INITIALIZE: /* Initialize UVC device */ + break; + + case BOARDIOC_USBDEV_CONNECT: /* Connect the UVC device */ + { + FAR const struct uvc_params_s *params; + + DEBUGASSERT(ctrl->handle != NULL); + + /* Application passes video params via *handle */ + + params = (FAR const struct uvc_params_s *)*ctrl->handle; + *ctrl->handle = usbdev_uvc_initialize(params); + if (*ctrl->handle == NULL) + { + ret = -EIO; + } + } + break; + + case BOARDIOC_USBDEV_DISCONNECT: /* Disconnect the UVC device */ + { + DEBUGASSERT(ctrl->handle != NULL && *ctrl->handle != NULL); + usbdev_uvc_uninitialize(*ctrl->handle); + } + break; + + default: + ret = -EINVAL; + break; + } + break; +#endif + #ifdef CONFIG_USBDEV_COMPOSITE case BOARDIOC_USBDEV_COMPOSITE: /* Composite device */ switch (ctrl->action) diff --git a/drivers/usbdev/CMakeLists.txt b/drivers/usbdev/CMakeLists.txt index cae55d63e39bf..261c90ce0c604 100644 --- a/drivers/usbdev/CMakeLists.txt +++ b/drivers/usbdev/CMakeLists.txt @@ -61,6 +61,10 @@ if(CONFIG_USBDEV) list(APPEND SRCS mtp.c) endif() + if(CONFIG_USBUVC) + list(APPEND SRCS uvc.c) + endif() + if(CONFIG_NET_CDCECM) list(APPEND SRCS cdcecm.c) endif() diff --git a/drivers/usbdev/Kconfig b/drivers/usbdev/Kconfig index e1ad9ddde96ec..6a369baf9892a 100644 --- a/drivers/usbdev/Kconfig +++ b/drivers/usbdev/Kconfig @@ -948,6 +948,51 @@ config USBADB_INTERFACESTR endif # USBADB +menuconfig USBUVC + bool "USB Video Class (UVC) gadget support" + default n + depends on USBDEV + ---help--- + Enables USB Video Class (UVC) gadget device support. + The device appears as a USB webcam to the host. + +if USBUVC + +config USBUVC_COMPOSITE + bool "UVC composite support" + default n + depends on USBDEV_COMPOSITE + ---help--- + Configure the UVC driver as part of a composite USB device. + When enabled, the UVC gadget can be combined with other USB + class drivers (e.g., CDC/ACM) in a single composite device. + +if !USBUVC_COMPOSITE + +config USBUVC_EPBULKIN + int "Bulk IN endpoint number" + default 1 + +config USBUVC_EP0MAXPACKET + int "Max packet size for EP0" + default 64 + +endif # !USBUVC_COMPOSITE + +config USBUVC_EPBULKIN_FSSIZE + int "Bulk IN full-speed max packet size" + default 64 + +config USBUVC_NWRREQS + int "Number of write requests" + default 4 + +config USBUVC_NPOLLWAITERS + int "Number of poll waiters" + default 2 + +endif # USBUVC + menuconfig USBMSC bool "USB Mass storage class device" default n diff --git a/drivers/usbdev/Make.defs b/drivers/usbdev/Make.defs index 8532b800f389e..bf426bbc2a4c5 100644 --- a/drivers/usbdev/Make.defs +++ b/drivers/usbdev/Make.defs @@ -56,6 +56,10 @@ ifeq ($(CONFIG_USBADB),y) CSRCS += adb.c endif +ifeq ($(CONFIG_USBUVC),y) + CSRCS += uvc.c +endif + ifeq ($(CONFIG_USBMTP),y) CSRCS += mtp.c endif diff --git a/drivers/usbdev/uvc.c b/drivers/usbdev/uvc.c new file mode 100644 index 0000000000000..5b90cacf0164c --- /dev/null +++ b/drivers/usbdev/uvc.c @@ -0,0 +1,1665 @@ +/**************************************************************************** + * drivers/usbdev/uvc.c + * + * SPDX-License-Identifier: Apache-2.0 + * + * USB Video Class (UVC) 1.1 Gadget Driver for NuttX + * Bulk transport, uncompressed YUY2, QVGA (320x240) + * + * Architecture: modeled after drivers/usbdev/adb.c (usbdev_fs pattern) + * but with custom class-specific setup handling for UVC PROBE/COMMIT. + * + ****************************************************************************/ + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#ifdef CONFIG_USBDEV_COMPOSITE +# include +#endif +#include +#include +#include +#include +#include + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +#define USBUVC_CHARDEV_PATH "/dev/uvc0" + +#ifdef CONFIG_USBDEV_SELFPOWERED +# define USBUVC_SELFPOWERED USB_CONFIG_ATTR_SELFPOWER +#else +# define USBUVC_SELFPOWERED (0) +#endif + +#ifdef CONFIG_USBDEV_REMOTEWAKEUP +# define USBUVC_REMOTEWAKEUP USB_CONFIG_ATTR_WAKEUP +#else +# define USBUVC_REMOTEWAKEUP (0) +#endif + +#define USBUVC_MXDESCLEN (64) +#define USBUVC_MAXSTRLEN (USBUVC_MXDESCLEN - 2) + +#define USBUVC_VERSIONNO (0x0100) +#define USBUVC_STR_LANGUAGE (0x0409) + +/* String descriptor IDs — standalone mode uses absolute IDs; + * composite mode uses relative IDs offset by devinfo.strbase. + */ + +#ifndef CONFIG_USBUVC_COMPOSITE +# define USBUVC_MANUFACTURERSTRID (1) +# define USBUVC_PRODUCTSTRID (2) +# define USBUVC_SERIALSTRID (3) +# define USBUVC_CONFIGSTRID (4) +# define USBUVC_VCIFSTRID (5) +# define USBUVC_VSIFSTRID (6) +# define USBUVC_NSTDSTRIDS (6) /* Total standalone string IDs */ +#endif + +/* In composite mode, we only contribute 2 interface strings. + * The composite framework assigns strbase; our strings are + * strbase+1 (VC) and strbase+2 (VS). + */ + +#define USBUVC_VCIFSTRID_REL (1) /* Relative: VC interface string */ +#define USBUVC_VSIFSTRID_REL (2) /* Relative: VS interface string */ + +/* Video parameters — defaults when no runtime params supplied */ + +#define USBUVC_DEF_WIDTH 320 +#define USBUVC_DEF_HEIGHT 240 +#define USBUVC_DEF_FPS 5 +#define USBUVC_BPP 2 /* YUY2 = 2 bytes/pixel */ + +/* Number of poll waiters */ + +#ifndef CONFIG_USBUVC_NPOLLWAITERS +# define CONFIG_USBUVC_NPOLLWAITERS 2 +#endif + +/* Number of write requests */ + +#ifndef CONFIG_USBUVC_NWRREQS +# define CONFIG_USBUVC_NWRREQS 4 +#endif + +/* Bulk IN EP max packet size (Full-Speed) */ + +#ifndef CONFIG_USBUVC_EPBULKIN_FSSIZE +# define CONFIG_USBUVC_EPBULKIN_FSSIZE 64 +#endif + +#ifndef CONFIG_USBUVC_EP0MAXPACKET +# define CONFIG_USBUVC_EP0MAXPACKET 64 +#endif + +/* UVC descriptor unit/terminal IDs */ + +#define USBUVC_IT_ID 1 /* Input Terminal (Camera) */ +#define USBUVC_OT_ID 2 /* Output Terminal (USB Streaming) */ + +/**************************************************************************** + * Private Types + ****************************************************************************/ + +/* Container for write requests — flink must be first for sq_entry_t cast */ + +struct uvc_wrreq_s +{ + FAR struct uvc_wrreq_s *flink; /* Singly linked list link */ + FAR struct usbdev_req_s *req; /* The contained USB request */ +}; + +struct uvc_dev_s +{ + FAR struct usbdev_s *usbdev; + FAR struct usbdev_ep_s *epbulkin; + FAR struct usbdev_req_s *ctrlreq; + FAR struct usbdev_ep_s *ep0; + + struct uvc_streaming_control_s probe; + struct uvc_streaming_control_s commit; + struct usbdev_devinfo_s devinfo; + + /* Runtime video parameters from application */ + + uint16_t width; + uint16_t height; + uint8_t fps; + uint32_t frame_size; + uint32_t frame_interval; /* in 100ns units */ + uint32_t bitrate; + + mutex_t lock; + sem_t wrsem; /* Signaled when wrreq available */ + bool streaming; + FAR struct pollfd *fds[CONFIG_USBUVC_NPOLLWAITERS]; + uint8_t fid; /* Frame ID toggle bit */ + uint8_t config; + + /* Write request pool */ + + struct sq_queue_s wrfree; /* Available write request containers */ + struct uvc_wrreq_s wrreqs[CONFIG_USBUVC_NWRREQS]; + int nwralloc; +}; + +/* Forward declarations */ + +/* EP0 submit helper — in composite mode, route through composite layer */ + +#ifdef CONFIG_USBUVC_COMPOSITE +# define UVC_EP0SUBMIT(driver, dev, ctrlreq, ctrl) \ + composite_ep0submit(driver, dev, ctrlreq, ctrl) +#else +# define UVC_EP0SUBMIT(driver, dev, ctrlreq, ctrl) \ + EP_SUBMIT(dev->ep0, ctrlreq) +#endif + +static int uvc_bind(FAR struct usbdevclass_driver_s *driver, + FAR struct usbdev_s *dev); +static void uvc_unbind(FAR struct usbdevclass_driver_s *driver, + FAR struct usbdev_s *dev); +static int uvc_setup(FAR struct usbdevclass_driver_s *driver, + FAR struct usbdev_s *dev, + FAR const struct usb_ctrlreq_s *ctrl, + FAR uint8_t *dataout, size_t outlen); +static void uvc_disconnect(FAR struct usbdevclass_driver_s *driver, + FAR struct usbdev_s *dev); +static void uvc_streaming_stop(FAR struct uvc_dev_s *priv); + +static const struct usbdevclass_driverops_s g_uvc_driverops = +{ + .bind = uvc_bind, + .unbind = uvc_unbind, + .setup = uvc_setup, + .disconnect = uvc_disconnect, +}; + +/* File operations for /dev/uvc0 */ + +static ssize_t uvc_write(FAR struct file *filep, + FAR const char *buffer, size_t buflen); +static int uvc_open(FAR struct file *filep); +static int uvc_close(FAR struct file *filep); +static int uvc_poll(FAR struct file *filep, + FAR struct pollfd *fds, bool setup); + +static const struct file_operations g_uvc_fops = +{ + .open = uvc_open, + .close = uvc_close, + .write = uvc_write, + .poll = uvc_poll, +}; + +/**************************************************************************** + * Private Data — USB Descriptors + ****************************************************************************/ + +#ifndef CONFIG_USBUVC_COMPOSITE + +/* Device Descriptor — standalone mode only */ + +static const struct usb_devdesc_s g_uvc_devdesc = +{ + .len = USB_SIZEOF_DEVDESC, + .type = USB_DESC_TYPE_DEVICE, + .usb = + { + LSBYTE(0x0200), MSBYTE(0x0200) + }, + .classid = USB_CLASS_MISC, /* 0xEF - IAD device */ + .subclass = 0x02, /* Common Class */ + .protocol = 0x01, /* IAD */ + .mxpacketsize = CONFIG_USBUVC_EP0MAXPACKET, + .vendor = + { + LSBYTE(0x1d6b), MSBYTE(0x1d6b) /* Linux Foundation */ + }, + .product = + { + LSBYTE(0x0102), MSBYTE(0x0102) /* Webcam gadget */ + }, + .device = + { + LSBYTE(USBUVC_VERSIONNO), + MSBYTE(USBUVC_VERSIONNO) + }, + .imfgr = USBUVC_MANUFACTURERSTRID, + .iproduct = USBUVC_PRODUCTSTRID, + .serno = USBUVC_SERIALSTRID, + .nconfigs = USBUVC_NCONFIGS, +}; + +static const struct usb_cfgdesc_s g_uvc_cfgdesc = +{ + .len = USB_SIZEOF_CFGDESC, + .type = USB_DESC_TYPE_CONFIG, + .ninterfaces = USBUVC_NINTERFACES, + .cfgvalue = 1, + .icfg = USBUVC_CONFIGSTRID, + .attr = USB_CONFIG_ATTR_ONE | USBUVC_SELFPOWERED | + USBUVC_REMOTEWAKEUP, + .mxpower = (CONFIG_USBDEV_MAXPOWER + 1) / 2, +}; + +#endif /* !CONFIG_USBUVC_COMPOSITE */ + +/* Interface Association Descriptor (IAD) for UVC + * Note: bFirstInterface and iFunction are patched at runtime. + */ + +static const uint8_t g_uvc_iad[] = +{ + 0x08, /* bLength */ + USB_DESC_TYPE_INTERFACEASSOCIATION, /* bDescriptorType */ + 0x00, /* bFirstInterface: patched */ + 0x02, /* bInterfaceCount (VC + VS) */ + USB_CLASS_VIDEO, /* bFunctionClass */ + UVC_SC_VIDEO_INTERFACE_COLLECTION, /* bFunctionSubClass */ + UVC_PC_PROTOCOL_UNDEFINED, /* bFunctionProtocol */ + 0x00, /* iFunction: patched */ +}; + +/* VideoControl Interface Descriptor + * Note: ifno and iif are patched at runtime. + */ + +static const struct usb_ifdesc_s g_uvc_vc_ifdesc = +{ + .len = USB_SIZEOF_IFDESC, + .type = USB_DESC_TYPE_INTERFACE, + .ifno = 0, /* patched: ifnobase */ + .alt = 0, + .neps = 0, /* No interrupt EP for now */ + .classid = USB_CLASS_VIDEO, + .subclass = UVC_SC_VIDEOCONTROL, + .protocol = UVC_PC_PROTOCOL_UNDEFINED, + .iif = 0, /* patched: strbase + VCIFSTRID_REL */ +}; + +/* VC Header Descriptor (12 + 1*n bytes, n=1 VS interface) + * Note: baInterfaceNr is patched at runtime to ifnobase + 1. + */ + +static const uint8_t g_uvc_vc_header[] = +{ + 0x0d, /* bLength (13) */ + UVC_CS_INTERFACE, /* bDescriptorType */ + UVC_VC_HEADER, /* bDescriptorSubtype */ + LSBYTE(0x0110), /* bcdUVC: 1.10 */ + MSBYTE(0x0110), + LSBYTE(0x0042), /* wTotalLength: will be patched */ + MSBYTE(0x0042), + (0x005b8d80 >> 0) & 0xff, /* dwClockFrequency: 6MHz */ + (0x005b8d80 >> 8) & 0xff, + (0x005b8d80 >> 16) & 0xff, + (0x005b8d80 >> 24) & 0xff, + 0x01, /* bInCollection: 1 VS interface */ + 0x00, /* baInterfaceNr(1): patched */ +}; + +/* Input Terminal Descriptor (Camera) — 17 bytes */ + +static const uint8_t g_uvc_input_terminal[] = +{ + 0x11, /* bLength (17) */ + UVC_CS_INTERFACE, /* bDescriptorType */ + UVC_VC_INPUT_TERMINAL, /* bDescriptorSubtype */ + USBUVC_IT_ID, /* bTerminalID */ + LSBYTE(UVC_ITT_CAMERA), /* wTerminalType */ + MSBYTE(UVC_ITT_CAMERA), + 0x00, /* bAssocTerminal */ + 0x00, /* iTerminal */ + 0x00, 0x00, /* wObjectiveFocalLengthMin */ + 0x00, 0x00, /* wObjectiveFocalLengthMax */ + 0x00, 0x00, /* wOcularFocalLength */ + 0x02, /* bControlSize */ + 0x00, 0x00, /* bmControls (none) */ +}; + +/* Output Terminal Descriptor — 9 bytes */ + +static const uint8_t g_uvc_output_terminal[] = +{ + 0x09, /* bLength */ + UVC_CS_INTERFACE, /* bDescriptorType */ + UVC_VC_OUTPUT_TERMINAL, /* bDescriptorSubtype */ + USBUVC_OT_ID, /* bTerminalID */ + LSBYTE(UVC_TT_STREAMING), /* wTerminalType */ + MSBYTE(UVC_TT_STREAMING), + 0x00, /* bAssocTerminal */ + USBUVC_IT_ID, /* bSourceID */ + 0x00, /* iTerminal */ +}; + +/* VideoStreaming Interface Descriptor + * Note: ifno and iif are patched at runtime. + */ + +static const struct usb_ifdesc_s g_uvc_vs_ifdesc = +{ + .len = USB_SIZEOF_IFDESC, + .type = USB_DESC_TYPE_INTERFACE, + .ifno = 0, /* patched: ifnobase + 1 */ + .alt = 0, + .neps = 1, /* 1 Bulk IN EP */ + .classid = USB_CLASS_VIDEO, + .subclass = UVC_SC_VIDEOSTREAMING, + .protocol = UVC_PC_PROTOCOL_UNDEFINED, + .iif = 0, /* patched: strbase + VSIFSTRID_REL */ +}; + +/* VS Input Header — 14 bytes (1 format, bControlSize=1) */ + +static const uint8_t g_uvc_vs_input_header[] = +{ + 0x0e, /* bLength (14) */ + UVC_CS_INTERFACE, /* bDescriptorType */ + UVC_VS_INPUT_HEADER, /* bDescriptorSubtype */ + 0x01, /* bNumFormats: 1 */ + 0x00, 0x00, /* wTotalLength: patched later */ + 0x00, /* bEndpointAddress: patched later */ + 0x00, /* bmInfo: no dynamic format change */ + USBUVC_OT_ID, /* bTerminalLink */ + 0x00, /* bStillCaptureMethod: none */ + 0x00, /* bTriggerSupport */ + 0x00, /* bTriggerUsage */ + 0x01, /* bControlSize: 1 byte */ + 0x00, /* bmaControls(1): no controls */ +}; + +/* VS Uncompressed Format Descriptor — 27 bytes */ + +static const uint8_t g_uvc_vs_format_uncomp[] = +{ + 0x1b, /* bLength (27) */ + UVC_CS_INTERFACE, /* bDescriptorType */ + UVC_VS_FORMAT_UNCOMPRESSED, /* bDescriptorSubtype */ + 0x01, /* bFormatIndex: 1 */ + 0x01, /* bNumFrameDescriptors: 1 */ + + /* guidFormat: YUY2 */ + + 'Y', 'U', 'Y', '2', 0x00, 0x00, 0x10, 0x00, + 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71, + + 0x10, /* bBitsPerPixel: 16 */ + 0x01, /* bDefaultFrameIndex: 1 */ + 0x00, /* bAspectRatioX */ + 0x00, /* bAspectRatioY */ + 0x00, /* bmInterlaceFlags */ + 0x00, /* bCopyProtect */ +}; + +/* VS Uncompressed Frame Descriptor length - 30 bytes (1 discrete interval) */ + +#define USBUVC_VS_FRAME_DESC_LEN 30 + +/* VS Color Matching Descriptor — 6 bytes */ + +static const uint8_t g_uvc_vs_color[] = +{ + 0x06, /* bLength */ + UVC_CS_INTERFACE, /* bDescriptorType */ + UVC_VS_COLOR_FORMAT, /* bDescriptorSubtype */ + 0x01, /* bColorPrimaries: BT.709 */ + 0x01, /* bTransferCharacteristics: BT.709 */ + 0x04, /* bMatrixCoefficients: SMPTE 170M */ +}; + +/* Bulk IN Endpoint info */ + +static const struct usbdev_epinfo_s g_uvc_epbulkin = +{ + .desc = + { + .len = USB_SIZEOF_EPDESC, + .type = USB_DESC_TYPE_ENDPOINT, + .addr = USB_DIR_IN, + .attr = USB_EP_ATTR_XFER_BULK | + USB_EP_ATTR_NO_SYNC | + USB_EP_ATTR_USAGE_DATA, + .interval = 0, + }, + .reqnum = CONFIG_USBUVC_NWRREQS, + .fssize = CONFIG_USBUVC_EPBULKIN_FSSIZE, +}; + +/* Endpoint info array for composite framework */ + +#ifdef CONFIG_USBDEV_COMPOSITE +static FAR const struct usbdev_epinfo_s *g_uvc_epinfos[USBUVC_NUM_EPS] = +{ + &g_uvc_epbulkin, +}; +#endif + +/* Global device instance (singleton) */ + +static struct uvc_dev_s *g_uvc_dev; + +/**************************************************************************** + * Private Functions — Helpers + ****************************************************************************/ + +static void uvc_default_streaming_ctrl( + FAR struct uvc_dev_s *priv, + FAR struct uvc_streaming_control_s *ctrl) +{ + memset(ctrl, 0, sizeof(*ctrl)); + ctrl->bmhint = 1; + ctrl->bformatindex = 1; + ctrl->bframeindex = 1; + ctrl->dwframeinterval = priv->frame_interval; + ctrl->dwmaxvideoframesize = priv->frame_size; + ctrl->dwmaxpayloadtransfersize = priv->frame_size + + UVC_PAYLOAD_HEADER_LEN; + ctrl->dwclockfrequency = 6000000; /* 6 MHz */ + ctrl->bmframinginfo = 0x03; /* FID + EOF required */ + ctrl->bpreferedversion = 1; + ctrl->bminversion = 1; + ctrl->bmaxversion = 1; +} + +static void uvc_ep0incomplete(FAR struct usbdev_ep_s *ep, + FAR struct usbdev_req_s *req) +{ + if (req->result || req->xfrd != req->len) + { + uerr("EP0 complete: result=%d xfrd=%d len=%d\n", + req->result, req->xfrd, req->len); + } +} + +static void uvc_wrreq_callback(FAR struct usbdev_ep_s *ep, + FAR struct usbdev_req_s *req) +{ + FAR struct uvc_dev_s *priv = g_uvc_dev; + + if (priv) + { + FAR struct uvc_wrreq_s *wrcontainer; + irqstate_t flags; + + if (req->result != OK) + { + uerr("UVC bulk IN xfer failed: %d\n", req->result); + } + + wrcontainer = (FAR struct uvc_wrreq_s *)req->priv; + flags = enter_critical_section(); + sq_addlast((FAR sq_entry_t *)wrcontainer, &priv->wrfree); + leave_critical_section(flags); + + nxsem_post(&priv->wrsem); + } +} + +/**************************************************************************** + * Private Functions — Configuration Descriptor Builder + ****************************************************************************/ + +static int16_t uvc_mkcfgdesc(FAR uint8_t *buf, + FAR struct usbdev_devinfo_s *devinfo, + uint8_t speed, uint8_t type) +{ + FAR struct uvc_dev_s *priv = g_uvc_dev; + FAR uint8_t *p = buf; + uint16_t vs_total; + uint16_t vc_total; + int16_t totallen; + uint8_t vc_ifno; + uint8_t vs_ifno; + uint8_t vc_strid; + uint8_t vs_strid; + + UNUSED(speed); + UNUSED(type); + + /* Compute interface numbers and string IDs from devinfo */ + + vc_ifno = devinfo->ifnobase; + vs_ifno = devinfo->ifnobase + 1; + vc_strid = devinfo->strbase + USBUVC_VCIFSTRID_REL; + vs_strid = devinfo->strbase + USBUVC_VSIFSTRID_REL; + + /* Calculate total descriptor size (without config header for composite) */ + + totallen = (int16_t)(sizeof(g_uvc_iad) + + sizeof(g_uvc_vc_ifdesc) + + sizeof(g_uvc_vc_header) + + sizeof(g_uvc_input_terminal) + + sizeof(g_uvc_output_terminal) + + sizeof(g_uvc_vs_ifdesc) + + sizeof(g_uvc_vs_input_header) + + sizeof(g_uvc_vs_format_uncomp) + + USBUVC_VS_FRAME_DESC_LEN + + sizeof(g_uvc_vs_color) + + USB_SIZEOF_EPDESC); + +#ifndef CONFIG_USBUVC_COMPOSITE + totallen += sizeof(g_uvc_cfgdesc); +#endif + + if (!buf) + { + return totallen; + } + +#ifndef CONFIG_USBUVC_COMPOSITE + /* Configuration descriptor header — standalone only */ + + { + FAR struct usb_cfgdesc_s *dest = (FAR struct usb_cfgdesc_s *)p; + memcpy(p, &g_uvc_cfgdesc, sizeof(g_uvc_cfgdesc)); + dest->type = type; + dest->totallen[0] = LSBYTE(totallen); + dest->totallen[1] = MSBYTE(totallen); + } + + p += sizeof(g_uvc_cfgdesc); +#endif + + /* IAD — patch bFirstInterface and iFunction */ + + memcpy(p, g_uvc_iad, sizeof(g_uvc_iad)); + p[2] = vc_ifno; /* bFirstInterface */ + p[7] = vc_strid; /* iFunction */ + p += sizeof(g_uvc_iad); + + /* VC Interface — patch ifno and iif */ + + memcpy(p, &g_uvc_vc_ifdesc, sizeof(g_uvc_vc_ifdesc)); + ((FAR struct usb_ifdesc_s *)p)->ifno = vc_ifno; + ((FAR struct usb_ifdesc_s *)p)->iif = vc_strid; + p += sizeof(g_uvc_vc_ifdesc); + + /* VC Header — patch wTotalLength and baInterfaceNr */ + + vc_total = sizeof(g_uvc_vc_header) + + sizeof(g_uvc_input_terminal) + + sizeof(g_uvc_output_terminal); + + memcpy(p, g_uvc_vc_header, sizeof(g_uvc_vc_header)); + p[5] = LSBYTE(vc_total); + p[6] = MSBYTE(vc_total); + p[12] = vs_ifno; /* baInterfaceNr(1) */ + p += sizeof(g_uvc_vc_header); + + /* Input Terminal */ + + memcpy(p, g_uvc_input_terminal, sizeof(g_uvc_input_terminal)); + p += sizeof(g_uvc_input_terminal); + + /* Output Terminal */ + + memcpy(p, g_uvc_output_terminal, sizeof(g_uvc_output_terminal)); + p += sizeof(g_uvc_output_terminal); + + /* VS Interface — patch ifno and iif */ + + memcpy(p, &g_uvc_vs_ifdesc, sizeof(g_uvc_vs_ifdesc)); + ((FAR struct usb_ifdesc_s *)p)->ifno = vs_ifno; + ((FAR struct usb_ifdesc_s *)p)->iif = vs_strid; + p += sizeof(g_uvc_vs_ifdesc); + + /* VS Input Header — patch wTotalLength and bEndpointAddress */ + + vs_total = sizeof(g_uvc_vs_input_header) + + sizeof(g_uvc_vs_format_uncomp) + + USBUVC_VS_FRAME_DESC_LEN + + sizeof(g_uvc_vs_color); + + memcpy(p, g_uvc_vs_input_header, sizeof(g_uvc_vs_input_header)); + p[4] = LSBYTE(vs_total); + p[5] = MSBYTE(vs_total); + p[6] = USB_EPIN(devinfo->epno[USBUVC_EP_BULKIN_IDX]); + p += sizeof(g_uvc_vs_input_header); + + /* VS Format */ + + memcpy(p, g_uvc_vs_format_uncomp, sizeof(g_uvc_vs_format_uncomp)); + p += sizeof(g_uvc_vs_format_uncomp); + + /* VS Frame - built at runtime from video params */ + + /* bLength (30) */ + + p[0] = USBUVC_VS_FRAME_DESC_LEN; + + /* bDescriptorType */ + + p[1] = UVC_CS_INTERFACE; + + /* bDescriptorSubtype */ + + p[2] = UVC_VS_FRAME_UNCOMPRESSED; + + /* bFrameIndex: 1 */ + + p[3] = 0x01; + + /* bmCapabilities */ + + p[4] = 0x00; + + /* wWidth */ + + p[5] = LSBYTE(priv->width); + p[6] = MSBYTE(priv->width); + + /* Height (2 bytes LE) */ + + p[7] = LSBYTE(priv->height); + p[8] = MSBYTE(priv->height); + + /* dwMinBitRate */ + + p[9] = (priv->bitrate >> 0) & 0xff; + p[10] = (priv->bitrate >> 8) & 0xff; + p[11] = (priv->bitrate >> 16) & 0xff; + p[12] = (priv->bitrate >> 24) & 0xff; + + /* dwMaxBitRate */ + + p[13] = (priv->bitrate >> 0) & 0xff; + p[14] = (priv->bitrate >> 8) & 0xff; + p[15] = (priv->bitrate >> 16) & 0xff; + p[16] = (priv->bitrate >> 24) & 0xff; + + /* dwMaxVideoFrameBufferSize */ + + p[17] = (priv->frame_size >> 0) & 0xff; + p[18] = (priv->frame_size >> 8) & 0xff; + p[19] = (priv->frame_size >> 16) & 0xff; + p[20] = (priv->frame_size >> 24) & 0xff; + + /* dwDefaultFrameInterval */ + + p[21] = (priv->frame_interval >> 0) & 0xff; + p[22] = (priv->frame_interval >> 8) & 0xff; + p[23] = (priv->frame_interval >> 16) & 0xff; + p[24] = (priv->frame_interval >> 24) & 0xff; + + /* bFrameIntervalType: 1 discrete */ + + p[25] = 0x01; + + /* dwFrameInterval(1) */ + + p[26] = (priv->frame_interval >> 0) & 0xff; + p[27] = (priv->frame_interval >> 8) & 0xff; + p[28] = (priv->frame_interval >> 16) & 0xff; + p[29] = (priv->frame_interval >> 24) & 0xff; + p += USBUVC_VS_FRAME_DESC_LEN; + + /* VS Color Matching */ + + memcpy(p, g_uvc_vs_color, sizeof(g_uvc_vs_color)); + p += sizeof(g_uvc_vs_color); + + /* Bulk IN EP descriptor */ + + usbdev_copy_epdesc((FAR struct usb_epdesc_s *)p, + devinfo->epno[USBUVC_EP_BULKIN_IDX], + speed, &g_uvc_epbulkin); + p += USB_SIZEOF_EPDESC; + + return (int16_t)(p - buf); +} + +/**************************************************************************** + * Private Functions — String Descriptor + ****************************************************************************/ + +static int uvc_mkstrdesc(uint8_t id, FAR struct usb_strdesc_s *strdesc) +{ + FAR uint8_t *data = (FAR uint8_t *)(strdesc + 1); + FAR const char *str; + int len; + int ndata; + int i; + + switch (id) + { +#ifndef CONFIG_USBUVC_COMPOSITE + case 0: + { + /* Descriptor 0 is the language id */ + + strdesc->len = 4; + strdesc->type = USB_DESC_TYPE_STRING; + data[0] = LSBYTE(USBUVC_STR_LANGUAGE); + data[1] = MSBYTE(USBUVC_STR_LANGUAGE); + return 4; + } + + case USBUVC_MANUFACTURERSTRID: + str = "NuttX"; + break; + case USBUVC_PRODUCTSTRID: + str = "NuttX UVC Camera"; + break; + case USBUVC_SERIALSTRID: + str = "0001"; + break; + case USBUVC_CONFIGSTRID: + str = "UVC Config"; + break; +#endif /* !CONFIG_USBUVC_COMPOSITE */ + + /* Interface strings — used in both standalone and composite. + * In standalone: absolute IDs (USBUVC_VCIFSTRID/VSIFSTRID). + * In composite: relative IDs (USBUVC_VCIFSTRID_REL/VSIFSTRID_REL), + * the composite framework adds strbase. + */ + +#ifdef CONFIG_USBUVC_COMPOSITE + case USBUVC_VCIFSTRID_REL: +#else + case USBUVC_VCIFSTRID: +#endif + str = "Video Control"; + break; + +#ifdef CONFIG_USBUVC_COMPOSITE + case USBUVC_VSIFSTRID_REL: +#else + case USBUVC_VSIFSTRID: +#endif + str = "Video Streaming"; + break; + default: + return -EINVAL; + } + + len = strlen(str); + if (len > (USBUVC_MAXSTRLEN / 2)) + { + len = (USBUVC_MAXSTRLEN / 2); + } + + for (i = 0, ndata = 0; i < len; i++, ndata += 2) + { + data[ndata] = str[i]; + data[ndata + 1] = 0; + } + + strdesc->len = ndata + 2; + strdesc->type = USB_DESC_TYPE_STRING; + return strdesc->len; +} + +/**************************************************************************** + * Private Functions — UVC Class-Specific Setup (EP0) + ****************************************************************************/ + +static int uvc_class_setup(FAR struct uvc_dev_s *priv, + FAR struct usbdev_s *dev, + FAR const struct usb_ctrlreq_s *ctrl, + FAR struct usbdev_req_s *ctrlreq) +{ + uint8_t req = ctrl->req; + uint8_t cs = ctrl->value[1]; /* Control selector */ + uint16_t len = GETUINT16(ctrl->len); + int ret = -EOPNOTSUPP; + + uinfo("UVC class setup: req=0x%02x cs=0x%02x len=%d\n", req, cs, len); + + if (cs == UVC_VS_PROBE_CONTROL || cs == UVC_VS_COMMIT_CONTROL) + { + FAR struct uvc_streaming_control_s *target; + + target = (cs == UVC_VS_PROBE_CONTROL) ? + &priv->probe : &priv->commit; + + switch (req) + { + case UVC_SET_CUR: + /* Host sends probe/commit data — we accept it. + * The data arrives in dataout phase, handled below. + * For now just return 0 to ACK the setup. + */ + + if (len >= UVC_PROBE_COMMIT_SIZE) + { + len = UVC_PROBE_COMMIT_SIZE; + } + + ret = 0; + break; + + case UVC_GET_CUR: + case UVC_GET_MIN: + case UVC_GET_MAX: + case UVC_GET_DEF: + if (len > UVC_PROBE_COMMIT_SIZE) + { + len = UVC_PROBE_COMMIT_SIZE; + } + + memcpy(ctrlreq->buf, target, len); + ctrlreq->len = len; + ret = len; + break; + + default: + break; + } + } + + return ret; +} + +/**************************************************************************** + * Private Functions — USB Class Driver Operations + ****************************************************************************/ + +static int uvc_bind(FAR struct usbdevclass_driver_s *driver, + FAR struct usbdev_s *dev) +{ + FAR struct uvc_dev_s *priv = g_uvc_dev; + FAR struct usbdev_req_s *req; + int i; + + uinfo("UVC bind\n"); + + priv->usbdev = dev; + priv->ep0 = dev->ep0; + + /* Allocate control request */ + + priv->ctrlreq = usbdev_allocreq(dev->ep0, 256); + if (!priv->ctrlreq) + { + return -ENOMEM; + } + + priv->ctrlreq->callback = uvc_ep0incomplete; + + /* Allocate Bulk IN endpoint — use devinfo.epno[] set by + * standalone init or composite framework. + */ + + priv->epbulkin = DEV_ALLOCEP(dev, + USB_EPIN(priv->devinfo.epno[ + USBUVC_EP_BULKIN_IDX]), + true, + USB_EP_ATTR_XFER_BULK); + if (!priv->epbulkin) + { + uerr("Failed to allocate bulk IN EP\n"); + return -ENODEV; + } + + priv->epbulkin->priv = priv; + priv->devinfo.epno[USBUVC_EP_BULKIN_IDX] = + USB_EPNO(priv->epbulkin->eplog); + + /* Allocate write requests */ + + sq_init(&priv->wrfree); + for (i = 0; i < CONFIG_USBUVC_NWRREQS; i++) + { + req = usbdev_allocreq(priv->epbulkin, + CONFIG_USBUVC_EPBULKIN_FSSIZE); + if (!req) + { + break; + } + + req->callback = uvc_wrreq_callback; + req->priv = &priv->wrreqs[i]; + priv->wrreqs[i].req = req; + sq_addlast((FAR sq_entry_t *)&priv->wrreqs[i], &priv->wrfree); + priv->nwralloc++; + } + + uvc_default_streaming_ctrl(priv, &priv->probe); + uvc_default_streaming_ctrl(priv, &priv->commit); + + DEV_CONNECT(dev); + return OK; +} + +static void uvc_unbind(FAR struct usbdevclass_driver_s *driver, + FAR struct usbdev_s *dev) +{ + FAR struct uvc_dev_s *priv = g_uvc_dev; + FAR struct uvc_wrreq_s *wrcontainer; + + uinfo("UVC unbind\n"); + + /* Free write requests */ + + while ((wrcontainer = (FAR struct uvc_wrreq_s *) + sq_remfirst(&priv->wrfree))) + { + usbdev_freereq(priv->epbulkin, wrcontainer->req); + wrcontainer->req = NULL; + } + + if (priv->epbulkin) + { + DEV_FREEEP(dev, priv->epbulkin); + priv->epbulkin = NULL; + } + + if (priv->ctrlreq) + { + usbdev_freereq(dev->ep0, priv->ctrlreq); + priv->ctrlreq = NULL; + } + + DEV_DISCONNECT(dev); +} + +static int uvc_setup(FAR struct usbdevclass_driver_s *driver, + FAR struct usbdev_s *dev, + FAR const struct usb_ctrlreq_s *ctrl, + FAR uint8_t *dataout, size_t outlen) +{ + FAR struct uvc_dev_s *priv = g_uvc_dev; + FAR struct usbdev_req_s *ctrlreq = priv->ctrlreq; + uint16_t value = GETUINT16(ctrl->value); + uint16_t index = GETUINT16(ctrl->index); + uint16_t len = GETUINT16(ctrl->len); + int ret = -EOPNOTSUPP; + + UNUSED(index); + + uinfo("UVC setup: type=0x%02x req=0x%02x value=0x%04x len=%d\n", + ctrl->type, ctrl->req, value, len); + + /* Handle class-specific requests */ + + if ((ctrl->type & USB_REQ_TYPE_MASK) == USB_REQ_TYPE_CLASS) + { + /* SET_CUR with data phase — copy incoming data */ + + if (ctrl->req == UVC_SET_CUR && dataout && outlen > 0) + { + uint8_t cs = ctrl->value[1]; + if (cs == UVC_VS_PROBE_CONTROL) + { + memcpy(&priv->probe, dataout, + outlen < UVC_PROBE_COMMIT_SIZE ? + outlen : UVC_PROBE_COMMIT_SIZE); + + /* Clamp to our only supported format */ + + priv->probe.bformatindex = 1; + priv->probe.bframeindex = 1; + priv->probe.dwframeinterval = priv->frame_interval; + priv->probe.dwmaxvideoframesize = priv->frame_size; + priv->probe.dwmaxpayloadtransfersize = + priv->frame_size + UVC_PAYLOAD_HEADER_LEN; + + /* Send ZLP status stage for OUT control transfer */ + + ctrlreq->len = 0; + ctrlreq->flags = USBDEV_REQFLAGS_NULLPKT; + UVC_EP0SUBMIT(driver, dev, ctrlreq, ctrl); + return 0; + } + else if (cs == UVC_VS_COMMIT_CONTROL) + { + memcpy(&priv->commit, dataout, + outlen < UVC_PROBE_COMMIT_SIZE ? + outlen : UVC_PROBE_COMMIT_SIZE); + + /* Reset wrsem to 0 before starting new stream. + * Stale counts from previous EP_CANCEL callbacks + * would cause NULL wrcontainer dereferences. + */ + + while (nxsem_trywait(&priv->wrsem) == OK); + + priv->streaming = true; + poll_notify(priv->fds, CONFIG_USBUVC_NPOLLWAITERS, + POLLOUT); + uinfo("UVC streaming committed\n"); + + /* Send ZLP status stage for OUT control transfer */ + + ctrlreq->len = 0; + ctrlreq->flags = USBDEV_REQFLAGS_NULLPKT; + UVC_EP0SUBMIT(driver, dev, ctrlreq, ctrl); + return 0; + } + } + + ret = uvc_class_setup(priv, dev, ctrl, ctrlreq); + if (ret > 0) + { + ctrlreq->len = ret; + ret = UVC_EP0SUBMIT(driver, dev, ctrlreq, ctrl); + if (ret < 0) + { + uerr("EP_SUBMIT failed: %d\n", ret); + } + + return ret; + } + + /* ret == 0 means SET_CUR accepted, waiting for data phase. + * ret < 0 means unsupported, will fall through. + */ + + if (ret == 0) + { + return ret; + } + } + + /* Handle standard requests — descriptor requests are standalone only; + * SET_CONFIGURATION / SET_INTERFACE are needed in both modes. + */ + + if ((ctrl->type & USB_REQ_TYPE_MASK) == USB_REQ_TYPE_STANDARD) + { + switch (ctrl->req) + { +#ifndef CONFIG_USBUVC_COMPOSITE + case USB_REQ_GETDESCRIPTOR: + { + switch (ctrl->value[1]) + { + case USB_DESC_TYPE_DEVICE: + { + ret = usbdev_copy_devdesc(ctrlreq->buf, + &g_uvc_devdesc, + dev->speed); + } + break; + + case USB_DESC_TYPE_CONFIG: + { + ret = uvc_mkcfgdesc(ctrlreq->buf, + &priv->devinfo, + dev->speed, + ctrl->value[1]); + } + break; + + case USB_DESC_TYPE_STRING: + { + ret = uvc_mkstrdesc(ctrl->value[0], + (FAR struct usb_strdesc_s *)ctrlreq->buf); + } + break; + + default: + break; + } + } + break; +#endif /* !CONFIG_USBUVC_COMPOSITE */ + + case USB_REQ_SETCONFIGURATION: + if (value == 1) + { + /* Configure the bulk IN endpoint */ + + struct usb_epdesc_s epdesc; + epdesc.len = USB_SIZEOF_EPDESC; + epdesc.type = USB_DESC_TYPE_ENDPOINT; + epdesc.addr = USB_EPIN( + USB_EPNO(priv->epbulkin->eplog)); + epdesc.attr = USB_EP_ATTR_XFER_BULK; + epdesc.mxpacketsize[0] = + LSBYTE(CONFIG_USBUVC_EPBULKIN_FSSIZE); + epdesc.mxpacketsize[1] = + MSBYTE(CONFIG_USBUVC_EPBULKIN_FSSIZE); + epdesc.interval = 0; + + EP_CONFIGURE(priv->epbulkin, &epdesc, true); + priv->config = value; + ret = 0; + } + else if (value == 0) + { + uvc_streaming_stop(priv); + EP_DISABLE(priv->epbulkin); + priv->config = 0; + ret = 0; + } + break; + + case USB_REQ_SETINTERFACE: + + /* alt=0 means host is stopping the stream */ + + if ((value & 0xff) == 0 && priv->streaming) + { + uvc_streaming_stop(priv); + } + + ret = 0; + break; + + case USB_REQ_GETINTERFACE: + ctrlreq->buf[0] = 0; + ctrlreq->len = 1; + ret = UVC_EP0SUBMIT(driver, dev, ctrlreq, ctrl); + return ret; + + default: + break; + } + } + + /* Respond to the setup command if data was returned. On an error + * return value (ret < 0), the USB driver will stall EP0. + */ + + if (ret >= 0) + { + ctrlreq->len = len < ret ? len : ret; + ctrlreq->flags = USBDEV_REQFLAGS_NULLPKT; + ret = UVC_EP0SUBMIT(driver, dev, ctrlreq, ctrl); + if (ret < 0) + { + uerr("EP_SUBMIT failed: %d\n", ret); + } + } + + return ret; +} + +static void uvc_streaming_stop(FAR struct uvc_dev_s *priv) +{ + /* Called from interrupt context (disconnect/setup) or from + * uvc_write timeout — no mutex! + * Cancel all pending bulk IN requests so callbacks fire immediately. + * EP_CANCEL callbacks will post wrsem for each cancelled request, + * which unblocks uvc_write(). + * + * Guard against double-cancel: uvc_write timeout may call this, + * then the USB disconnect ISR calls it again. Calling EP_CANCEL + * on an already-cancelled endpoint can corrupt the request list. + */ + + if (!priv->streaming) + { + return; + } + + priv->streaming = false; + poll_notify(priv->fds, CONFIG_USBUVC_NPOLLWAITERS, POLLHUP); + + if (priv->epbulkin) + { + EP_CANCEL(priv->epbulkin, NULL); + } +} + +static void uvc_disconnect(FAR struct usbdevclass_driver_s *driver, + FAR struct usbdev_s *dev) +{ + FAR struct uvc_dev_s *priv = g_uvc_dev; + + uinfo("UVC disconnect\n"); + + /* Called from USB interrupt context — do NOT use mutex */ + + priv->config = 0; + uvc_streaming_stop(priv); +} + +/**************************************************************************** + * Private Functions — File Operations (/dev/uvc0) + ****************************************************************************/ + +static int uvc_open(FAR struct file *filep) +{ + return OK; +} + +static int uvc_close(FAR struct file *filep) +{ + return OK; +} + +/**************************************************************************** + * Name: uvc_poll + * + * Description: + * Poll for POLLOUT — reports writable when host is streaming. + * + ****************************************************************************/ + +static int uvc_poll(FAR struct file *filep, FAR struct pollfd *fds, + bool setup) +{ + FAR struct uvc_dev_s *priv = g_uvc_dev; + irqstate_t flags; + int ret = OK; + int i; + + if (!setup) + { + /* Teardown — clear the slot */ + + FAR struct pollfd **slot = (FAR struct pollfd **)fds->priv; + if (slot) + { + *slot = NULL; + } + + fds->priv = NULL; + return OK; + } + + /* Setup — find an available slot */ + + nxmutex_lock(&priv->lock); + flags = enter_critical_section(); + + for (i = 0; i < CONFIG_USBUVC_NPOLLWAITERS; i++) + { + if (!priv->fds[i]) + { + priv->fds[i] = fds; + fds->priv = &priv->fds[i]; + break; + } + } + + if (i >= CONFIG_USBUVC_NPOLLWAITERS) + { + ret = -EBUSY; + } + else if (priv->config && priv->streaming) + { + poll_notify(&priv->fds[i], 1, POLLOUT); + } + + leave_critical_section(flags); + nxmutex_unlock(&priv->lock); + return ret; +} + +/**************************************************************************** + * Name: uvc_write + * + * Description: + * Write a complete video frame to the UVC bulk endpoint. + * The driver prepends UVC Payload Headers and splits the frame + * into max-packet-sized USB transfers. + * + * Expected input: raw YUY2 frame data + * + ****************************************************************************/ + +static ssize_t uvc_write(FAR struct file *filep, + FAR const char *buffer, size_t buflen) +{ + FAR struct uvc_dev_s *priv = g_uvc_dev; + FAR struct uvc_wrreq_s *wrcontainer; + FAR struct usbdev_req_s *req; + irqstate_t flags; + size_t remaining = buflen; + size_t offset = 0; + size_t payload_max; + size_t chunk; + int ret; + + nxmutex_lock(&priv->lock); + + if (!priv->epbulkin || !priv->config || !priv->streaming) + { + nxmutex_unlock(&priv->lock); + return -EAGAIN; + } + + /* Zero-length write is used to probe readiness */ + + if (buflen == 0) + { + nxmutex_unlock(&priv->lock); + return 0; + } + + payload_max = CONFIG_USBUVC_EPBULKIN_FSSIZE - UVC_PAYLOAD_HEADER_LEN; + + while (remaining > 0) + { + /* Get a write request from the pool */ + + flags = enter_critical_section(); + wrcontainer = (FAR struct uvc_wrreq_s *)sq_remfirst(&priv->wrfree); + leave_critical_section(flags); + + if (!wrcontainer) + { + /* No request available — wait with timeout. + * If host stops reading (e.g. cheese closed), bulk IN + * requests never complete. Timeout and treat as host-gone. + * + * NOTE: When the host truly disconnects or closes the + * video device, the USB disconnect ISR or a CLEAR_FEATURE + * control transfer will call uvc_streaming_stop() + * immediately — the timeout is only a safety net. + * + * It must be long enough to cover the worst case: host + * sends COMMIT, then spends many seconds on REQBUFS / + * MMAP / QBUF / STREAMON before reading bulk data. + * After an unclean close (e.g. v4l2-ctl killed by + * SIGTERM), the host UVC driver reset can add 5-10s. + * 30 seconds is generous enough to never false-trigger. + */ + + nxmutex_unlock(&priv->lock); + ret = nxsem_tickwait(&priv->wrsem, 30 * TICK_PER_SEC); + nxmutex_lock(&priv->lock); + + if (ret == -ETIMEDOUT) + { + uwarn("UVC write timeout — host not reading\n"); + uvc_streaming_stop(priv); + nxmutex_unlock(&priv->lock); + return -EAGAIN; + } + + /* Re-check state after re-acquiring lock. + * streaming_stop() cancels EP requests which unblocks us. + */ + + if (!priv->streaming || !priv->config || !priv->epbulkin) + { + nxmutex_unlock(&priv->lock); + return -EAGAIN; + } + + /* wrcontainer might still be NULL if wrsem had stale count */ + + continue; + } + + req = wrcontainer->req; + + /* Build UVC Payload Header */ + + chunk = remaining > payload_max ? payload_max : remaining; + + req->buf[0] = UVC_PAYLOAD_HEADER_LEN; /* bHeaderLength */ + req->buf[1] = priv->fid & UVC_STREAM_FID; + + if (remaining <= payload_max) + { + /* Last packet of this frame */ + + req->buf[1] |= UVC_STREAM_EOF; + } + + /* Copy video data after header */ + + memcpy(req->buf + UVC_PAYLOAD_HEADER_LEN, buffer + offset, chunk); + req->len = chunk + UVC_PAYLOAD_HEADER_LEN; + + ret = EP_SUBMIT(priv->epbulkin, req); + if (ret < 0) + { + /* Return request to pool */ + + flags = enter_critical_section(); + sq_addlast((FAR sq_entry_t *)wrcontainer, &priv->wrfree); + leave_critical_section(flags); + + if (ret == -ESHUTDOWN) + { + uwarn("UVC EP shutdown — stopping stream\n"); + priv->streaming = false; + poll_notify(priv->fds, CONFIG_USBUVC_NPOLLWAITERS, + POLLHUP); + nxmutex_unlock(&priv->lock); + return -EAGAIN; + } + + uerr("EP_SUBMIT failed: %d\n", ret); + nxmutex_unlock(&priv->lock); + return ret; + } + + offset += chunk; + remaining -= chunk; + } + + /* Toggle FID for next frame */ + + priv->fid ^= 1; + + nxmutex_unlock(&priv->lock); + return buflen; +} + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: usbdev_uvc_classobject + * + * Description: + * Create a UVC class driver instance. Used by both standalone init + * and composite framework. + * + ****************************************************************************/ + +int usbdev_uvc_classobject(int minor, + FAR struct usbdev_devinfo_s *devinfo, + FAR const struct uvc_params_s *params, + FAR struct usbdevclass_driver_s **classdev) +{ + FAR struct uvc_dev_s *priv; + FAR struct usbdevclass_driver_s *drvr; + int ret; + + /* Allocate device struct + class driver struct */ + + priv = kmm_zalloc(sizeof(struct uvc_dev_s) + + sizeof(struct usbdevclass_driver_s)); + if (!priv) + { + return -ENOMEM; + } + + /* Store runtime video parameters */ + + if (params) + { + priv->width = params->width; + priv->height = params->height; + priv->fps = params->fps; + } + else + { + priv->width = USBUVC_DEF_WIDTH; + priv->height = USBUVC_DEF_HEIGHT; + priv->fps = USBUVC_DEF_FPS; + } + + priv->frame_size = (uint32_t)priv->width * priv->height * USBUVC_BPP; + priv->frame_interval = 10000000 / priv->fps; /* 100ns units */ + priv->bitrate = priv->frame_size * 8 * priv->fps; + + /* Save devinfo — interface/string/endpoint bases from caller */ + + memcpy(&priv->devinfo, devinfo, sizeof(struct usbdev_devinfo_s)); + + g_uvc_dev = priv; + nxmutex_init(&priv->lock); + nxsem_init(&priv->wrsem, 0, 0); + + /* Set up the class driver structure right after priv */ + + drvr = (FAR struct usbdevclass_driver_s *)(priv + 1); + drvr->ops = &g_uvc_driverops; + drvr->speed = USB_SPEED_FULL; + + /* Register the character device */ + + ret = register_driver(USBUVC_CHARDEV_PATH, &g_uvc_fops, 0666, priv); + if (ret < 0) + { + uerr("register_driver failed: %d\n", ret); + kmm_free(priv); + g_uvc_dev = NULL; + return ret; + } + + uinfo("UVC gadget initialized: %s\n", USBUVC_CHARDEV_PATH); + *classdev = drvr; + return OK; +} + +/**************************************************************************** + * Name: usbdev_uvc_classuninitialize + * + * Description: + * Uninitialize a UVC class driver instance. + * + ****************************************************************************/ + +void usbdev_uvc_classuninitialize( + FAR struct usbdevclass_driver_s *classdev) +{ + FAR struct uvc_dev_s *priv; + + if (!classdev) + { + return; + } + + /* priv is right before classdev in the allocation */ + + priv = (FAR struct uvc_dev_s *)classdev - 1; + + unregister_driver(USBUVC_CHARDEV_PATH); + nxmutex_destroy(&priv->lock); + nxsem_destroy(&priv->wrsem); + kmm_free(priv); + + if (g_uvc_dev == priv) + { + g_uvc_dev = NULL; + } +} + +/**************************************************************************** + * Name: usbdev_uvc_initialize + * + * Description: + * Standalone mode initialization — creates the class driver and + * registers it directly with the USB device controller. + * + ****************************************************************************/ + +#ifndef CONFIG_USBUVC_COMPOSITE +FAR void *usbdev_uvc_initialize(FAR const struct uvc_params_s *params) +{ + FAR struct usbdevclass_driver_s *classdev = NULL; + struct usbdev_devinfo_s devinfo; + int ret; + + memset(&devinfo, 0, sizeof(devinfo)); + devinfo.ninterfaces = USBUVC_NINTERFACES; + devinfo.nstrings = USBUVC_NSTDSTRIDS; + devinfo.nendpoints = USBUVC_NUM_EPS; + devinfo.epno[USBUVC_EP_BULKIN_IDX] = CONFIG_USBUVC_EPBULKIN; + + ret = usbdev_uvc_classobject(0, &devinfo, params, &classdev); + if (ret < 0) + { + return NULL; + } + + ret = usbdev_register(classdev); + if (ret < 0) + { + uerr("usbdev_register failed: %d\n", ret); + usbdev_uvc_classuninitialize(classdev); + return NULL; + } + + /* Return priv as handle (priv is right before classdev) */ + + return (FAR void *)((FAR struct uvc_dev_s *)classdev - 1); +} + +/**************************************************************************** + * Name: usbdev_uvc_uninitialize + ****************************************************************************/ + +void usbdev_uvc_uninitialize(FAR void *handle) +{ + FAR struct uvc_dev_s *priv = handle; + FAR struct usbdevclass_driver_s *drvr; + + if (!priv) + { + return; + } + + drvr = (FAR struct usbdevclass_driver_s *)(priv + 1); + usbdev_unregister(drvr); + usbdev_uvc_classuninitialize(drvr); +} +#endif /* !CONFIG_USBUVC_COMPOSITE */ + +/**************************************************************************** + * Name: usbdev_uvc_get_composite_devdesc + * + * Description: + * Fill in a composite_devdesc_s for the UVC gadget. + * Board code must set classobject/uninitialize, ifnobase, strbase, + * and epno[] before passing to composite_initialize(). + * + ****************************************************************************/ + +#ifdef CONFIG_USBDEV_COMPOSITE +void usbdev_uvc_get_composite_devdesc( + FAR struct composite_devdesc_s *dev) +{ + memset(dev, 0, sizeof(struct composite_devdesc_s)); + + dev->mkconfdesc = uvc_mkcfgdesc; + dev->mkstrdesc = uvc_mkstrdesc; + + /* classobject/uninitialize are left NULL — board code must wrap + * usbdev_uvc_classobject() to pass uvc_params_s. + */ + + dev->nconfigs = USBUVC_NCONFIGS; + dev->configid = 1; + dev->cfgdescsize = uvc_mkcfgdesc(NULL, NULL, + USB_SPEED_UNKNOWN, 0); + dev->devinfo.ninterfaces = USBUVC_NINTERFACES; + dev->devinfo.nstrings = USBUVC_NSTRIDS; + dev->devinfo.nendpoints = USBUVC_NUM_EPS; + dev->devinfo.epinfos = g_uvc_epinfos; + dev->devinfo.name = USBUVC_CHARDEV_PATH; +} +#endif diff --git a/include/nuttx/usb/uvc.h b/include/nuttx/usb/uvc.h new file mode 100644 index 0000000000000..cbbcfffd749d0 --- /dev/null +++ b/include/nuttx/usb/uvc.h @@ -0,0 +1,282 @@ +/**************************************************************************** + * include/nuttx/usb/uvc.h + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. The + * ASF licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + ****************************************************************************/ + +#ifndef __INCLUDE_NUTTX_USB_UVC_H +#define __INCLUDE_NUTTX_USB_UVC_H + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include +#include + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +/* UVC device descriptor counts - used by both standalone and composite */ + +#define USBUVC_NINTERFACES 2 /* VC + VS */ +#define USBUVC_NUM_EPS 1 /* Bulk IN only */ +#define USBUVC_EP_BULKIN_IDX 0 +#define USBUVC_NSTRIDS 2 /* VC iInterface + VS iInterface */ +#define USBUVC_NCONFIGS 1 + +/* UVC Class-Specific Descriptor Types (USB Video Class 1.1, Table A-4) */ + +#define UVC_CS_UNDEFINED 0x20 +#define UVC_CS_DEVICE 0x21 +#define UVC_CS_CONFIGURATION 0x22 +#define UVC_CS_STRING 0x23 +#define UVC_CS_INTERFACE 0x24 +#define UVC_CS_ENDPOINT 0x25 + +/* UVC Interface Subclass Codes (Table A-2) */ + +#define UVC_SC_UNDEFINED 0x00 +#define UVC_SC_VIDEOCONTROL 0x01 +#define UVC_SC_VIDEOSTREAMING 0x02 +#define UVC_SC_VIDEO_INTERFACE_COLLECTION 0x03 + +/* UVC Interface Protocol Codes (Table A-3) */ + +#define UVC_PC_PROTOCOL_UNDEFINED 0x00 +#define UVC_PC_PROTOCOL_15 0x01 + +/* UVC VideoControl Interface Descriptor Subtypes (Table A-5) */ + +#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 + +/* UVC VideoStreaming Interface Descriptor Subtypes (Table A-6) */ + +#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_COLOR_FORMAT 0x0d + +/* UVC Terminal Types (Table B-1, B-2) */ + +#define UVC_TT_VENDOR_SPECIFIC 0x0100 +#define UVC_TT_STREAMING 0x0101 +#define UVC_ITT_VENDOR_SPECIFIC 0x0200 +#define UVC_ITT_CAMERA 0x0201 +#define UVC_ITT_MEDIA_TRANSPORT_INPUT 0x0202 +#define UVC_OTT_VENDOR_SPECIFIC 0x0300 +#define UVC_OTT_DISPLAY 0x0301 +#define UVC_OTT_MEDIA_TRANSPORT_OUTPUT 0x0302 + +/* UVC Class-Specific Request Codes (Table A-8) */ + +#define UVC_RC_UNDEFINED 0x00 +#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 + +/* UVC VideoStreaming Interface Control Selectors (Table A-11) */ + +#define UVC_VS_CONTROL_UNDEFINED 0x00 +#define UVC_VS_PROBE_CONTROL 0x01 +#define UVC_VS_COMMIT_CONTROL 0x02 + +/* UVC Payload Header bits (Table 2-6) */ + +#define UVC_STREAM_FID (1 << 0) +#define UVC_STREAM_EOF (1 << 1) +#define UVC_STREAM_PTS (1 << 2) +#define UVC_STREAM_SCR (1 << 3) +#define UVC_STREAM_RES (1 << 4) +#define UVC_STREAM_STI (1 << 5) +#define UVC_STREAM_ERR (1 << 6) +#define UVC_STREAM_EOH (1 << 7) + +/* UVC Payload Header Length (minimum, no PTS/SCR) */ + +#define UVC_PAYLOAD_HEADER_LEN 2 + +/* GUID for YUY2 (MEDIASUBTYPE_YUY2) — we use this for uncompressed + * Note: For RGB565 there is no standard UVC GUID. We advertise YUY2 + * and do a trivial RGB565→YUY2 conversion in the app, OR we define + * a custom GUID. For Phase 1, we use YUY2 which is universally supported. + */ + +#define UVC_GUID_FORMAT_YUY2 \ + { 'Y', 'U', 'Y', '2', 0x00, 0x00, 0x10, 0x00, \ + 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71 } + +/**************************************************************************** + * Public Types + ****************************************************************************/ + +/* UVC Video Probe and Commit Controls (Table 4-75) */ + +begin_packed_struct struct uvc_streaming_control_s +{ + 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; + uint8_t bpreferedversion; + uint8_t bminversion; + uint8_t bmaxversion; +} end_packed_struct; + +#define UVC_PROBE_COMMIT_SIZE sizeof(struct uvc_streaming_control_s) + +/* UVC gadget initialization parameters. + * The application queries the video device at runtime and passes + * these to usbdev_uvc_initialize() so that USB descriptors match + * the actual sensor capabilities. + */ + +struct uvc_params_s +{ + uint16_t width; /* Frame width in pixels */ + uint16_t height; /* Frame height in pixels */ + uint8_t fps; /* Frames per second */ +}; + +/**************************************************************************** + * Public Function Prototypes + ****************************************************************************/ + +#undef EXTERN +#if defined(__cplusplus) +# define EXTERN extern "C" +extern "C" +{ +#else +# define EXTERN extern +#endif + +/**************************************************************************** + * Name: usbdev_uvc_initialize + * + * Description: + * Initialize the USB Video Class device driver. + * + * Input Parameters: + * params - Video parameters (width, height, fps) queried from the + * camera sensor. If NULL, defaults to 320x240 @ 5fps. + * + * Returned Value: + * A non-NULL "handle" is returned on success. + * + ****************************************************************************/ + +FAR void *usbdev_uvc_initialize(FAR const struct uvc_params_s *params); + +/**************************************************************************** + * Name: usbdev_uvc_uninitialize + * + * Description: + * Uninitialize the USB Video Class device driver. + * + ****************************************************************************/ + +void usbdev_uvc_uninitialize(FAR void *handle); + +/**************************************************************************** + * Name: usbdev_uvc_classobject + * + * Description: + * Create a UVC class driver instance for composite device usage. + * Called by the composite framework (or board code) to instantiate + * the UVC class driver with assigned interface/string/endpoint bases. + * + * Input Parameters: + * minor - Device minor number (unused, pass 0). + * devinfo - USB device info with ifnobase, strbase, epno[] assigned + * by the composite framework. + * params - Video parameters (width, height, fps). NULL = defaults. + * classdev - Location to return the class driver instance. + * + * Returned Value: + * OK on success; negative errno on failure. + * + ****************************************************************************/ + +int usbdev_uvc_classobject(int minor, + FAR struct usbdev_devinfo_s *devinfo, + FAR const struct uvc_params_s *params, + FAR struct usbdevclass_driver_s **classdev); + +/**************************************************************************** + * Name: usbdev_uvc_classuninitialize + * + * Description: + * Uninitialize a UVC class driver instance created by classobject. + * + ****************************************************************************/ + +void usbdev_uvc_classuninitialize( + FAR struct usbdevclass_driver_s *classdev); + +/**************************************************************************** + * Name: usbdev_uvc_get_composite_devdesc + * + * Description: + * Fill in a composite_devdesc_s structure for the UVC gadget. + * Board code calls this, then sets ifnobase/strbase/epno[] before + * passing to composite_initialize(). + * + * Note: classobject/uninitialize are left NULL because UVC needs + * extra params (uvc_params_s). Board code must set them. + * + ****************************************************************************/ + +#ifdef CONFIG_USBDEV_COMPOSITE +void usbdev_uvc_get_composite_devdesc( + FAR struct composite_devdesc_s *dev); +#endif + +#undef EXTERN +#if defined(__cplusplus) +} +#endif + +#endif /* __INCLUDE_NUTTX_USB_UVC_H */ diff --git a/include/sys/boardctl.h b/include/sys/boardctl.h index d0508efef7cd5..23cd8f61e3087 100644 --- a/include/sys/boardctl.h +++ b/include/sys/boardctl.h @@ -373,6 +373,9 @@ enum boardioc_usbdev_identifier_e #ifdef CONFIG_USBMSC , BOARDIOC_USBDEV_MSC /* Mass storage class */ #endif +#ifdef CONFIG_USBUVC + , BOARDIOC_USBDEV_UVC /* USB Video Class */ +#endif #ifdef CONFIG_USBDEV_COMPOSITE , BOARDIOC_USBDEV_COMPOSITE /* Composite device */ #endif From e4babac55082a02feef90ad9ed128a5c5ad84648 Mon Sep 17 00:00:00 2001 From: wangjianyu3 Date: Thu, 19 Mar 2026 19:38:06 +0800 Subject: [PATCH 4/4] boards/lckfb-szpi-esp32s3: add UVC camera standalone defconfig Add UVC configuration for lckfb-szpi-esp32s3 board based on gc0308 camera config, with USB OTG and UVC gadget driver enabled in standalone (non-composite) mode. - defconfig: enable ESP32S3_OTG, USBUVC, UVC example app - board doc: add uvc section with usage and host verification Signed-off-by: wangjianyu3 --- .../applications/examples/uvc_cam/index.rst | 72 +++++++++++++++++++ .../boards/lckfb-szpi-esp32s3/index.rst | 47 ++++++++++++ .../lckfb-szpi-esp32s3/configs/uvc/defconfig | 66 +++++++++++++++++ 3 files changed, 185 insertions(+) create mode 100644 Documentation/applications/examples/uvc_cam/index.rst create mode 100644 boards/xtensa/esp32s3/lckfb-szpi-esp32s3/configs/uvc/defconfig diff --git a/Documentation/applications/examples/uvc_cam/index.rst b/Documentation/applications/examples/uvc_cam/index.rst new file mode 100644 index 0000000000000..a6d4eba6eb40b --- /dev/null +++ b/Documentation/applications/examples/uvc_cam/index.rst @@ -0,0 +1,72 @@ +============================================= +``uvc_cam`` UVC Camera streaming application +============================================= + +Captures frames from a V4L2 camera sensor and streams them to a USB host +via the UVC (USB Video Class) gadget driver (``/dev/uvc0``). + +The application queries the sensor's native pixel format, resolution and +frame rate via V4L2, then continuously captures frames and writes them to +the UVC device node. + +Dependencies +========================== + +- :code:`CONFIG_USBUVC=y` – UVC gadget driver +- :code:`CONFIG_VIDEO=y` – Video subsystem +- :code:`CONFIG_BOARDCTL_USBDEVCTRL=y` – USB device control via boardctl + +Configuration +========================== + +- :code:`CONFIG_EXAMPLES_UVC_CAM=y` – Enable the UVC camera example +- :code:`CONFIG_EXAMPLES_UVC_CAM_NFRAMES` – Number of frames to stream (0 = infinite, default) +- :code:`CONFIG_EXAMPLES_UVC_CAM_PRIORITY` – Task priority (default 100) +- :code:`CONFIG_EXAMPLES_UVC_CAM_STACKSIZE` – Stack size (default 4096) + +Supported Pixel Formats +========================== + +- ``YUYV`` (2 bytes per pixel) +- ``RGB565`` (2 bytes per pixel) +- ``RGB24`` (3 bytes per pixel) + +Usage +========================== + +.. code-block:: bash + + uvc_cam [nframes] [video_dev] [uvc_dev] + +- ``nframes`` – Number of frames to capture (default from Kconfig, 0 = infinite) +- ``video_dev`` – V4L2 camera device path (default ``/dev/video0``) +- ``uvc_dev`` – UVC gadget device path (default ``/dev/uvc0``) + +Examples +========================== + +- Stream indefinitely with defaults: :code:`uvc_cam` +- Stream 100 frames: :code:`uvc_cam 100` +- Use a different camera device: :code:`uvc_cam 0 /dev/video1` +- Specify both camera and UVC device: :code:`uvc_cam 0 /dev/video0 /dev/uvc1` + +Board Support +========================== + +The ``lckfb-szpi-esp32s3`` board provides a ``uvc`` configuration with +camera and UVC gadget driver pre-enabled:: + + $ ./tools/configure.sh lckfb-szpi-esp32s3:uvc + +This configuration enables the ESP32-S3 camera interface (GC0308 sensor), +USB OTG and the UVC gadget driver. The ``uvc_cam`` example application is +not enabled by default and must be turned on manually:: + + $ kconfig-tweak --enable CONFIG_EXAMPLES_UVC_CAM + +Or via ``make menuconfig``: +:menuselection:`Application Configuration --> Examples --> UVC Camera streaming example` + +After flashing, run the application manually from the NSH shell:: + + nsh> uvc_cam diff --git a/Documentation/platforms/xtensa/esp32s3/boards/lckfb-szpi-esp32s3/index.rst b/Documentation/platforms/xtensa/esp32s3/boards/lckfb-szpi-esp32s3/index.rst index 3a28c65433bc2..70a492c12866c 100644 --- a/Documentation/platforms/xtensa/esp32s3/boards/lckfb-szpi-esp32s3/index.rst +++ b/Documentation/platforms/xtensa/esp32s3/boards/lckfb-szpi-esp32s3/index.rst @@ -576,3 +576,50 @@ Then use the camera example to capture a frame:: $ adb -s 1234 shell nsh> camera + +uvc +--- + +USB Video Class (UVC) webcam configuration. Streams YUYV frames from the +GC0308 DVP camera to a USB host via the UVC gadget driver (Bulk transport). +The application queries the sensor resolution at runtime and configures the +UVC descriptors accordingly. Console is accessible over UART0 (serial). + +The UVC driver also supports composite USB device mode +(``CONFIG_USBUVC_COMPOSITE``), allowing it to be combined with other USB +class drivers (e.g., CDC/ACM) in a single composite device. + +You can run the configuration and compilation procedure:: + + $ ./tools/configure.sh lckfb-szpi-esp32s3:uvc + $ make flash -j$(nproc) ESPTOOL_PORT=/dev/ttyUSB0 + +The application should work as follows: + +1. Create ``/dev/video0`` via ``capture_initialize()`` and open it. +2. Query the sensor's native resolution with ``VIDIOC_ENUM_FRAMESIZES`` + (pixel format ``V4L2_PIX_FMT_YUYV``), then ``VIDIOC_S_FMT`` to configure. +3. Fill ``struct uvc_params_s`` with the queried width, height and fps, + pass it to ``boardctl(BOARDIOC_USBDEV_CONTROL)`` via ``ctrl.handle`` + so the UVC gadget builds USB descriptors matching the actual sensor. +4. Open ``/dev/uvc0``, use ``poll()`` with ``POLLOUT`` to wait for the + USB host to start streaming. +5. Loop: ``VIDIOC_QBUF`` / ``VIDIOC_DQBUF`` to capture a YUYV frame, + then ``write()`` to ``/dev/uvc0``. + +On the host side, verify the device is recognized:: + + $ sudo dmesg + [32982831.662622] usb 1-9.3.3: new full-speed USB device number 72 using xhci_hcd + [32982831.752856] usb 1-9.3.3: New USB device found, idVendor=1d6b, idProduct=0102, bcdDevice= 1.00 + [32982831.752860] usb 1-9.3.3: New USB device strings: Mfr=1, Product=2, SerialNumber=3 + [32982831.752861] usb 1-9.3.3: Product: NuttX UVC Camera + [32982831.752862] usb 1-9.3.3: Manufacturer: NuttX + [32982831.752863] usb 1-9.3.3: SerialNumber: 0001 + [32982831.756625] usb 1-9.3.3: Found UVC 1.10 device NuttX UVC Camera (1d6b:0102) + +Then open the webcam with any UVC viewer (e.g. ``cheese``, ``guvcview``, +or ``ffplay``):: + + $ cheese + diff --git a/boards/xtensa/esp32s3/lckfb-szpi-esp32s3/configs/uvc/defconfig b/boards/xtensa/esp32s3/lckfb-szpi-esp32s3/configs/uvc/defconfig new file mode 100644 index 0000000000000..398f99986c651 --- /dev/null +++ b/boards/xtensa/esp32s3/lckfb-szpi-esp32s3/configs/uvc/defconfig @@ -0,0 +1,66 @@ +# +# This file is autogenerated: PLEASE DO NOT EDIT IT. +# +# You can use "make menuconfig" to make any modifications to the installed .config file. +# You can then do "make savedefconfig" to generate a new defconfig file that includes your +# modifications. +# +# CONFIG_ARCH_LEDS is not set +# CONFIG_NSH_ARGCAT is not set +# CONFIG_NSH_CMDOPT_HEXDUMP is not set +CONFIG_ARCH="xtensa" +CONFIG_ARCH_BOARD="lckfb-szpi-esp32s3" +CONFIG_ARCH_BOARD_COMMON=y +CONFIG_ARCH_BOARD_ESP32S3_LCKFB_SZPI=y +CONFIG_ARCH_CHIP="esp32s3" +CONFIG_ARCH_CHIP_ESP32S3=y +CONFIG_ARCH_CHIP_ESP32S3WROOM1N16R8=y +CONFIG_ARCH_INTERRUPTSTACK=2048 +CONFIG_ARCH_STACKDUMP=y +CONFIG_ARCH_XTENSA=y +CONFIG_BOARDCTL_USBDEVCTRL=y +CONFIG_BOARD_LOOPSPERMSEC=16717 +CONFIG_BUILTIN=y +CONFIG_DEBUG_FULLOPT=y +CONFIG_DEBUG_SYMBOLS=y +CONFIG_DEV_GPIO=y +CONFIG_ESP32S3_CAM=y +CONFIG_ESP32S3_GPIO_IRQ=y +CONFIG_ESP32S3_I2C0=y +CONFIG_ESP32S3_OTG=y +CONFIG_ESP32S3_OTG_ENDPOINT_NUM=2 +CONFIG_ESP32S3_UART0=y +CONFIG_ESPRESSIF_LEDC=y +CONFIG_ESPRESSIF_LEDC_CHANNEL0_PIN=42 +CONFIG_ESPRESSIF_LEDC_TIMER0=y +CONFIG_FS_PROCFS=y +CONFIG_FS_TMPFS=y +CONFIG_GPIO_LOWER_HALF=y +CONFIG_HAVE_CXX=y +CONFIG_HAVE_CXXINITIALIZE=y +CONFIG_IDLETHREAD_STACKSIZE=3072 +CONFIG_INIT_ENTRYPOINT="nsh_main" +CONFIG_INIT_STACKSIZE=4096 +CONFIG_INTELHEX_BINARY=y +CONFIG_IOEXPANDER=y +CONFIG_IOEXPANDER_PCA9557=y +CONFIG_LINE_MAX=64 +CONFIG_NSH_ARCHINIT=y +CONFIG_NSH_BUILTIN_APPS=y +CONFIG_NSH_FILEIOSIZE=512 +CONFIG_NSH_READLINE=y +CONFIG_PREALLOC_TIMERS=4 +CONFIG_RAM_SIZE=114688 +CONFIG_RAM_START=0x20000000 +CONFIG_RR_INTERVAL=200 +CONFIG_SCHED_WAITPID=y +CONFIG_START_DAY=6 +CONFIG_START_MONTH=12 +CONFIG_START_YEAR=2011 +CONFIG_SYSLOG_BUFFER=y +CONFIG_SYSTEM_NSH=y +CONFIG_UART0_SERIAL_CONSOLE=y +CONFIG_USBUVC=y +CONFIG_VIDEO=y +CONFIG_VIDEO_GC0308=y +CONFIG_VIDEO_STREAM=y