Skip to content

UAC: take each packet in the USB interrupt, so a busy interpreter no longer costs packets (#39) - #47

Merged
bdbarnett merged 2 commits into
mainfrom
uac-isr-rearm
Sep 26, 2026
Merged

bdbarnett merged 2 commits into
mainfrom
uac-isr-rearm

Conversation

@bdbarnett

@bdbarnett bdbarnett commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #39 on the ESP32-P4. With the interpreter busy, the P4 sound card now gets the whole stream, and the steady tick under the music is gone.

Needs micropython-pydevices#21; merge that first. It gives TinyUSB 0.21 to the P4 only. The S2 and S3 stay on 0.18 for now (#48). There this change builds and behaves as before, so an S3 sound card still ticks under load. That remainder lives in #48.

What changed

  • The packet hook runs in the USB interrupt. On TinyUSB 0.19+ it's tud_audio_rx_done_isr(), and TinyUSB has already re-armed the endpoint by the time it runs. On older TinyUSB it's still tud_audio_rx_done_post_read_cb(). Either way it does only ISR-safe work: two counters, a FIFO level read and a task notification. No MicroPython, no FIFO reads, nothing that blocks.
  • usbif_pump_notify() checks where it's called from. From the ISR it uses vTaskNotifyGiveFromISR + portYIELD_FROM_ISR; from a task (rate change, stop) it uses xTaskNotifyGive.
  • The no-pump "shed to half" is gone. It read the FIFO from the callback, and an ISR mustn't do that while Python may be reading too. Feedback is disabled, so a full FIFO no longer stalls the host, and TinyUSB's FIFO overwrites its oldest audio. uac_stats() still counts overflows (FIFO above three-quarters with no pump).
  • High speed goes back to bInterval 1 (a packet every microframe) on 0.19+. It has to. 0.21's DWC2 driver arms an isochronous OUT endpoint for the microframe after the current one, so at bInterval 4 the host's packet lands on the other frame parity and is dropped. The P4 got no packets at all at 4. Re-arming in the ISR is what makes 125 µs safe now. On older TinyUSB it stays 4 (Sound card over high speed: the pump receives 80 % of the stream, at every rate #37).
  • TUD_AUDIO_EP_SIZE took a speed argument in 0.20; a small wrapper keeps both versions building.

Evidence

This was on the P4 panel, using tests/hardware/uac_stream_check.py plus uac_play_tone.py from Windows WASAPI, with a 440 Hz sine, 10 s windows and at least 80 s per run. The board volume was 30 %.

build load host delivered pitch at I2S
main (TinyUSB 0.18) busy (STRESS=1) 48 kHz shared 90.6 to 95.5 %, every window below 99.5 441 to 576 Hz, unstable
this PR + 0.21 busy 48 kHz shared 100.00 % (8,000.1 to 8,000.4 packets/s) 440.0, 439.8
this PR + 0.21 idle 48 kHz shared 99.92 to 100.00 % 440.2; 438.5 and 446.7 in shared mode
this PR + 0.21 busy 44.1 kHz exclusive 99.89 to 100.00 % 440.0 ×3
this PR + 0.21 busy, 48 kHz wire as in soundcard.py 48 kHz exclusive 100.00 % after the stream-start window (99.46) 440.0 ×3

The checker failed on the old build (every window below the 99.5 % gate) and passes on the new one. At 44.1 kHz the pump still times out 7 or 8 times per 10 s, which is #40 (no feedback), unchanged.

Also on the 0.21 image:

  • All 63 costumes enumerate and mount. The sweep waits to see each one detach and come back.
  • The CDC REPL works on the same cable while the card streams: 8,000.5 packets/s during an exec, and 7,999.8 during a 200-line print.
  • MSC works: a FAT RAM disk read back on Windows.
  • The change builds against the old TinyUSB too.

Both TinyUSB versions, one source (2026-09-25)

Both boards ran images from this branch (b7ee671; the later commit only touches the README) on micropython-pydevices#21's per-target overlay, with a quiet 440 Hz tone over WASAPI, 48 kHz shared.

board TinyUSB hook linked load delivered
P4 panel (high speed) 0.21 (v0.21.0.1-micropython1) tud_audio_rx_done_isr busy 100.00 %, 8,000.2 to 8,000.3 packets/s, 8 windows after stream start; pitch 440.0
T-Embed (S3, full speed) 0.18 pin tud_audio_rx_done_post_read_cb idle 99.91 to 99.93 % (999.1 to 999.3 of 1,000 packets/s)
T-Embed 0.18 pin tud_audio_rx_done_post_read_cb busy 87.4 to 93.9 %, #39 as expected on 0.18
T-Embed, an experiment for #48 v0.21.0.1-micropython1 tud_audio_rx_done_isr busy 100.00 %, 1,000.0 packets/s, 9 windows

The version guards pick the right callback, TUD_AUDIO_EP_SIZE form and high-speed interval on each: both builds compile, and the link maps name the hook shown. On the T-Embed the pitch at I2S read 440.0 in a mono run; the checker's tap only reads a mono sink. On 0.18 under load the T-Embed's CDC console also went silent to the host, so its busy runs logged to a file on the board.

…y interpreter no longer costs packets (usbif#39)
@bdbarnett
bdbarnett merged commit c37ce17 into main Sep 26, 2026
12 checks passed
@bdbarnett
bdbarnett deleted the uac-isr-rearm branch September 26, 2026 03:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Sound card: every packet waits for the interpreter, because the OUT endpoint is re-armed from the MicroPython task

1 participant