Skip to main content
← PSoXide SDK guides and examples · SDK f2fec1c301f1 · All Cargo features enabled; check feature gates before use.

Crate psx_pad

Crate psx_pad 

Source
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:

TXRXMeaning
01FFAddress byte / select controller
4241Poll command / digital pad ID low
005AFill byte / ID high
00b0Buttons group 1 (active-low)
00b1Buttons 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§

ActionBinding
One logical game action bound to up to two physical button masks.
ActionInput
Per-tick logical action view over a pair of pad samples.
ActionMap
Fixed, allocation-free mapping from game-defined action indices to pads.
AnalogSticks
Raw DualShock stick bytes from one poll.
ButtonState
Result of one pad poll. bits() gives the raw active-high mask; ButtonState::is_held is 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 /ACK observations so a diagnostic can show whether the controller acknowledges each byte, and which pacing produced a clean handshake.

Enums§

AnalogRequirement
What a port held after require_analog_on asked 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 5A41 digital 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 socket to enter DualShock analog mode. Returns true when a follow-up poll reports analog mode.
enable_analog_port1Deprecated
Ask the port-1 controller to enter analog mode.
enable_analog_port2Deprecated
Ask the port-2 controller to enter analog mode.
poll_diagnostics_on
Poll socket once with explicit fixed timing, for hardware diagnostics: setup_spins of delay after asserting the select line, plus interbyte_spins of fixed delay after each byte (bounded STAT reads – no CTRL writes, no /ACK wait, 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 socket once.
poll_port1Deprecated
Poll port 1 once.
poll_port2Deprecated
Poll port 2 once.
poll_port1_diagnosticsDeprecated
Poll port 1 once with fixed timing.
poll_raw_on
Poll socket once and return the raw wire bytes plus /ACK observations, using the requested Pacing. Intended for diagnostics that want to show the unfiltered handshake (or reproduce the legacy Pacing::NoAckWait failure); normal game code should use poll_on.
require_analog_on
Put the controller in socket in 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 not AnalogRequirement::Analog, so a pad plugged in later is picked up.
require_analog_port1Deprecated
require_analog_on for port 1.