feat(serial): support disconnected Joybus configuration and local registers

This commit is contained in:
0xrsydn 2026-10-07 06:36:54 +07:00
commit 7546d17630
15 changed files with 462 additions and 62 deletions

View file

@ -8,8 +8,8 @@ use crate::audio::Audio;
pub use crate::audio::{FIFO_A, FIFO_B, SOUNDBIAS, SOUNDCNT_H, SOUNDCNT_X, SOUND_START, WAVE_RAM};
pub(crate) use serial::Serial;
pub use serial::{
JOYCNT, JOY_RECV, JOY_TRANS, RCNT, SIOCNT, SIODATA32, SIODATA8, SIOMLT_SEND, SIOMULTI0,
SIOMULTI1, SIOMULTI2, SIOMULTI3,
JOYCNT, JOYSTAT, JOY_RECV, JOY_TRANS, RCNT, SIOCNT, SIODATA32, SIODATA8, SIOMLT_SEND,
SIOMULTI0, SIOMULTI1, SIOMULTI2, SIOMULTI3,
};
mod timer_step;
pub(crate) use timer_step::TimerStep;

View file

@ -1,4 +1,4 @@
//! Disconnected serial: normal shifting and idle multiplayer child configuration.
//! Disconnected serial: normal shifting, idle multiplayer, and local Joybus registers.
use super::replace_byte;
pub const SIODATA32: u32 = 0x0400_0120;
@ -13,6 +13,7 @@ pub const RCNT: u32 = 0x0400_0134;
pub const JOYCNT: u32 = 0x0400_0140;
pub const JOY_RECV: u32 = 0x0400_0150;
pub const JOY_TRANS: u32 = 0x0400_0154;
pub const JOYSTAT: u32 = 0x0400_0158;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Serial {
@ -21,6 +22,10 @@ pub(crate) struct Serial {
send: u16, // Shared low byte; high byte is accessible only in multiplayer format.
control: u16,
rcnt: u16,
joy_control: u8, // IRQ enable only; no external master can set event flags.
joy_receive: u32,
joy_transmit: u32,
joy_status: u8, // Software flags and transmit pending; no remote reception.
bits_left: u8,
until_bit: u32,
}
@ -30,28 +35,28 @@ impl Serial {
matches!(address,
SIODATA32..=0x0400_012b |
RCNT..=0x0400_0135 | JOYCNT..=0x0400_0141 |
JOY_RECV..=0x0400_0157)
JOY_RECV..=0x0400_0159)
}
pub(crate) fn unsupported(address: u32, value: u8) -> Option<&'static str> {
match address {
0x0400_0129 if value & 0x30 == 0x30 => Some("UART serial mode"),
0x0400_0135 if value & 0xc0 == 0xc0 => Some("Joybus serial mode"),
0x0400_0135 if value & 0x81 == 0x81 => Some("GPIO serial interrupt enable"),
JOYCNT if value & 0x40 != 0 => Some("Joybus interrupt enable"),
JOY_RECV..=0x0400_0157 if value != 0 => Some("Joybus data access beyond reset"),
0x0400_0135 if value & 0xc1 == 0x81 => Some("GPIO serial interrupt enable"),
_ => None,
}
}
fn gpio(&self) -> bool {
self.rcnt & 0x8000 != 0
self.rcnt & 0xc000 == 0x8000
}
fn joybus(&self) -> bool {
self.rcnt & 0xc000 == 0xc000
}
fn multiplayer_format(&self) -> bool {
self.control & 0x3000 == 0x2000
}
fn normal(&self) -> bool {
!self.gpio() && !self.multiplayer_format()
self.rcnt & 0x8000 == 0 && !self.multiplayer_format()
}
fn busy(&self) -> bool {
self.control & 0x80 != 0
@ -137,7 +142,14 @@ impl Serial {
.multiplayer_format()
.then(|| self.multi_extra.to_le_bytes()[(address - SIOMULTI2) as usize]);
}
for (base, data) in [(JOY_RECV, self.joy_receive), (JOY_TRANS, self.joy_transmit)] {
if (base..base + 4).contains(&address) {
// No master can set receive status, so its read-clear bit is already zero.
return Some(data.to_le_bytes()[(address - base) as usize]);
}
}
let value = match address & !1 {
SIOCNT if self.joybus() => self.control, // Unused; do not invent pin samples.
SIOCNT => {
let pins = if self.gpio() { self.gpio_pins() } else { 15 };
// SI is pulled high without a cable. Multiplayer drives SD high while idle.
@ -163,7 +175,8 @@ impl Serial {
}
(self.rcnt & !15) | self.gpio_pins()
}
JOYCNT | JOY_RECV..=0x0400_0156 => 0,
JOYCNT => u16::from(self.joy_control),
JOYSTAT => u16::from(self.joy_status),
_ => return None,
};
Some(value.to_le_bytes()[(address & 1) as usize])
@ -198,8 +211,22 @@ impl Serial {
let shift = (at - SIOMULTI2) * 8;
self.multi_extra =
(self.multi_extra & !(255 << shift)) | (u32::from(value) << shift);
} else if (JOY_RECV..JOY_TRANS).contains(&at) {
// GBATEK describes R/W data latches. CPU writes are not remote events.
let shift = (at - JOY_RECV) * 8;
self.joy_receive =
(self.joy_receive & !(255 << shift)) | (u32::from(value) << shift);
} else if (JOY_TRANS..JOYSTAT).contains(&at) {
let shift = (at - JOY_TRANS) * 8;
self.joy_transmit =
(self.joy_transmit & !(255 << shift)) | (u32::from(value) << shift);
self.joy_status |= 2; // Data available, not transmission complete.
} else {
match at & !1 {
JOYCNT if at & 1 == 0 => self.joy_control = value & 0x40,
JOYSTAT if at & 1 == 0 => {
self.joy_status = (self.joy_status & 2) | (value & 0x30);
}
SIODATA8 if self.multiplayer_format() || at & 1 == 0 => {
self.send = replace_byte(self.send, at, value);
}

View file

@ -15,6 +15,8 @@ mod iwram_bus;
#[cfg(test)]
mod iwram_history_tests;
#[cfg(test)]
mod joybus_step_tests;
#[cfg(test)]
mod multiplayer_step_tests;
#[cfg(test)]
mod prefetch_tests;

View file

@ -0,0 +1,91 @@
//! Original Joybus transaction, CPU access, and preflight regressions.
use super::{timing_event_tests::prepared, *};
use crate::{
io::{IF, JOYCNT, JOYSTAT, JOY_RECV, JOY_TRANS, RCNT, SIOCNT},
machine::Machine,
};
#[test]
fn arm_and_thumb_transmit_stores_commit_pending_status_once() {
for thumb in [false, true] {
let (cpu, mut memory) = prepared(
thumb,
&[if thumb { 0x6008 } else { 0xe5810000 }], // STR r0,[r1]
&[(0, 0x12345678), (1, JOY_TRANS)],
);
memory.write16(RCNT, 0xc000).unwrap();
memory.write16(JOYCNT, 0x40).unwrap();
let mut machine = Machine::new(cpu, memory);
machine.step().unwrap();
assert_eq!(machine.memory().read32(JOY_TRANS).unwrap(), 0x12345678);
assert_eq!(machine.memory().read16(JOYSTAT).unwrap(), 2);
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
assert_eq!(machine.memory().io.serial.next_event_cycles(), None);
}
}
#[test]
fn joybus_mode_selection_observes_the_normal_completion_bus_phase() {
for elapsed in [56, 57] {
let (cpu, mut memory) = prepared(false, &[0xe5c10000], &[(0, 0xc1), (1, RCNT + 1)]);
memory.write16(SIOCNT, 0x4083).unwrap();
memory.advance_cycles(elapsed);
let serial = memory.io.serial;
let mut machine = Machine::new(cpu, memory);
let cpu = machine.cpu().clone();
if elapsed == 57 {
machine.step().unwrap();
assert_eq!(machine.cycles(), 64);
assert_eq!(machine.memory().read8(RCNT + 1).unwrap(), 0xc1);
assert_eq!(machine.memory().read16(IF).unwrap(), 0x80);
assert_eq!(machine.memory().io.serial.next_event_cycles(), None);
} else {
let error = machine.step().unwrap_err();
assert!(error.to_string().contains("serial reconfiguration"));
assert_eq!(machine.step(), Err(error));
assert_eq!(machine.cpu(), &cpu);
assert_eq!(machine.cycles(), 56);
assert_eq!(machine.memory().io.serial, serial);
}
}
}
#[test]
fn later_block_padding_failure_discards_data_pending_status_and_writeback() {
let (cpu, mut memory) = prepared(
false,
&[0xe8a0000e], // STMIA r0!,{r1-r3}; final word hits JOYSTAT padding.
&[(0, JOY_RECV), (1, 0x12345678), (2, 0xabcdef90), (3, 0x30)],
);
memory.write16(RCNT, 0xc000).unwrap();
let serial = memory.io.serial;
let mut machine = Machine::new(cpu, memory);
let cpu = machine.cpu().clone();
let error = machine.step().unwrap_err();
assert!(error.to_string().contains("0x0400015a"));
assert_eq!(machine.step(), Err(error));
assert_eq!(machine.cpu(), &cpu);
assert_eq!(machine.cycles(), 0);
assert_eq!(machine.memory().io.serial, serial);
}
#[test]
fn staged_reads_see_transmit_pending_before_commit_and_discard_restores_every_lane() {
let (_, mut memory) = prepared(false, &[0xe1a00000], &[]);
let serial = memory.io.serial;
memory.begin_timer_step();
memory.write16(RCNT, 0xc100).unwrap();
memory.write16(JOYCNT, 0x40).unwrap();
memory.write32(JOY_TRANS, 0x12345678).unwrap();
memory.write32(JOY_RECV, 0xabcdef90).unwrap();
memory.write16(JOYSTAT, 0x30).unwrap();
assert_eq!(memory.read32(JOY_TRANS).unwrap(), 0x12345678);
assert_eq!(memory.read32(JOY_RECV).unwrap(), 0xabcdef90);
assert_eq!(memory.read16(JOYSTAT).unwrap(), 0x32);
assert_eq!(memory.read16(JOYCNT).unwrap(), 0x40);
assert_eq!(memory.read8(RCNT), Err(MemoryError::Unmapped(RCNT)));
assert_eq!(memory.io.serial, serial);
memory.discard_timer_step();
assert_eq!(memory.io.serial, serial);
assert_eq!(memory.read16(JOYSTAT).unwrap(), 0);
}