fix(memory): ignore CPU and DMA writes to unused I/O addresses

Stores to I/O addresses with no register returned Unmapped and stopped the
machine. Hardware ignores them. Emulated writes there now pay their bus cost and
change nothing; host writes stay strict. The unmodeled internal memory control
register at 0x04xx0800 and all unused I/O reads remain diagnostics.

Rollback tests that used unused I/O as their failing word now run the block on
to a register that rejects the stored value: SIOMULTI2 outside multiplayer
mode, an RCNT GPIO serial IRQ, or 64-sample wave playback. Where no reachable
register can fail (JOYSTAT, POSTFLG/HALTCNT, IME, DMA3CNT), the test now checks
that the block commits and the unused word is ignored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhfHfHEuzv2AUTd6erW1gU
This commit is contained in:
0xrsydn 2026-10-14 11:38:51 +07:00
commit 20176716d9
16 changed files with 166 additions and 92 deletions

View file

@ -192,6 +192,13 @@ impl Io {
|| matches!(address, 0x0400_0000..=0x0400_0057 | DMA_BASE..=DMA_END | 0x0400_0100..=0x0400_010f | 0x0400_0130..=0x0400_0133 | 0x0400_0200..=0x0400_020b | POSTFLG..=0x0400_0303) || matches!(address, 0x0400_0000..=0x0400_0057 | DMA_BASE..=DMA_END | 0x0400_0100..=0x0400_010f | 0x0400_0130..=0x0400_0133 | 0x0400_0200..=0x0400_020b | POSTFLG..=0x0400_0303)
} }
/// I/O-page addresses with no register. CPU and DMA writes there are ignored.
/// The mirrored internal memory control register at `0x04xx0800` is a real,
/// unmodeled register and stays a diagnostic.
pub(crate) fn unused(address: u32) -> bool {
address >> 24 == 0x04 && !Self::mapped(address) && address & 0xfffc != 0x0800
}
pub(crate) fn unsupported_write(address: u32, value: u8) -> Option<&'static str> { pub(crate) fn unsupported_write(address: u32, value: u8) -> Option<&'static str> {
Serial::unsupported(address, value) Serial::unsupported(address, value)
} }

View file

@ -1035,6 +1035,9 @@ impl Memory {
} }
for (offset, byte) in bytes.into_iter().enumerate() { for (offset, byte) in bytes.into_iter().enumerate() {
let byte_address = address + offset as u32; let byte_address = address + offset as u32;
if !Io::mapped(byte_address) {
continue; // Validated unused I/O; the write has no effect.
}
if matches!(byte_address, POSTFLG | HALTCNT) && !self.can_write_power_control() { if matches!(byte_address, POSTFLG | HALTCNT) && !self.can_write_power_control() {
continue; // BIOS-only CPU writes; ignored elsewhere, including STOP requests. continue; // BIOS-only CPU writes; ignored elsewhere, including STOP requests.
} }
@ -1116,8 +1119,11 @@ impl Memory {
0x07 => Ok((address & 0x3ff) as usize), 0x07 => Ok((address & 0x3ff) as usize),
0x04 => { 0x04 => {
for offset in 0..N as u32 { for offset in 0..N as u32 {
if !Io::mapped(address + offset) { let at = address + offset;
return Err(MemoryError::Unmapped(address + offset)); // Unused I/O ignores emulated writes; host setup stays strict.
let ignored = access == WriteAccess::Emulated && Io::unused(at);
if !(Io::mapped(at) || ignored) {
return Err(MemoryError::Unmapped(at));
} }
} }
Ok(0) // I/O does not use a RAM index. Ok(0) // I/O does not use a RAM index.

View file

@ -26,12 +26,12 @@ fn samples_before_store_completion_keep_old_level_and_publish_only_once() {
#[test] #[test]
fn failed_cpu_and_dma_steps_discard_speculative_samples_and_phase() { fn failed_cpu_and_dma_steps_discard_speculative_samples_and_phase() {
for dma in [false, true] { for dma in [false, true] {
let (cpu, mut memory) = prepared(false, &[0xe8910005], &[(1, SOUNDBIAS)]); // LDM crosses unmapped 8c. let (cpu, mut memory) = prepared(false, &[0xe8910005], &[(1, SOUNDBIAS)]); // LDM reads unmapped 8c.
memory.set_audio_capture(true); memory.set_audio_capture(true);
memory.advance_cycles(508); memory.advance_cycles(508);
if dma { if dma {
memory.write32(DMA_BASE, 0x02000000).unwrap(); memory.write32(DMA_BASE, 0x02000000).unwrap();
memory.write32(DMA_BASE + 4, 0x0400008c).unwrap(); memory.write32(DMA_BASE + 4, 0x0400_0800).unwrap(); // Unmodeled memory control.
memory.write32(DMA_BASE + 8, 0x84000001).unwrap(); memory.write32(DMA_BASE + 8, 0x84000001).unwrap();
} }
let before = memory.io.audio_capture.clone(); let before = memory.io.audio_capture.clone();

View file

@ -51,22 +51,20 @@ fn joybus_mode_selection_observes_the_normal_completion_bus_phase() {
} }
#[test] #[test]
fn later_block_padding_failure_discards_data_pending_status_and_writeback() { fn block_store_over_joystat_padding_commits_data_pending_status_and_writeback() {
let (cpu, mut memory) = prepared( let (cpu, mut memory) = prepared(
false, false,
&[0xe8a0000e], // STMIA r0!,{r1-r3}; final word hits JOYSTAT padding. &[0xe8a0000e], // STMIA r0!,{r1-r3}; final word covers JOYSTAT and unused padding.
&[(0, JOY_RECV), (1, 0x12345678), (2, 0xabcdef90), (3, 0x30)], &[(0, JOY_RECV), (1, 0x12345678), (2, 0xabcdef90), (3, 0x30)],
); );
memory.write16(RCNT, 0xc000).unwrap(); memory.write16(RCNT, 0xc000).unwrap();
let serial = memory.io.serial;
let mut machine = Machine::new(cpu, memory); let mut machine = Machine::new(cpu, memory);
let cpu = machine.cpu().clone(); machine.step().unwrap();
let error = machine.step().unwrap_err(); assert_eq!(machine.cpu().registers()[0], JOY_RECV + 12);
assert!(error.to_string().contains("0x0400015a")); assert_eq!(machine.memory().read32(JOY_RECV).unwrap(), 0x12345678);
assert_eq!(machine.step(), Err(error)); assert_eq!(machine.memory().read32(JOY_TRANS).unwrap(), 0xabcdef90);
assert_eq!(machine.cpu(), &cpu); // Software flags from the store, plus transmit pending from the JOY_TRANS write.
assert_eq!(machine.cycles(), 0); assert_eq!(machine.memory().read16(JOYSTAT).unwrap(), 0x32);
assert_eq!(machine.memory().io.serial, serial);
} }
#[test] #[test]

View file

@ -95,8 +95,9 @@ fn single_and_block_store_validation_uses_completion_phase_and_does_not_double_a
fn later_block_failure_restores_completion_and_partial_serial_writes() { fn later_block_failure_restores_completion_and_partial_serial_writes() {
let (cpu, mut memory) = prepared( let (cpu, mut memory) = prepared(
false, false,
&[0xe8a10005], // STMIA r1!,{r0,r2-r4}: SIOCNT, ignored padding, KEYCNT, then an unsupported RCNT.
&[(0, 0x00204081), (1, SIOCNT), (2, 0)], &[0xe8a1001d],
&[(0, 0x00204081), (1, SIOCNT), (2, 0), (3, 0), (4, 0x8100)],
); );
start(&mut memory, 57); start(&mut memory, 57);
let serial = memory.io.serial; let serial = memory.io.serial;
@ -107,7 +108,7 @@ fn later_block_failure_restores_completion_and_partial_serial_writes() {
.step() .step()
.unwrap_err() .unwrap_err()
.to_string() .to_string()
.contains("0x0400012c")); .contains("0x04000135"));
assert_eq!(machine.memory().io.serial, serial); assert_eq!(machine.memory().io.serial, serial);
assert_eq!(machine.memory().read16(IF).unwrap(), 0); assert_eq!(machine.memory().read16(IF).unwrap(), 0);
assert_eq!(machine.cpu(), &cpu); assert_eq!(machine.cpu(), &cpu);

View file

@ -178,7 +178,7 @@ fn failed_instructions_discard_partial_timer_progress_and_pending_irqs() {
for (instruction, base) in [ for (instruction, base) in [
(0xe591_0000, 0x0e00_0000), (0xe591_0000, 0x0e00_0000),
(0xe891_0005, TIMER_BASE + 12), (0xe891_0005, TIMER_BASE + 12),
(0xe881_0005, TIMER_BASE + 12), (0xe881_00fd, TIMER_BASE + 12), // STMIA r1,{r0,r2-r7}; r7 reaches SIOMULTI2.
(0xffff_ffff, 0), (0xffff_ffff, 0),
] { ] {
let (cpu, mut memory) = prepared(false, &[instruction], &[(1, base)]); let (cpu, mut memory) = prepared(false, &[instruction], &[(1, base)]);

View file

@ -375,26 +375,38 @@ fn display_sources_enter_an_original_irq_handler_acknowledge_and_return() {
#[test] #[test]
fn failed_block_store_does_not_change_window_controls_or_advance_display() { fn failed_block_store_does_not_change_window_controls_or_advance_display() {
let rom = [ // STMIA r12!,{r0-r11} stores WININ/WINOUT through SOUND3CNT_X. Unused words are
0xe3a0_2301_u32, // MOV r2,#0x04000000 // ignored. With 64-sample wave mode selected, r11 requests playback, a diagnostic.
0xe282_2048, // ADD r2,r2,#0x48 (WININ/WINOUT) let operands = [
0xe3a0_003f, // MOV r0,#0x3f (12, 0x0400_0048),
0xe3a0_1001, // MOV r1,#1 (0, 0x3f),
0xe8a2_001f, // STMIA r2!,{r0-r4}; last word is unmapped 0x04000058 (1, 1),
] (10, 0xa0),
.into_iter() (11, 0x8000),
.flat_map(u32::to_le_bytes) ];
let offset = (operands.len() as u32 - 1) * 4;
let mut code: Vec<u32> = operands
.iter()
.map(|&(register, _)| 0xe59f_0000 | (register as u32) << 12 | offset)
.collect(); .collect();
let mut bus = Memory::new(rom).unwrap(); code.push(0xe8ac_0fff);
code.extend(operands.iter().map(|&(_, value)| value));
let mut bus = Memory::new(code.into_iter().flat_map(u32::to_le_bytes).collect()).unwrap();
bus.write16(0x0400_0084, 0x80).unwrap(); // Master sound enable.
bus.write16(0x0400_0070, 0xa0).unwrap(); // Wave: 64-sample mode, DAC on.
let mut cpu = Cpu::new(ROM_START); let mut cpu = Cpu::new(ROM_START);
for _ in 0..4 { for _ in operands {
cpu.step(&mut bus).unwrap(); cpu.step(&mut bus).unwrap();
} }
bus.advance_cycles(7 * CYCLES_PER_LINE); bus.advance_cycles(7 * CYCLES_PER_LINE);
let before = cpu.clone(); let before = cpu.clone();
let position = bus.display_position(); let position = bus.display_position();
let mut machine = Machine::new(cpu, bus); let mut machine = Machine::new(cpu, bus);
assert!(machine.step().is_err()); let error = machine.step().unwrap_err();
assert!(
error.to_string().contains("64-sample wave playback"),
"{error}"
);
assert_eq!(machine.cpu(), &before); assert_eq!(machine.cpu(), &before);
assert_eq!(machine.memory().display_position(), position); assert_eq!(machine.memory().display_position(), position);
assert_eq!(machine.memory().read32(gba_core::io::WININ).unwrap(), 0); assert_eq!(machine.memory().read32(gba_core::io::WININ).unwrap(), 0);

View file

@ -749,13 +749,14 @@ fn failing_later_unit_preserves_earlier_data_but_not_partial_current_unit() {
} }
#[test] #[test]
fn failed_io_write_does_not_partially_change_display_control() { fn unused_io_destination_is_ignored_without_changing_display_control() {
let mut machine = machine(); let mut machine = machine();
machine.memory_mut().write32(SOURCE, 0xffff_ffff).unwrap(); machine.memory_mut().write32(SOURCE, 0xffff_ffff).unwrap();
configure(machine.memory_mut(), 3, SOURCE, 0x0400_0058, 1, 0x400); configure(machine.memory_mut(), 3, SOURCE, 0x0400_0058, 1, 0x400);
assert!(machine.step().is_err()); units(&mut machine, 3, 1);
assert!(!enabled(&machine, 3));
assert_eq!(machine.memory().read16(DISPCNT).unwrap(), 0); assert_eq!(machine.memory().read16(DISPCNT).unwrap(), 0);
assert_eq!(machine.cycles(), 0); assert!(machine.cycles() > 0);
} }
#[test] #[test]
@ -787,11 +788,11 @@ fn cpu_can_enable_dma_with_a_combined_count_control_store() {
} }
#[test] #[test]
fn failed_cpu_block_store_cannot_enable_dma_or_change_its_hidden_latches() { fn cpu_block_store_past_dma3_control_enables_the_channel_and_ignores_unused_io() {
let rom = words(&[ let rom = words(&[
0xe59f_000c, // LDR r0,[pc,#12] (DMA3CNT) 0xe59f_000c, // LDR r0,[pc,#12] (DMA3CNT)
0xe59f_100c, // LDR r1,[pc,#12] (count/control) 0xe59f_100c, // LDR r1,[pc,#12] (count/control)
0xe8a0_0006, // STMIA r0!,{r1,r2}; second word unmapped. 0xe8a0_0006, // STMIA r0!,{r1,r2}; the second word is unused I/O.
0xeaff_fffe, 0xeaff_fffe,
0xe1a0_0000, 0xe1a0_0000,
base(3) + 8, base(3) + 8,
@ -803,15 +804,14 @@ fn failed_cpu_block_store_cannot_enable_dma_or_change_its_hidden_latches() {
cpu.step(&mut bus).unwrap(); cpu.step(&mut bus).unwrap();
configure(&mut bus, 3, SOURCE, DEST, 2, 0); configure(&mut bus, 3, SOURCE, DEST, 2, 0);
bus.write16(base(3) + 10, 0).unwrap(); bus.write16(base(3) + 10, 0).unwrap();
let before = cpu.clone(); bus.write32(SOURCE, 0x1234_5678).unwrap();
let mut machine = Machine::new(cpu, bus); let mut machine = Machine::new(cpu, bus);
assert!(matches!(machine.step(), Err(MachineError::Cpu(_)))); assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.cpu(), &before); assert_eq!(machine.cpu().registers()[0], base(3) + 16);
assert!(!enabled(&machine, 3)); assert!(enabled(&machine, 3));
assert_eq!(machine.cycles(), 0); units(&mut machine, 3, 1); // The store latched a count of 1.
machine.memory_mut().write16(base(3) + 10, 0x8000).unwrap();
units(&mut machine, 3, 2); // The count latch is still 2, not 1.
assert!(!enabled(&machine, 3)); assert!(!enabled(&machine, 3));
assert_eq!(machine.memory().read32(DEST).unwrap(), 0x1234_5678);
} }
#[test] #[test]

View file

@ -146,7 +146,7 @@ fn failed_units_preserve_the_old_latch_without_new_data_or_irq() {
for (source, destination, control) in [ for (source, destination, control) in [
(0x0e00_0000, DEST, 0x4400), // Unmapped reads do not use a known latch. (0x0e00_0000, DEST, 0x4400), // Unmapped reads do not use a known latch.
(SOURCE, 0x0d00_0000, 0x4400), (SOURCE, 0x0d00_0000, 0x4400),
(SOURCE, 0x0400_0058, 0x4400), (SOURCE, 0x0400_0800, 0x4400),
(SOURCE, DMA_BASE, 0x4400), (SOURCE, DMA_BASE, 0x4400),
(SOURCE, 0x0400_0300, 0x4400), (SOURCE, 0x0400_0300, 0x4400),
(SOURCE, DEST, 0x4580), // Unsupported source mode. (SOURCE, DEST, 0x4580), // Unsupported source mode.

View file

@ -584,7 +584,7 @@ fn bios_stop_store_finishes_before_freezing_and_access_context_is_cleared() {
assert_eq!(machine.step().unwrap(), StepKind::StopIdle); assert_eq!(machine.step().unwrap(), StepKind::StopIdle);
assert_eq!(machine.last_timing(), StepTiming::default()); assert_eq!(machine.last_timing(), StepTiming::default());
// A failed non-BIOS instruction must also clear its access context. // A failed non-BIOS instruction must also clear its access context.
let mut machine = store_machine(ROM_START, 0xe581_0000, 0x0400_0058, 0, true); let mut machine = store_machine(ROM_START, 0xe581_0000, 0x0400_0800, 0, true);
assert!(machine.step().is_err()); assert!(machine.step().is_err());
machine.memory_mut().write8(POSTFLG, 1).unwrap(); machine.memory_mut().write8(POSTFLG, 1).unwrap();
halt(&mut machine); halt(&mut machine);
@ -604,15 +604,14 @@ fn non_bios_stop_write_is_ignored_not_a_diagnostic() {
} }
#[test] #[test]
fn failed_block_store_cannot_partially_halt_or_change_postflg() { fn bios_block_store_halts_and_ignores_the_unused_word_after_haltcnt() {
// The first word is mapped; the second is not. Validation precedes all writes. // STMIA r1!,{r0,r2}: POSTFLG=1 and HALTCNT=0, then an unused I/O word.
let mut machine = store_machine(0x100, 0xe8a1_0005, POSTFLG, 1, false); // STMIA r1!,{r0,r2} let mut machine = store_machine(0x100, 0xe8a1_0005, POSTFLG, 1, false);
let before = machine.cpu().clone(); assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert!(machine.step().is_err()); assert!(machine.halted());
assert_eq!(machine.cpu(), &before); assert!(!machine.stopped());
assert!(!machine.halted()); assert_eq!(machine.memory().read8(POSTFLG).unwrap(), 1);
assert_eq!(machine.memory().read8(POSTFLG).unwrap(), 0); assert_eq!(machine.cpu().registers()[1], POSTFLG + 8);
assert_eq!(machine.cycles(), 0);
} }
#[test] #[test]

View file

@ -371,8 +371,23 @@ fn cpu_store_samples_keys_then_irq_enters_on_the_following_step() {
#[test] #[test]
fn failed_block_store_does_not_commit_key_control_irq_history_or_halt_wake() { fn failed_block_store_does_not_commit_key_control_irq_history_or_halt_wake() {
// STMIA r0!,{r1,r2}: keypad word is valid, but RCNT's upper padding is not mapped. // STMIA r0!,{r1,r2}: the keypad word is valid, but RCNT enables a GPIO serial IRQ.
let mut m = prepared(0xe8a0_0006, KEYINPUT, 0x4001_0000); let mut mem = Memory::new(words(&[
0xe59f_000c,
0xe59f_100c,
0xe59f_200c,
0xe8a0_0006,
0xeaff_fffe,
KEYINPUT,
0x4001_0000,
0x0000_8100,
]))
.unwrap();
let mut cpu = Cpu::new(ROM_START);
for _ in 0..3 {
cpu.step(&mut mem).unwrap();
}
let mut m = Machine::new(cpu, mem);
press(m.memory_mut(), 1); press(m.memory_mut(), 1);
m.memory_mut().write16(IE, IRQ).unwrap(); m.memory_mut().write16(IE, IRQ).unwrap();
let before = m.cpu().clone(); let before = m.cpu().clone();
@ -380,9 +395,13 @@ fn failed_block_store_does_not_commit_key_control_irq_history_or_halt_wake() {
let timing = m.last_timing(); let timing = m.last_timing();
assert_eq!( assert_eq!(
m.step(), m.step(),
Err(MachineError::Cpu(CpuError::Memory(MemoryError::Unmapped( Err(MachineError::Cpu(CpuError::Memory(
KEYINPUT + 6 MemoryError::UnsupportedIo {
)))) address: KEYINPUT + 5,
value: 0x81,
operation: "GPIO serial interrupt enable",
}
)))
); );
assert_eq!(m.cpu(), &before); assert_eq!(m.cpu(), &before);
assert_eq!(m.cycles(), cycles); assert_eq!(m.cycles(), cycles);

View file

@ -194,7 +194,7 @@ fn diagnostics_do_not_advance_timers_or_clock() {
#[test] #[test]
fn late_block_store_error_does_not_change_timer_reload_or_enable() { fn late_block_store_error_does_not_change_timer_reload_or_enable() {
let mut machine = prepared( let mut machine = prepared(
0xe8a0_003e, 0xe8a0_07fe,
&[ &[
(0, TIMER_BASE), (0, TIMER_BASE),
(1, 0x00c0_ffff), (1, 0x00c0_ffff),
@ -203,7 +203,7 @@ fn late_block_store_error_does_not_change_timer_reload_or_enable() {
(4, 0x00c0_ffff), (4, 0x00c0_ffff),
(5, 123), (5, 123),
], ],
); // STMIA r0!, {r1-r5}; fifth word is unmapped ); // STMIA r0!, {r1-r10}; unused words are ignored, and r10 reaches SIOMULTI2
let before = machine.cpu().clone(); let before = machine.cpu().clone();
assert!(machine.step().is_err()); assert!(machine.step().is_err());
assert_eq!(machine.cpu(), &before); assert_eq!(machine.cpu(), &before);
@ -217,23 +217,19 @@ fn late_block_store_error_does_not_change_timer_reload_or_enable() {
} }
#[test] #[test]
fn late_block_store_error_does_not_acknowledge_if_or_modify_ie() { fn block_store_over_ime_padding_commits_ie_if_waitcnt_and_ime() {
let mut machine = prepared( let mut machine = prepared(
0xe8a0_001e, 0xe8a0_001e,
&[(0, IE), (1, 0x0008_ffff), (2, 123), (3, 1), (4, 42)], &[(0, IE), (1, 0x0008_ffff), (2, 123), (3, 1), (4, 42)],
); );
// IE/IF, WAITCNT, IME are valid. The fourth word at 0x0400020c is not. // IE/IF, WAITCNT, and IME are written. The fourth word at 0x0400020c is unused.
pending_timer(machine.memory_mut()); pending_timer(machine.memory_mut());
machine.memory_mut().write32(IME, 0).unwrap(); // Let the invalid store execute machine.memory_mut().write32(IME, 0).unwrap(); // Let the store execute
let before = machine.cpu().clone(); assert_eq!(machine.step().unwrap(), StepKind::Instruction);
let cycles = machine.cycles(); assert_eq!(machine.cpu().registers()[0], IE + 16);
assert!(machine.step().is_err()); assert_eq!(machine.memory().read32(IE).unwrap(), 0x3fff); // IF bit 3 acknowledged.
assert_eq!(machine.cpu(), &before); assert_eq!(machine.memory().read32(WAITCNT).unwrap(), 123);
assert_eq!(machine.cycles(), cycles); assert_eq!(machine.memory().read32(IME).unwrap(), 1);
assert_eq!(machine.memory().read32(IE).unwrap(), 0x0008_0008);
assert_eq!(machine.memory().read32(WAITCNT).unwrap(), 0);
assert_eq!(machine.memory().read32(IME).unwrap(), 0);
assert_eq!(machine.last_timing().total(), 0);
} }
#[test] #[test]
@ -271,3 +267,28 @@ fn cpu_only_step_does_not_advance_device_time_or_deliver_irq() {
assert_eq!(bus.cycles(), cycles); assert_eq!(bus.cycles(), cycles);
assert!(bus.irq_pending()); assert!(bus.irq_pending());
} }
#[test]
fn cpu_stores_to_unused_io_are_ignored_but_memory_control_and_host_writes_stay_strict() {
for address in [
0x0400_0058,
0x0400_00e0,
0x0400_020c,
0x0400_0304,
0x0401_0100,
] {
let mut machine = prepared(0xe580_1000, &[(0, address), (1, u32::MAX)]); // STR r1,[r0]
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(
machine.memory_mut().write32(address, 0),
Err(MemoryError::Unmapped(address))
);
}
let mut machine = prepared(0xe580_1000, &[(0, 0x0400_0800), (1, 0)]);
let before = machine.cpu().clone();
assert_eq!(
machine.step(),
Err(MachineError::Cpu(MemoryError::Unmapped(0x0400_0800).into()))
);
assert_eq!(machine.cpu(), &before);
}

View file

@ -168,10 +168,27 @@ fn invalid_word_and_block_writes_do_not_leave_a_partial_external_start() {
assert_eq!(bus.read16(SIOCNT).unwrap(), 0x5084); assert_eq!(bus.read16(SIOCNT).unwrap(), 0x5084);
assert_eq!(bus.read16(SIODATA8).unwrap(), 0xab); assert_eq!(bus.read16(SIODATA8).unwrap(), 0xab);
let mut machine = prepared(false, 0xe8a00006, 0x00ab5080); // STMIA r0!,{r1,r2}; second word unmapped. // STMIA r0!,{r1-r4}: SIOCNT, ignored padding, KEYCNT, then RCNT enabling a GPIO serial IRQ.
let code = [
0xe59f_000c_u32,
0xe59f_100c,
0xe59f_400c,
0xe8a0_001e,
0xeaff_fffe,
SIOCNT,
0x00ab_5080,
0x0000_8100,
];
let mut bus = Memory::new(code.into_iter().flat_map(u32::to_le_bytes).collect()).unwrap();
let mut cpu = Cpu::new(ROM_START);
for _ in 0..3 {
cpu.step(&mut bus).unwrap();
}
bus.write16(SIOCNT, 0x1003).unwrap();
let mut machine = Machine::new(cpu, bus);
let before = machine.cpu().clone(); let before = machine.cpu().clone();
let error = machine.step().unwrap_err(); let error = machine.step().unwrap_err();
assert!(error.to_string().contains("unmapped memory at 0x0400012c")); assert!(error.to_string().contains("0x04000135"), "{error}");
assert_eq!(machine.cpu(), &before); assert_eq!(machine.cpu(), &before);
assert_eq!(machine.cycles(), 0); assert_eq!(machine.cycles(), 0);
assert_eq!(machine.memory().read16(SIOCNT).unwrap(), 0x1007); assert_eq!(machine.memory().read16(SIOCNT).unwrap(), 0x1007);

View file

@ -1,11 +1,11 @@
use gba_core::{ use gba_core::{
cpu::{Cpu, CpuError, InstructionSet}, cpu::{Cpu, InstructionSet},
display::{CYCLES_PER_FRAME, VBLANK_START}, display::{CYCLES_PER_FRAME, VBLANK_START},
dma::{DmaError, DMA_BASE}, dma::{DmaError, DMA_BASE},
input::Buttons, input::Buttons,
io::{DISPCNT, HALTCNT, IE, IF, IME, KEYCNT, POSTFLG, TIMER_BASE}, io::{DISPCNT, HALTCNT, IE, IF, IME, KEYCNT, POSTFLG, TIMER_BASE},
machine::{FrameRunError, Machine, MachineError, StepKind}, machine::{FrameRunError, Machine, MachineError, StepKind},
memory::{Memory, MemoryError, BIOS_SIZE, ROM_START}, memory::{Memory, BIOS_SIZE, ROM_START},
timing::StepTiming, timing::StepTiming,
video::Framebuffer, video::Framebuffer,
}; };
@ -355,12 +355,12 @@ fn arm_and_thumb_stop_stores_finish_with_normal_cycles_before_freezing() {
} }
#[test] #[test]
fn failed_block_store_cannot_partially_enter_stop_or_set_postflg() { fn bios_block_store_enters_stop_and_ignores_the_unused_word_after_haltcnt() {
let mut bios = vec![0; BIOS_SIZE]; let mut bios = vec![0; BIOS_SIZE];
bios[..24].copy_from_slice(&words(&[ bios[..24].copy_from_slice(&words(&[
0xe59f_0008, 0xe59f_0008,
0xe59f_1008, 0xe59f_1008,
0xe8a0_0006, 0xe8a0_0006, // STMIA r0!,{r1,r2}: POSTFLG=1, HALTCNT=0x80, then unused I/O.
0xeaff_fffe, 0xeaff_fffe,
POSTFLG, POSTFLG,
0x8001, 0x8001,
@ -370,19 +370,11 @@ fn failed_block_store_cannot_partially_enter_stop_or_set_postflg() {
cpu.step(&mut mem).unwrap(); cpu.step(&mut mem).unwrap();
cpu.step(&mut mem).unwrap(); cpu.step(&mut mem).unwrap();
let mut m = Machine::new(cpu, mem); let mut m = Machine::new(cpu, mem);
let before = m.cpu().clone(); assert_eq!(m.step().unwrap(), StepKind::Instruction);
assert_eq!( assert!(m.stopped());
m.step(),
Err(MachineError::Cpu(CpuError::Memory(MemoryError::Unmapped(
POSTFLG + 4
))))
);
assert_eq!(m.cpu(), &before);
assert!(!m.stopped());
assert!(!m.halted()); assert!(!m.halted());
assert_eq!(m.memory().read8(POSTFLG).unwrap(), 0); assert_eq!(m.memory().read8(POSTFLG).unwrap(), 1);
assert_eq!(m.cycles(), 0); assert_eq!(m.cpu().registers()[0], POSTFLG + 8);
assert_eq!(m.last_timing(), StepTiming::default());
} }
#[test] #[test]

View file

@ -68,7 +68,7 @@ Writes into the mapped 16 KiB BIOS [complete without changing its image](bios.md
Source reads, channel data, address progression, timing, and completion IRQs still apply to these ignored writes. Source reads, channel data, address progression, timing, and completion IRQs still apply to these ignored writes.
DMA0–2 mask destination bit 27, so a programmed cartridge address can instead select the BIOS region. DMA0–2 mask destination bit 27, so a programmed cartridge address can instead select the BIOS region.
Other accesses use the existing memory map, alignment checks, mirrors, and I/O write rules. Other accesses use the existing memory map, alignment checks, mirrors, and I/O write rules.
Other unmapped reads remain diagnostics, not general DMA open-bus behavior. Writes to the `0x08`–`0x0c` ROM windows are ignored; the `0x0d` window remains a diagnostic. Other unmapped reads remain diagnostics, not general DMA open-bus behavior. Writes to the `0x08`–`0x0c` ROM windows and to unused I/O addresses are ignored; the `0x0d` window and `0x04000800` remain diagnostics.
A failed unit changes no memory, device state, or clock. Earlier units remain committed; the failed unit remains pending. A failed unit changes no memory, device state, or clock. Earlier units remain committed; the failed unit remains pending.
Successful units update local IWRAM lanes for their actual IWRAM reads and writes, in that order. Successful units update local IWRAM lanes for their actual IWRAM reads and writes, in that order.
No CPU continuation is required; DMA can establish lanes before the first CPU instruction. No CPU continuation is required; DMA can establish lanes before the first CPU instruction.

View file

@ -227,6 +227,8 @@ Complete general-bus history, exact boundary timing, general DMA handoff, and un
[DMA channel data latches](hardware/dma.md#retained-channel-data) support blocked-source reuse, but never replace CPU bus history. [DMA channel data latches](hardware/dma.md#retained-channel-data) support blocked-source reuse, but never replace CPU bus history.
BIOS protection uses separate retained BIOS fetch snapshots as described in [BIOS behavior](hardware/bios.md#cpu-bios-read-protection). BIOS protection uses separate retained BIOS fetch snapshots as described in [BIOS behavior](hardware/bios.md#cpu-bios-read-protection).
Host inspection and instruction fetches remain strict. Other unmapped reads return errors. Host inspection and instruction fetches remain strict. Other unmapped reads return errors.
CPU and DMA writes to unused I/O addresses are ignored, as on hardware; host writes there stay strict.
The unmodeled internal memory control register at `0x04xx0800` and all unused I/O reads remain diagnostics.
Reads beyond the supplied cartridge bytes also return errors. Reads beyond the supplied cartridge bytes also return errors.
Direct memory-bus halfword and word accesses require alignment. Direct memory-bus halfword and word accesses require alignment.
CPU load/store instructions apply ARM7TDMI alignment and rotation rules before accessing the bus. CPU load/store instructions apply ARM7TDMI alignment and rotation rules before accessing the bus.