dcd_dwc2: serialize diepempmsk read-modify-write with usbd_spin_lock
diepempmsk gates the TXFE (TxFIFO empty) interrupt of every IN endpoint in a
single 32-bit register, and two places read-modify-write it:
edpt_schedule_packets() sets the endpoint's bit
handle_epin_slave() clears the endpoint's bit
They touch *different* endpoints' bits of the *same* word, so a write based on a
stale snapshot wipes the other writer's bit.
Observed interleaving (core0 = USB ISR, core1 = task submitting CDC data):
core0 (ISR) core1 (task)
set ep2: 0x00 -> 0x04
read 0x04, write 0x14
clear ep2: read 0x04 (stale)
write 0x00 ------------------> ep4's just-set bit is gone too
The victim is a bulk IN endpoint: its gate is now closed, so the already armed
transfer (DIEPTSIZ.XferSize > 0) never gets its TXFE interrupt, nothing is moved
into the TxFIFO, and the transfer never completes - a bulk endpoint is only
re-armed on completion. Isochronous endpoints re-arm every frame and therefore
self-heal, which is why the failure presents as "audio keeps working, CDC
silently dies".
Latch snapshot: DIEPTSIZ.XferSize > 0, DTXFSTS = 0, the endpoint's diepempmsk
bit cleared.
The set side is already serialized: it only runs for non-EP0 endpoints, and
dcd_edpt_xfer()/dcd_edpt_xfer_fifo() hold usbd_spin_lock() across the whole
scheduling (dcd_edpt_close_all() zeroes the same register under that lock as
well). The clear side runs in the endpoint ISR, which does not hold it, so take
the same lock there - around the epin->tsiz read as well as the update. The ISR
queues the transfer completion before reading tsiz, so the task can re-arm that
endpoint in between, and a clear based on the pre-arm tsiz would wipe the bit of
the transfer that was just armed.
That is deliberately the existing lock and not a dedicated one: OSAL spinlocks
must not be nested. On OPT_OS_NONE each spinlock keeps its own nesting count, so
releasing an inner lock re-enables interrupts while the outer critical section
is still running. Sharing it also keeps a single protection domain for the
register.
Validated on ESP32-S3 with ESP-IDF 6.0 (composite UAC + CDC-ACM + DFU, host
recording audio and streaming CDC at the same time): putting a critical section
around these two statements took the latch rate from 4 events / 6120 s to 0
events / 9600 s (~2.0e8 mask writes). That run serialized them with a separate
OSAL spinlock in espressif/tinyusb 0.19.0~3; this patch gives the same mutual
exclusion through the lock the writers already share.
1 file changed