Skip to content

[dwc2/ESP32-S2] UAC2 stream restart stalls ~5s: dcd_edpt_iso_activate() busy-waits in edpt_disable() when re-activating an idle ISO OUT endpoint #3689

Description

@weiruei

Operating System

Windows 11

Commit SHA

0.20.1 (as bundled in arduino-esp32 3.3.10)

Board

ESP32-S2 (generic ESP32-S2 Dev Module), Synopsys dwc2 DCD, USB-OTG Full-Speed

Firmware

Custom UAC2 speaker (sink): CFG_TUD_AUDIO=1, EP OUT + explicit async feedback EP, 48kHz/16-bit/stereo, using the stock audio_device.c.
Note: arduino-esp32 does not ship CFG_TUD_AUDIO (see espressif/arduino-esp32#12053), so audio_device.c here is user-supplied; but the root cause below is in dcd_dwc2.c which IS shipped.

What happened ?

A UAC2 speaker (sink) on ESP32-S2 stalls ~5 s every time the host re-opens the audio streaming interface (SET_INTERFACE alt=0 then alt=1) -- e.g. when resuming playback after the host closed the stream on idle/pause.

audiod_set_interface() calls usbd_edpt_iso_activate() again on the ISO OUT (and feedback) endpoints. On dwc2, dcd_edpt_iso_activate() disables the EP first:

bool dcd_edpt_iso_activate(uint8_t rhport, tusb_desc_endpoint_t const * p_endpoint_desc) {
  edpt_disable(rhport, p_endpoint_desc->bEndpointAddress, false);
  edpt_activate(rhport, p_endpoint_desc);
  return true;
}

edpt_disable() for an OUT endpoint busy-waits on the disable handshake:

dwc2->dctl |= DCTL_SGONAK;
while ((dwc2->gintsts & GINTSTS_BOUTNAKEFF_Msk) == 0) {}
dep->doepctl |= DOEPCTL_EPDIS | stall_mask;
while ((dep->doepint & DOEPINT_EPDISD_Msk) == 0) {}

When re-activating an IDLE ISO OUT EP (no tokens arrive during the brief alt toggle), this handshake does not complete promptly -- measured ~5000 ms per call on ESP32-S2 -- and it runs in the usbd task, so the device stops servicing ALL USB (control + every EP) for that window. The host restart is alt=1 -> alt=0 -> alt=1, so total user-visible stall is ~10 s.

Real-world symptom: ESP32-S2 USB DAC set as the Windows default output. Pause YouTube in Chrome for >~10 s (Windows audio engine then closes the stream). Press play -> audio and the A/V-synced video freeze ~10 s before resuming. A built-in sound card never does this close/reopen cycle, so it never stalls.

Confirmed it is NOT USB selective suspend: reproduced with the device kept awake (CDC interface held open), tud_suspended() == false throughout, device millis() never resets.

Tested workaround (application side): with TUP_DCD_EDPT_ISO_ALLOC, the alt=0 close path does not deactivate the EP hardware, so re-activation is both redundant and triggers the slow disable. Activating each ISO EP only once eliminates the stall:

// in audiod_set_interface() EP-open loop
volatile bool* act = (ep_addr & 0x80) ? &iso_act_in : &iso_act_out;
if (!*act) { TU_ASSERT(usbd_edpt_iso_activate(rhport, desc_ep)); *act = true; }
// flags reset in audiod_reset()

Resume latency dropped from >9.7 s to ~0.5 s.

Questions for maintainers:

  1. Should dcd_edpt_iso_activate() skip edpt_disable() when the EP is not currently enabled, or bound the disable busy-wait with a timeout?
  2. Should audio_device.c avoid re-activating an ISO EP that was never deactivated (the alt=0 path does not call usbd_edpt_close under ISO_ALLOC)?

Code quoted from src/portable/synopsys/dwc2/dcd_dwc2.c @ master. Happy to test patches.

How to reproduce ?

  1. Build a UAC2 speaker on ESP32-S2 (CFG_TUD_AUDIO, async ISO EP OUT + explicit feedback EP), 48 kHz / 16-bit / stereo. Set it as the Windows default playback device.
  2. Start audio playback (e.g. YouTube in Chrome).
  3. Pause long enough for the host to issue SET_INTERFACE alt=0 (Windows audio-engine idle timeout, ~10 s of silence).
  4. Resume playback -> host issues SET_INTERFACE alt=1.
    => Playback freezes ~5-10 s before audio resumes.

Debug Log as txt file (LOG/CFG_TUSB_DEBUG=2)

Device-side event trace (millis captured INSIDE the TinyUSB callbacks, so independent of host timing):

ms=1998   SET_INTERFACE(alt=1) entered audiod_set_interface()
ms=6998   tud_audio_set_itf_cb() reached        <-- exactly 5000 ms later
ms=30038  SET_INTERFACE(alt=1) entered
ms=35039  tud_audio_set_itf_cb() reached        <-- 5001 ms
ms=35040  SET_INTERFACE(alt=1) entered
ms=40043  tud_audio_set_itf_cb() reached        <-- 5003 ms

No control requests are serviced during each ~5 s gap; millis() keeps advancing (no reset/reboot). After the activate-once workaround, the same alt=1 sequence completes within the same millisecond.

Screenshots

No response

I have checked existing issues, discussion and documentation

  • I confirm I have checked existing issues, discussion and documentation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions