Parallax Propeller2 USB driver - the MashUp

Getting started: gamepad / joystick
Login

Getting started: gamepad / joystick

Status: validated live (ticket 2150, 2026-08-17) on a Sony DualShock 4 ($054C:$09CC, standard HID, kind=$05, 7 axes, 14 buttons) sitting on the second host channel while a keyboard/mouse receiver used the first. int_in is a boolean — joy_poll takes the payload size from last_rx_len(), and bind does not pin the kbd/mouse hid_bus.

What you need

pnut-ts -d -I <path-to>/src -D USB_HID -D USB_JOY my_pad.spin2

The smallest program

#IFDEF USB_HID
#PRAGMA EXPORTDEF USB_HID
#ENDIF
#IFDEF USB_JOY
#PRAGMA EXPORTDEF USB_JOY
#ENDIF

CON
  _clkfreq = 200_000_000

OBJ
  usb : "usb_app"

PUB main() | i
  usb.begin(16)                             ' bring up the whole bus

  repeat until usb.joy_present()            ' pads may attach late - poll()
    usb.poll()                              ' keeps scanning while you wait
    waitms(50)

  ' -- discover: the pad tells you what it has --
  debug("axes=", udec_(usb.joy_axes()), " buttons=", udec_(usb.joy_button_count()))
  repeat i from 0 to usb.joy_axes() - 1
    debug("axis ", udec_(i), " usage=", uhex_(usb.joy_axis_usage(i)), " range ", sdec_(usb.joy_axis_min(i)), "..", sdec_(usb.joy_axis_max(i)))

  ' -- read: poll, then use the accessors --
  repeat
    if usb.joy_poll()
      debug("x=", sdec_(usb.joy_axis_scaled(0)), " y=", sdec_(usb.joy_axis_scaled(1)), " btn=", uhex_(usb.joy_buttons()), " hat=", sdec_(usb.joy_hat()))
    usb.poll()                              ' hot-plug stays live
    waitms(8)

What bring-up actually does

  1. usb.begin(16) powers the port, starts the one USB cog, resets the bus, and enumerates everything it finds (hub or direct). No fixed delays — readiness is sampled.
  2. When a HID interface with protocol 0 appears, its report descriptor is fetched and parsed (this is the key step — pads have no fixed layout; the descriptor says where every control lives). Only a device whose descriptor declares a joystick/gamepad/multi-axis collection binds to joy_*; keyboards and mice are never touched.
  3. joy_present() goes true. From here everything is table lookups — the parse happened once, at bind.
  4. joy_poll() fetches one report (~1 ms when idle, nothing blocks); the joy_* accessors read fields out of it.

Recognizing and mapping controls (the wizard)

You never hardcode "button 3 = fire" — you ask the user:

  repeat until usb.joy_poll()               ' one fresh report first
    waitms(8)
  usb.joy_mark()                            ' snapshot the resting state
  debug("press the control you want for FIRE")
  repeat
    if usb.joy_poll()
      code := usb.joy_changed()             ' first control that moved
      if code
        quit
    waitms(8)
  ' store `code`; forever after:
  fire := usb.joy_control(code)             ' 0/1 for buttons, -32768..32767
                                            ' for axes, 0..7/-1 for the hat

joy_changed() ignores stick noise (it needs a quarter of the axis span) and reports buttons, axes, and the hat uniformly. Persist your stored codes keyed by dev_vid()/dev_pid() and the same pad model maps itself on every future attach.

Troubleshooting

Symptom Likely cause
joy_present() never true Pad is in a vendor mode (PS/Xbox pairing state) — power-cycle it into standard HID; or it's low-speed (ticket 1190, not yet supported).
Axes read 0 Call joy_poll() first — accessors read the last polled report.
Wrong stick moves Nothing's wrong — check joy_axis_usage(i): sticks map X/Y/Z/Rz differently per vendor. That's why the wizard exists.

Next