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

@ -288,6 +288,37 @@ fn serial_sound_reset_flags_are_selective_and_all_devices_return() {
}
}
#[test]
fn serial_reset_clears_joybus_data_and_irq_enable_without_fabricating_a_master() {
use gba_core::io::{JOYCNT, JOYSTAT, JOY_RECV, JOY_TRANS, RCNT};
for thumb in [false, true] {
for flags in [0, 0x20, 0xff] {
let (mut m, pc) = prepare(flags, thumb, 0x1f);
m.memory_mut().write16(RCNT, 0xc100).unwrap();
m.memory_mut().write16(JOYCNT, 0x40).unwrap();
m.memory_mut().write32(JOY_RECV, MARKER).unwrap();
m.memory_mut().write32(JOY_TRANS, MARKER).unwrap();
m.memory_mut().write16(JOYSTAT, 0x30).unwrap();
complete(&mut m, pc);
let selected = flags & 0x20 != 0;
for address in [JOY_RECV, JOY_TRANS] {
assert_eq!(
m.memory().read32(address).unwrap(),
if selected { 0 } else { MARKER }
);
}
assert_eq!(
m.memory().read16(JOYCNT).unwrap(),
if selected { 0 } else { 0x40 }
);
// The replacement writes zero to TRANS, but does not write JOYSTAT.
// Pending data is not a completed transfer and cannot request an IRQ.
assert_eq!(m.memory().read16(JOYSTAT).unwrap(), 0x32);
assert_eq!(m.memory().read16(IF).unwrap() & 0x80, 0);
}
}
}
#[test]
fn serial_reset_cancels_an_external_clock_request_only_when_selected() {
use gba_core::io::{RCNT, SIOCNT};

View file

@ -2,10 +2,7 @@
use gba_core::{
cpu::Cpu,
dma::DMA_BASE,
io::{
JOYCNT, JOY_RECV, RCNT, SIOCNT, SIODATA32, SIODATA8, SOUNDBIAS, SOUNDCNT_H, SOUNDCNT_X,
WAVE_RAM,
},
io::{JOYCNT, RCNT, SIOCNT, SIODATA32, SIODATA8, SOUNDBIAS, SOUNDCNT_H, SOUNDCNT_X, WAVE_RAM},
machine::Machine,
memory::{Memory, MemoryError, ROM_START},
};
@ -81,10 +78,7 @@ fn unsupported_operations_have_specific_retryable_diagnostics() {
let mut bus = memory();
for (address, value, description) in [
(SIOCNT + 1, 0x30, "UART serial mode"),
(RCNT + 1, 0xc0, "Joybus serial mode"),
(RCNT + 1, 0x81, "GPIO serial interrupt"),
(JOYCNT, 0x40, "Joybus interrupt"),
(JOY_RECV, 1, "Joybus data access"),
] {
let error = bus.write8(address, value).unwrap_err();
assert!(matches!(error, MemoryError::UnsupportedIo { .. }));
@ -92,7 +86,7 @@ fn unsupported_operations_have_specific_retryable_diagnostics() {
assert_eq!(bus.write8(address, value), Err(error));
}
bus.write16(RCNT, 0x8055).unwrap();
assert!(bus.write16(RCNT, 0xc0aa).is_err());
assert!(bus.write16(RCNT, 0x81aa).is_err());
assert_eq!(bus.read16(RCNT).unwrap(), 0x805f); // Low byte was not committed.
bus.write16(SIOCNT, 0x4008).unwrap();
assert!(bus.write32(SIOCNT, 0xaabb3080).is_err());

View file

@ -14,7 +14,7 @@ fn memory() -> Memory {
#[test]
fn rcnt_high_byte_selection_is_independent_of_previous_mode_and_low_byte() {
let mut bus = memory();
for previous in [0x8000, 0x80f5, 0x0100, 0x4100] {
for previous in [0x8000, 0x80f5, 0x0100, 0x4100, 0xc155] {
for low in [0, 0x55, 0xff] {
for high in 0..=u8::MAX {
for halfword in [false, true] {
@ -28,25 +28,21 @@ fn rcnt_high_byte_selection_is_independent_of_previous_mode_and_low_byte() {
// Decode individual mode bits, separately from the implementation masks.
let gpio_or_joybus = high & 0x80 != 0;
let joybus = gpio_or_joybus && high & 0x40 != 0;
let gpio_irq = gpio_or_joybus && high & 1 != 0;
if joybus || gpio_irq {
let gpio_irq = gpio_or_joybus && !joybus && high & 1 != 0;
if gpio_irq {
assert_eq!(
result,
Err(MemoryError::UnsupportedIo {
address: RCNT + 1,
value: high,
operation: if joybus {
"Joybus serial mode"
} else {
"GPIO serial interrupt enable"
},
operation: "GPIO serial interrupt enable",
})
);
assert_eq!((bus.read8(RCNT), bus.read8(RCNT + 1)), before);
} else {
result.unwrap();
assert_eq!(bus.read8(RCNT + 1).unwrap(), high & 0xc1);
if gpio_or_joybus {
if gpio_or_joybus && !joybus {
let latch = if halfword { low } else { previous as u8 };
let outputs = latch >> 4;
let pins = (latch & outputs | !outputs) & 15;
@ -109,17 +105,13 @@ fn cpu_byte_and_halfword_writes_accept_inactive_bits_but_reject_active_modes_ato
for high in [1, 0x41, 0x81, 0xc0, 0xc1] {
let mut machine = prepared_store(instruction, address, high << shift);
let cpu = machine.cpu().clone();
if high & 0x80 == 0 {
if high != 0x81 {
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.memory().read8(RCNT + 1).unwrap(), high as u8);
assert!(machine.cycles() > 0);
} else {
let error = machine.step().unwrap_err();
assert!(error.to_string().contains(if high & 0x40 != 0 {
"Joybus serial mode"
} else {
"GPIO serial interrupt enable"
}));
assert!(error.to_string().contains("GPIO serial interrupt enable"));
assert_eq!(machine.cpu(), &cpu);
assert_eq!(machine.cycles(), 0);
assert_eq!(machine.memory().read16(RCNT).unwrap(), 0x80f5);
@ -132,7 +124,7 @@ fn cpu_byte_and_halfword_writes_accept_inactive_bits_but_reject_active_modes_ato
#[test]
fn dma_halfword_rejection_is_retryable_then_inactive_configuration_completes() {
for high in [0x81, 0xc0, 0xc1] {
for high in [0x81, 0x83, 0xbf] {
let mut bus = memory();
bus.write16(RCNT, 0x80f5).unwrap();
bus.write16(0x02000000, high << 8).unwrap();

View file

@ -0,0 +1,142 @@
//! Original disconnected Joybus probes. No external commands or linked transfers.
use gba_core::{
cpu::Cpu,
dma::DMA_BASE,
io::{HALTCNT, IE, IF, IME, JOYCNT, JOYSTAT, JOY_RECV, JOY_TRANS, RCNT, SIOCNT, SIODATA8},
machine::{Machine, StepKind},
memory::{Memory, MemoryError, ROM_START},
};
fn memory() -> Memory {
Memory::new(0xeaff_fffe_u32.to_le_bytes().to_vec()).unwrap()
}
#[test]
fn local_register_masks_do_not_fabricate_remote_events_in_any_mode() {
let mut bus = memory();
for mode in [0, 0x8000, 0xc000, 0xc100, 0x4100] {
bus.write16(RCNT, mode).unwrap();
for value in 0..=255 {
bus.write16(JOYCNT, 0xff00 | value).unwrap();
bus.write16(JOYSTAT, 0xff00 | value).unwrap();
assert_eq!(bus.read16(JOYCNT).unwrap(), value & 0x40);
assert_eq!(bus.read16(JOYSTAT).unwrap(), value & 0x30);
bus.write8(JOYCNT + 1, 0xff).unwrap();
bus.write8(JOYSTAT + 1, 0xff).unwrap();
assert_eq!(bus.read16(JOYCNT).unwrap(), value & 0x40);
assert_eq!(bus.read16(JOYSTAT).unwrap(), value & 0x30);
}
bus.advance_cycles(1000000);
assert_eq!(bus.read16(IF).unwrap(), 0);
}
}
#[test]
fn data_lanes_retain_words_and_transmit_writes_set_pending_not_completion() {
for mode in [0, 0x8000, 0xc000] {
for base in [JOY_RECV, JOY_TRANS] {
let mut bus = memory();
bus.write16(RCNT, mode).unwrap();
for lane in 0..4 {
bus.write8(base + lane, (0x12 + lane * 0x22) as u8).unwrap();
}
assert_eq!(bus.read32(base).unwrap(), 0x78563412);
bus.write16(base + 2, 0xabcd).unwrap();
assert_eq!(bus.read32(base).unwrap(), 0xabcd3412);
bus.write32(base, 0x98765432).unwrap();
assert_eq!(bus.read16(base).unwrap(), 0x5432);
assert_eq!(bus.read16(base + 2).unwrap(), 0x9876);
let pending = if base == JOY_TRANS { 2 } else { 0 };
assert_eq!(bus.read16(JOYSTAT).unwrap(), pending);
bus.write16(JOYSTAT, 0xffff).unwrap();
assert_eq!(bus.read16(JOYSTAT).unwrap(), pending | 0x30);
bus.write16(JOYSTAT, 0).unwrap(); // Software cannot clear transmit pending.
bus.write16(JOYCNT, 0x47).unwrap();
bus.read32(JOY_RECV).unwrap();
bus.advance_cycles(1000000);
assert_eq!(bus.read16(JOYSTAT).unwrap(), pending);
assert_eq!(bus.read16(JOYCNT).unwrap(), 0x40);
assert_eq!(bus.read16(IF).unwrap(), 0);
bus.write16(RCNT, 0x8000).unwrap();
bus.write16(RCNT, 0xc100).unwrap();
assert_eq!(bus.read32(base).unwrap(), 0x98765432);
assert_eq!(bus.read16(JOYSTAT).unwrap(), pending);
}
}
// Writing zero also makes a reply available; zero is not a reset strobe.
let mut bus = memory();
bus.write8(JOY_TRANS + 3, 0).unwrap();
assert_eq!(bus.read16(JOYSTAT).unwrap(), 2);
}
#[test]
fn joybus_does_not_run_normal_clocks_or_wake_halt_or_stop() {
for stop in [false, true] {
let mut bus = memory();
bus.write16(RCNT, 0xc100).unwrap(); // Bit 8 is inactive, not a GPIO IRQ enable.
bus.write16(SIOCNT, 0x4083).unwrap(); // Unused in Joybus, even with internal start retained.
bus.write8(SIODATA8, 0x12).unwrap();
bus.write16(JOYCNT, 0x40).unwrap();
bus.write32(JOY_TRANS, 0xaabbccdd).unwrap();
bus.write16(IE, 0x80).unwrap();
bus.write16(IME, 1).unwrap();
bus.write8(HALTCNT, if stop { 0x80 } else { 0 }).unwrap();
bus.advance_cycles(1000000);
assert_eq!(bus.stopped(), stop);
assert_eq!(bus.halted(), !stop);
assert_eq!(bus.read8(SIODATA8).unwrap(), 0x12);
assert_eq!(bus.read16(SIOCNT).unwrap(), 0x4083);
assert_eq!(bus.read16(JOYCNT).unwrap(), 0x40);
assert_eq!(bus.read16(JOYSTAT).unwrap(), 2);
assert_eq!(bus.read16(IF).unwrap(), 0);
assert_eq!(bus.read8(RCNT), Err(MemoryError::Unmapped(RCNT)));
}
}
#[test]
fn live_normal_transfer_requires_completion_or_explicit_cancellation_before_joybus() {
let mut bus = memory();
bus.write16(SIOCNT, 0x4083).unwrap();
for elapsed in [0, 8] {
bus.advance_cycles(elapsed);
let error = bus.write16(RCNT, 0xc100).unwrap_err();
assert!(error.to_string().contains("serial reconfiguration"));
assert_eq!(bus.write16(RCNT, 0xc100), Err(error));
assert_eq!(bus.read8(RCNT + 1).unwrap(), 0);
}
bus.write16(SIOCNT, 0).unwrap();
bus.write16(RCNT, 0xc100).unwrap();
bus.advance_cycles(1000000);
assert_eq!(bus.read16(IF).unwrap(), 0);
}
#[test]
fn joy_control_and_status_word_padding_fail_without_partial_side_effects() {
let mut bus = memory();
for address in [JOYCNT, JOYSTAT] {
assert_eq!(
bus.write32(address, u32::MAX),
Err(MemoryError::Unmapped(address + 2))
);
assert_eq!(bus.read32(address), Err(MemoryError::Unmapped(address + 2)));
assert_eq!(bus.read16(address).unwrap(), 0);
}
}
#[test]
fn dma_transmit_write_has_only_dma_completion_irq() {
let mut bus = memory();
bus.write16(RCNT, 0xc000).unwrap();
bus.write16(JOYCNT, 0x40).unwrap();
bus.write32(0x02000000, 0x12345678).unwrap();
bus.write32(DMA_BASE, 0x02000000).unwrap();
bus.write32(DMA_BASE + 4, JOY_TRANS).unwrap();
bus.write32(DMA_BASE + 8, 0xc4000001).unwrap();
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 0 });
assert_eq!(machine.memory().read32(JOY_TRANS).unwrap(), 0x12345678);
assert_eq!(machine.memory().read16(JOYSTAT).unwrap(), 2);
assert_eq!(machine.memory().read16(IF).unwrap(), 0x100);
machine.memory_mut().advance_cycles(1000000);
assert_eq!(machine.memory().read16(IF).unwrap(), 0x100);
}