feat(memory): map GBA memory and device registers
Add memory mirrors, video-bus write rules, I/O latches, timers, and interrupt registers.
This commit is contained in:
parent
535c69e09f
commit
156f1eb839
4 changed files with 1535 additions and 0 deletions
360
tests/timers.rs
Normal file
360
tests/timers.rs
Normal file
|
|
@ -0,0 +1,360 @@
|
|||
use gba_rust::{
|
||||
io::{IE, IF, IME, TIMER_BASE},
|
||||
memory::{Memory, MemoryError},
|
||||
};
|
||||
|
||||
fn memory() -> Memory {
|
||||
Memory::new(vec![]).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn io_reset_values_masks_and_unused_bytes() {
|
||||
let mut bus = memory();
|
||||
for address in (TIMER_BASE..TIMER_BASE + 16)
|
||||
.chain(IE..IE + 4)
|
||||
.chain(IME..IME + 4)
|
||||
{
|
||||
assert_eq!(bus.read8(address).unwrap(), 0);
|
||||
}
|
||||
bus.write16(IE, 0xffff).unwrap();
|
||||
assert_eq!(bus.read16(IE).unwrap(), 0x3fff);
|
||||
bus.write32(IME, u32::MAX).unwrap();
|
||||
assert_eq!(bus.read32(IME).unwrap(), 1);
|
||||
bus.write32(IME, 0xffff_fffe).unwrap();
|
||||
assert_eq!(bus.read32(IME).unwrap(), 0);
|
||||
for index in 0..4 {
|
||||
let address = TIMER_BASE + index * 4 + 2;
|
||||
bus.write16(address, 0xffff).unwrap();
|
||||
assert_eq!(
|
||||
bus.read16(address).unwrap(),
|
||||
if index == 0 { 0xc3 } else { 0xc7 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timer_data_reads_counter_not_reload_and_start_loads_reload() {
|
||||
let mut bus = memory();
|
||||
bus.write16(TIMER_BASE, 0x1234).unwrap();
|
||||
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0);
|
||||
bus.advance_cycles(100);
|
||||
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0);
|
||||
bus.write16(TIMER_BASE + 2, 0x80).unwrap();
|
||||
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0x1234);
|
||||
bus.advance_cycles(5);
|
||||
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0x1239);
|
||||
bus.write16(TIMER_BASE, 0xabcd).unwrap();
|
||||
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0x1239);
|
||||
bus.write16(TIMER_BASE + 2, 0x80).unwrap(); // No rising enable edge
|
||||
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0x1239);
|
||||
bus.write16(TIMER_BASE + 2, 0).unwrap();
|
||||
bus.advance_cycles(100);
|
||||
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0x1239);
|
||||
bus.write16(TIMER_BASE + 2, 0x80).unwrap();
|
||||
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0xabcd);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn combined_word_write_uses_new_reload_before_enabling() {
|
||||
let mut bus = memory();
|
||||
for index in 0..4 {
|
||||
let address = TIMER_BASE + index * 4;
|
||||
bus.write32(address, 0x0080_abcd).unwrap();
|
||||
assert_eq!(bus.read32(address).unwrap(), 0x0080_abcd);
|
||||
}
|
||||
bus.advance_cycles(1);
|
||||
for index in 0..4 {
|
||||
assert_eq!(bus.read16(TIMER_BASE + index * 4).unwrap(), 0xabce);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reload_byte_writes_merge_with_hidden_latch_not_counter() {
|
||||
let mut bus = memory();
|
||||
bus.write16(TIMER_BASE, 0x1234).unwrap();
|
||||
bus.write8(TIMER_BASE, 0x56).unwrap();
|
||||
bus.write8(TIMER_BASE + 2, 0x80).unwrap();
|
||||
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0x1256);
|
||||
bus.advance_cycles(1000);
|
||||
bus.write8(TIMER_BASE + 1, 0xab).unwrap();
|
||||
bus.write8(TIMER_BASE + 2, 0).unwrap();
|
||||
bus.write8(TIMER_BASE + 2, 0x80).unwrap();
|
||||
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0xab56);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_prescalers_retain_fractional_cycles_between_advances() {
|
||||
for (selection, divisor) in [1, 64, 256, 1024].into_iter().enumerate() {
|
||||
let mut bus = memory();
|
||||
bus.write32(TIMER_BASE, ((0x80 | selection as u32) << 16) | 123)
|
||||
.unwrap();
|
||||
bus.advance_cycles(divisor - 1);
|
||||
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 123);
|
||||
bus.advance_cycles(1);
|
||||
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 124);
|
||||
bus.advance_cycles(divisor * 3 + divisor - 1);
|
||||
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 127);
|
||||
bus.advance_cycles(1);
|
||||
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 128);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clock_source_changes_and_restart_reset_provisional_phase() {
|
||||
let mut bus = memory();
|
||||
bus.write32(TIMER_BASE, 0x0081_0000).unwrap(); // /64
|
||||
bus.advance_cycles(63);
|
||||
bus.write16(TIMER_BASE + 2, 0xc1).unwrap(); // IRQ change preserves phase
|
||||
bus.advance_cycles(1);
|
||||
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 1);
|
||||
bus.advance_cycles(63);
|
||||
bus.write16(TIMER_BASE + 2, 0x82).unwrap(); // /256 resets phase
|
||||
bus.advance_cycles(255);
|
||||
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 1);
|
||||
bus.advance_cycles(1);
|
||||
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 2);
|
||||
bus.write16(TIMER_BASE + 2, 0).unwrap();
|
||||
bus.advance_cycles(255);
|
||||
bus.write16(TIMER_BASE + 2, 0x82).unwrap();
|
||||
bus.advance_cycles(255);
|
||||
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overflow_reloads_and_latches_request_without_delivery_enables() {
|
||||
let mut bus = memory();
|
||||
for index in 0..4 {
|
||||
bus.write32(TIMER_BASE + index * 4, 0x00c0_fffe).unwrap();
|
||||
}
|
||||
bus.advance_cycles(1);
|
||||
assert_eq!(bus.read16(IF).unwrap(), 0);
|
||||
bus.advance_cycles(1);
|
||||
assert_eq!(bus.read16(IF).unwrap(), 0x78);
|
||||
assert!(!bus.irq_pending()); // IE and IME remain zero
|
||||
for index in 0..4 {
|
||||
assert_eq!(bus.read16(TIMER_BASE + index * 4).unwrap(), 0xfffe);
|
||||
}
|
||||
bus.advance_cycles(5);
|
||||
assert_eq!(bus.read16(IF).unwrap(), 0x78);
|
||||
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0xffff);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overflow_uses_updated_reload_without_resetting_current_counter() {
|
||||
let mut bus = memory();
|
||||
bus.write32(TIMER_BASE, 0x00c0_fffe).unwrap();
|
||||
bus.write16(TIMER_BASE, 0x1234).unwrap();
|
||||
bus.advance_cycles(1);
|
||||
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0xffff);
|
||||
bus.advance_cycles(1);
|
||||
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0x1234);
|
||||
assert_eq!(bus.read16(IF).unwrap(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overflow_with_local_irq_disabled_does_not_set_request() {
|
||||
let mut bus = memory();
|
||||
bus.write32(TIMER_BASE, 0x0080_ffff).unwrap();
|
||||
bus.advance_cycles(100);
|
||||
assert_eq!(bus.read16(IF).unwrap(), 0);
|
||||
bus.write16(TIMER_BASE + 2, 0xc0).unwrap();
|
||||
assert_eq!(bus.read16(IF).unwrap(), 0); // No retroactive request
|
||||
bus.advance_cycles(1);
|
||||
assert_eq!(bus.read16(IF).unwrap(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cascades_count_all_overflows_including_multiple_levels() {
|
||||
let mut bus = memory();
|
||||
bus.write32(TIMER_BASE, 0x00c0_fffe).unwrap(); // Overflow every 2 cycles
|
||||
for index in 1..4 {
|
||||
bus.write32(TIMER_BASE + index * 4, 0x00c7_fffe).unwrap(); // /1024 ignored
|
||||
}
|
||||
bus.advance_cycles(15);
|
||||
assert_eq!(bus.read16(IF).unwrap(), 0x38);
|
||||
for index in 0..4 {
|
||||
assert_eq!(bus.read16(TIMER_BASE + index * 4).unwrap(), 0xffff);
|
||||
}
|
||||
bus.advance_cycles(1);
|
||||
assert_eq!(bus.read16(IF).unwrap(), 0x78);
|
||||
for index in 0..4 {
|
||||
assert_eq!(bus.read16(TIMER_BASE + index * 4).unwrap(), 0xfffe);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabled_cascade_stage_blocks_downstream_pulses() {
|
||||
let mut bus = memory();
|
||||
bus.write32(TIMER_BASE, 0x0080_ffff).unwrap();
|
||||
bus.write32(TIMER_BASE + 4, 0x0004_ffff).unwrap(); // Disabled timer 1
|
||||
bus.write32(TIMER_BASE + 8, 0x00c4_1234).unwrap();
|
||||
bus.advance_cycles(100_000);
|
||||
assert_eq!(bus.read16(TIMER_BASE + 8).unwrap(), 0x1234);
|
||||
assert_eq!(bus.read16(IF).unwrap(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normal_timer_breaks_cascade_dependency_and_timer_zero_ignores_count_up() {
|
||||
let mut bus = memory();
|
||||
bus.write32(TIMER_BASE, 0x0084_ffff).unwrap(); // Bit 2 ignored
|
||||
bus.write32(TIMER_BASE + 8, 0x0080_fffe).unwrap(); // Independent timer 2
|
||||
bus.write32(TIMER_BASE + 12, 0x00c4_ffff).unwrap();
|
||||
bus.advance_cycles(2);
|
||||
assert_eq!(bus.read16(TIMER_BASE + 2).unwrap(), 0x80);
|
||||
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0xffff);
|
||||
assert_eq!(bus.read16(IF).unwrap(), 0x40);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bulk_advance_handles_maximum_cycle_count_and_period_one() {
|
||||
let mut bus = memory();
|
||||
bus.write32(TIMER_BASE, 0x00c0_ffff).unwrap();
|
||||
for index in 1..4 {
|
||||
bus.write32(TIMER_BASE + index * 4, 0x00c4_ffff).unwrap();
|
||||
}
|
||||
bus.advance_cycles(u32::MAX);
|
||||
assert_eq!(bus.cycles(), u64::from(u32::MAX));
|
||||
for index in 0..4 {
|
||||
assert_eq!(bus.read16(TIMER_BASE + index * 4).unwrap(), 0xffff);
|
||||
}
|
||||
assert_eq!(bus.read16(IF).unwrap(), 0x78);
|
||||
bus.advance_cycles(u32::MAX);
|
||||
assert_eq!(bus.cycles(), 2 * u64::from(u32::MAX));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bulk_advance_matches_independent_cycle_by_cycle_reference() {
|
||||
// A small literal reference: count prescaler ticks and propagate one pulse
|
||||
// per cycle. Unlike the implementation, this does not use overflow division.
|
||||
for selection in 0..4 {
|
||||
for cascade_mask in 0..8 {
|
||||
let mut bus = memory();
|
||||
let reloads = [0xfffd_u16, 0xfffe, 0xffff, 0xfffc];
|
||||
let mut counters = reloads;
|
||||
let mut phases = [0_u32; 4];
|
||||
let mut pending = 0_u16;
|
||||
for (index, reload) in reloads.into_iter().enumerate() {
|
||||
let cascade = index > 0 && cascade_mask & (1 << (index - 1)) != 0;
|
||||
let control = 0xc0 | selection | if cascade { 4 } else { 0 };
|
||||
bus.write32(
|
||||
TIMER_BASE + index as u32 * 4,
|
||||
(control << 16) | u32::from(reload),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
for chunk in [0, 1, 17, 63, 1000, 8193] {
|
||||
bus.advance_cycles(chunk);
|
||||
for _ in 0..chunk {
|
||||
let mut overflow = false;
|
||||
for index in 0..4 {
|
||||
let cascade = index > 0 && cascade_mask & (1 << (index - 1)) != 0;
|
||||
let tick = if cascade {
|
||||
overflow
|
||||
} else {
|
||||
phases[index] += 1;
|
||||
if phases[index] == [1, 64, 256, 1024][selection as usize] {
|
||||
phases[index] = 0;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
overflow = tick && counters[index] == u16::MAX;
|
||||
if overflow {
|
||||
counters[index] = reloads[index];
|
||||
pending |= 1 << (index + 3);
|
||||
} else if tick {
|
||||
counters[index] += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (index, counter) in counters.into_iter().enumerate() {
|
||||
assert_eq!(bus.read16(TIMER_BASE + index as u32 * 4).unwrap(), counter);
|
||||
}
|
||||
assert_eq!(bus.read16(IF).unwrap(), pending);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interrupt_delivery_requires_both_ie_and_ime_without_clearing_if() {
|
||||
let mut bus = memory();
|
||||
bus.write32(TIMER_BASE, 0x00c0_ffff).unwrap();
|
||||
bus.advance_cycles(1);
|
||||
for (enable, master, expected) in [
|
||||
(0, 0, false),
|
||||
(8, 0, false),
|
||||
(0, 1, false),
|
||||
(16, 1, false),
|
||||
(8, 1, true),
|
||||
] {
|
||||
bus.write16(IE, enable).unwrap();
|
||||
bus.write32(IME, master).unwrap();
|
||||
assert_eq!(bus.irq_pending(), expected);
|
||||
assert_eq!(bus.read16(IF).unwrap(), 8);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interrupt_acknowledgement_is_write_one_to_clear_for_all_widths() {
|
||||
let mut bus = memory();
|
||||
for index in 0..4 {
|
||||
bus.write32(TIMER_BASE + index * 4, 0x00c0_ffff).unwrap();
|
||||
}
|
||||
bus.advance_cycles(1);
|
||||
bus.write16(IF, 0).unwrap();
|
||||
assert_eq!(bus.read16(IF).unwrap(), 0x78);
|
||||
bus.write8(IF + 1, 0xff).unwrap(); // High byte must not clear low flags
|
||||
assert_eq!(bus.read16(IF).unwrap(), 0x78);
|
||||
bus.write8(IF, 8).unwrap();
|
||||
assert_eq!(bus.read16(IF).unwrap(), 0x70);
|
||||
bus.write16(IF, 0x20).unwrap();
|
||||
assert_eq!(bus.read16(IF).unwrap(), 0x50);
|
||||
bus.write32(IE, 0x0010_0078).unwrap(); // IE=0x78, clear only Timer 1
|
||||
assert_eq!(bus.read32(IE).unwrap(), 0x0040_0078);
|
||||
bus.write16(IF, 0xffff).unwrap();
|
||||
assert_eq!(bus.read16(IF).unwrap(), 0);
|
||||
bus.advance_cycles(1);
|
||||
assert_eq!(bus.read16(IF).unwrap(), 0x78); // Later events relatch
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_byte_writes_do_not_reset_control_or_modify_low_enable_bits() {
|
||||
let mut bus = memory();
|
||||
bus.write32(TIMER_BASE, 0x0080_1234).unwrap();
|
||||
bus.write8(TIMER_BASE + 3, 0xff).unwrap();
|
||||
bus.write8(IE, 8).unwrap();
|
||||
bus.write8(IE + 1, 0xff).unwrap();
|
||||
assert_eq!(bus.read16(IE).unwrap(), 0x3f08);
|
||||
bus.write8(IME, 1).unwrap();
|
||||
for offset in 1..4 {
|
||||
bus.write8(IME + offset, 0xff).unwrap();
|
||||
}
|
||||
assert_eq!(bus.read32(IME).unwrap(), 1);
|
||||
bus.advance_cycles(1);
|
||||
assert_eq!(bus.read32(TIMER_BASE).unwrap(), 0x0080_1235);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_io_and_mirrors_stay_unmapped_and_unaligned_writes_have_no_effect() {
|
||||
let mut bus = memory();
|
||||
for address in [0x0400_0058, TIMER_BASE + 16, IE + 12, IME + 4, 0x0401_0100] {
|
||||
assert_eq!(bus.read8(address), Err(MemoryError::Unmapped(address)));
|
||||
assert_eq!(
|
||||
bus.write32(address, u32::MAX),
|
||||
Err(MemoryError::Unmapped(address))
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
bus.write32(TIMER_BASE + 2, u32::MAX),
|
||||
Err(MemoryError::Unaligned(TIMER_BASE + 2))
|
||||
);
|
||||
assert_eq!(
|
||||
bus.write16(IF + 1, u16::MAX),
|
||||
Err(MemoryError::Unaligned(IF + 1))
|
||||
);
|
||||
assert_eq!(bus.read32(TIMER_BASE).unwrap(), 0);
|
||||
assert_eq!(bus.read32(IE).unwrap(), 0);
|
||||
assert_eq!(bus.cycles(), 0);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue