Status: awaiting hardware validation (ticket 2140) — compile-verified; the bulk and poll-once machinery underneath is bench-proven.
What you need
- A P2 board with the USB serial-host header (base pin 16 assumed below)
- Any class-compliant USB-MIDI controller — virtually every modern keyboard/pad controller is (no driver disks since ~2005 = class-compliant)
- Build gate:
USB_MIDI(addUSB_AUDIOif you want to make sound too)
pnut-ts -d -I <path-to>/src -D USB_MIDI my_midi.spin2
The smallest program
#IFDEF USB_MIDI
#PRAGMA EXPORTDEF USB_MIDI
#ENDIF
CON
_clkfreq = 200_000_000
OBJ
usb : "usb_app"
PUB main() | ev
usb.begin(16)
repeat until usb.midi_present()
usb.poll()
waitms(50)
debug("controller bound - play something")
repeat
repeat ' drain the whole burst each pass -
ev := usb.midi_read() ' one poll can carry 16 events
if ev == 0
quit
if usb.midi_is_note_on(ev)
debug("note ON ", udec_(usb.midi_ev_data1(ev)), " vel ", udec_(usb.midi_ev_data2(ev)))
elseif usb.midi_is_note_off(ev)
debug("note OFF ", udec_(usb.midi_ev_data1(ev)))
elseif usb.midi_ev_cin(ev) == usb.CIN_PITCH_BEND
debug("bend ", sdec_(usb.midi_bend(ev)))
elseif usb.midi_ev_cin(ev) == usb.CIN_CTRL_CHANGE
debug("cc", udec_(usb.midi_ev_data1(ev)), " = ", udec_(usb.midi_ev_data2(ev)))
usb.poll()
waitms(8)
What bring-up actually does
usb.begin(16)enumerates the bus. A device with a MIDIStreaming interface (USB Audio class, subclass 3) binds tomidi_*— including audio interfaces that carry a MIDI port alongside their audio.midi_present()goes true. There is no rate, protocol, or mode to negotiate — MIDI is stateless per event, which is why this is the shortest bring-up in the stack.midi_read()returns 32-bit event packets: the low nibble (the CIN) says what each packet is, so there's no running-status parsing — decode withmidi_ev_*()/midi_is_note_*().- An idle controller costs ~1 ms per
midi_read(); a chord arrives as a burst that one poll buffers and successive calls hand out — hence the inner drain loop above.
Sending works too, if the device has a MIDI OUT jack
(midi_out_present()):
usb.midi_write(usb.midi_pack(0, usb.CIN_NOTE_ON, $90, 60, 100)) ' middle C
Making sound
Pair with the audio plane: decode note on/off into a phase-accumulator
oscillator feeding aud_write. The complete pattern — exact
equal-temperament note table, velocity scaling, pitch bend — is
examples/top_midi_example.spin2 (build with -D USB_MIDI -D USB_AUDIO);
see also the audio guide.
Troubleshooting
| Symptom | Likely cause |
|---|---|
midi_present() never true |
Device isn't class-compliant (very old, or needs vendor drivers), or it's on the other bus — check dev_count()/dev_vid() roster. |
| Notes stick "on" | Handle velocity-0 NOTE ON: it is a note-off (the helpers do this for you — use midi_is_note_off(), not the CIN alone). |
| Events arrive in clumps | Expected — drain until midi_read() returns 0 each pass. |
Next
- Full worked example (synth + monitor):
examples/top_midi_example.spin2 - API reference: docs/api/usb-app-api.md (MIDI section)