Expand description
Digital / DualShock pad polling via SIO0.
Talks directly to the SIO0 hardware (no BIOS syscalls) so the same code works whether we side-load a homebrew (HLE BIOS) or boot through the real BIOS. The controller protocol is simple enough that a hand-rolled select + four-byte exchange beats opening events and waiting on them.
Typical use from a game loop. The controller port has one owner, the
ControllerPort token from psx_rt::Peripherals::take(), which every pad
poll and memory-card transfer borrows for the length of its transaction:
let mut port = peripherals.controller_port;
let mut reader = psx_pad::PadReader::port1();
let pad = reader.poll_on(&mut port);
if pad.buttons.is_held(psx_pad::button::START) {
// …
}The protocol spec, reproduced from nocash PSX-SPX:
TX | RX | Meaning |
|---|---|---|
01 | FF | Address byte / select controller |
42 | 41 | Poll command / digital pad ID low |
00 | 5A | Fill byte / ID high |
00 | b0 | Buttons group 1 (active-low) |
00 | b1 | Buttons group 2 (active-low) |
DualShock analog mode uses the same first four bytes but reports
ID low 0x73 and appends four stick bytes:
right X/Y, then left X/Y. Fresh DualShocks boot digital, so games
that require sticks should either call enable_analog_on or
show an “enable analog mode” prompt when PadState::is_analog
is false. Programs that require a DualShock call
require_analog_on at boot: it locks the pad in analog mode, so
the Analog button cannot switch it back, and says whether an
analog-capable pad is there at all (sdk/docs/PAD-ANALOG.md).
Every poll is a complete, ACK-paced packet whose length follows the ID it
reports, so a 0x41 digital frame and a 0x73 analog frame decode the same
buttons. A packet that fails part way reports PadMode::Unknown; read
through a PadReader to get the last clean state instead.
Our ButtonState stores active-high so buttons.is_held feels
natural in game code.
Re-exports§
pub use tracker::PadTracker;
Modules§
- button
- Named button bitmasks (active-high in this representation).
Hardware’s active-low wire format is hidden inside
poll_port1. - tracker
- Frame-to-frame button tracking: edges, auto-repeat, handoff suppression.
Structs§
- Action
Binding - One logical game action bound to up to two physical button masks.
- Action
Input - Per-tick logical action view over a pair of pad samples.
- Action
Map - Fixed, allocation-free mapping from game-defined action indices to pads.
- Analog
Sticks - Raw DualShock stick bytes from one poll.
- Button
State - Result of one pad poll.
bits()gives the raw active-high mask;ButtonState::is_heldis the ergonomic per-button check. - Deadzone
- A radial dead region around stick centre, with the scaled response the literature settles on.
- PadReader
- A port reader that never hands a garbled packet to the game.
- PadState
- Result of one controller poll.
- RawPoll
- Raw wire result of one poll, before active-low button inversion. Carries the
per-byte
/ACKobservations so a diagnostic can show whether the controller acknowledges each byte, and which pacing produced a clean handshake.
Enums§
- Analog
Requirement - What a port held after
require_analog_onasked it for analog mode. - Pacing
- How a transaction paces its bytes across the serial link.
- PadMode
- Controller operating mode inferred from the poll ID byte.
- Port
- Which socket a transaction addresses.
Constants§
- DEFAULT_
SETUP_ SPINS - Setup delay (bounded STAT reads) after asserting the select line, before the
first clock. The original SCPH-1200 gives NO response without it and a clean
5A41digital read with it; fast clones tolerate it either way (silicon, 2026-06-22). The on-console sweep put the floor between 384 (no response) and 768 (clean) under a 5-polls-per-frame stress harder than the in-game one poll per frame, so 1024 is a comfortable margin while keeping the per-poll busy-wait small. Production retains this measured setup margin and also waits for each non-final byte’s ACK readiness; the BIOS likewise paces bytes. - STICK_
CENTER - Default analog-stick reading.
0x80= centred,0x00= full negative,0xFF= full positive. - STICK_
FULL - Full deflection on one axis, from a stick that reads 0..=255 about 128.
Functions§
- aim_
curve - Fine-aim response shared by first-person games.
- aim_
curve_ symmetric - Fine-aim curve accepting both -128 and +128, for axes inverted after
centering. Uses the same sequential integer rounding as
aim_curve. - enable_
analog_ on - Ask the controller in
socketto enter DualShock analog mode. Returnstruewhen a follow-up poll reports analog mode. - enable_
analog_ port1 Deprecated - Ask the port-1 controller to enter analog mode.
- enable_
analog_ port2 Deprecated - Ask the port-2 controller to enter analog mode.
- poll_
diagnostics_ on - Poll
socketonce with explicit fixed timing, for hardware diagnostics:setup_spinsof delay after asserting the select line, plusinterbyte_spinsof fixed delay after each byte (bounded STAT reads – no CTRL writes, no/ACKwait, no DSR IRQ). This isolates the two timings a strict original pad (SCPH-1200) might need – setup time after/CS, and an inter-byte gap – without the machinery that corrupted the ack-wait path on silicon. - poll_on
- Poll the controller in
socketonce. - poll_
port1 Deprecated - Poll port 1 once.
- poll_
port2 Deprecated - Poll port 2 once.
- poll_
port1_ diagnostics Deprecated - Poll port 1 once with fixed timing.
- poll_
raw_ on - Poll
socketonce and return the raw wire bytes plus/ACKobservations, using the requestedPacing. Intended for diagnostics that want to show the unfiltered handshake (or reproduce the legacyPacing::NoAckWaitfailure); normal game code should usepoll_on. - require_
analog_ on - Put the controller in
socketin analog mode, lock it there, and report what it settled on. For programs that need sticks and must not let the Analog button flip the pad back to digital mid-session. Call it at boot (a chain-loaded program starts from whatever the previous one left) and again while the answer is notAnalogRequirement::Analog, so a pad plugged in later is picked up. - require_
analog_ port1 Deprecated require_analog_onfor port 1.