#3934·tinyusb

dwc2 slave mode on ESP32-P4: CDC IN starves while MSC IN is busy

Author: hathachCreated Sep 18, 2026Updated Sep 18, 2026
LabelsPrio 📌followup

On ESP32-P4 (espressif_p4_function_ev, HS) in dwc2 slave mode (no DMA), CDC IN nearly stops while MSC IN is busy on the same device. DMA mode and the S3 (FS) share bandwidth fairly.

Found while adding FreeRTOS/ESP-IDF support to examples/device/cdc_msc_throughput (branch add-cdc_msc_throughput-freertos, no PR yet). The HIL test runs CDC and MSC one after the other, so it passes 4/4 on S3/P4 × slave/DMA; this only shows under concurrent load.

Evidence (ci.lan, P4, 20–45 s runs: dd from the CDC tty while looping dd iflag=direct MSC reads)

P4 build CDC read alone CDC read + concurrent MSC read MSC read
slave, TX refilled in tud_cdc_tx_complete_cb 13.1 MB/s 37.2, 18.5 kB/s; one run hit EOF after 0.1 s ~17 MB/s
slave, callback removed 102 kB/s 102 kB/s ~17 MB/s
DMA, with callback 15.7 MB/s 10.8 MB/s ~15 MB/s
  • Without the callback CDC is tick-bound: one 1 KiB refill per 10 ms tick (ESP-IDF default 100 Hz). With it, concurrent CDC drops below that bound.
  • S3 (FS, slave and DMA): CDC 480 kB/s alongside MSC 500 kB/s.
  • No task-watchdog, panic or reset on the UART console in any run.
  • Example setup: usbd task (prio max-1) runs tud_task(); cdc task (max-2) loops cdc_throughput_task(); vTaskDelay(1); tud_cdc_tx_complete_cb also calls cdc_throughput_task(). CFG_TUD_CDC_TX_EPSIZE/BUFSIZE = 1024, MSC EP buffer 4096.

Leads (source-read, unverified)

  • Slave mode enables dedicated HW FIFO operation (src/tusb_option.h:345-350,383), so CDC has no endpoint buffer (src/class/cdc/cdc_device.c:236-241) and the tu_fifo is drained straight into the TX FIFO (src/tusb.c:349-352,390-393; dcd_dwc2.c epin_write_tx_fifo, handle_epin_slave). DMA copies into an EP buffer, so DMA is not a clean control for the controller alone.
  • A 1024-byte CDC transfer needs the TXFE refill path (dcd_dwc2.c edpt_schedule_packets, first packets written in task context, rest via TXFE).
  • ESP-IDF FreeRTOS is SMP: usbd and cdc tasks are unpinned and can run on both cores at once; both write the CDC TX stream. Claim arbitration and the FIFO write mutex look sound, so this is a lead only. Pinning usbd also moves the USB IRQ (esp_intr_alloc on the calling core), so keep usbd on core 0 and vary only the cdc task's core.
  • Not #1292 (RX endpoint-buffer race): this is IN bandwidth, and in slave mode CDC has no endpoint buffer.

Remaining work

  1. Hold one CDC reader open: CDC alone → start MSC → stop MSC, with per-second bytes, usbmon submissions/completions and dmesg, to tell reversible contention from a wedge and explain the EOF.
  2. Discriminating builds: 512-byte CDC TX EP size; buffered CDC in slave mode; cdc task pinned to core 0 vs core 1.
  3. Target-side capture during starvation: CDC/MSC IN DIEPCTL/DIEPTSIZ/DTXFSTS/DIEPINT, DIEPEMPMSK, GINTMSK/GINTSTS, tu_fifo counts, submitted/completed counters.
  4. Fix in dcd_dwc2.c (or the stream layer) and add a concurrent CDC+MSC load check to HIL so it stays fixed.

A first hardware pass covering experiments 1–3 was stopped midway when the debug agent roles were being reworked; its raw results were kept on the dev machine but not analysed.

Why deferred

Separate scope from the example's FreeRTOS support: the example and HIL coverage are done, and this looks like a dwc2 slave-mode driver issue that needs its own hardware diagnosis.