Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Documentation/components/drivers/special/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ following section.
syslog.rst
sdio.rst
usbdev.rst
uvc.rst
usbhost.rst
usbmisc.rst
usbmonitor.rst
Expand Down
127 changes: 127 additions & 0 deletions Documentation/components/drivers/special/uvc.rst
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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

140 changes: 83 additions & 57 deletions arch/xtensa/src/esp32s3/esp32s3_cam.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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;
Expand All @@ -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);
}
}
}
}
Expand Down Expand Up @@ -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();
Expand All @@ -522,7 +563,9 @@ static int esp32s3_cam_init(struct imgdata_s *data)

/* Configure CAM controller */

return esp32s3_cam_config(priv);
int ret = esp32s3_cam_config(priv);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
int ret = esp32s3_cam_config(priv);
return esp32s3_cam_config(priv);


return ret;
}

/****************************************************************************
Expand Down Expand Up @@ -554,6 +597,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)
Expand Down Expand Up @@ -723,31 +774,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;
}

Expand Down
Loading
Loading