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