midi_out: skip USB gadget when no host is enumerated (fixes MIDI clock out over TRS without USB)#955
Conversation
tud_midi_stream_write has no mount check: with no USB host attached (e.g. AMYboard on eurorack power alone) it fills its tx FIFO, then returns 0 forever, and midi_out's stall-recovery loop burns ~1s per byte before falling through to the UART write. MIDI clock out (external_midi_sync send mode, 24 PPQ) hits this from the render path, wedging the sequencer and reducing TRS/DIN clock from 48/sec to ~1/sec at 120 bpm. Guard the gadget branch with tud_ready() so USB is only attempted when a host has enumerated us and the bus isn't suspended. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
🎛️ AMY HW CI (AMYboard bench)Flashed this PR's AMY (LoadTestChord: 6-voice Juno ✅ PASS — the bench ran the test to completion.
Full chord settled render μs: 3164 (was 3170, Δ -0.2%) (peak 3173, 39 samples) ⬇️ Artifacts: serial log · load trace · report Self-hosted bench (amyboardci). FAIL means only that the test could not run — the load values are informational, with no threshold and no audio compare. See |
⛓️ tulipcc integration PR openedThis merge was pinned into tulipcc for full-system CI: shorepine/tulipcc#1215 Test it there and merge that PR to move tulipcc onto this AMY. |
Problem
Reported on Discord: with
tulip.external_midi_sync(send=True)on AMYboard, MIDI clock goes out over both USB and the TRS MIDI out port while USB is attached — but with USB disconnected (eurorack power only), the TRS out goes nearly silent and the board appears to hang; a downstream device "occasionally responds".Root cause
midi_out()always tries the USB gadget branch whenAMY_MIDI_IS_USB_GADGETis configured. TinyUSB'stud_midi_stream_writehas no mount check — it just fills the class driver's tx FIFO, andwrite_flushsilently fails when no host has enumerated the device. So with no USB host attached, the FIFO fills after ~16 messages and every subsequent write returns 0, which sendsmidi_outinto its stall-recovery loop (mp_usbd_task()+vTaskDelay(1)× 1000) — ~1 second per byte — before falling through to the UART write.MIDI clock out ticks come from
sequencer_check_and_fill()→midi_clock_out_tick()in the render path, so at 24 PPQ this wedges the sequencer and reduces the TRS/DIN clock from 48 bytes/sec to ~1/sec at 120 bpm — exactly the observed "occasionally responds" behavior. The same stall hits anymidi_outcaller (e.g. Tulip'stulip.midi_out()) once USB is unplugged.Fix
Guard the gadget branch with
tud_ready()(enumerated and not suspended). Unpowered / charger-only / detached USB skips the gadget write instantly and the UART gets bytes at full rate; behavior with a real host attached is unchanged.Testing
make test: 117 tests passidf.py -DMICROPY_BOARD=AMYBOARD build) builds clean against this branch🤖 Generated with Claude Code