This is an automated email from the ASF dual-hosted git repository.

acassis pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/nuttx.git

commit 4775b36316407997992174a6c74057293bd17988
Author: wangjianyu3 <[email protected]>
AuthorDate: Sun Mar 22 10:00:00 2026 +0800

    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 <[email protected]>
---
 Documentation/components/drivers/special/index.rst |    1 +
 Documentation/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(+)

diff --git a/Documentation/components/drivers/special/index.rst 
b/Documentation/components/drivers/special/index.rst
index 919362b8950..1444d74eb9b 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 00000000000..9c39a1aad9d
--- /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 *)&params;
+
+  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 12743473edb..258ee99aa22 100644
--- a/boards/boardctl.c
+++ b/boards/boardctl.c
@@ -56,6 +56,7 @@
 #  include <nuttx/usb/cdcacm.h>
 #  include <nuttx/usb/pl2303.h>
 #  include <nuttx/usb/usbmsc.h>
+#  include <nuttx/usb/uvc.h>
 #  include <nuttx/usb/composite.h>
 #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 cae55d63e39..261c90ce0c6 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 e1ad9ddde96..6a369baf989 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 8532b800f38..bf426bbc2a4 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 00000000000..5b90cacf016
--- /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 <nuttx/config.h>
+
+#include <assert.h>
+#include <errno.h>
+#include <debug.h>
+#include <string.h>
+
+#include <nuttx/nuttx.h>
+#include <nuttx/kmalloc.h>
+#include <nuttx/usb/usb.h>
+#include <nuttx/usb/usbdev.h>
+#include <nuttx/usb/usbdev_trace.h>
+#include <nuttx/usb/uvc.h>
+#ifdef CONFIG_USBDEV_COMPOSITE
+#  include <nuttx/usb/composite.h>
+#endif
+#include <nuttx/fs/fs.h>
+#include <nuttx/wqueue.h>
+#include <poll.h>
+#include <nuttx/mutex.h>
+#include <nuttx/lib/lib.h>
+
+/****************************************************************************
+ * 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 00000000000..cbbcfffd749
--- /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 <nuttx/config.h>
+#include <nuttx/usb/usbdev.h>
+
+/****************************************************************************
+ * 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 d0508efef7c..23cd8f61e30 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

Reply via email to