psx_pad/lib.rs
1// SPDX-License-Identifier: GPL-2.0-or-later
2//! Digital / DualShock pad polling via SIO0.
3//!
4//! Talks directly to the SIO0 hardware (no BIOS syscalls) so the
5//! same code works whether we side-load a homebrew (HLE BIOS) or
6//! boot through the real BIOS. The controller protocol is simple
7//! enough that a hand-rolled select + four-byte exchange beats
8//! opening events and waiting on them.
9//!
10//! Typical use from a game loop. The controller port has one owner, the
11//! `ControllerPort` token from `psx_rt::Peripherals::take()`, which every pad
12//! poll and memory-card transfer borrows for the length of its transaction:
13//!
14//! ```text
15//! let mut port = peripherals.controller_port;
16//! let mut reader = psx_pad::PadReader::port1();
17//! let pad = reader.poll_on(&mut port);
18//! if pad.buttons.is_held(psx_pad::button::START) {
19//! // …
20//! }
21//! ```
22//!
23//! The protocol spec, reproduced from nocash PSX-SPX:
24//!
25//! | `TX` | `RX` | Meaning |
26//! |------|------|------------------------------------|
27//! | `01` | `FF` | Address byte / select controller |
28//! | `42` | `41` | Poll command / digital pad ID low |
29//! | `00` | `5A` | Fill byte / ID high |
30//! | `00` | `b0` | Buttons group 1 (active-low) |
31//! | `00` | `b1` | Buttons group 2 (active-low) |
32//!
33//! DualShock analog mode uses the same first four bytes but reports
34//! ID low `0x73` and appends four stick bytes:
35//! right X/Y, then left X/Y. Fresh DualShocks boot digital, so games
36//! that require sticks should either call [`enable_analog_on`] or
37//! show an "enable analog mode" prompt when [`PadState::is_analog`]
38//! is false. Programs that require a DualShock call
39//! [`require_analog_on`] at boot: it locks the pad in analog mode, so
40//! the Analog button cannot switch it back, and says whether an
41//! analog-capable pad is there at all (`sdk/docs/PAD-ANALOG.md`).
42//!
43//! Every poll is a complete, ACK-paced packet whose length follows the ID it
44//! reports, so a 0x41 digital frame and a 0x73 analog frame decode the same
45//! buttons. A packet that fails part way reports [`PadMode::Unknown`]; read
46//! through a [`PadReader`] to get the last clean state instead.
47//!
48//! Our [`ButtonState`] stores active-high so `buttons.is_held` feels
49//! natural in game code.
50
51#![no_std]
52#![deny(unsafe_op_in_unsafe_fn)]
53#![warn(missing_docs)]
54
55use psx_hw::sio::sio0;
56use psx_io::controller_port::{ExchangeError, Timing, Transport};
57use psx_io::periph::ControllerPort;
58
59pub use psx_io::controller_port::Port;
60
61pub mod tracker;
62pub use tracker::PadTracker;
63
64// Under test the driver talks to a controller model, not the registers.
65#[cfg(test)]
66mod mock_sio;
67#[cfg(test)]
68mod transport_tests;
69
70/// Named button bitmasks (active-high in this representation).
71/// Hardware's active-low wire format is hidden inside [`poll_port1`].
72pub mod button {
73 /// SELECT.
74 pub const SELECT: u16 = 1 << 0;
75 /// Left stick click (DualShock L3).
76 pub const L3: u16 = 1 << 1;
77 /// Right stick click (DualShock R3).
78 pub const R3: u16 = 1 << 2;
79 /// START.
80 pub const START: u16 = 1 << 3;
81 /// D-pad up.
82 pub const UP: u16 = 1 << 4;
83 /// D-pad right.
84 pub const RIGHT: u16 = 1 << 5;
85 /// D-pad down.
86 pub const DOWN: u16 = 1 << 6;
87 /// D-pad left.
88 pub const LEFT: u16 = 1 << 7;
89 /// L2 shoulder.
90 pub const L2: u16 = 1 << 8;
91 /// R2 shoulder.
92 pub const R2: u16 = 1 << 9;
93 /// L1 shoulder.
94 pub const L1: u16 = 1 << 10;
95 /// R1 shoulder.
96 pub const R1: u16 = 1 << 11;
97 /// Triangle face button.
98 pub const TRIANGLE: u16 = 1 << 12;
99 /// Circle face button.
100 pub const CIRCLE: u16 = 1 << 13;
101 /// Cross (X) face button.
102 pub const CROSS: u16 = 1 << 14;
103 /// Square face button.
104 pub const SQUARE: u16 = 1 << 15;
105}
106
107/// Result of one pad poll. `bits()` gives the raw active-high mask;
108/// [`ButtonState::is_held`] is the ergonomic per-button check.
109#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
110pub struct ButtonState(u16);
111
112impl ButtonState {
113 /// Empty -- nothing held.
114 pub const NONE: Self = Self(0);
115
116 /// Construct from an active-high mask.
117 pub const fn from_bits(bits: u16) -> Self {
118 Self(bits)
119 }
120
121 /// Raw bitmask.
122 #[inline]
123 pub const fn bits(self) -> u16 {
124 self.0
125 }
126
127 /// True when any masked button is down now and none was down previously.
128 /// For multi-button masks this is a group edge, not a per-button edge.
129 #[inline]
130 pub const fn pressed_since(self, previous: Self, mask: u16) -> bool {
131 self.is_held(mask) && !previous.is_held(mask)
132 }
133
134 /// `true` when `mask` (any single-bit [`button`] constant or an
135 /// OR of several) is currently pressed.
136 #[inline]
137 pub const fn is_held(self, mask: u16) -> bool {
138 self.0 & mask != 0
139 }
140}
141
142/// Default analog-stick reading. `0x80` = centred, `0x00` = full
143/// negative, `0xFF` = full positive.
144pub const STICK_CENTER: u8 = 0x80;
145
146/// Controller operating mode inferred from the poll ID byte.
147#[derive(Copy, Clone, Debug, PartialEq, Eq)]
148pub enum PadMode {
149 /// No controller answered the poll.
150 Disconnected,
151 /// SCPH-1080 digital-pad shape: ID `0x41`, buttons only.
152 Digital,
153 /// DualShock analog shape: ID `0x73`, buttons plus stick bytes.
154 Analog,
155 /// DualShock config/escape shape: ID `0xF3`.
156 Config,
157 /// A controller answered with an ID this SDK does not classify yet.
158 Unknown,
159}
160
161impl PadMode {
162 /// `true` when a controller answered the poll.
163 #[inline]
164 pub const fn is_connected(self) -> bool {
165 !matches!(self, Self::Disconnected)
166 }
167
168 /// `true` when the controller is currently reporting stick bytes.
169 #[inline]
170 pub const fn has_sticks(self) -> bool {
171 matches!(self, Self::Analog | Self::Config)
172 }
173}
174
175/// Raw DualShock stick bytes from one poll.
176#[derive(Copy, Clone, Debug, PartialEq, Eq)]
177pub struct AnalogSticks {
178 /// Right stick horizontal axis.
179 pub right_x: u8,
180 /// Right stick vertical axis.
181 pub right_y: u8,
182 /// Left stick horizontal axis.
183 pub left_x: u8,
184 /// Left stick vertical axis.
185 pub left_y: u8,
186}
187
188impl AnalogSticks {
189 /// Centred sticks.
190 pub const CENTERED: Self = Self {
191 right_x: STICK_CENTER,
192 right_y: STICK_CENTER,
193 left_x: STICK_CENTER,
194 left_y: STICK_CENTER,
195 };
196
197 /// Left stick as signed deltas from centre.
198 #[inline]
199 pub const fn left_centered(self) -> (i16, i16) {
200 (
201 self.left_x as i16 - STICK_CENTER as i16,
202 self.left_y as i16 - STICK_CENTER as i16,
203 )
204 }
205
206 /// Right stick as signed deltas from centre.
207 #[inline]
208 pub const fn right_centered(self) -> (i16, i16) {
209 (
210 self.right_x as i16 - STICK_CENTER as i16,
211 self.right_y as i16 - STICK_CENTER as i16,
212 )
213 }
214}
215
216/// A radial dead region around stick centre, with the scaled response the
217/// literature settles on.
218///
219/// Three games on the PSoXide demo disc had grown one of these and they did not
220/// agree. hl-psx tests `x*x + y*y` against a squared radius. VoXide gates each
221/// axis separately, which makes its dead region a square: a stick pushed gently
222/// along a diagonal clears the threshold on one axis and not the other, so the
223/// input snaps to a cardinal instead of going where it was pushed. Sutphin's
224/// write-up calls that the axial dead zone and its snapping is the classic
225/// complaint about it.
226///
227/// A stick's centre drift is radial, so the region that ignores it is a circle,
228/// and [`Deadzone::scaled`] is the scaled-radial form: remapped so the first
229/// real reading starts from zero rather than jumping to the boundary magnitude.
230/// That is the recommended default. [`Deadzone::gate`] is the plain radial one,
231/// kept because the games being replaced were tuned against it.
232///
233/// This is for two-axis input. A single-axis control is a different question
234/// and a scalar threshold is right for it: NitroXide steers on the left stick's
235/// X alone, and folding Y into the test there would let noise on an axis it
236/// does not read enable steering. See [`Deadzone::is_outside_axis`].
237#[derive(Copy, Clone, Debug, PartialEq, Eq)]
238pub struct Deadzone {
239 inner_squared: i32,
240 inner: i16,
241 outer: i16,
242}
243
244/// Full deflection on one axis, from a stick that reads 0..=255 about 128.
245pub const STICK_FULL: i16 = 127;
246
247/// Fine-aim response shared by first-person games.
248///
249/// The centre is deliberately gentle while full deflection remains near the
250/// full input range: 45% linear plus 55% cubic. This is the response used by
251/// `hl-psx`, promoted here so other PSoXide games do not grow subtly different
252/// controller curves.
253#[inline]
254pub const fn aim_curve(value: i16) -> i16 {
255 let value = if value < -128 {
256 -128
257 } else if value > 127 {
258 127
259 } else {
260 value
261 };
262 aim_curve_symmetric(value)
263}
264
265/// Fine-aim curve accepting both -128 and +128, for axes inverted after
266/// centering. Uses the same sequential integer rounding as [`aim_curve`].
267#[inline]
268pub const fn aim_curve_symmetric(value: i16) -> i16 {
269 let value = if value < -128 {
270 -128
271 } else if value > 128 {
272 128
273 } else {
274 value
275 };
276 let sign = if value < 0 { -1 } else { 1 };
277 let magnitude = if value < 0 {
278 -(value as i32)
279 } else {
280 value as i32
281 };
282 let cubic = magnitude * magnitude / 128 * magnitude / 128;
283 (sign * ((magnitude * 45 + cubic * 55) / 100)) as i16
284}
285
286impl Deadzone {
287 /// A dead region of `radius` counts around centre. Typical radii are 12 to
288 /// 30 of the 127 counts a healthy stick reaches. A negative radius is no
289 /// dead region (zero): the squared test would otherwise treat it as its
290 /// absolute value while [`scaled`](Self::scaled) divided by a zero length.
291 pub const fn new(radius: i16) -> Self {
292 let radius = if radius < 0 { 0 } else { radius };
293 Self {
294 inner_squared: (radius as i32) * (radius as i32),
295 inner: radius,
296 outer: STICK_FULL,
297 }
298 }
299
300 /// Treat any deflection at or beyond `outer` as full scale.
301 ///
302 /// A worn stick often cannot reach its corners, so without this the player
303 /// simply never gets full input. Sutphin recommends exposing it as a
304 /// setting for anything where magnitude matters.
305 pub const fn with_outer(mut self, outer: i16) -> Self {
306 self.outer = outer;
307 self
308 }
309
310 /// The inner radius this was built with.
311 pub const fn radius(self) -> i16 {
312 self.inner
313 }
314
315 /// Is this reading real input rather than centre drift?
316 ///
317 /// Strictly outside, matching the boundary behaviour of the games this
318 /// replaces.
319 #[inline]
320 pub const fn is_outside(self, x: i16, y: i16) -> bool {
321 let (x, y) = (x as i32, y as i32);
322 x * x + y * y > self.inner_squared
323 }
324
325 /// The reading unchanged if it is real input, or `None` inside the dead
326 /// region. The plain radial form: direction is preserved, but magnitude
327 /// jumps from zero to the boundary the moment it is crossed.
328 #[inline]
329 pub const fn gate(self, x: i16, y: i16) -> Option<(i16, i16)> {
330 if self.is_outside(x, y) {
331 Some((x, y))
332 } else {
333 None
334 }
335 }
336
337 /// Scaled radial: `None` inside the dead region, otherwise the reading with
338 /// its magnitude remapped from the dead edge onto full scale, keeping
339 /// direction.
340 ///
341 /// The result is clamped to [`STICK_FULL`] per axis. Without that a full
342 /// diagonal, which is 179 counts long rather than 127, remaps to 138 on
343 /// each axis and hands the game 9% more than it believes the stick can
344 /// produce.
345 #[inline]
346 pub fn scaled(self, x: i16, y: i16) -> Option<(i16, i16)> {
347 let (fx, fy) = (x as i32, y as i32);
348 let magnitude = psx_math::int32::isqrt_i32(fx * fx + fy * fy);
349 let inner = self.inner as i32;
350 if magnitude <= inner {
351 return None;
352 }
353 let span = (self.outer as i32 - inner).max(1);
354 let scaled = (((magnitude - inner) * STICK_FULL as i32) / span).min(STICK_FULL as i32);
355 Some((
356 ((fx * scaled) / magnitude) as i16,
357 ((fy * scaled) / magnitude) as i16,
358 ))
359 }
360
361 /// The one-axis form, for a control that reads a single axis.
362 #[inline]
363 pub const fn is_outside_axis(self, v: i16) -> bool {
364 let v = v as i32;
365 v * v > self.inner_squared
366 }
367
368 /// The one-axis form of [`Deadzone::scaled`].
369 #[inline]
370 pub const fn scaled_axis(self, v: i16) -> Option<i16> {
371 if !self.is_outside_axis(v) {
372 return None;
373 }
374 let span = if self.outer > self.inner {
375 (self.outer - self.inner) as i32
376 } else {
377 1
378 };
379 let magnitude = if v < 0 { -(v as i32) } else { v as i32 };
380 let scaled = (magnitude - self.inner as i32) * STICK_FULL as i32 / span;
381 let scaled = if scaled > STICK_FULL as i32 {
382 STICK_FULL as i32
383 } else {
384 scaled
385 };
386 Some(if v < 0 { -scaled as i16 } else { scaled as i16 })
387 }
388}
389
390impl Default for AnalogSticks {
391 fn default() -> Self {
392 Self::CENTERED
393 }
394}
395
396/// Result of one controller poll.
397#[derive(Copy, Clone, Debug, PartialEq, Eq)]
398pub struct PadState {
399 /// Active-high button state.
400 pub buttons: ButtonState,
401 /// Inferred controller mode.
402 pub mode: PadMode,
403 /// Stick bytes. Centred when the controller is not reporting sticks.
404 pub sticks: AnalogSticks,
405 /// Raw low ID byte returned by the controller.
406 pub id_low: u8,
407}
408
409impl PadState {
410 /// No controller connected / no response.
411 pub const NONE: Self = Self {
412 buttons: ButtonState::NONE,
413 mode: PadMode::Disconnected,
414 sticks: AnalogSticks::CENTERED,
415 id_low: 0xFF,
416 };
417
418 /// `true` when a controller answered the poll.
419 #[inline]
420 pub const fn is_connected(self) -> bool {
421 self.mode.is_connected()
422 }
423
424 /// `true` when the controller is in DualShock analog mode.
425 #[inline]
426 pub const fn is_analog(self) -> bool {
427 matches!(self.mode, PadMode::Analog)
428 }
429}
430
431/// One logical game action bound to up to two physical button masks.
432///
433/// A mask may contain more than one button; any held bit activates the
434/// binding. Zero means that binding slot is unused.
435#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
436pub struct ActionBinding {
437 /// Primary physical button mask.
438 pub primary: u16,
439 /// Optional secondary physical button mask.
440 pub secondary: u16,
441}
442
443impl ActionBinding {
444 /// An action with no physical button assigned.
445 pub const UNBOUND: Self = Self::new(0, 0);
446
447 /// Bind an action to primary and secondary button masks.
448 pub const fn new(primary: u16, secondary: u16) -> Self {
449 Self { primary, secondary }
450 }
451
452 #[inline]
453 const fn mask(self) -> u16 {
454 self.primary | self.secondary
455 }
456}
457
458/// Fixed, allocation-free mapping from game-defined action indices to pads.
459#[derive(Copy, Clone, Debug, PartialEq, Eq)]
460pub struct ActionMap<const ACTIONS: usize> {
461 bindings: [ActionBinding; ACTIONS],
462}
463
464impl<const ACTIONS: usize> ActionMap<ACTIONS> {
465 /// Construct a map in the same order as the game's action enum.
466 pub const fn new(bindings: [ActionBinding; ACTIONS]) -> Self {
467 Self { bindings }
468 }
469
470 /// Read the complete binding table.
471 pub const fn bindings(&self) -> &[ActionBinding; ACTIONS] {
472 &self.bindings
473 }
474
475 /// Replace one binding. Out-of-range indices are ignored.
476 pub fn set(&mut self, action: usize, binding: ActionBinding) {
477 if let Some(slot) = self.bindings.get_mut(action) {
478 *slot = binding;
479 }
480 }
481
482 /// Read one binding, returning [`ActionBinding::UNBOUND`] out of range.
483 pub const fn binding(&self, action: usize) -> ActionBinding {
484 if action < ACTIONS {
485 self.bindings[action]
486 } else {
487 ActionBinding::UNBOUND
488 }
489 }
490
491 /// Interpret current and previous pad states through this map.
492 pub const fn input<'a>(
493 &'a self,
494 current: PadState,
495 previous: PadState,
496 ) -> ActionInput<'a, ACTIONS> {
497 ActionInput {
498 map: self,
499 current,
500 previous,
501 }
502 }
503}
504
505/// Per-tick logical action view over a pair of pad samples.
506#[derive(Copy, Clone, Debug)]
507pub struct ActionInput<'a, const ACTIONS: usize> {
508 map: &'a ActionMap<ACTIONS>,
509 current: PadState,
510 previous: PadState,
511}
512
513impl<const ACTIONS: usize> ActionInput<'_, ACTIONS> {
514 /// Whether an action is held now.
515 #[inline]
516 pub fn is_held(self, action: usize) -> bool {
517 let mask = self.map.binding(action).mask();
518 mask != 0 && self.current.buttons.is_held(mask)
519 }
520
521 /// Whether an action transitioned from released to pressed this tick.
522 #[inline]
523 pub fn just_pressed(self, action: usize) -> bool {
524 let mask = self.map.binding(action).mask();
525 mask != 0 && self.current.buttons.is_held(mask) && !self.previous.buttons.is_held(mask)
526 }
527
528 /// Whether an action transitioned from pressed to released this tick.
529 #[inline]
530 pub fn just_released(self, action: usize) -> bool {
531 let mask = self.map.binding(action).mask();
532 mask != 0 && !self.current.buttons.is_held(mask) && self.previous.buttons.is_held(mask)
533 }
534}
535
536/// How a transaction paces its bytes across the serial link.
537#[derive(Copy, Clone, Debug, PartialEq, Eq)]
538pub enum Pacing {
539 /// Wait only for the RX FIFO between bytes; never wait for `/ACK`. This is
540 /// the legacy timing: it works with fast (often third-party) controllers and
541 /// in emulation, but desyncs slower original units (e.g. SCPH-1200) that
542 /// pull `/ACK` low later. Exposed so a diagnostic can reproduce the failure
543 /// side-by-side with [`Pacing::AckWait`].
544 NoAckWait,
545 /// Wait for the device's `/ACK` (DSR) pulse after each non-final byte before
546 /// clocking the next one. Matches the BIOS pacing, but corrupted frames on a
547 /// third-party clone pad, so this IRQ-latched path remains diagnostic.
548 /// Production polling observes live ACK without rewriting CTRL between
549 /// bytes; the analog-enable handshake retains its separate fixed timing.
550 /// Kept so on-console diagnostics can compare the legacy pacings.
551 AckWait,
552}
553
554/// Raw wire result of one poll, before active-low button inversion. Carries the
555/// per-byte `/ACK` observations so a diagnostic can show whether the controller
556/// acknowledges each byte, and which pacing produced a clean handshake.
557#[derive(Copy, Clone, Debug, PartialEq, Eq)]
558pub struct RawPoll {
559 /// ID low byte: 0x41 digital, 0x73 analog, 0xF3 config, 0xFF none.
560 pub id_low: u8,
561 /// ID high byte; 0x5A on a valid controller, anything else is a desync.
562 pub id_high: u8,
563 /// First button byte, raw active-low wire value.
564 pub buttons_low: u8,
565 /// Second button byte, raw active-low wire value.
566 pub buttons_high: u8,
567 /// Raw stick bytes (centre 0x80) when the controller reports them.
568 pub sticks: AnalogSticks,
569 /// Mode inferred from the ID bytes (`Unknown` when the 0x5A magic is wrong).
570 pub mode: PadMode,
571 /// Bit `i` set if exchange `i` observed an `/ACK` pulse (bit 0 = select byte).
572 pub ack_seen: u16,
573 /// Number of exchanges performed (select byte + payload bytes).
574 pub exchanges: u8,
575}
576
577impl RawPoll {
578 /// A poll where nothing answered (id_low `0xFF`). Useful as a const seed.
579 pub const NONE: Self = Self::disconnected(0xFF);
580
581 /// A poll where nothing answered.
582 const fn disconnected(id_low: u8) -> Self {
583 Self {
584 id_low,
585 id_high: 0xFF,
586 buttons_low: 0xFF,
587 buttons_high: 0xFF,
588 sticks: AnalogSticks::CENTERED,
589 mode: PadMode::Disconnected,
590 ack_seen: 0,
591 exchanges: 0,
592 }
593 }
594
595 /// `true` when every payload exchange that should have been acknowledged
596 /// was. The select byte (bit 0) and all bytes up to (but not including) the
597 /// final, never-acknowledged byte must each show an `/ACK`.
598 #[inline]
599 pub fn is_fully_acknowledged(self) -> bool {
600 if self.exchanges < 2 {
601 return false;
602 }
603 // The last exchange is the final packet byte; the device does not ACK
604 // it. Every earlier exchange must have been acknowledged.
605 let acked_needed = self.exchanges - 1;
606 let mask = (1u16 << acked_needed) - 1;
607 self.ack_seen & mask == mask
608 }
609
610 /// Convert to the cleaned [`PadState`] used by game code.
611 pub fn to_state(self) -> PadState {
612 PadState {
613 buttons: decode_buttons(self.buttons_low, self.buttons_high),
614 mode: self.mode,
615 sticks: self.sticks,
616 id_low: self.id_low,
617 }
618 }
619}
620
621// The driver talks to the port through `psx_io::controller_port::Transport`: the
622// production poll and the configuration sequence use its select / exchange /
623// finish steps, the diagnostic pacings below use its register accesses. A
624// transaction needs the port for its whole length, so every entry point takes
625// the `ControllerPort` token by `&mut` (a memory-card transfer borrows it the
626// same way), and `PadReader` keeps no hardware state of its own.
627
628// The controller IRQ stays masked in `I_MASK` (the runtime only unmasks VBlank),
629// but arming it in CTRL is what latches `STAT` bit 9 on the `/ACK` edge for the
630// diagnostic ack-wait pacing.
631const CTRL_ACK: u16 = sio0::ctrl::ACK;
632
633const STAT_TX_READY: u32 = sio0::stat::TX_READY;
634const STAT_RX_NOT_EMPTY: u32 = sio0::stat::RX_NOT_EMPTY;
635const STAT_DSR_LEVEL: u32 = sio0::stat::DSR_LEVEL;
636const STAT_IRQ: u32 = sio0::stat::IRQ;
637
638/// Spin budget waiting for the byte shift itself (TX-ready / RX-not-empty).
639const EXCHANGE_WAIT_SPINS: u32 = Timing::PAD.byte_spins;
640/// Spin budget waiting for the `/ACK` pulse. Comfortably exceeds the kernel's
641/// ~100us DSR timeout on hardware and the emulator's ~1k-cycle ACK deadline,
642/// so a genuinely slow original controller is still given time to answer.
643const ACK_WAIT_SPINS: u32 = Timing::PAD.ack_spins;
644/// Setup delay (bounded STAT reads) after asserting the select line, before the
645/// first clock. The original SCPH-1200 gives NO response without it and a clean
646/// `5A41` digital read with it; fast clones tolerate it either way (silicon,
647/// 2026-06-22). The on-console sweep put the floor between 384 (no response) and
648/// 768 (clean) under a 5-polls-per-frame stress harder than the in-game one poll
649/// per frame, so 1024 is a comfortable margin while keeping the per-poll
650/// busy-wait small. Production retains this measured setup margin and also
651/// waits for each non-final byte's ACK readiness; the BIOS likewise paces bytes.
652pub const DEFAULT_SETUP_SPINS: u32 = Timing::PAD.setup_spins;
653
654/// Poll the controller in `socket` once.
655///
656/// The returned [`PadState`] always contains active-high buttons; in
657/// analog mode it also contains the four DualShock stick bytes. A packet
658/// that cannot be completed after bounded retries reports [`PadMode::Unknown`]
659/// rather than synthetic button bytes. A consistently absent device reports
660/// [`PadMode::Disconnected`] after the bounded acquisition attempts.
661///
662/// `port` is the [`ControllerPort`](psx_io::periph::ControllerPort) token; it
663/// stays borrowed for the length of the poll.
664#[doc(alias = "PadRead")]
665pub fn poll_on<T: Transport>(port: &mut T, socket: Port) -> PadState {
666 poll_state(port, socket)
667}
668
669/// Poll port 1 once.
670#[deprecated(note = "use `poll_on` with the `ControllerPort` token")]
671pub fn poll_port1() -> PadState {
672 poll_on(&mut steal_port(), Port::One)
673}
674
675/// Poll port 2 once.
676#[deprecated(note = "use `poll_on` with the `ControllerPort` token")]
677pub fn poll_port2() -> PadState {
678 poll_on(&mut steal_port(), Port::Two)
679}
680
681/// The token for a deprecated forwarder that never took one.
682fn steal_port() -> ControllerPort {
683 // SAFETY: a token is a logic guard, not a memory-safety one (see
684 // `psx_io::periph`), and the old free functions never took one.
685 unsafe { ControllerPort::steal() }
686}
687
688/// Poll `socket` once and return the raw wire bytes plus `/ACK` observations,
689/// using the requested [`Pacing`]. Intended for diagnostics that want to show
690/// the unfiltered handshake (or reproduce the legacy [`Pacing::NoAckWait`]
691/// failure); normal game code should use [`poll_on`].
692pub fn poll_raw_on<T: Transport>(port: &mut T, socket: Port, pacing: Pacing) -> RawPoll {
693 poll_once_raw(port, socket, pacing)
694}
695
696/// Poll `socket` once with explicit fixed timing, for hardware diagnostics:
697/// `setup_spins` of delay after asserting the select line, plus `interbyte_spins`
698/// of fixed delay after each byte (bounded STAT reads -- no CTRL writes, no
699/// `/ACK` wait, no DSR IRQ). This isolates the two timings a strict original pad
700/// (SCPH-1200) might need -- setup time after `/CS`, and an inter-byte gap --
701/// without the machinery that corrupted the ack-wait path on silicon.
702pub fn poll_diagnostics_on<T: Transport>(
703 port: &mut T,
704 socket: Port,
705 setup_spins: u32,
706 interbyte_spins: u32,
707) -> RawPoll {
708 poll_once_timed(port, socket, setup_spins, interbyte_spins)
709}
710
711/// Poll port 1 once with fixed timing.
712#[deprecated(note = "use `poll_diagnostics_on` with the `ControllerPort` token")]
713pub fn poll_port1_diagnostics(setup_spins: u32, interbyte_spins: u32) -> RawPoll {
714 poll_diagnostics_on(&mut steal_port(), Port::One, setup_spins, interbyte_spins)
715}
716
717/// Ask the controller in `socket` to enter DualShock analog mode. Returns
718/// `true` when a follow-up poll reports analog mode.
719///
720/// Digital-only controllers simply keep reporting digital mode, so
721/// callers should still gate analog-only controls on
722/// [`PadState::is_analog`].
723pub fn enable_analog_on<T: Transport>(port: &mut T, socket: Port) -> bool {
724 enable_analog(port, socket)
725}
726
727/// Ask the port-1 controller to enter analog mode.
728#[deprecated(note = "use `enable_analog_on` with the `ControllerPort` token")]
729pub fn enable_analog_port1() -> bool {
730 enable_analog_on(&mut steal_port(), Port::One)
731}
732
733/// Ask the port-2 controller to enter analog mode.
734#[deprecated(note = "use `enable_analog_on` with the `ControllerPort` token")]
735pub fn enable_analog_port2() -> bool {
736 enable_analog_on(&mut steal_port(), Port::Two)
737}
738
739/// What a port held after [`require_analog_on`] asked it for analog mode.
740#[derive(Copy, Clone, Debug, PartialEq, Eq)]
741pub enum AnalogRequirement {
742 /// A DualShock answered in analog mode, now locked there: the Analog
743 /// button no longer switches it back to digital.
744 Analog,
745 /// A controller answered but did not settle in analog mode: an original
746 /// digital pad, or a DualShock that refused or ignored the request.
747 DigitalOnly,
748 /// No controller gave a clean reply.
749 Absent,
750}
751
752impl AnalogRequirement {
753 /// Classify the mode a pad reported after the analog request.
754 pub const fn from_mode(mode: PadMode) -> Self {
755 match mode {
756 PadMode::Analog => Self::Analog,
757 PadMode::Digital | PadMode::Config => Self::DigitalOnly,
758 PadMode::Disconnected | PadMode::Unknown => Self::Absent,
759 }
760 }
761}
762
763/// Put the controller in `socket` in analog mode, lock it there, and report
764/// what it settled on. For programs that need sticks and must not let the
765/// Analog button flip the pad back to digital mid-session. Call it at boot (a
766/// chain-loaded program starts from whatever the previous one left) and
767/// again while the answer is not [`AnalogRequirement::Analog`], so a pad
768/// plugged in later is picked up.
769pub fn require_analog_on<T: Transport>(port: &mut T, socket: Port) -> AnalogRequirement {
770 AnalogRequirement::from_mode(request_analog(port, socket, REQUIRE_ANALOG_GAP_SPINS).mode)
771}
772
773/// [`require_analog_on`] for port 1.
774#[deprecated(note = "use `require_analog_on` with the `ControllerPort` token")]
775pub fn require_analog_port1() -> AnalogRequirement {
776 require_analog_on(&mut steal_port(), Port::One)
777}
778
779/// A port reader that never hands a garbled packet to the game.
780///
781/// A poll that fails validation reports [`PadMode::Unknown`] with every
782/// button released. Taken at face value, a held button would read as released
783/// for that one frame and then as a fresh press on the next, so a held Select
784/// or Cross fires twice. The reader returns the last clean state instead.
785/// A clean report of an empty port is accepted as it is: unplugging a pad
786/// does release its buttons.
787#[derive(Copy, Clone, Debug)]
788pub struct PadReader {
789 socket: Port,
790 last: PadState,
791}
792
793impl PadReader {
794 /// A reader for port 1 that has seen nothing yet.
795 pub const fn port1() -> Self {
796 Self {
797 socket: Port::One,
798 last: PadState::NONE,
799 }
800 }
801
802 /// A reader for port 2 that has seen nothing yet.
803 pub const fn port2() -> Self {
804 Self {
805 socket: Port::Two,
806 last: PadState::NONE,
807 }
808 }
809
810 /// Poll the port once and return the latest clean state.
811 pub fn poll_on<T: Transport>(&mut self, port: &mut T) -> PadState {
812 let polled = poll_state(port, self.socket);
813 self.accept(polled)
814 }
815
816 /// Poll the port once and return the latest clean state.
817 #[deprecated(note = "use `poll_on` with the `ControllerPort` token")]
818 pub fn poll(&mut self) -> PadState {
819 self.poll_on(&mut steal_port())
820 }
821
822 /// Fold one poll result into the reader and return the latest clean
823 /// state. [`PadReader::poll_on`] calls this; it is public so a caller that
824 /// polls some other way gets the same rule.
825 pub fn accept(&mut self, state: PadState) -> PadState {
826 if state.mode != PadMode::Unknown {
827 self.last = state;
828 }
829 self.last
830 }
831
832 /// The latest clean state without polling.
833 pub const fn last(&self) -> PadState {
834 self.last
835 }
836}
837
838/// Poll a port, retrying a garbled response.
839///
840/// On real hardware a poll occasionally desyncs -- a stale byte lingering in
841/// the RX FIFO from the previous transaction, or a slipped /ACK between bytes --
842/// and the ID handshake comes back wrong. A single such frame reads as "every
843/// button released", which makes a *held* button (jump, Start) look like a fresh
844/// press the next frame. So: when a controller answered but the DualShock ID
845/// handshake didn't validate, retry a few times and take the first clean read.
846/// Transport failures are retried as whole transactions. Four address-byte
847/// replies of FF without ACK report Disconnected; a failure at any other
848/// stage reports Unknown instead of accepting partial button bytes.
849fn poll_state<T: Transport>(bus: &mut T, socket: Port) -> PadState {
850 let mut last = PadState::NONE;
851 let mut all_absent = true;
852 let mut tries = 0;
853 while tries < 4 {
854 let s = poll_once(bus, socket).to_state();
855 if matches!(s.mode, PadMode::Digital | PadMode::Analog | PadMode::Config) {
856 return s;
857 }
858 all_absent &= s.mode == PadMode::Disconnected;
859 last = s;
860 tries += 1;
861 }
862 // Only four complete address-byte replies of FF without ACK establish
863 // an absent device. Any other failed stage means an invalid transaction.
864 if !all_absent {
865 last.mode = PadMode::Unknown;
866 }
867 last
868}
869
870fn poll_once_raw<T: Transport>(bus: &mut T, socket: Port, pacing: Pacing) -> RawPoll {
871 // Raise JOYN so the device's state machine starts from idle, then drain
872 // any stale RX byte. Only the ack-wait path arms the DSR IRQ.
873 bus.select_with(socket, matches!(pacing, Pacing::AckWait));
874 bus.drain_receive();
875
876 let mut ack = 0u16;
877 let mut i = 0u8;
878
879 // The select byte and the poll command are never the final byte of any
880 // packet, so they are always `/ACK`-paced under `AckWait`.
881 let _select = ex(bus, socket, pacing, 0x01, false, &mut ack, &mut i);
882 let id_low = ex(bus, socket, pacing, 0x42, false, &mut ack, &mut i);
883 let mut mode = mode_from_id_low(id_low);
884 if !mode.is_connected() {
885 bus.deselect();
886 return RawPoll {
887 exchanges: i,
888 ack_seen: ack,
889 ..RawPoll::disconnected(id_low)
890 };
891 }
892
893 let analog = mode.has_sticks();
894 let id_high = ex(bus, socket, pacing, 0x00, false, &mut ack, &mut i);
895 if id_high != 0x5A {
896 // Garbled handshake -- treat as Unknown and stop after the two
897 // button bytes (we can no longer trust the reported length).
898 mode = PadMode::Unknown;
899 }
900
901 // For a digital pad the second button byte is the final byte (no `/ACK`
902 // follows). For an analog pad the four stick bytes come after it.
903 let read_sticks = analog && mode != PadMode::Unknown;
904 let b0 = ex(bus, socket, pacing, 0x00, false, &mut ack, &mut i);
905 let b1 = ex(bus, socket, pacing, 0x00, !read_sticks, &mut ack, &mut i);
906
907 let sticks = if read_sticks {
908 let right_x = ex(bus, socket, pacing, 0x00, false, &mut ack, &mut i);
909 let right_y = ex(bus, socket, pacing, 0x00, false, &mut ack, &mut i);
910 let left_x = ex(bus, socket, pacing, 0x00, false, &mut ack, &mut i);
911 let left_y = ex(bus, socket, pacing, 0x00, true, &mut ack, &mut i);
912 AnalogSticks {
913 right_x,
914 right_y,
915 left_x,
916 left_y,
917 }
918 } else {
919 AnalogSticks::CENTERED
920 };
921
922 bus.deselect();
923
924 RawPoll {
925 id_low,
926 id_high,
927 buttons_low: b0,
928 buttons_high: b1,
929 sticks,
930 mode,
931 ack_seen: ack,
932 exchanges: i,
933 }
934}
935
936/// Clock one byte of a transaction under the given pacing, recording whether the
937/// device acknowledged it. `is_last` marks the final byte of the packet, which
938/// the device never acknowledges, so it is always sent without an `/ACK` wait.
939#[inline]
940fn ex<T: Transport>(
941 bus: &mut T,
942 socket: Port,
943 pacing: Pacing,
944 tx: u8,
945 is_last: bool,
946 ack_seen: &mut u16,
947 idx: &mut u8,
948) -> u8 {
949 let i = *idx;
950 *idx = idx.wrapping_add(1);
951 match pacing {
952 Pacing::NoAckWait => exchange_nowait(bus, tx),
953 Pacing::AckWait if is_last => exchange_nowait(bus, tx),
954 Pacing::AckWait => {
955 let (byte, acked) = exchange_ack(bus, socket, tx);
956 if acked && i < 16 {
957 *ack_seen |= 1u16 << i;
958 }
959 byte
960 }
961 }
962}
963
964/// The production poll: retain the measured post-select setup delay, then
965/// wait for each non-final byte's live ACK assertion and release. RX-ready
966/// only establishes that the current byte arrived, not that the controller
967/// is ready for the next one. No IRQ enable or CTRL rewrite is needed.
968fn poll_once<T: Transport>(bus: &mut T, socket: Port) -> RawPoll {
969 // A previous aborted transaction may have left ACK asserted: `begin` waits
970 // for it to release, so an old pulse never counts as the new address
971 // byte's ACK.
972 let result = if bus.begin(socket, Timing::PAD) {
973 poll_selected(bus)
974 } else {
975 None
976 };
977 // End the peripheral transaction on every path. On a failed byte, also
978 // reset the deselected UART: deselect alone need not cancel a late RX byte
979 // still in its shifter/FIFO. The next select restores MODE and BAUD before
980 // asserting the port again.
981 bus.finish(result.is_some());
982 result.unwrap_or(RawPoll {
983 mode: PadMode::Unknown,
984 ..RawPoll::NONE
985 })
986}
987
988/// A complete selected-port poll, or no usable packet. The current ID
989/// determines the length; a mode toggle never reuses an earlier length.
990fn poll_selected<T: Transport>(bus: &mut T) -> Option<RawPoll> {
991 match bus.exchange(0x01, false, Timing::PAD) {
992 Ok(_) => {}
993 Err(ExchangeError::AckTimeout { reply: 0xFF }) => return Some(RawPoll::NONE),
994 Err(_) => return None,
995 }
996 let id_low = bus.exchange(0x42, false, Timing::PAD).ok()?;
997 let mode = mode_from_id_low(id_low);
998 if matches!(mode, PadMode::Unknown | PadMode::Disconnected) {
999 return None;
1000 }
1001 let id_high = bus.exchange(0x00, false, Timing::PAD).ok()?;
1002 if id_high != 0x5A {
1003 return None;
1004 }
1005 let analog = mode.has_sticks();
1006 let buttons_low = bus.exchange(0x00, false, Timing::PAD).ok()?;
1007 let buttons_high = bus.exchange(0x00, !analog, Timing::PAD).ok()?;
1008 let sticks = if analog {
1009 AnalogSticks {
1010 right_x: bus.exchange(0x00, false, Timing::PAD).ok()?,
1011 right_y: bus.exchange(0x00, false, Timing::PAD).ok()?,
1012 left_x: bus.exchange(0x00, false, Timing::PAD).ok()?,
1013 left_y: bus.exchange(0x00, true, Timing::PAD).ok()?,
1014 }
1015 } else {
1016 AnalogSticks::CENTERED
1017 };
1018 Some(RawPoll {
1019 id_low,
1020 id_high,
1021 buttons_low,
1022 buttons_high,
1023 sticks,
1024 mode,
1025 ack_seen: if analog { 0xff } else { 0x0f },
1026 exchanges: if analog { 9 } else { 5 },
1027 })
1028}
1029
1030/// Poll with fixed setup and inter-byte delays (no `/ACK` wait). Mirrors
1031/// [`poll_once_raw`]'s byte sequence but paces purely with time. Always inlined
1032/// so each caller above is specialised on its own timing.
1033#[inline(always)]
1034fn poll_once_timed<T: Transport>(
1035 bus: &mut T,
1036 socket: Port,
1037 setup_spins: u32,
1038 interbyte_spins: u32,
1039) -> RawPoll {
1040 bus.select(socket);
1041 // Setup time after asserting /CS, before the first clock -- the strict
1042 // original pad may need this where a fast clone does not.
1043 bus.delay(setup_spins);
1044 bus.drain_receive();
1045
1046 let mut i = 0u8;
1047 let _select = exchange_delayed(bus, 0x01, interbyte_spins);
1048 i += 1;
1049 let id_low = exchange_delayed(bus, 0x42, interbyte_spins);
1050 i += 1;
1051 let mut mode = mode_from_id_low(id_low);
1052 if !mode.is_connected() {
1053 bus.deselect();
1054 return RawPoll {
1055 exchanges: i,
1056 ..RawPoll::disconnected(id_low)
1057 };
1058 }
1059
1060 let analog = mode.has_sticks();
1061 let id_high = exchange_delayed(bus, 0x00, interbyte_spins);
1062 i += 1;
1063 if id_high != 0x5A {
1064 mode = PadMode::Unknown;
1065 }
1066 let read_sticks = analog && mode != PadMode::Unknown;
1067 let b0 = exchange_delayed(bus, 0x00, interbyte_spins);
1068 i += 1;
1069 let b1 = exchange_delayed(bus, 0x00, interbyte_spins);
1070 i += 1;
1071
1072 let sticks = if read_sticks {
1073 let right_x = exchange_delayed(bus, 0x00, interbyte_spins);
1074 let right_y = exchange_delayed(bus, 0x00, interbyte_spins);
1075 let left_x = exchange_delayed(bus, 0x00, interbyte_spins);
1076 let left_y = exchange_delayed(bus, 0x00, interbyte_spins);
1077 i += 4;
1078 AnalogSticks {
1079 right_x,
1080 right_y,
1081 left_x,
1082 left_y,
1083 }
1084 } else {
1085 AnalogSticks::CENTERED
1086 };
1087
1088 bus.deselect();
1089
1090 RawPoll {
1091 id_low,
1092 id_high,
1093 buttons_low: b0,
1094 buttons_high: b1,
1095 sticks,
1096 mode,
1097 ack_seen: 0,
1098 exchanges: i,
1099 }
1100}
1101
1102/// Spins between the configuration commands of [`enable_analog`], a few
1103/// milliseconds each. Sony's libpad spaces configuration commands one per
1104/// video frame; issuing the three back to back left an SCPH-110 (PS one
1105/// DualShock) parked in configuration mode on a console, answering ID 0xF3
1106/// with buttons and sticks intact but never analog (controller-test capture,
1107/// 2026-08-16), while an SCPH-1200 took the same burst fine. Not yet
1108/// re-measured on silicon.
1109const CONFIG_COMMAND_GAP_SPINS: u32 = 8 * DEFAULT_SETUP_SPINS;
1110
1111/// Spins between the configuration commands of [`require_analog_on`]:
1112/// about one video frame, the spacing Sony's libpad uses, so the request
1113/// that a program must win (the SCPH-110 is the pad that needed spacing at
1114/// all) uses the reference pacing rather than the shorter, unmeasured one
1115/// above. A spin costs about 6.7 CPU cycles on silicon (the 2026-07-26
1116/// setup-delay sweep: 192 spins polled in 8587 cycles, 1536 in 17615), so
1117/// this is about 660k cycles, a little over one 60 Hz frame. Only boot-time
1118/// and redetect callers pay it; [`enable_analog_on`] keeps its short gap
1119/// because games call it from their frame loops.
1120const REQUIRE_ANALOG_GAP_SPINS: u32 = 96 * DEFAULT_SETUP_SPINS;
1121
1122/// Exit-config retries when a pad is still answering ID 0xF3 after the
1123/// analog request. Each costs one gap; a pad left in configuration mode
1124/// still reports buttons, but never analog, and ignores the Analog lock.
1125const CONFIG_EXIT_RETRIES: u32 = 3;
1126
1127fn enable_analog<T: Transport>(bus: &mut T, socket: Port) -> bool {
1128 request_analog(bus, socket, CONFIG_COMMAND_GAP_SPINS).is_analog()
1129}
1130
1131/// Send the analog-and-lock request, spacing the commands by `gap` spins,
1132/// and return the state the pad settled on. A pad still parked in
1133/// configuration mode (ID 0xF3, the SCPH-110 failure) is sent the exit
1134/// command again, a bounded number of times, before its state is reported.
1135fn request_analog<T: Transport>(bus: &mut T, socket: Port, gap: u32) -> PadState {
1136 // Enter config mode.
1137 transaction(
1138 bus,
1139 socket,
1140 [0x43, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00],
1141 );
1142 bus.delay(gap);
1143 // Request analog mode and lock it so the pad cannot toggle
1144 // back underneath analog-only game controls.
1145 transaction(
1146 bus,
1147 socket,
1148 [0x44, 0x00, 0x01, 0x03, 0x00, 0x00, 0x00, 0x00],
1149 );
1150 bus.delay(gap);
1151 // Exit config mode, restoring the requested analog mode.
1152 transaction(
1153 bus,
1154 socket,
1155 [0x43, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
1156 );
1157 bus.delay(gap);
1158 let mut state = poll_state(bus, socket);
1159 let mut retries = 0;
1160 while state.mode == PadMode::Config && retries < CONFIG_EXIT_RETRIES {
1161 // The exit did not take: leave the pad in a playable mode rather
1162 // than parked in configuration, then re-read what it settled on.
1163 transaction(
1164 bus,
1165 socket,
1166 [0x43, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
1167 );
1168 bus.delay(gap);
1169 state = poll_state(bus, socket);
1170 retries += 1;
1171 }
1172 state
1173}
1174
1175#[inline]
1176fn mode_from_id_low(id_low: u8) -> PadMode {
1177 match id_low {
1178 0x41 => PadMode::Digital,
1179 0x73 => PadMode::Analog,
1180 0xF3 => PadMode::Config,
1181 0xFF => PadMode::Disconnected,
1182 _ => PadMode::Unknown,
1183 }
1184}
1185
1186#[inline]
1187fn decode_buttons(b0: u8, b1: u8) -> ButtonState {
1188 // Wire bytes are active-low; invert to match our active-high
1189 // ButtonState convention.
1190 ButtonState::from_bits(!((b0 as u16) | ((b1 as u16) << 8)))
1191}
1192
1193/// CTRL value held while a port is selected: JOYN asserted, TX enabled, and
1194/// normal receive supplied implicitly by the active `/CS`,
1195/// (only when `ack_irq`) the DSR (`/ACK`) interrupt armed so STAT bit 9 latches
1196/// each pulse. The default no-wait poll leaves the DSR IRQ off -- enabling it
1197/// turned out to disturb real-hardware transfers (the official SCPH-1200 stopped
1198/// answering, the clone's bytes corrupted), so it is reserved for the opt-in
1199/// ack-wait diagnostic only.
1200#[inline]
1201const fn active_ctrl(socket: Port, ack_irq: bool) -> u16 {
1202 sio0::selected_ctrl(socket.is_two(), ack_irq)
1203}
1204
1205/// Run a fixed eight-byte DualShock command after the port-level controller
1206/// select byte, using the default no-wait timing (matching the reverted poll
1207/// path).
1208#[inline]
1209fn transaction<T: Transport>(bus: &mut T, socket: Port, bytes: [u8; 8]) -> [u8; 8] {
1210 bus.select(socket);
1211 bus.delay(DEFAULT_SETUP_SPINS);
1212 let mut ack = 0u16;
1213 let mut idx = 0u8;
1214 let _select = ex(
1215 bus,
1216 socket,
1217 Pacing::NoAckWait,
1218 0x01,
1219 false,
1220 &mut ack,
1221 &mut idx,
1222 );
1223 let mut out = [0u8; 8];
1224 let mut i = 0;
1225 while i < bytes.len() {
1226 let is_last = i == bytes.len() - 1;
1227 out[i] = ex(
1228 bus,
1229 socket,
1230 Pacing::NoAckWait,
1231 bytes[i],
1232 is_last,
1233 &mut ack,
1234 &mut idx,
1235 );
1236 i += 1;
1237 }
1238 bus.deselect();
1239 out
1240}
1241
1242/// Clock one byte across the serial link without waiting for `/ACK`: wait for
1243/// TX-ready, write the byte, wait for RX to fill, read it. This is the legacy
1244/// timing -- correct for the final byte of a packet (which is never
1245/// acknowledged) and for the [`Pacing::NoAckWait`] diagnostic path.
1246///
1247/// The STAT register layout (from PSX-SPX):
1248/// - bit 0: TX-ready-1 (FIFO can accept a byte)
1249/// - bit 1: RX FIFO not empty
1250/// - bit 7: `/ACK` (DSR) live input level
1251/// - bit 9: latched DSR/ACK interrupt
1252#[inline]
1253fn exchange_nowait<T: Transport>(bus: &mut T, tx: u8) -> u8 {
1254 if !bus.wait_status_set(STAT_TX_READY, EXCHANGE_WAIT_SPINS) {
1255 return 0xFF;
1256 }
1257 bus.transmit(tx);
1258 if !bus.wait_status_set(STAT_RX_NOT_EMPTY, EXCHANGE_WAIT_SPINS) {
1259 return 0xFF;
1260 }
1261 bus.receive()
1262}
1263
1264/// One no-wait byte exchange followed by a fixed inter-byte delay, giving a
1265/// strict pad time to be ready for the next byte without any `/ACK`/CTRL games.
1266#[inline]
1267fn exchange_delayed<T: Transport>(bus: &mut T, tx: u8, interbyte_spins: u32) -> u8 {
1268 let rx = exchange_nowait(bus, tx);
1269 bus.delay(interbyte_spins);
1270 rx
1271}
1272
1273/// Clock one byte, then wait for the device's `/ACK` (DSR) pulse before
1274/// returning so the caller does not clock the next byte until the controller is
1275/// ready. Returns `(rx_byte, ack_observed)`.
1276///
1277/// The wait is non-fatal: on timeout we still return the received byte (the
1278/// byte shift itself already completed), so a device that never `/ACK`s degrades
1279/// to legacy timing rather than dropping the poll entirely.
1280#[inline]
1281fn exchange_ack<T: Transport>(bus: &mut T, socket: Port, tx: u8) -> (u8, bool) {
1282 if !bus.wait_status_set(STAT_TX_READY, EXCHANGE_WAIT_SPINS) {
1283 return (0xFF, false);
1284 }
1285 bus.transmit(tx);
1286 if !bus.wait_status_set(STAT_RX_NOT_EMPTY, EXCHANGE_WAIT_SPINS) {
1287 return (0xFF, false);
1288 }
1289 let rx = bus.receive();
1290 // Wait for the latched `/ACK` interrupt (STAT bit 9). The latch cannot be
1291 // missed, unlike the brief live level; the controller IRQ is masked in
1292 // `I_MASK`, so this never reaches the CPU.
1293 let acked = bus.wait_status_set(STAT_IRQ, ACK_WAIT_SPINS);
1294 if acked {
1295 // SIO0 STAT.9 is not edge-triggered: it can only be cleared once the
1296 // live `/ACK` line has released (STAT.7 low). Wait for that, then
1297 // pulse CTRL.ACK while keeping JOYN asserted so the device stays
1298 // selected for the next byte.
1299 let _ = bus.wait_status_clear(STAT_DSR_LEVEL, ACK_WAIT_SPINS);
1300 bus.set_control(active_ctrl(socket, true) | CTRL_ACK);
1301 }
1302 (rx, acked)
1303}
1304
1305#[cfg(test)]
1306mod tests {
1307
1308 #[test]
1309 fn symmetric_aim_and_group_edges_preserve_axis_policy() {
1310 for v in -128..=128 {
1311 assert_eq!(aim_curve_symmetric(v), -aim_curve_symmetric(-v));
1312 }
1313 assert_eq!(aim_curve_symmetric(128), 128);
1314 assert_eq!(aim_curve(128), 125);
1315 let a = ButtonState::from_bits(1);
1316 let b = ButtonState::from_bits(2);
1317 assert!(a.pressed_since(ButtonState::NONE, 3));
1318 assert!(!a.pressed_since(b, 3));
1319 assert!(a.pressed_since(b, 1));
1320 }
1321
1322 #[test]
1323 fn a_radial_deadzone_is_a_circle_not_a_square() {
1324 // The whole reason this exists. A per-axis threshold of 24, which is
1325 // what NitroXide used, passes (20, 20) because neither axis clears it
1326 // on its own -- so a gentle diagonal push reads as nothing, and the
1327 // moment one axis does clear it the input snaps to a cardinal. Radially
1328 // (20, 20) is 28.3 counts out and is real input.
1329 let dz = Deadzone::new(24);
1330 assert!(dz.is_outside(20, 20), "diagonal push must count as input");
1331 assert!(
1332 !dz.is_outside(20, 0),
1333 "same magnitude per axis is still drift"
1334 );
1335 assert!(!dz.is_outside(0, 0));
1336 assert!(dz.is_outside(25, 0));
1337 }
1338
1339 #[test]
1340 fn a_negative_deadzone_radius_is_no_deadzone_not_a_division_by_zero() {
1341 // `scaled(0, 0)` passed the `magnitude <= inner` test with inner = -1
1342 // and divided by a zero magnitude.
1343 for radius in [-1, -24, i16::MIN] {
1344 let dz = Deadzone::new(radius);
1345 assert_eq!(dz.radius(), 0);
1346 assert_eq!(dz, Deadzone::new(0));
1347 assert_eq!(dz.scaled(0, 0), None);
1348 assert_eq!(dz.scaled_axis(0), None);
1349 assert!(!dz.is_outside(0, 0));
1350 // A real deflection behaves as it does with a radius of zero.
1351 assert_eq!(dz.scaled(60, 0), Deadzone::new(0).scaled(60, 0));
1352 assert!(dz.is_outside(1, 0));
1353 }
1354 }
1355
1356 #[test]
1357 fn the_one_axis_form_ignores_the_other_axis() {
1358 // NitroXide's steering: Y is not read, so it must not gate X.
1359 let dz = Deadzone::new(24);
1360 assert!(dz.is_outside_axis(30));
1361 assert!(!dz.is_outside_axis(20));
1362 // Rescaled onto full span like the two-axis form, not merely offset:
1363 // (30 - 24) of the 103 counts left maps to 7 of 127.
1364 assert_eq!(dz.scaled_axis(30), Some(7), "starts from zero, not 30");
1365 assert_eq!(dz.scaled_axis(-30), Some(-7), "sign survives");
1366 assert_eq!(dz.scaled_axis(127), Some(STICK_FULL), "full stays full");
1367 assert_eq!(dz.scaled_axis(10), None);
1368 }
1369
1370 #[test]
1371 fn the_gate_hands_back_the_reading_unchanged() {
1372 let dz = Deadzone::new(16);
1373 assert_eq!(dz.gate(4, 4), None);
1374 assert_eq!(dz.gate(100, -40), Some((100, -40)));
1375 }
1376
1377 #[test]
1378 fn scaling_starts_real_input_from_zero_instead_of_the_boundary() {
1379 // Ungated, a reading one count outside a radius of 30 arrives at
1380 // magnitude 31: a lurch. Scaled, it starts near zero and keeps its
1381 // direction.
1382 let dz = Deadzone::new(30);
1383 let (x, y) = dz.scaled(31, 0).expect("just outside is real input");
1384 assert!(x < 5, "expected a small first step, got {x}");
1385 assert_eq!(dz.scaled(127, 0), Some((STICK_FULL, 0)), "full stays full");
1386 assert_eq!(y, 0, "direction must survive the rescale");
1387 assert_eq!(dz.scaled(10, 10), None);
1388 }
1389
1390 #[test]
1391 fn a_full_diagonal_does_not_exceed_full_scale() {
1392 // A diagonal is 179 counts long, not 127. Remapping that onto full
1393 // scale without a clamp hands the game 138 on each axis and it has no
1394 // reason to expect anything over 127.
1395 let dz = Deadzone::new(30);
1396 let (x, y) = dz.scaled(127, 127).expect("full deflection is input");
1397 assert!(x <= STICK_FULL && y <= STICK_FULL, "got ({x}, {y})");
1398 assert_eq!(x, y, "a diagonal must stay a diagonal");
1399 }
1400
1401 #[test]
1402 fn an_outer_zone_lets_a_worn_stick_reach_full_scale() {
1403 // A stick that only manages 100 counts never gives full input without
1404 // this, which is the tip Sutphin gives for anything where magnitude
1405 // matters.
1406 let worn = Deadzone::new(20).with_outer(100);
1407 assert_eq!(worn.scaled(100, 0), Some((STICK_FULL, 0)));
1408 // And past the outer edge it saturates rather than overshooting.
1409 assert_eq!(worn.scaled(120, 0), Some((STICK_FULL, 0)));
1410 }
1411
1412 #[test]
1413 fn aim_curve_is_gentle_at_center_and_near_full_at_the_edge() {
1414 assert_eq!(aim_curve(0), 0);
1415 assert_eq!(aim_curve(16), 7);
1416 assert_eq!(aim_curve(-16), -7);
1417 assert_eq!(aim_curve(127), 125);
1418 assert_eq!(aim_curve(-128), -128);
1419 assert!(aim_curve(64) < 64);
1420 assert_eq!(aim_curve(-64), -aim_curve(64));
1421 }
1422
1423 use super::*;
1424
1425 #[test]
1426 fn mode_ids_match_dualshock_wire_values() {
1427 assert_eq!(mode_from_id_low(0x41), PadMode::Digital);
1428 assert_eq!(mode_from_id_low(0x73), PadMode::Analog);
1429 assert_eq!(mode_from_id_low(0xF3), PadMode::Config);
1430 assert_eq!(mode_from_id_low(0xFF), PadMode::Disconnected);
1431 assert_eq!(mode_from_id_low(0x12), PadMode::Unknown);
1432 }
1433
1434 #[test]
1435 fn decode_buttons_converts_active_low_to_active_high() {
1436 let state = decode_buttons(0xEF, 0xBF);
1437 assert!(state.is_held(button::UP));
1438 assert!(state.is_held(button::CROSS));
1439 assert!(!state.is_held(button::DOWN));
1440 }
1441
1442 #[test]
1443 fn centred_stick_helpers_return_zero() {
1444 assert_eq!(AnalogSticks::CENTERED.left_centered(), (0, 0));
1445 assert_eq!(AnalogSticks::CENTERED.right_centered(), (0, 0));
1446 }
1447
1448 #[test]
1449 fn raw_poll_to_state_inverts_buttons() {
1450 let raw = RawPoll {
1451 id_low: 0x41,
1452 id_high: 0x5A,
1453 buttons_low: 0xEF, // UP pressed (active-low)
1454 buttons_high: 0xBF, // CROSS pressed (active-low)
1455 sticks: AnalogSticks::CENTERED,
1456 mode: PadMode::Digital,
1457 ack_seen: 0,
1458 exchanges: 5,
1459 };
1460 let state = raw.to_state();
1461 assert!(state.buttons.is_held(button::UP));
1462 assert!(state.buttons.is_held(button::CROSS));
1463 assert_eq!(state.mode, PadMode::Digital);
1464 assert_eq!(state.id_low, 0x41);
1465 }
1466
1467 #[test]
1468 fn ack_complete_requires_every_non_final_byte_acked() {
1469 // Digital poll: 5 exchanges (select, cmd, id_hi, b0, b1). The first four
1470 // must ACK; the final b1 does not.
1471 let mut raw = RawPoll {
1472 id_low: 0x41,
1473 id_high: 0x5A,
1474 buttons_low: 0xFF,
1475 buttons_high: 0xFF,
1476 sticks: AnalogSticks::CENTERED,
1477 mode: PadMode::Digital,
1478 ack_seen: 0b0_1111, // exchanges 0..=3 acked
1479 exchanges: 5,
1480 };
1481 assert!(raw.is_fully_acknowledged());
1482
1483 // Drop one ACK in the middle -> incomplete handshake.
1484 raw.ack_seen = 0b0_1011;
1485 assert!(!raw.is_fully_acknowledged());
1486
1487 // A no-ack poll (legacy / slow original under NoAckWait) is incomplete.
1488 raw.ack_seen = 0;
1489 assert!(!raw.is_fully_acknowledged());
1490 }
1491
1492 #[test]
1493 fn pacing_round_trips() {
1494 assert_ne!(Pacing::NoAckWait, Pacing::AckWait);
1495 }
1496
1497 #[test]
1498 fn action_map_unifies_primary_secondary_and_edges() {
1499 let map = ActionMap::new([
1500 ActionBinding::new(button::CROSS, button::CIRCLE),
1501 ActionBinding::new(button::START, 0),
1502 ]);
1503 let previous = PadState {
1504 buttons: ButtonState::from_bits(button::CROSS),
1505 mode: PadMode::Digital,
1506 sticks: AnalogSticks::CENTERED,
1507 id_low: 0x41,
1508 };
1509 let current = PadState {
1510 buttons: ButtonState::from_bits(button::CIRCLE | button::START),
1511 ..previous
1512 };
1513 let input = map.input(current, previous);
1514 assert!(input.is_held(0));
1515 assert!(!input.just_pressed(0), "the logical action stayed held");
1516 assert!(input.just_pressed(1));
1517 assert!(!input.just_released(0));
1518 assert!(!input.is_held(99));
1519 }
1520
1521 fn state(mode: PadMode, buttons: u16) -> PadState {
1522 PadState {
1523 buttons: ButtonState::from_bits(buttons),
1524 mode,
1525 sticks: AnalogSticks::CENTERED,
1526 id_low: match mode {
1527 PadMode::Digital => 0x41,
1528 PadMode::Analog => 0x73,
1529 PadMode::Config => 0xF3,
1530 _ => 0xFF,
1531 },
1532 }
1533 }
1534
1535 #[test]
1536 fn analog_requirement_classifies_every_mode() {
1537 use AnalogRequirement as R;
1538 assert_eq!(R::from_mode(PadMode::Analog), R::Analog);
1539 assert_eq!(R::from_mode(PadMode::Digital), R::DigitalOnly);
1540 assert_eq!(R::from_mode(PadMode::Config), R::DigitalOnly);
1541 assert_eq!(R::from_mode(PadMode::Disconnected), R::Absent);
1542 assert_eq!(R::from_mode(PadMode::Unknown), R::Absent);
1543 }
1544
1545 #[test]
1546 fn analog_and_digital_frames_decode_the_same_buttons() {
1547 let digital = RawPoll {
1548 id_low: 0x41,
1549 id_high: 0x5A,
1550 buttons_low: 0xFE, // SELECT (active-low)
1551 buttons_high: 0xBF, // CROSS
1552 sticks: AnalogSticks::CENTERED,
1553 mode: PadMode::Digital,
1554 ack_seen: 0x0f,
1555 exchanges: 5,
1556 };
1557 let sticks = AnalogSticks {
1558 right_x: 0x80,
1559 right_y: 0x80,
1560 left_x: 0x00,
1561 left_y: 0xFF,
1562 };
1563 let analog = RawPoll {
1564 id_low: 0x73,
1565 sticks,
1566 mode: PadMode::Analog,
1567 ack_seen: 0xff,
1568 exchanges: 9,
1569 ..digital
1570 };
1571 let (d, a) = (digital.to_state(), analog.to_state());
1572 assert_eq!(d.buttons, a.buttons);
1573 assert_eq!(a.buttons.bits(), button::SELECT | button::CROSS);
1574 assert!(a.is_analog() && !d.is_analog());
1575 assert_eq!(a.sticks, sticks);
1576 assert_eq!(d.sticks, AnalogSticks::CENTERED);
1577 }
1578
1579 #[test]
1580 fn a_packet_slipped_by_one_byte_reads_as_select_and_both_directions() {
1581 // Why every packet must be complete and paced: an analog packet that
1582 // slips one byte delivers the ID's 0x5A as the first button byte.
1583 // That decodes as Select, R3, Left and Right held together, the
1584 // phantom-Select pattern seen on a console in analog mode.
1585 let slipped = decode_buttons(0x5A, 0xFF);
1586 assert_eq!(
1587 slipped.bits(),
1588 button::SELECT | button::R3 | button::LEFT | button::RIGHT
1589 );
1590 }
1591
1592 #[test]
1593 fn reader_keeps_the_last_clean_state_across_a_garbled_poll() {
1594 let mut reader = PadReader::port1();
1595 let mut tracker = PadTracker::new();
1596 let held = state(PadMode::Analog, button::SELECT);
1597 tracker.update(reader.accept(held).buttons.bits());
1598 assert!(tracker.just_pressed(button::SELECT));
1599 // The failed packet must neither release nor re-press Select.
1600 let garbled = PadState {
1601 mode: PadMode::Unknown,
1602 ..PadState::NONE
1603 };
1604 let seen = reader.accept(garbled);
1605 assert_eq!(seen, held);
1606 tracker.update(seen.buttons.bits());
1607 assert!(!tracker.just_released(button::SELECT));
1608 tracker.update(reader.accept(held).buttons.bits());
1609 assert!(!tracker.just_pressed(button::SELECT), "no second press");
1610 }
1611
1612 #[test]
1613 fn reader_accepts_every_clean_mode_including_an_empty_port() {
1614 let mut reader = PadReader::port1();
1615 assert_eq!(reader.last(), PadState::NONE);
1616 for clean in [
1617 state(PadMode::Digital, button::CROSS),
1618 state(PadMode::Analog, button::START),
1619 state(PadMode::Config, 0),
1620 PadState::NONE,
1621 ] {
1622 assert_eq!(reader.accept(clean), clean);
1623 }
1624 assert_eq!(reader.last(), PadState::NONE, "unplugging releases");
1625 }
1626}