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

@ -77,6 +77,41 @@ fn raw_original_program_runs_with_a_bounded_report_without_changing_the_file() {
assert_eq!(fs::read(path).unwrap(), original); assert_eq!(fs::read(path).unwrap(), original);
} }
fn joybus_setup() -> Vec<u32> {
// Original ARM: select Joybus, enable IRQ, supply a reply, inspect local status/data.
vec![
0xe59f1024, // LDR r1, RCNT
0xe3a00903, // MOV r0, #0xc000
0xe1c100b0, // STRH r0, [r1]
0xe3a00040, // MOV r0, #0x40
0xe1c100bc, // STRH r0, [r1, #12] (JOYCNT)
0xe3a000a5, // MOV r0, #0xa5
0xe5810020, // STR r0, [r1, #32] (JOY_TRANS)
0xe1d122b4, // LDRH r2, [r1, #36] (JOYSTAT)
0xe1d130bc, // LDRH r3, [r1, #12]
0xe5914020, // LDR r4, [r1, #32]
0xeafffffe, 0x04000134,
]
}
#[test]
fn original_joybus_program_reports_pending_data_without_a_remote_completion() {
let fixture = Fixture::new();
let path = fixture.rom(&joybus_setup());
let original = fs::read(&path).unwrap();
let output = run(&path, "1000");
assert!(output.status.success(), "{}", stderr(&output));
for expected in [
"r2=0x00000002",
"r3=0x00000040",
"r4=0x000000a5",
"IRQ entries: 0",
] {
assert!(stdout(&output).contains(expected), "{}", stdout(&output));
}
assert_eq!(fs::read(path).unwrap(), original);
}
fn rtc_setup() -> Vec<u32> { fn rtc_setup() -> Vec<u32> {
// Original LDR/MOV/STRH/LDRH/loop: enable GPIO reads, then inspect control. // Original LDR/MOV/STRH/LDRH/loop: enable GPIO reads, then inspect control.
vec![ vec![
@ -423,6 +458,22 @@ fn native_rom_window_presents_file_bytes_and_reports_stop_cpu_and_video_errors()
); );
assert!(stdout(&output).contains("Captured ROM frames: 3")); assert!(stdout(&output).contains("Captured ROM frames: 3"));
assert!(stdout(&output).contains("Result: window frame limit reached")); assert!(stdout(&output).contains("Result: window frame limit reached"));
let joybus_path = fixture.rom(&joybus_setup());
let joybus_output = window(&joybus_path, false, None);
assert!(joybus_output.status.success(), "{}", stderr(&joybus_output));
for expected in [
"Captured ROM frames: 3",
"r2=0x00000002",
"r3=0x00000040",
"r4=0x000000a5",
"IRQ entries: 0",
] {
assert!(
stdout(&joybus_output).contains(expected),
"{}",
stdout(&joybus_output)
);
}
let rtc_path = fixture.rom(&rtc_setup()); let rtc_path = fixture.rom(&rtc_setup());
let rtc_output = window(&rtc_path, true, None); let rtc_output = window(&rtc_path, true, None);
assert!(rtc_output.status.success(), "{}", stderr(&rtc_output)); assert!(rtc_output.status.success(), "{}", stderr(&rtc_output));

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 use crate::audio::{FIFO_A, FIFO_B, SOUNDBIAS, SOUNDCNT_H, SOUNDCNT_X, SOUND_START, WAVE_RAM};
pub(crate) use serial::Serial; pub(crate) use serial::Serial;
pub use serial::{ pub use serial::{
JOYCNT, JOY_RECV, JOY_TRANS, RCNT, SIOCNT, SIODATA32, SIODATA8, SIOMLT_SEND, SIOMULTI0, JOYCNT, JOYSTAT, JOY_RECV, JOY_TRANS, RCNT, SIOCNT, SIODATA32, SIODATA8, SIOMLT_SEND,
SIOMULTI1, SIOMULTI2, SIOMULTI3, SIOMULTI0, SIOMULTI1, SIOMULTI2, SIOMULTI3,
}; };
mod timer_step; mod timer_step;
pub(crate) use timer_step::TimerStep; 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; use super::replace_byte;
pub const SIODATA32: u32 = 0x0400_0120; pub const SIODATA32: u32 = 0x0400_0120;
@ -13,6 +13,7 @@ pub const RCNT: u32 = 0x0400_0134;
pub const JOYCNT: u32 = 0x0400_0140; pub const JOYCNT: u32 = 0x0400_0140;
pub const JOY_RECV: u32 = 0x0400_0150; pub const JOY_RECV: u32 = 0x0400_0150;
pub const JOY_TRANS: u32 = 0x0400_0154; pub const JOY_TRANS: u32 = 0x0400_0154;
pub const JOYSTAT: u32 = 0x0400_0158;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Serial { 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. send: u16, // Shared low byte; high byte is accessible only in multiplayer format.
control: u16, control: u16,
rcnt: 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, bits_left: u8,
until_bit: u32, until_bit: u32,
} }
@ -30,28 +35,28 @@ impl Serial {
matches!(address, matches!(address,
SIODATA32..=0x0400_012b | SIODATA32..=0x0400_012b |
RCNT..=0x0400_0135 | JOYCNT..=0x0400_0141 | 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> { pub(crate) fn unsupported(address: u32, value: u8) -> Option<&'static str> {
match address { match address {
0x0400_0129 if value & 0x30 == 0x30 => Some("UART serial mode"), 0x0400_0129 if value & 0x30 == 0x30 => Some("UART serial mode"),
0x0400_0135 if value & 0xc0 == 0xc0 => Some("Joybus serial mode"), 0x0400_0135 if value & 0xc1 == 0x81 => Some("GPIO serial interrupt enable"),
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"),
_ => None, _ => None,
} }
} }
fn gpio(&self) -> bool { 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 { fn multiplayer_format(&self) -> bool {
self.control & 0x3000 == 0x2000 self.control & 0x3000 == 0x2000
} }
fn normal(&self) -> bool { fn normal(&self) -> bool {
!self.gpio() && !self.multiplayer_format() self.rcnt & 0x8000 == 0 && !self.multiplayer_format()
} }
fn busy(&self) -> bool { fn busy(&self) -> bool {
self.control & 0x80 != 0 self.control & 0x80 != 0
@ -137,7 +142,14 @@ impl Serial {
.multiplayer_format() .multiplayer_format()
.then(|| self.multi_extra.to_le_bytes()[(address - SIOMULTI2) as usize]); .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 { let value = match address & !1 {
SIOCNT if self.joybus() => self.control, // Unused; do not invent pin samples.
SIOCNT => { SIOCNT => {
let pins = if self.gpio() { self.gpio_pins() } else { 15 }; let pins = if self.gpio() { self.gpio_pins() } else { 15 };
// SI is pulled high without a cable. Multiplayer drives SD high while idle. // 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() (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, _ => return None,
}; };
Some(value.to_le_bytes()[(address & 1) as usize]) Some(value.to_le_bytes()[(address & 1) as usize])
@ -198,8 +211,22 @@ impl Serial {
let shift = (at - SIOMULTI2) * 8; let shift = (at - SIOMULTI2) * 8;
self.multi_extra = self.multi_extra =
(self.multi_extra & !(255 << shift)) | (u32::from(value) << shift); (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 { } else {
match at & !1 { 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 => { SIODATA8 if self.multiplayer_format() || at & 1 == 0 => {
self.send = replace_byte(self.send, at, value); self.send = replace_byte(self.send, at, value);
} }

View file

@ -15,6 +15,8 @@ mod iwram_bus;
#[cfg(test)] #[cfg(test)]
mod iwram_history_tests; mod iwram_history_tests;
#[cfg(test)] #[cfg(test)]
mod joybus_step_tests;
#[cfg(test)]
mod multiplayer_step_tests; mod multiplayer_step_tests;
#[cfg(test)] #[cfg(test)]
mod prefetch_tests; 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);
}

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] #[test]
fn serial_reset_cancels_an_external_clock_request_only_when_selected() { fn serial_reset_cancels_an_external_clock_request_only_when_selected() {
use gba_core::io::{RCNT, SIOCNT}; use gba_core::io::{RCNT, SIOCNT};

View file

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

View file

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

View file

@ -75,11 +75,13 @@ The repository is a Cargo workspace with three crates:
| `crates/gba-core/tests/pulse_sound/channel2.rs` | Channel 2 mapping, register gaps, independent state, shared clocks, and sum-before-rounding mixing | | `crates/gba-core/tests/pulse_sound/channel2.rs` | Channel 2 mapping, register gaps, independent state, shared clocks, and sum-before-rounding mixing |
| `crates/gba-core/tests/direct_sound.rs` | FIFO lanes/reset, signed mixing, timer selection, DMA refill, and HALT/STOP | | `crates/gba-core/tests/direct_sound.rs` | FIFO lanes/reset, signed mixing, timer selection, DMA refill, and HALT/STOP |
| `crates/gba-core/src/memory/audio_step_tests.rs` | Audio access phases, shadow validation, and CPU/DMA failure rollback | | `crates/gba-core/src/memory/audio_step_tests.rs` | Audio access phases, shadow validation, and CPU/DMA failure rollback |
| `crates/gba-core/src/io/serial.rs` | Disconnected normal serial shifter, internal clocks, external waiting, idle multiplayer registers, GPIO latches, and live-write validation | | `crates/gba-core/src/io/serial.rs` | Disconnected normal serial shifter, internal clocks, external waiting, idle multiplayer and local Joybus registers, GPIO latches, and live-write validation |
| `crates/gba-core/tests/inactive_devices.rs` | Device masks, wave RAM, GPIO pull-ups, serial data, and failed CPU/DMA write isolation | | `crates/gba-core/tests/inactive_devices.rs` | Device masks, wave RAM, GPIO pull-ups, serial data, and failed CPU/DMA write isolation |
| `crates/gba-core/tests/serial_control.rs` | RCNT mode gating, inactive bits, byte/halfword lanes, CPU/DMA diagnostics, and interrupt isolation | | `crates/gba-core/tests/serial_control.rs` | RCNT mode gating, inactive bits, byte/halfword lanes, CPU/DMA diagnostics, and interrupt isolation |
| `crates/gba-core/tests/serial_external.rs` | External-clock waiting, cancellation, HALT/STOP, ARM/Thumb/DMA starts, and atomic diagnostics | | `crates/gba-core/tests/serial_external.rs` | External-clock waiting, cancellation, HALT/STOP, ARM/Thumb/DMA starts, and atomic diagnostics |
| `crates/gba-core/tests/serial_internal.rs` | Nominal bit edges, widths/rates, cancellation, completion IRQs, HALT/STOP, and live-write limits | | `crates/gba-core/tests/serial_internal.rs` | Nominal bit edges, widths/rates, cancellation, completion IRQs, HALT/STOP, and live-write limits |
| `crates/gba-core/tests/serial_joybus.rs` | Local Joybus masks/data, pending status, inactive IRQs, mode gating, HALT/STOP, DMA, and padding diagnostics |
| `crates/gba-core/src/memory/joybus_step_tests.rs` | Joybus ARM/Thumb stores, staged reads, normal completion boundaries, and failed block-store isolation |
| `crates/gba-core/tests/serial_multiplayer.rs` | Disconnected child status, receive/send lanes, mode aliases, ignored child start, HALT/STOP, and ARM/Thumb/DMA access | | `crates/gba-core/tests/serial_multiplayer.rs` | Disconnected child status, receive/send lanes, mode aliases, ignored child start, HALT/STOP, and ARM/Thumb/DMA access |
| `crates/gba-core/src/memory/multiplayer_step_tests.rs` | Multiplayer mode changes at normal completion, block-transfer preflight, staged reads, and atomic diagnostics | | `crates/gba-core/src/memory/multiplayer_step_tests.rs` | Multiplayer mode changes at normal completion, block-transfer preflight, staged reads, and atomic diagnostics |
| `crates/gba-core/src/memory/serial_step_tests.rs` | Serial bus-phase observations, preflight validation, DMA, IF ordering, capture isolation, and rollback | | `crates/gba-core/src/memory/serial_step_tests.rs` | Serial bus-phase observations, preflight validation, DMA, IF ordering, capture isolation, and rollback |

View file

@ -1,6 +1,6 @@
# Disconnected serial subset # Disconnected serial subset
The core supports disconnected normal-mode transfers, external-clock waiting, idle multiplayer configuration, and general-purpose pins. The core supports disconnected normal-mode transfers, external-clock waiting, idle multiplayer, local Joybus registers, and general-purpose pins.
Internal clocks shift pulled-high input at nominal GBA rates. No link partner is emulated. Internal clocks shift pulled-high input at nominal GBA rates. No link partner is emulated.
External clock edges, connected multiplayer transfers, UART, Joybus communication, and GPIO interrupts remain unsupported. External clock edges, connected multiplayer transfers, UART, Joybus communication, and GPIO interrupts remain unsupported.
Unsupported activity remains diagnostic rather than reporting a fabricated completion. Unsupported activity remains diagnostic rather than reporting a fabricated completion.
@ -16,13 +16,12 @@ Unsupported activity remains diagnostic rather than reporting a fabricated compl
Completion or software cancellation clears start. Multiplayer uses the separate format described below; UART remains diagnostic. Completion or software cancellation clears start. Multiplayer uses the separate format described below; UART remains diagnostic.
- `RCNT` at `0x04000134` retains mode bits, interrupt configuration, output latches, and pin directions with mask `0xc1ff`. - `RCNT` at `0x04000134` retains mode bits, interrupt configuration, output latches, and pin directions with mask `0xc1ff`.
With bit 15 clear, bits 8 and 14 are writable latches without GPIO interrupt or Joybus effects. With bit 15 clear, bits 8 and 14 are writable latches without GPIO interrupt or Joybus effects.
With bit 15 set, bit 14 selects Joybus; otherwise bit 8 enables GPIO interrupts. Both active configurations remain diagnostic. With bit 15 set, bit 14 selects Joybus; bit 8 is unused in that mode.
With bit 15 set and bit 14 clear, bit 8 enables GPIO interrupts, which remain diagnostic.
In general-purpose mode, each input reads high through its pull-up. Each output reads its output latch. In general-purpose mode, each input reads high through its pull-up. Each output reads its output latch.
`RCNT=0x8000` therefore reads back as `0x800f`, not `0x8000`. `RCNT=0x8000` therefore reads back as `0x800f`, not `0x8000`.
Normal and multiplayer low-byte pin readback remains unmapped rather than guessed. Normal, multiplayer, and Joybus low-byte pin readback remains unmapped rather than guessed.
- `JOYCNT` permits clearing already-empty status flags. Joybus interrupt enable is unsupported. - `JOYCNT`, `JOY_RECV`, `JOY_TRANS`, and `JOYSTAT` support the disconnected configuration described below.
- `JOY_RECV` and `JOY_TRANS` permit only zero writes and zero readback for reset initialization.
Nonzero writes return an explicit error. Joybus status and data-transfer side effects are not implemented.
GPIO means general-purpose input/output. UART means universal asynchronous receiver/transmitter. GPIO means general-purpose input/output. UART means universal asynchronous receiver/transmitter.
No external serial device is connected. Only an enabled internal serial clock can shift data or generate a completion IRQ. No external serial device is connected. Only an enabled internal serial clock can shift data or generate a completion IRQ.
@ -86,7 +85,7 @@ The actual transfers then execute once from the original state. Other IF sources
### Bounded behavior ### Bounded behavior
After any bit has shifted, changes to width, clock selection/rate, or the selected data register remain diagnostic while start stays set. After any bit has shifted, changes to width, clock selection/rate, or the selected data register remain diagnostic while start stays set.
Selecting GPIO during an internally clocked request also remains diagnostic. Clear start before these operations. Selecting GPIO or Joybus during an internally clocked request also remains diagnostic. Clear start before these operations.
IRQ-enable and idle SO configuration writes remain supported without restarting progress. IRQ-enable and idle SO configuration writes remain supported without restarting progress.
These limits also apply to BIOS services that write serial registers; cancel active transfers before requesting serial reset. These limits also apply to BIOS services that write serial registers; cancel active transfers before requesting serial reset.
Unverified live reconfiguration is not silently approximated. Unverified live reconfiguration is not silently approximated.
@ -141,11 +140,52 @@ Original tests cover control masks, all baud selections, register lanes, aliases
Bus-phase tests cover normal completion, retryable diagnostics, block loads/stores, and mode-dependent staged reads. Bus-phase tests cover normal completion, retryable diagnostics, block loads/stores, and mode-dependent staged reads.
There is no parent, cable topology, external clock input, multiplayer timeout, or linked-game compatibility claim. There is no parent, cable topology, external clock input, multiplayer timeout, or linked-game compatibility claim.
## Disconnected Joybus configuration
RCNT bits 14–15 both set select Joybus. The GBA is a slave; an external master must initiate communication.
This subset has no master, cable, commands, or external clock input. Selecting the mode does not start communication.
RCNT bit 8 remains a writable inactive latch, not a GPIO interrupt enable.
RCNT low-byte pin samples remain unmapped: GPIO output latches must not substitute for unverified Joybus pin levels.
SIOCNT is unused in Joybus. Its existing format latches remain readable without synthetic SI/SD status or internal shifting.
UART-format writes still return the existing diagnostic, including while Joybus is selected.
- `JOYCNT` at `0x04000140` retains interrupt enable in bit 6. Unused bits read zero.
Reset, receive-complete, and send-complete flags in bits 0–2 are write-one-to-clear.
They remain zero because no master can produce those events. Enabling interrupts cannot itself request IF bit 7.
- `JOY_RECV` at `0x04000150` and `JOY_TRANS` at `0x04000154` retain independent 32-bit data latches.
Byte, halfword, and word accesses preserve other lanes. Synthetic initial data is zero, not received peer data.
Local receive writes do not set receive status or a completion flag.
- `JOYSTAT` at `0x04000158` retains software flags in bits 4–5.
Every local transmit-data write sets bit 1, including a zero write or a write to an upper lane.
This means a reply is available for a master; it does not mean transmission completed.
Software cannot directly set or clear hardware status bits 1/3 by writing JOYSTAT.
Receive-pending bit 3 remains zero. Receive reads therefore have no observable read-clear effect in this disconnected subset.
Transmit pending survives elapsed time, mode changes, status writes, and empty-event acknowledgements.
No Joybus event wakes HALT or STOP. Joybus selection disables the normal serial clock even if SIOCNT retains start.
The existing live normal-transfer limits still apply when entering Joybus. Clear start or wait for normal completion first.
A normal completion at the write's bus phase remains pending in IF after the mode change.
Registers participate in serial transactions, including failed CPU/DMA access rollback and block-store preflight.
Word accesses to JOYCNT and JOYSTAT reject unmapped upper padding without partial writes.
Local Joybus registers remain accessible in other modes under this bounded latch model.
Receive writes follow GBATEK's R/W description; the pinned mGBA implementation leaves them unhandled rather than storing them.
mGBA corroborates JOYCNT/JOYSTAT masks in Joybus and sets transmit pending from either halfword outside mode dispatch.
Cross-mode masks, CPU receive writes, synthetic reset values, and byte-lane behavior lack physical-hardware validation.
These choices do not establish GameCube communication, hardware reset accuracy, or linked-game compatibility.
Original tests cover masks, lanes, pending status, mode gating, inactive IRQ bits, HALT/STOP, and ARM/Thumb/DMA accesses.
Staged tests cover completion boundaries, local readback, rollback, and later block-store failures.
Original desktop programs also check status/data through terminal and native-window execution.
## BIOS reset ## BIOS reset
RegisterRamReset always clears the low halfword of SIODATA32, even without bit 5. RegisterRamReset always clears the low halfword of SIODATA32, even without bit 5.
The upper halfword remains unchanged in this functional subset. The upper halfword remains unchanged in this functional subset.
With bit 5 selected, the firmware clears SIOCNT and SIODATA8, selects general-purpose inputs, and clears the supported Joybus reset registers. With bit 5 selected, the firmware clears SIOCNT and SIODATA8 and selects general-purpose inputs.
It clears JOYCNT interrupt enable and both Joybus data latches.
Its zero write to JOY_TRANS sets transmit pending. It does not write JOYSTAT; software flags remain unchanged.
This is the replacement's functional sequence, not a claim about complete hardware BIOS reset side effects.
It uses ordinary ARM bus stores, not a host-side reset bypass. It uses ordinary ARM bus stores, not a host-side reset bypass.
Clearing SIOCNT cancels a pending external-clock request. Unselected serial reset leaves that request pending. Clearing SIOCNT cancels a pending external-clock request. Unselected serial reset leaves that request pending.
The firmware writes SIODATA8 after selecting normal format. This does not clear the retained upper multiplayer SEND byte. The firmware writes SIODATA8 after selecting normal format. This does not clear the retained upper multiplayer SEND byte.
@ -163,6 +203,8 @@ Inactive configuration bits do not wake HALT. RCNT word accesses still reject th
- [GBATEK normal serial mode](https://problemkaputt.de/gbatek-sio-normal-mode.htm): data widths, MSB-first shifting, rates, pulled-high SI, start/completion, IRQ selection, and external-clock waiting. - [GBATEK normal serial mode](https://problemkaputt.de/gbatek-sio-normal-mode.htm): data widths, MSB-first shifting, rates, pulled-high SI, start/completion, IRQ selection, and external-clock waiting.
- [GBATEK multiplayer mode](https://problemkaputt.de/gbatek-sio-multi-player-mode.htm): child start restrictions, SI/SD status, undefined pre-transfer ID, register widths, and R/W receive registers. - [GBATEK multiplayer mode](https://problemkaputt.de/gbatek-sio-multi-player-mode.htm): child start restrictions, SI/SD status, undefined pre-transfer ID, register widths, and R/W receive registers.
- [LinkRawCable at c61bf351](https://github.com/afska/gba-link-connection/blob/c61bf351f68ad2d6e1c9d72d70e21bec19adfc0b/lib/LinkRawCable.hpp): public API usage for mode selection, SEND, receive words, and separate role/readiness/ID checks. Its disconnected-cable warning is not a pin-level measurement. - [LinkRawCable at c61bf351](https://github.com/afska/gba-link-connection/blob/c61bf351f68ad2d6e1c9d72d70e21bec19adfc0b/lib/LinkRawCable.hpp): public API usage for mode selection, SEND, receive words, and separate role/readiness/ID checks. Its disconnected-cable warning is not a pin-level measurement.
- [GBATEK Joybus mode](https://problemkaputt.de/gbatek-sio-joy-bus-mode.htm): slave-only communication, RCNT/SIOCNT roles, local register masks, R/W data, and status side effects.
- [mGBA I/O implementation at 3a5e34be](https://github.com/mgba-emu/mgba/blob/3a5e34be33dc7f8f707e5bc9db69e8a430046f21/src/gba/io.c): transmit-pending writes and receive-status read clearing; compare with masks and unhandled receive writes in its serial implementation below.
- [GBATEK mode summary](https://problemkaputt.de/gbatek-sio-control-registers-summary.htm): RCNT/SIOCNT mode selection. - [GBATEK mode summary](https://problemkaputt.de/gbatek-sio-control-registers-summary.htm): RCNT/SIOCNT mode selection.
- [mGBA serial implementation at 3a5e34be](https://github.com/mgba-emu/mgba/blob/3a5e34be33dc7f8f707e5bc9db69e8a430046f21/src/gba/sio.c): cross-checked normal transfer-duration arithmetic and completion IRQ handling. Its no-driver scheduled completion and zero receive data are not copied; they do not establish disconnected hardware behavior. Multiplayer comparison covers SI/SD defaults, receive-write disagreement, and uncertain floating SC timing. - [mGBA serial implementation at 3a5e34be](https://github.com/mgba-emu/mgba/blob/3a5e34be33dc7f8f707e5bc9db69e8a430046f21/src/gba/sio.c): cross-checked normal transfer-duration arithmetic and completion IRQ handling. Its no-driver scheduled completion and zero receive data are not copied; they do not establish disconnected hardware behavior. Multiplayer comparison covers SI/SD defaults, receive-write disagreement, and uncertain floating SC timing.
- [GBATEK](https://mgba-emu.github.io/gbatek/): GBA general-purpose pin directions, internal pull-ups, SI falling-edge interrupts, and the SIO mode-selection table. - [GBATEK](https://mgba-emu.github.io/gbatek/): GBA general-purpose pin directions, internal pull-ups, SI falling-edge interrupts, and the SIO mode-selection table.

View file

@ -26,7 +26,7 @@ Use a local ROM that you may lawfully use:
```sh ```sh
direnv exec . cargo run --locked --release -- --rom roms/pokemon-emerald.gba --rtc --save-type flash128 --steps 3000000 direnv exec . cargo run --locked --release -- --rom roms/pokemon-emerald.gba --rtc --save-type flash128 --steps 3000000
direnv exec . cargo run --locked --release -- --rom roms/pokemon-emerald.gba --rtc --save-type flash128 --window --frames 60 direnv exec . cargo run --locked --release -- --rom roms/pokemon-emerald.gba --rtc --save-type flash128 --window --frames 600
``` ```
The earlier reset-only implementation stopped at master sound enable (`0x04000084`, PC `0x082e0518`). The earlier reset-only implementation stopped at master sound enable (`0x04000084`, PC `0x082e0518`).
@ -36,7 +36,8 @@ Both pulse channels then reached the noise trigger (`0x0400007d`, PC `0x082e03c4
Noise channel 4 support then reached an incorrectly rejected RCNT write: `0x0100` at `0x04000134`, PC `0x082e2aac`. Noise channel 4 support then reached an incorrectly rejected RCNT write: `0x0100` at `0x04000134`, PC `0x082e2aac`.
RCNT bit 15 was clear, so bit 8 did not enable general-purpose input/output (GPIO) interrupts. RCNT bit 15 was clear, so bit 8 did not enable general-purpose input/output (GPIO) interrupts.
GBATEK documents bits 8 and 14 as writable but unused in normal mode. GBATEK documents bits 8 and 14 as writable but unused in normal mode.
The serial model now retains these inactive bits. Effective GPIO interrupt enable and Joybus selection still return explicit errors. The serial model now retains these inactive bits. Effective GPIO interrupt enable still returns an explicit error.
Joybus configuration was added later, as described below.
That correction reached `read-only memory at 0x00000000` inside the original BIOS replacement, PC `0x00000378`. That correction reached `read-only memory at 0x00000000` inside the original BIOS replacement, PC `0x00000378`.
A bounded local trace found a Thumb `CpuSet` call with source `0x02022a74`, destination zero, and count `0x1c` halfwords. A bounded local trace found a Thumb `CpuSet` call with source `0x02022a74`, destination zero, and count `0x1c` halfwords.
@ -77,19 +78,34 @@ Select `--save-type flash128` for this bounded run; the device begins erased and
A later redundant `0xf0` write occurred after returning to idle array mode. A later redundant `0xf0` write occurred after returning to idle array mode.
The subset accepts that no-op without claiming direct Flash128 ID exit or busy-command cancellation. The subset accepts that no-op without claiming direct Flash128 ID exit or busy-command cancellation.
Both terminal and native runs with these options now reach Joybus mode selection: Flash identification then reached Joybus selection (`RCNT=0xc000`, PC `0x082deeca`) after sixteen captured frames.
The [disconnected Joybus subset](../hardware/serial.md#disconnected-joybus-configuration) now accepts local configuration and status/data accesses.
No master, command, remote receive word, or completion IRQ is fabricated.
The terminal run reaches its requested three-million-step limit with exit status zero:
```text ```text
Steps: 1389249; instructions: 1386088; IRQ entries: 89; DMA units: 3072; HALT idle: 0 Result: step limit reached
Nominal cycles: 4688039 Steps: 3000000; instructions: 2985620; IRQ entries: 124; DMA units: 14256; HALT idle: 0
PC=0x082deeca Nominal cycles: 9626104
unsupported Joybus serial mode: write 0xc0 at 0x04000135 PC=0x080008cc
``` ```
The native run captures sixteen startup frames. These do not establish a working title screen or gameplay. Both 60-frame and 600-frame native runs now reach their frame limits with exit status zero.
The longer run reports:
```text
Result: window frame limit reached
Captured ROM frames: 600
Steps: 58456876; instructions: 58087737; IRQ entries: 1251; DMA units: 367888; HALT idle: 0
Nominal cycles: 168453824
PC=0x080008ca
```
These runs establish bounded execution without a diagnostic, not visible menu progress or gameplay.
No scripted input or pixel assertions were used. Repeated frames can still come from a waiting loop.
Audio-device behavior is validated by original tests, not by audible game output. Audio-device behavior is validated by original tests, not by audible game output.
The window run does not reach its requested frame limit and correctly exits with failure. Host RTC values can change between runs; these totals are observations, not deterministic golden assertions.
Terminal execution is also diagnostic, not a compatibility pass.
Results apply to this local file; no cartridge revision or checksum was inferred from its filename. Results apply to this local file; no cartridge revision or checksum was inferred from its filename.
## Validation and next requirement ## Validation and next requirement
@ -98,8 +114,9 @@ Workspace debug/release tests, strict Clippy, public ARM/Thumb/memory/BIOS check
Native ROM-window tests and graphics smoke modes pass on Darwin arm64. Native ROM-window tests and graphics smoke modes pass on Darwin arm64.
The pinned Pong debug/release reports match. The pinned Pong debug/release reports match.
The next startup requirement is disconnected Joybus configuration, beginning with `RCNT=0xc000`. Next, inspect visible startup/menu progress and exercise button presses and releases in a bounded run.
Research idle registers, absent-device status, and mode transitions before implementing them. Do not fabricate a connected peer. Identify any waiting loop or new diagnostic before selecting another compatibility correction.
Do not treat frame-limit success as a title-screen or gameplay pass.
Flash programming, erase, busy behavior, and safe persistence remain required before claiming save compatibility. Flash programming, erase, busy behavior, and safe persistence remain required before claiming save compatibility.
RTC persistence and cartridge IRQ behavior remain separate missing features. RTC persistence and cartridge IRQ behavior remain separate missing features.
Ordinary ROM writes remain read-only. Without `--rtc`, the earlier GPIO diagnostic is still expected. Ordinary ROM writes remain read-only. Without `--rtc`, the earlier GPIO diagnostic is still expected.

View file

@ -85,6 +85,7 @@ Update this file when a feature lands or a limit is removed.
- Raw ROM-file loading with original BIOS boot, terminal diagnostics, and final CPU/step reports. - Raw ROM-file loading with original BIOS boot, terminal diagnostics, and final CPU/step reports.
- Firmware boot initializes BG2/BG3 identity scales for bitmap and affine programs that do not program their own matrices. - Firmware boot initializes BG2/BG3 identity scales for bitmap and affine programs that do not program their own matrices.
- RegisterRamReset supports disabled sound and disconnected serial initialization, including the common `r0=0xff` request. - RegisterRamReset supports disabled sound and disconnected serial initialization, including the common `r0=0xff` request.
- Disconnected Joybus selection, local data/status registers, and transmit-pending state without invented remote events or IRQs.
- Direct Sound A/B support timer-driven FIFO playback, stereo digital levels, and DMA1/A or DMA2/B refill blocks. - Direct Sound A/B support timer-driven FIFO playback, stereo digital levels, and DMA1/A or DMA2/B refill blocks.
- PSG pulse channels 1 and 2 support independent nominal frequency/duty playback, length, envelope, status, and stereo mixing. Channel 1 also supports sweep. - PSG pulse channels 1 and 2 support independent nominal frequency/duty playback, length, envelope, status, and stereo mixing. Channel 1 also supports sweep.
- Noise channel 4 supports nominal 7/15-bit generation, divider clocks, shared length/envelope behavior, status, and stereo mixing. - Noise channel 4 supports nominal 7/15-bit generation, divider clocks, shared length/envelope behavior, status, and stereo mixing.
@ -157,8 +158,9 @@ The [serial subset](hardware/serial.md) supports disconnected normal 8/32-bit tr
Internal clocks shift high input at nominal rates, clear busy, and request completion IRQs. External requests retain busy without clock edges. Internal clocks shift high input at nominal rates, clear busy, and request completion IRQs. External requests retain busy without clock edges.
Disconnected multiplayer exposes child/ready status and 16-bit send/receive registers without inventing transfers or completion IRQs. Disconnected multiplayer exposes child/ready status and 16-bit send/receive registers without inventing transfers or completion IRQs.
Pre-transfer ID is a zero placeholder; receive-write behavior and reset/alias details lack physical-hardware validation. Pre-transfer ID is a zero placeholder; receive-write behavior and reset/alias details lack physical-hardware validation.
Connected links, multiplayer transfers, UART, Joybus, GPIO interrupts, and unverified mid-transfer reconfiguration remain unsupported. Joybus supports local configuration, data latches, and transmit-pending status, without an external master or completion IRQs.
RCNT retains inactive bits 8/14 in normal mode. Effective GPIO interrupt enable and Joybus selection remain diagnostic. Connected links, multiplayer transfers, UART, Joybus commands, GPIO interrupts, and unverified mid-transfer reconfiguration remain unsupported.
RCNT retains inactive bits 8/14 in normal mode and bit 8 in Joybus. Effective GPIO interrupt enable remains diagnostic.
Mapped I/O covers DMA, timers, interrupts, WAITCNT, display control/status, background control/scroll/affine registers, window/effect registers, KEYINPUT/KEYCNT, POSTFLG, and HALTCNT. Mapped I/O covers DMA, timers, interrupts, WAITCNT, display control/status, background control/scroll/affine registers, window/effect registers, KEYINPUT/KEYCNT, POSTFLG, and HALTCNT.
Keypad IRQs use a documented polling model; hardware retrigger edges and asynchronous input timing remain unverified. Keypad IRQs use a documented polling model; hardware retrigger edges and asynchronous input timing remain unverified.
STOP implements keypad wake only. Serial/Game Pak wake sources, oscillator restart delays, and exact wake timing remain unimplemented or unverified. STOP implements keypad wake only. Serial/Game Pak wake sources, oscillator restart delays, and exact wake timing remain unimplemented or unverified.
@ -174,7 +176,9 @@ It also accepts an external-clock serial request without inventing clock edges o
It now also completes internally clocked normal transfers and selects idle multiplayer mode. It now also completes internally clocked normal transfers and selects idle multiplayer mode.
With explicit `--rtc` selection, it also passes Game Pak GPIO configuration and RTC calendar reads. With explicit `--rtc` selection, it also passes Game Pak GPIO configuration and RTC calendar reads.
With `--save-type flash128`, it also passes Flash identification and reaches a later startup check. With `--save-type flash128`, it also passes Flash identification and reaches a later startup check.
It next stops at Joybus mode selection (`0x04000135`, PC `0x082deeca`). Flash writing and persistence remain unimplemented. It now passes disconnected Joybus setup and completes a bounded 600-frame native run without a diagnostic error.
This run uses no scripted input or pixel assertions; menu progress and gameplay remain unverified.
Flash writing and persistence remain unimplemented.
Without `--rtc`, cartridge GPIO writes still return read-only diagnostics. Without `--rtc`, cartridge GPIO writes still return read-only diagnostics.
No title-screen, gameplay, or audible-game compatibility is established. No title-screen, gameplay, or audible-game compatibility is established.
See [the startup result](research/emerald-reset.md) for the bounded run and next investigation. See [the startup result](research/emerald-reset.md) for the bounded run and next investigation.

View file

@ -311,6 +311,9 @@ Bus-phase tests cover start/load timing, IF acknowledgement order, CPU/DMA rollb
Disconnected multiplayer tests cover every control value in that format, all baud settings, send/receive lanes, and normal-mode aliases. Disconnected multiplayer tests cover every control value in that format, all baud settings, send/receive lanes, and normal-mode aliases.
They verify idle child status, ignored start, no completion IRQ or HALT wake, GPIO sampling, and ARM/Thumb/DMA stores. They verify idle child status, ignored start, no completion IRQ or HALT wake, GPIO sampling, and ARM/Thumb/DMA stores.
Staged tests check mode changes at normal completion, block loads/stores, rollback, and reads after leaving multiplayer format. Staged tests check mode changes at normal completion, block loads/stores, rollback, and reads after leaving multiplayer format.
Joybus tests cover mode selection, local control/data/status lanes, transmit pending, and absence of remote events or IRQs.
They check HALT/STOP, BIOS reset stores, unmapped padding, ARM/Thumb/DMA accesses, staged reads, and failed block-store rollback.
Original desktop programs check Joybus status and reply data in both terminal and native-window execution.
These tests validate the documented bounded serial model, not physical pin timing, receive-write behavior, or linked-game compatibility. These tests validate the documented bounded serial model, not physical pin timing, receive-write behavior, or linked-game compatibility.
Timer/IRQ tests verify that changing WAITCNT changes when an interrupt is sampled. Timer/IRQ tests verify that changing WAITCNT changes when an interrupt is sampled.
Timer tests cover all prescalers, reload changes, start/stop behavior, byte writes, large clock advances, and cascades. Timer tests cover all prescalers, reload changes, start/stop behavior, byte writes, large clock advances, and cascades.

View file

@ -42,10 +42,11 @@ Frame-based scenarios and the preparation adapter now make the result repeatable
A local Emerald run now passes its initial all-device reset, master sound enable, and pulse/noise triggers. A local Emerald run now passes its initial all-device reset, master sound enable, and pulse/noise triggers.
It also passes normal serial transfers and idle multiplayer configuration. It also passes normal serial transfers and idle multiplayer configuration.
With `--rtc --save-type flash128`, it passes Game Pak GPIO setup, RTC reads, and Flash identification. With `--rtc --save-type flash128`, it passes Game Pak GPIO setup, RTC reads, and Flash identification.
It next stops at Joybus serial-mode selection. It also passes disconnected Joybus configuration and completes a bounded 600-frame native run without a diagnostic error.
That run does not verify screen contents, button responses, or gameplay.
The core has tested Direct Sound, both pulse channels, and noise channel 4. The core has tested Direct Sound, both pulse channels, and noise channel 4.
Wave channel 3, a continuous sample stream, and desktop audio output remain missing. Wave channel 3, a continuous sample stream, and desktop audio output remain missing.
See [the runtime result](research/emerald-reset.md). Disconnected Joybus configuration is the next demonstrated startup requirement. See [the runtime result](research/emerald-reset.md). Next, verify visible menu progress and button responses rather than treating frame counts as playability.
The separate save requirement includes Flash programming/erase and safe host persistence. Identification/read support is not successful saving. The separate save requirement includes Flash programming/erase and safe host persistence. Identification/read support is not successful saving.
SRAM is byte-addressable battery-backed save memory. Flash and EEPROM remain separate protocols. SRAM is byte-addressable battery-backed save memory. Flash and EEPROM remain separate protocols.
@ -57,7 +58,8 @@ A reproducible source build or an accessible upstream ROM is needed before recor
Next implementation steps: Next implementation steps:
1. Research the demonstrated Joybus configuration failure without inventing a connected device. 1. Inspect visible startup/menu progress and exercise input in the bounded local game run. Record the first demonstrated failure.
Use original regressions for any hardware correction; do not invent connected serial devices.
2. Extend explicit Flash selection with verified programming, erase, and busy behavior. Keep absent devices and unsupported commands diagnostic. 2. Extend explicit Flash selection with verified programming, erase, and busy behavior. Keep absent devices and unsupported commands diagnostic.
Add SRAM separately when the independent candidate requires it; verify its widths, mirroring, and wait behavior first. Add SRAM separately when the independent candidate requires it; verify its widths, mirroring, and wait behavior first.
3. Add bounded save loading and safe persistence in the desktop crate. Do not put file operations in the core. 3. Add bounded save loading and safe persistence in the desktop crate. Do not put file operations in the core.