Status: validated live (Unifying receiver, composite keyboard+mouse, including across hot detach/re-attach).
What you need
- A P2 board with the USB serial-host header (base pin 16 assumed below)
- Any USB keyboard/mouse or a combo wireless receiver, full-speed
(most receivers and gaming keyboards; some bargain keyboards are
low-speed — see tickets 1190/1210 — and won't enumerate yet. Behind a
hub the driver now says so itself: the log names the port
LOW-SPEED, and the HID example adds aRESULT1line confirming it via the hub's own port status) - Build gate:
USB_HID
pnut-ts -d -I <path-to>/src -D USB_HID my_kbd.spin2
The smallest program
#IFDEF USB_HID
#PRAGMA EXPORTDEF USB_HID
#ENDIF
CON
_clkfreq = 200_000_000
OBJ
usb : "usb_app"
PUB main() | c
usb.begin(16)
if usb.await_kbd(5000) ' blocks up to 5 s, scanning -
debug("keyboard ready") ' late attachers still arrive
repeat
if usb.kbd_poll() ' ~1 ms; false = no new report
c := usb.kbd_char() ' best-effort ASCII (mods applied)
if c
debug(zstr_(@c))
if usb.mouse_ready()
if usb.mouse_poll()
debug("mouse dx=", sdec_(usb.mouse_dx()), " dy=", sdec_(usb.mouse_dy()), " b=", ubin_(usb.mouse_buttons()))
usb.poll() ' hot-plug: unplug/replug just works
waitms(8)
What bring-up actually does
usb.begin(16)enumerates the bus and binds keyboard and mouse by interface boot protocol — a composite receiver's keyboard interface (protocol 1) and mouse interface (protocol 2) get their own endpoints, so both work at once.- Each bound interface is switched to boot protocol: a fixed report layout (8-byte keyboard, 3+ byte mouse) that every keyboard and mouse supports — no descriptor parsing needed on this plane.
kbd_poll()/mouse_poll()each fetch one interrupt-IN report, poll-once (~1 ms idle cost, NAK just means "no change").- Raw access when you want it:
kbd_mods()(modifier bits) andkbd_key(i)(up to 6 HID usage codes) under the ASCII convenience ofkbd_char().
Troubleshooting
| Symptom | Likely cause |
|---|---|
| Keyboard never binds | It may be low-speed — behind a hub the log says leaf is LOW-SPEED ... ticket 1210 and the HID example prints a RESULT1 confirmation; direct-attached it looks like a dead bus (1190). Use a full-speed keyboard (receiver / gaming keyboard); check the roster with dev_count()/dev_vid(). |
| Keys repeat oddly / missed | Poll at least every ~16 ms; set_int_interval() can pace polling against SOF frames if you poll faster. |
| Gamepad grabbed as keyboard | Build with -D USB_JOY too — pads are recognized by descriptor and routed to joy_* (joystick guide). |
| Unifying receiver binds but keys are wrong / missing | The receiver lists a report-only (protocol 0) DJ iface first, then boot kbd (1) and mouse (2). The host keeps four HID ifaces and binds only protocol 1 as the keyboard. Protocol 0 is never a boot keyboard — polling it delivers the real key plus a second HID++/consumer report (field: s then 9). |
Next
- Full worked example:
examples/top_hid_example.spin2 - Keyboard-driven audio:
examples/top_tone_keyboard_example.spin2 - API reference: docs/api/usb-app-api.md §5