Parallax Propeller2 USB driver - the MashUp

Getting started: USB audio (playback + microphone)
Login

Getting started: USB audio (playback + microphone)

Status: fully validated on hardware (tickets 1180/2110, 2026-08-12) — loopback-measured exact sample rates, 60 s of zero-underrun playback under concurrent storage load.

What you need

pnut-ts -d -I <path-to>/src -D USB_AUDIO my_audio.spin2

The smallest program (play a tone)

#IFDEF USB_AUDIO
#PRAGMA EXPORTDEF USB_AUDIO
#ENDIF

CON
  _clkfreq = 300_000_000

OBJ
  usb : "usb_app"

VAR
  word  frame[96]                           ' 48 stereo samples = one 1 ms frame

PUB main() | i, ph, k, w, off
  usb.begin(16)

  repeat until usb.aud_present()
    usb.poll()
    waitms(50)

  ifnot usb.aud_open(48_000)                ' the DEVICE decides if a rate is
    debug("rate refused - try 44_100")      ' supported; unsupported = clean fail
    repeat

  wordfill(@frame, 0, 96)                   ' prefill the ring with silence so
  repeat while usb.aud_write(@frame, 192) == 192   ' the stream starts clean

  ph := 0
  repeat
    repeat i from 0 to 47                   ' 1 kHz square (sine table & synth
      k := (ph++ // 48 < 24) ? 6000 : -6000 ' patterns: examples/top_*_example)
      word[@frame][i * 2] := k              ' left
      word[@frame][i * 2 + 1] := k          ' right
    off := 0
    repeat while off < 192                  ' write with backpressure: accept
      w := usb.aud_write(@frame + off, 192 - off)  ' whatever fits, retry rest
      if w < 1
        waitus(200)
      off += w
    usb.poll()

What bring-up actually does

  1. usb.begin(16) enumerates; a UAC1 device binds (aud_present()).
  2. aud_open(rate) selects the streaming interface's active alt-setting, asks the device for your sample rate (the device is the authority — an unsupported rate STALLs and the open fails cleanly; 48,000 and 44,100 Hz are both exact, verified by measurement), sizes the isochronous frame reservation, and arms the PHY.
  3. From then on the USB cog itself transmits audio every 1 ms frame from a hub-RAM ring — your code never races the wire. Your only job: keep the ring fed. aud_write never blocks; it returns how many bytes it accepted, and you pace on that (or aud_free()).
  4. Format is 16-bit little-endian interleaved PCM. Stereo at 48 kHz means 192 bytes per millisecond — the loop above stays comfortably ahead.
  5. Health is counted, not guessed: aud_underruns() must stay 0 while you're feeding properly (the acceptance bar was 60 s of 0 under concurrent file copies).

Microphone

  usb.aud_mic_open(48_000)
  repeat
    n := usb.aud_read(@buf, 256)            ' returns WHOLE mic frames only -
    if n                                    ' size buf >= 252 bytes, and never
      process(@buf, n)                      ' expect partial frames
    usb.poll()

Capture and playback run together (full duplex — the validation rig was a loopback jumper, headphone out into mic in, measuring its own tones). aud_volume(v256) (1/256 dB units) and aud_mute(yn) drive the device's volume control where it has one.

Give audio its own cog: a DSP cog owns aud_read/aud_write (they're lock-free, single-producer/single-consumer) while your main cog keeps usb.poll() and everything else. Worked pattern: examples/top_audio_dsp_example.spin2.

Troubleshooting

Symptom Likely cause
aud_open fails Rate unsupported by the adapter — try 44_100; check aud_present() first.
Clicks / aud_underruns() climbing Your loop fell behind — synthesize into whole frames and write with the backpressure loop above; move DSP to its own cog; check clock (300 MHz for Spin synthesis).
Mic reads 0 forever Buffer smaller than one mic frame (needs ≥ 252 B), or aud_mic_open not called.
Capture rate sags under heavy bus load Known characteristic: synchronous-mode mics lose SOF lock when a degraded bus peer induces jitter; playback (adaptive) is immune.

Next