fix(timers): retain shared free-running prescaler phase
This commit is contained in:
parent
bfecc87e84
commit
27806ccf9e
13 changed files with 459 additions and 32 deletions
|
|
@ -1,6 +1,6 @@
|
|||
//! GBA I/O subset: DMA, timers, interrupts, WAITCNT, display, and keypad input.
|
||||
//! Timer phase resets on enable or clock-source changes. Hardware startup delays,
|
||||
//! shared prescaler phase, audio events, and interrupt delivery delays are not modeled.
|
||||
//! Timers share a free-running prescaler phase. Hardware startup/write delays,
|
||||
//! audio events, and interrupt delivery delays are not modeled.
|
||||
|
||||
mod timer_step;
|
||||
pub(crate) use timer_step::TimerStep;
|
||||
|
|
@ -67,7 +67,6 @@ struct Timer {
|
|||
reload: u16,
|
||||
counter: u16,
|
||||
control: u8,
|
||||
phase: u32,
|
||||
}
|
||||
|
||||
impl Timer {
|
||||
|
|
@ -102,25 +101,22 @@ impl Timer {
|
|||
if start {
|
||||
self.counter = self.reload;
|
||||
}
|
||||
if start || (self.control ^ value) & 7 != 0 {
|
||||
self.phase = 0;
|
||||
}
|
||||
self.control = value;
|
||||
}
|
||||
|
||||
/// Next independently clocked overflow. Cascaded timers receive their
|
||||
/// pulses at a predecessor's event and cannot overflow earlier.
|
||||
fn next_overflow(&self) -> Option<u32> {
|
||||
fn next_overflow(&self, phase: u16) -> Option<u32> {
|
||||
if !self.enabled() || self.count_up() {
|
||||
return None;
|
||||
}
|
||||
let divisor = [1, 64, 256, 1024][usize::from(self.control & 3)];
|
||||
Some((0x1_0000 - u32::from(self.counter)) * divisor - self.phase)
|
||||
Some((0x1_0000 - u32::from(self.counter)) * divisor - u32::from(phase) % divisor)
|
||||
}
|
||||
|
||||
/// Advance without looping once per cycle or overflow. Return the number of
|
||||
/// overflows, not just a boolean, so cascaded timers receive every pulse.
|
||||
fn advance(&mut self, cycles: u32, previous_overflows: u64) -> u64 {
|
||||
fn advance(&mut self, cycles: u32, previous_overflows: u64, phase: u16) -> u64 {
|
||||
if !self.enabled() {
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -128,9 +124,9 @@ impl Timer {
|
|||
previous_overflows
|
||||
} else {
|
||||
let divisor = [1, 64, 256, 1024][usize::from(self.control & 3)];
|
||||
let elapsed = u64::from(self.phase) + u64::from(cycles);
|
||||
self.phase = (elapsed % divisor) as u32;
|
||||
elapsed / divisor
|
||||
// Count shared divider edges in (start, end], independently of enable time.
|
||||
let start = u64::from(phase);
|
||||
(start + u64::from(cycles)) / divisor - start / divisor
|
||||
};
|
||||
let until_overflow = 0x1_0000 - u64::from(self.counter);
|
||||
if ticks < until_overflow {
|
||||
|
|
@ -155,6 +151,8 @@ enum PowerState {
|
|||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct Io {
|
||||
timers: [Timer; 4],
|
||||
// Low ten system-clock bits cover every supported divider. STOP freezes this phase.
|
||||
timer_phase: u16,
|
||||
enable: u16,
|
||||
pending: u16,
|
||||
master_enable: bool,
|
||||
|
|
@ -320,7 +318,7 @@ impl Io {
|
|||
pub(crate) fn next_event_cycles(&self) -> u32 {
|
||||
self.timers
|
||||
.iter()
|
||||
.filter_map(Timer::next_overflow)
|
||||
.filter_map(|timer| timer.next_overflow(self.timer_phase))
|
||||
.fold(self.display.next_event_cycles(), u32::min)
|
||||
}
|
||||
|
||||
|
|
@ -404,19 +402,22 @@ impl Io {
|
|||
|
||||
pub(crate) fn commit_timer_step(&mut self, step: TimerStep) {
|
||||
self.timers = step.timers;
|
||||
self.timer_phase = step.phase;
|
||||
self.pending = (self.pending & !TIMER_IRQ_MASK) | step.pending;
|
||||
self.wake_if_requested();
|
||||
}
|
||||
|
||||
fn advance_timer_bank(timers: &mut [Timer; 4], cycles: u32) -> u16 {
|
||||
fn advance_timer_bank(timers: &mut [Timer; 4], phase: &mut u16, cycles: u32) -> u16 {
|
||||
let mut pending = 0;
|
||||
let mut overflows = 0;
|
||||
for (index, timer) in timers.iter_mut().enumerate() {
|
||||
overflows = timer.advance(cycles, overflows);
|
||||
overflows = timer.advance(cycles, overflows, *phase);
|
||||
if overflows != 0 && timer.control & 0x40 != 0 {
|
||||
pending |= 1 << (3 + index);
|
||||
}
|
||||
}
|
||||
// Advance even when every timer is disabled; writes never reset this clock.
|
||||
*phase = ((u64::from(*phase) + u64::from(cycles)) & 1023) as u16;
|
||||
pending
|
||||
}
|
||||
|
||||
|
|
@ -453,7 +454,8 @@ impl Io {
|
|||
self.dma.trigger(vblank, hblank);
|
||||
self.pending |= self.display.advance(cycles);
|
||||
if advance_timers {
|
||||
self.pending |= Self::advance_timer_bank(&mut self.timers, cycles);
|
||||
self.pending |=
|
||||
Self::advance_timer_bank(&mut self.timers, &mut self.timer_phase, cycles);
|
||||
}
|
||||
self.wake_if_requested();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ pub(super) const TIMER_IRQ_MASK: u16 = 0x78;
|
|||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) struct TimerStep {
|
||||
pub(super) timers: [Timer; 4],
|
||||
pub(super) phase: u16,
|
||||
pub(super) pending: u16,
|
||||
elapsed: u32,
|
||||
}
|
||||
|
|
@ -15,6 +16,7 @@ impl TimerStep {
|
|||
pub(super) fn new(io: &Io) -> Self {
|
||||
Self {
|
||||
timers: io.timers,
|
||||
phase: io.timer_phase,
|
||||
pending: io.pending & TIMER_IRQ_MASK,
|
||||
elapsed: 0,
|
||||
}
|
||||
|
|
@ -22,7 +24,8 @@ impl TimerStep {
|
|||
|
||||
pub(crate) fn advance_to(&mut self, elapsed: u32) {
|
||||
assert!(elapsed >= self.elapsed, "timer step time must be monotonic");
|
||||
self.pending |= Io::advance_timer_bank(&mut self.timers, elapsed - self.elapsed);
|
||||
self.pending |=
|
||||
Io::advance_timer_bank(&mut self.timers, &mut self.phase, elapsed - self.elapsed);
|
||||
self.elapsed = elapsed;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -322,6 +322,78 @@ fn bios_power_store_pays_its_cycles_and_new_timer_irq_wakes_only_halt() {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn machine_enable_joins_the_shared_phase_and_commits_it_without_duplicate_ticks() {
|
||||
let (cpu, mut memory) = prepared(
|
||||
false,
|
||||
&[0xe581_0000, 0xe591_2000],
|
||||
&[(0, 0x0081_0000), (1, TIMER_BASE)],
|
||||
);
|
||||
memory.advance_cycles(56); // The disabled divider still runs.
|
||||
let mut machine = Machine::new(cpu, memory);
|
||||
machine.step().unwrap(); // Source 6 + store 1: enable at t=63.
|
||||
assert_eq!(machine.cycles(), 63);
|
||||
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 0);
|
||||
machine.step().unwrap(); // N source 8 + read 1 + internal 1: t=73.
|
||||
assert_eq!(machine.cpu().registers()[2], 0x0081_0001);
|
||||
assert_eq!(machine.cycles(), 73);
|
||||
machine.memory_mut().advance_cycles(54);
|
||||
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 1);
|
||||
machine.memory_mut().advance_cycles(1);
|
||||
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_cpu_and_dma_work_cannot_consume_a_shared_prescaler_edge() {
|
||||
let (cpu, mut memory) = prepared(false, &[0xe891_0005], &[(1, TIMER_BASE + 12)]);
|
||||
start(&mut memory, 0, 0xffff, 0xc1);
|
||||
memory.advance_cycles(63);
|
||||
let before = memory.io.timer_step();
|
||||
let mut machine = Machine::new(cpu, memory);
|
||||
assert!(machine.step().is_err()); // First data read succeeds after the t=64 overflow; second fails.
|
||||
assert_eq!(machine.memory().io.timer_step(), before);
|
||||
assert_eq!(machine.cycles(), 63);
|
||||
dma(machine.memory_mut(), 0x0e00_0000, RAM, 1);
|
||||
assert!(machine.step().is_err());
|
||||
assert_eq!(machine.memory().io.timer_step(), before);
|
||||
machine.memory_mut().advance_cycles(1);
|
||||
assert_eq!(machine.memory().read16(IF).unwrap(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dma_timer_phases_share_the_divider_with_later_host_clock_advances() {
|
||||
let mut memory = Memory::new(Vec::new()).unwrap();
|
||||
memory.advance_cycles(60);
|
||||
start(&mut memory, 0, 0, 0x81);
|
||||
dma(&mut memory, TIMER_BASE, RAM, 1);
|
||||
memory.step_dma().unwrap().unwrap(); // Read t=63, destination t=64.
|
||||
assert_eq!(memory.read32(RAM).unwrap(), 0x0081_0000);
|
||||
assert_eq!(memory.read16(TIMER_BASE).unwrap(), 1);
|
||||
memory.write32(RAM, 0x0081_0000).unwrap();
|
||||
dma(&mut memory, RAM, TIMER_BASE + 4, 1);
|
||||
memory.step_dma().unwrap().unwrap(); // Enable timer1 at t=68, not at a new divider origin.
|
||||
memory.advance_cycles(59);
|
||||
assert_eq!(memory.cycles(), 127);
|
||||
assert_eq!(memory.read16(TIMER_BASE + 4).unwrap(), 0);
|
||||
memory.advance_cycles(1);
|
||||
assert_eq!(memory.read16(TIMER_BASE).unwrap(), 2);
|
||||
assert_eq!(memory.read16(TIMER_BASE + 4).unwrap(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn halt_after_a_machine_timer_start_uses_the_committed_shared_phase() {
|
||||
let (cpu, mut memory) = prepared(false, &[0xe581_0000], &[(0, 0x00c1_ffff), (1, TIMER_BASE)]);
|
||||
memory.advance_cycles(56);
|
||||
memory.write16(IE, 8).unwrap();
|
||||
let mut machine = Machine::new(cpu, memory);
|
||||
machine.step().unwrap(); // Enable at t=63.
|
||||
machine.memory_mut().write8(HALTCNT, 0).unwrap();
|
||||
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
|
||||
assert_eq!(machine.last_timing().idle_cycles, 1);
|
||||
assert_eq!(machine.cycles(), 64);
|
||||
assert!(!machine.halted());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn halt_and_stop_keep_their_clock_ownership() {
|
||||
for stop in [false, true] {
|
||||
|
|
|
|||
230
crates/gba-core/tests/timer_prescaler.rs
Normal file
230
crates/gba-core/tests/timer_prescaler.rs
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
//! Original shared-divider checks. Startup and register-write delays remain outside this model.
|
||||
use gba_core::{
|
||||
cpu::Cpu,
|
||||
input::Buttons,
|
||||
io::{HALTCNT, IE, IF, KEYCNT, TIMER_BASE},
|
||||
machine::{Machine, StepKind},
|
||||
memory::{Memory, ROM_START},
|
||||
};
|
||||
|
||||
const DIVISORS: [u64; 4] = [1, 64, 256, 1024];
|
||||
|
||||
fn start(memory: &mut Memory, timer: u32, reload: u16, control: u16) {
|
||||
memory
|
||||
.write32(
|
||||
TIMER_BASE + timer * 4,
|
||||
u32::from(reload) | u32::from(control) << 16,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_shared_phase_selects_the_next_edge_without_replaying_the_current_edge() {
|
||||
for phase in 0..1024_u32 {
|
||||
let mut memory = Memory::new(Vec::new()).unwrap();
|
||||
memory.advance_cycles(phase); // The divider runs with all timers disabled.
|
||||
for selection in 0..4 {
|
||||
start(&mut memory, selection, 0, 0x80 | selection as u16);
|
||||
}
|
||||
memory.advance_cycles(0);
|
||||
for selection in 0..4 {
|
||||
assert_eq!(memory.read16(TIMER_BASE + selection * 4).unwrap(), 0);
|
||||
}
|
||||
// Cover two complete divider periods after every possible enable phase.
|
||||
let mut elapsed = 0;
|
||||
for delta in [1, 62, 1, 191, 1, 767, 1, 1024] {
|
||||
memory.advance_cycles(delta);
|
||||
elapsed += delta;
|
||||
for (selection, divisor) in DIVISORS.into_iter().enumerate() {
|
||||
let expected = (u64::from(phase + elapsed) / divisor) - u64::from(phase) / divisor;
|
||||
assert_eq!(
|
||||
memory.read16(TIMER_BASE + selection as u32 * 4).unwrap(),
|
||||
expected as u16,
|
||||
"phase={phase}, elapsed={elapsed}, divisor={divisor}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn staggered_timers_share_ticks_and_cascade_ignores_prescaler_selection() {
|
||||
let mut memory = Memory::new(Vec::new()).unwrap();
|
||||
memory.advance_cycles(10);
|
||||
start(&mut memory, 0, 0xfffe, 0xc1);
|
||||
memory.advance_cycles(15);
|
||||
start(&mut memory, 1, 0xfffe, 0xc1);
|
||||
memory.advance_cycles(38);
|
||||
start(&mut memory, 2, 0xfffe, 0xc1);
|
||||
start(&mut memory, 3, 0xffff, 0xc7); // Cascaded, /1024 field ignored.
|
||||
memory.advance_cycles(1); // All independent timers tick at system cycle 64.
|
||||
for index in 0..3 {
|
||||
assert_eq!(memory.read16(TIMER_BASE + index * 4).unwrap(), 0xffff);
|
||||
}
|
||||
assert_eq!(memory.read16(IF).unwrap(), 0);
|
||||
memory.advance_cycles(64);
|
||||
for index in 0..3 {
|
||||
assert_eq!(memory.read16(TIMER_BASE + index * 4).unwrap(), 0xfffe);
|
||||
}
|
||||
assert_eq!(memory.read16(TIMER_BASE + 12).unwrap(), 0xffff);
|
||||
assert_eq!(memory.read16(IF).unwrap(), 0x78);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leaving_cascade_joins_the_existing_divider_instead_of_starting_a_new_period() {
|
||||
let mut memory = Memory::new(Vec::new()).unwrap();
|
||||
start(&mut memory, 1, 0, 0x85); // Timer0 disabled: no cascade pulses.
|
||||
memory.advance_cycles(63);
|
||||
memory.write16(TIMER_BASE + 6, 0x81).unwrap();
|
||||
memory.advance_cycles(1);
|
||||
assert_eq!(memory.read16(TIMER_BASE + 4).unwrap(), 1);
|
||||
memory.write16(TIMER_BASE + 6, 0x85).unwrap();
|
||||
memory.advance_cycles(63);
|
||||
memory.write16(TIMER_BASE + 6, 0x81).unwrap();
|
||||
memory.advance_cycles(1);
|
||||
assert_eq!(memory.read16(TIMER_BASE + 4).unwrap(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn halt_stops_at_the_shared_edge_and_stop_freezes_the_divider_until_keypad_wake() {
|
||||
let mut memory = Memory::new(Vec::new()).unwrap();
|
||||
memory.advance_cycles(63);
|
||||
start(&mut memory, 0, 0xffff, 0xc1);
|
||||
memory.write16(IE, 8).unwrap();
|
||||
memory.write8(HALTCNT, 0).unwrap();
|
||||
let mut machine = Machine::new(Cpu::new(ROM_START), memory);
|
||||
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
|
||||
assert_eq!(machine.last_timing().idle_cycles, 1);
|
||||
assert_eq!(machine.cycles(), 64);
|
||||
assert_eq!(machine.memory().read16(IF).unwrap(), 8);
|
||||
assert!(!machine.halted());
|
||||
|
||||
let mut memory = Memory::new(Vec::new()).unwrap();
|
||||
memory.advance_cycles(63);
|
||||
start(&mut memory, 0, 0, 0x81);
|
||||
memory.write16(IE, 0x1000).unwrap();
|
||||
memory.write16(KEYCNT, 0x4001).unwrap();
|
||||
memory.write8(HALTCNT, 0x80).unwrap();
|
||||
memory.advance_cycles(1000);
|
||||
assert_eq!(memory.cycles(), 63);
|
||||
memory.set_buttons(Buttons::from_bits(1));
|
||||
assert!(!memory.stopped());
|
||||
memory.advance_cycles(1);
|
||||
assert_eq!(memory.cycles(), 64);
|
||||
assert_eq!(memory.read16(TIMER_BASE).unwrap(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maximum_batches_keep_low_divider_bits_without_per_cycle_iteration() {
|
||||
let mut memory = Memory::new(Vec::new()).unwrap();
|
||||
memory.advance_cycles(u32::MAX); // Low ten bits are all set, even with no enabled timer.
|
||||
start(&mut memory, 0, 0, 0x83);
|
||||
memory.advance_cycles(1);
|
||||
assert_eq!(memory.read16(TIMER_BASE).unwrap(), 1);
|
||||
memory.advance_cycles(u32::MAX); // 4,194,303 ticks, wrapping the 16-bit counter to zero.
|
||||
assert_eq!(memory.read16(TIMER_BASE).unwrap(), 0);
|
||||
memory.advance_cycles(1);
|
||||
assert_eq!(memory.read16(TIMER_BASE).unwrap(), 1);
|
||||
assert_eq!(memory.cycles(), 2 * (u64::from(u32::MAX) + 1));
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default)]
|
||||
struct ReferenceTimer {
|
||||
counter: u16,
|
||||
reload: u16,
|
||||
control: u16,
|
||||
}
|
||||
|
||||
struct Reference {
|
||||
timers: [ReferenceTimer; 4],
|
||||
clock: u64,
|
||||
pending: u16,
|
||||
}
|
||||
|
||||
impl Reference {
|
||||
fn control(&mut self, index: usize, value: u16) {
|
||||
let timer = &mut self.timers[index];
|
||||
let value = value & if index == 0 { 0xc3 } else { 0xc7 };
|
||||
if timer.control & 0x80 == 0 && value & 0x80 != 0 {
|
||||
timer.counter = timer.reload;
|
||||
}
|
||||
timer.control = value;
|
||||
}
|
||||
|
||||
fn advance(&mut self, cycles: u32) {
|
||||
// Independent literal clock-edge simulation. No phase accumulator or bulk division.
|
||||
for _ in 0..cycles {
|
||||
self.clock += 1;
|
||||
let mut overflow = false;
|
||||
for (index, timer) in self.timers.iter_mut().enumerate() {
|
||||
let tick = timer.control & 0x80 != 0
|
||||
&& if timer.control & 4 != 0 {
|
||||
overflow
|
||||
} else {
|
||||
self.clock
|
||||
.is_multiple_of(DIVISORS[usize::from(timer.control & 3)])
|
||||
};
|
||||
overflow = tick && timer.counter == 0xffff;
|
||||
if overflow {
|
||||
timer.counter = timer.reload;
|
||||
if timer.control & 0x40 != 0 {
|
||||
self.pending |= 1 << (index + 3);
|
||||
}
|
||||
} else if tick {
|
||||
timer.counter += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dynamic_control_reload_and_clock_sequences_match_independent_edge_simulation() {
|
||||
for mut seed in [0x1020_3040_u32, 0xdead_beef, 0x7654_3210] {
|
||||
let mut memory = Memory::new(Vec::new()).unwrap();
|
||||
let mut reference = Reference {
|
||||
timers: [ReferenceTimer::default(); 4],
|
||||
clock: 0,
|
||||
pending: 0,
|
||||
};
|
||||
for _ in 0..2000 {
|
||||
seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
|
||||
let index = ((seed >> 8) & 3) as usize;
|
||||
let address = TIMER_BASE + index as u32 * 4;
|
||||
match seed >> 29 {
|
||||
0 | 1 => {
|
||||
let value = ((seed >> 16) as u16 & 0xc7) | 0x80;
|
||||
memory.write16(address + 2, value).unwrap();
|
||||
reference.control(index, value);
|
||||
}
|
||||
2 => {
|
||||
memory.write16(address + 2, 0).unwrap();
|
||||
reference.control(index, 0);
|
||||
}
|
||||
3 => {
|
||||
let reload = 0xfff0 | ((seed >> 16) as u16 & 15);
|
||||
memory.write16(address, reload).unwrap();
|
||||
reference.timers[index].reload = reload;
|
||||
}
|
||||
4 => {
|
||||
let mask = (seed >> 16) as u16 & 0x78;
|
||||
memory.write16(IF, mask).unwrap();
|
||||
reference.pending &= !mask;
|
||||
}
|
||||
_ => {
|
||||
let cycles = (seed >> 16) & 2047;
|
||||
memory.advance_cycles(cycles);
|
||||
reference.advance(cycles);
|
||||
}
|
||||
}
|
||||
for (index, timer) in reference.timers.iter().enumerate() {
|
||||
assert_eq!(
|
||||
memory.read32(TIMER_BASE + index as u32 * 4).unwrap(),
|
||||
u32::from(timer.counter) | u32::from(timer.control) << 16
|
||||
);
|
||||
}
|
||||
assert_eq!(memory.read16(IF).unwrap(), reference.pending);
|
||||
assert_eq!(memory.cycles(), reference.clock);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -100,7 +100,7 @@ fn all_prescalers_retain_fractional_cycles_between_advances() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn clock_source_changes_and_restart_reset_provisional_phase() {
|
||||
fn clock_source_changes_and_restart_keep_the_shared_phase() {
|
||||
let mut bus = memory();
|
||||
bus.write32(TIMER_BASE, 0x0081_0000).unwrap(); // /64
|
||||
bus.advance_cycles(63);
|
||||
|
|
@ -108,16 +108,17 @@ fn clock_source_changes_and_restart_reset_provisional_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);
|
||||
bus.write16(TIMER_BASE + 2, 0x82).unwrap(); // t=127: next shared /256 edge is t=256.
|
||||
bus.advance_cycles(128);
|
||||
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.advance_cycles(255); // t=511; disabled time still advances the divider.
|
||||
bus.write16(TIMER_BASE + 2, 0x82).unwrap();
|
||||
bus.advance_cycles(255);
|
||||
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0);
|
||||
bus.advance_cycles(1);
|
||||
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ The repository is a Cargo workspace with three crates:
|
|||
| `crates/gba-core/src/cpu/fetch_history_tests.rs` | Shared sample consumption, no execution-entry rereads, and failed-step retry |
|
||||
| `crates/gba-core/src/cpu/fetch_boundary_tests.rs` | Actual fetch-region snapshots, split refills, IWRAM lane requirements, ROM-window boundaries, and nominal timing |
|
||||
| `crates/gba-core/src/io.rs` | I/O registers, timers, HALT/STOP wake-up, next-event bounds, and IRQ latches |
|
||||
| `crates/gba-core/src/io/timer_step.rs` | Staged timer bank and timer IF bits at ordered CPU/DMA bus phases |
|
||||
| `crates/gba-core/src/io/timer_step.rs` | Staged timer bank, shared divider phase, and timer IF bits at ordered CPU/DMA bus phases |
|
||||
| `crates/gba-core/src/memory/timer_step_tests.rs` | Timer access phases, coherent lanes, cascades, IF ordering, rollback, DMA, idle, and clock ownership |
|
||||
| `crates/gba-core/src/display.rs` | Display clock, scanline status, comparison edges, and display IRQ events |
|
||||
| `crates/gba-core/src/dma.rs` | DMA registers, internal pointers, retained channel data, trigger state, priority, and completion IRQs |
|
||||
|
|
@ -132,6 +132,7 @@ The repository is a Cargo workspace with three crates:
|
|||
| `crates/gba-core/tests/timers.rs` | Timer rules, interrupt registers, and a cycle-by-cycle reference |
|
||||
| `crates/gba-core/tests/machine.rs` | Device IRQ entry/return, clock policy, and I/O failure atomicity |
|
||||
| `crates/gba-core/tests/timing.rs` | WAITCNT fields, bus costs, and timer/IRQ timing integration |
|
||||
| `crates/gba-core/tests/timer_prescaler.rs` | Exhaustive divider phases, staggered starts, control/cascade transitions, independent edge simulation, large batches, and idle ownership |
|
||||
| `crates/gba-core/tests/keypad.rs` | Keypad register widths, IRQ sampling, CPU/DMA ordering, HALT wake-up, diagnostics, and BIOS IntrWait |
|
||||
| `crates/gba-core/tests/graphics.rs` | Video memory, rendering, KEYINPUT, and CPU-driven graphics integration |
|
||||
| `crates/gba-core/tests/tiles.rs` | Mode 0 registers, maps, palettes, composition, diagnostics, and CPU tile/sprite demo |
|
||||
|
|
|
|||
|
|
@ -422,4 +422,4 @@ Important timing limits:
|
|||
- Game Pak prefetch is nominal. Full-buffer restart, page boundaries, cancellation, and live WAITCNT changes need independent hardware validation.
|
||||
- PHI and SRAM wait fields are stored; PHI output and SRAM mapping are not implemented.
|
||||
- External work RAM timing is fixed. The undocumented memory-control register is not implemented.
|
||||
- Exact DMA startup/resumption delays, display-bus contention, shared timer prescaler phase, and timer startup delays remain unmodeled.
|
||||
- Exact DMA startup/resumption delays, display-bus contention, and timer startup/register-write delays remain unmodeled.
|
||||
|
|
|
|||
|
|
@ -34,7 +34,8 @@ The transaction also owns timer IF bits. Reads see overflows through the current
|
|||
A later internal cycle can raise an acknowledged timer request again. Non-timer IF bits retain their existing behavior.
|
||||
IRQ delivery still waits until the next machine step. A committed timer request can wake HALT, but not STOP.
|
||||
|
||||
Successful steps commit timers and timer IF bits. Failed steps discard staged counters, prescaler phases, reload/control writes, and requests.
|
||||
Successful steps commit timers, the shared divider phase, and timer IF bits.
|
||||
Failed steps discard staged counters, divider progress, reload/control writes, and requests.
|
||||
Display and other device clocks then advance once, without advancing timers again.
|
||||
Those devices still use the bulk scheduler. Mid-instruction display events, general bus arbitration, and IRQ synchronization delays remain unmodeled.
|
||||
Host clock advances and HALT idle batches retain ordinary bulk timer advancement; STOP idle does not advance timers.
|
||||
|
|
@ -81,8 +82,16 @@ Entering IRQ mode does not clear IF. Software must acknowledge the request.
|
|||
Unused register bits read as zero. Unknown I/O addresses and mirrors remain unmapped.
|
||||
Byte, halfword, and word accesses are supported for this register subset.
|
||||
|
||||
Timer startup delays and shared prescaler phase are not modeled.
|
||||
The provisional timer model resets its private prescaler phase on enable or clock-source changes.
|
||||
Independent timers share a free-running divider with a synthetic zero phase at reset.
|
||||
The divider advances even when all timers are disabled. Enable, restart, and clock-source changes do not reset it.
|
||||
A timer receives selected divider edges after the start of an interval and through its end.
|
||||
Changing out of cascade mode uses the existing shared phase; cascade mode ignores the divider selection.
|
||||
HALT next-event bounds use that phase. STOP freezes it, and wake resumes without resetting it.
|
||||
CPU/DMA timer transactions stage divider progress and commit it only on success, without a second bulk tick.
|
||||
See [prescaler evidence and original tests](../research/timer-prescaler.md).
|
||||
|
||||
Hardware startup and register-write delays are still not modeled. Writes retain the immediate bus-completion policy.
|
||||
The shared-phase tests validate this bounded model, not absolute enable/reload edges on hardware.
|
||||
Audio events and interrupt sources other than timers, display events, DMA completion, and keypad input are not implemented.
|
||||
|
||||
`--timer-demo` executes original ARM code that configures Timer 0, IE, and IME.
|
||||
|
|
@ -207,7 +216,7 @@ A later running-state input sample follows the normal keypad IRQ policy.
|
|||
Host/debug writes to IE or KEYCNT can also satisfy the wake condition; stopped CPU code cannot execute such writes.
|
||||
|
||||
After wake-up, ready DMA again takes priority, then IRQ delivery or CPU execution resumes.
|
||||
Timer prescaler phases, display position, RAM, and completed captured frames are retained, not reset.
|
||||
The shared timer divider phase, display position, RAM, and completed captured frames are retained, not reset.
|
||||
STOP does not automatically set forced blank or clear DISPCNT.
|
||||
Software can force blank before STOP if it wants the displayed image disabled.
|
||||
|
||||
|
|
|
|||
|
|
@ -223,6 +223,14 @@ Ordered timer data accesses:
|
|||
|
||||
See [staged timer clock ownership](research/ordered-timer-access.md). Relative probe agreement does not establish all absolute timer edges.
|
||||
|
||||
Shared timer prescaler phase:
|
||||
|
||||
- [Jgenesis timers at fab6e2cc](https://github.com/jsgroth/jgenesis/blob/fab6e2ccc60e492dd68b7f1e927b0829a6d80195/backend/gba-core/src/timers.rs), for absolute-clock divider edges and separate pending-write/enable state.
|
||||
- [NanoBoyAdvance timer logic at 55b5cf0a](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/hw/timer/timer.cc) and [channel definitions](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/hw/timer/timer.hh), for shared timestamp alignment and startup overflow corner cases.
|
||||
- Search leads: [VBA-M prescaler change](https://github.com/visualboyadvance-m/visualboyadvance-m/commit/92154dcd2ef7fca8c8afa8fced2f350ad2d2452a) and [mGBA timer suite](https://github.com/mgba-emu/suite/blob/04ada216ee13c56d786e54636ac980a71d791145/src/timers.c). Neither was used as a local hardware-test oracle.
|
||||
|
||||
See [shared divider scope and tests](research/timer-prescaler.md). The correction does not implement delayed register writes or startup edges.
|
||||
|
||||
Use [GBATEK](https://problemkaputt.de/gbatek.htm) and ARM7TDMI documentation for further hardware work.
|
||||
Only use game ROMs that you may lawfully use. Do not commit game ROMs, BIOS files, or game assets.
|
||||
The ignore file excludes `roms/`, common game ROM extensions, and save files.
|
||||
|
|
|
|||
|
|
@ -28,7 +28,8 @@ Those remain explicit validation limits.
|
|||
## Staged clock ownership
|
||||
|
||||
`Memory` owns an optional `TimerStep` during each machine instruction, IRQ entry, or DMA unit.
|
||||
The transaction copies only the four timers and timer IF bits, not RAM, display state, or the entire I/O object.
|
||||
The transaction copies the four timers, shared divider phase, and timer IF bits, not RAM or the entire I/O object.
|
||||
The later [shared-prescaler correction](timer-prescaler.md) replaced private timer remainders with this common divider.
|
||||
Its elapsed timestamp starts at zero and increases monotonically.
|
||||
It has no heap allocation or event log.
|
||||
|
||||
|
|
@ -44,7 +45,7 @@ One word access has one sampling time for all four byte lanes.
|
|||
Reload bytes merge before control bytes at that same time.
|
||||
Block transfers have separate completion times for successive words.
|
||||
A start write cannot count earlier source cycles. A stop write includes clocks through its completion.
|
||||
Prescaler remainders, cascaded overflow pulses, and reload behavior use the existing timer implementation.
|
||||
Shared divider edges, cascaded overflow pulses, and reload behavior use the timer-bank implementation.
|
||||
|
||||
Timer IF reads include requests raised through the access phase.
|
||||
Acknowledgement clears timer requests already present at that phase; later work can raise them again.
|
||||
|
|
@ -63,7 +64,7 @@ This extends the existing whole-unit scheduler; it does not establish physical D
|
|||
|
||||
### Errors and API isolation
|
||||
|
||||
CPU diagnostics discard staged time, counter values, prescaler remainders, and pending timer requests.
|
||||
CPU diagnostics discard staged time, counter values, shared divider progress, and pending timer requests.
|
||||
This includes failures before instruction fetch and failures after earlier block-load accesses.
|
||||
Existing store validation still prevents partial writes when a block transfer has an invalid destination.
|
||||
Previously completed machine steps remain committed.
|
||||
|
|
|
|||
94
docs/research/timer-prescaler.md
Normal file
94
docs/research/timer-prescaler.md
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
# Shared timer prescaler phase
|
||||
|
||||
## Result and scope
|
||||
|
||||
All four timers now derive independent-clock ticks from one free-running system-clock divider.
|
||||
Enabling, disabling, restarting, or changing a timer's clock source does not reset that divider.
|
||||
This replaces the previous per-timer phase, which restarted on enable or clock-source changes.
|
||||
|
||||
The correction covers divider ownership and edge selection in the current immediate-write timer model.
|
||||
It does **not** add hardware startup delays, delayed register writes, or IRQ synchronization delays.
|
||||
Those behaviors need separate edge tests; adding a fixed startup delay alone would be incomplete.
|
||||
No physical-hardware measurement or external-emulator differential run was performed locally.
|
||||
|
||||
## Evidence reviewed
|
||||
|
||||
- [Jgenesis timers at fab6e2cc](https://github.com/jsgroth/jgenesis/blob/fab6e2ccc60e492dd68b7f1e927b0829a6d80195/backend/gba-core/src/timers.rs) derive ticks from absolute clock values shifted by the selected divisor.
|
||||
Pending register writes and newly enabled reloads have separate handling.
|
||||
- [NanoBoyAdvance timers at 55b5cf0a](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/hw/timer/timer.cc) derive prescaler alignment from the scheduler timestamp.
|
||||
Its [channel definitions](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/hw/timer/timer.hh) separate pending writes from active timer control.
|
||||
The implementation also handles a possible overflow from an old `0xffff` counter during enable/reload timing.
|
||||
|
||||
Both implementations use a common clock origin rather than a private period restarted for each timer.
|
||||
Their startup handling also shows why divider phase and enable delay must be treated separately.
|
||||
The current correction uses the shared-clock rule without claiming their complete transition behavior.
|
||||
No implementation code was imported.
|
||||
|
||||
Search also identified a [VBA-M prescaler change](https://github.com/visualboyadvance-m/visualboyadvance-m/commit/92154dcd2ef7fca8c8afa8fced2f350ad2d2452a)
|
||||
and the [mGBA timer suite](https://github.com/mgba-emu/suite/blob/04ada216ee13c56d786e54636ac980a71d791145/src/timers.c).
|
||||
These remain follow-up leads, not local test passes or implementation oracles.
|
||||
|
||||
## Implemented divider
|
||||
|
||||
Synthetic reset starts the shared phase at zero.
|
||||
Only the low ten system-clock bits are needed for divisors 1, 64, 256, and 1024.
|
||||
The phase advances with emulated time even when all timers are disabled or cascaded.
|
||||
It does not depend on host wall time or display-frame position.
|
||||
|
||||
For an interval from clock `start` to clock `end`, an independently clocked timer receives:
|
||||
|
||||
```text
|
||||
ticks = floor(end / divisor) - floor(start / divisor)
|
||||
```
|
||||
|
||||
This counts divider edges after the starting timestamp and through the ending timestamp.
|
||||
An enable at an already completed edge does not replay that edge.
|
||||
A clock-source change selects another output of the same divider, not a new clock origin.
|
||||
Leaving cascade mode resumes from that existing divider position.
|
||||
Cascade mode still counts predecessor overflows and ignores the prescaler selection field.
|
||||
|
||||
The core counts ticks and overflow pulses in bulk without iterating once per cycle.
|
||||
Wide arithmetic handles maximum `u32` clock batches before retaining the low divider bits.
|
||||
Overflow scheduling subtracts the current shared remainder, so HALT can stop at the next actual divider edge.
|
||||
|
||||
For example, under the current immediate-enable policy, a /64 timer enabled at clock 63 ticks at clock 64.
|
||||
The old private-phase model incorrectly waited until clock 127.
|
||||
This example defines the implemented no-delay subset, not a claim about an enable write adjacent to a hardware startup edge.
|
||||
|
||||
## Clock ownership and rollback
|
||||
|
||||
The divider belongs to the timer bank, not an individual timer.
|
||||
Each machine `TimerStep` copies the shared phase with the counters and timer IF bits.
|
||||
CPU and DMA bus phases advance that copy. Success commits it; a failed step discards it.
|
||||
Other devices' bulk updates do not advance the committed divider again.
|
||||
|
||||
Host clock advances and HALT idle periods advance the same divider.
|
||||
STOP freezes it, and keypad wake resumes the retained phase.
|
||||
Host register access and CPU-only stepping do not advance time or reset the divider.
|
||||
See [ordered timer accesses](ordered-timer-access.md) for transaction and API ownership.
|
||||
|
||||
## Original regression coverage
|
||||
|
||||
Tests exercise every low-ten-bit phase and all divider selections, including zero-cycle advances and completed-edge enables.
|
||||
Staggered timer starts verify shared edges. Clock-source changes, restarts, and cascade transitions preserve the common origin.
|
||||
A literal cycle-by-cycle reference checks randomized control, reload, acknowledgement, and clock sequences.
|
||||
It uses absolute edge tests, not the implementation's phase accumulator or bulk overflow division.
|
||||
Maximum-size batches check bounded progress and divider wrap.
|
||||
|
||||
Machine tests cross a shared edge during CPU stores, timer loads, and DMA source/destination phases.
|
||||
Failed CPU and DMA steps must not consume that edge or retain its IRQ request.
|
||||
HALT after a machine timer start must use the committed phase for its next-event bound.
|
||||
STOP/keypad tests verify that stopped time does not move the divider.
|
||||
|
||||
## Validation and remaining work
|
||||
|
||||
Workspace/core/demo tests pass in debug and release on Darwin arm64.
|
||||
Formatting, clippy, rustdoc, Python tests, native ROM windows, and all graphics smoke modes pass.
|
||||
The release executable remains native Mach-O arm64.
|
||||
Public ARM, Thumb, memory, and BIOS reports remain byte-identical to the preceding ordered-timer change.
|
||||
The timer demo trace and both profiles of the cancellation probe are unchanged.
|
||||
The probe still matches all its published observations, but it uses divisor one and does not validate prescaler startup edges.
|
||||
|
||||
Next, use independent timer-edge observations to define delayed reload/control writes and enable behavior.
|
||||
Include overflow from the previous counter, cascade transitions, same-cycle reads, and IRQ timing before adding startup delays.
|
||||
Full-buffer prefetch restart and page-boundary validation remain separate work.
|
||||
|
|
@ -35,6 +35,7 @@ Update this file when a feature lands or a limit is removed.
|
|||
- CPU interrupt entry, interrupt masks, and FIQ priority.
|
||||
- GBA interrupt enable (`IE`), request flags (`IF`), and master enable (`IME`) registers.
|
||||
- Four 16-bit timers, with reloads, prescalers, count-up cascading, and timer-generated IRQs.
|
||||
- A shared free-running timer divider survives enable and clock-source changes; HALT advances it and STOP freezes it.
|
||||
- A 228-line display clock, VCOUNT/DISPSTAT, and VBlank/HBlank/VCount-match interrupts.
|
||||
- Four direct memory access (DMA) channels, with immediate, VBlank, and visible-line HBlank triggers.
|
||||
- Halfword/word DMA, address controls, repeat, channel priority, CPU pausing, and completion IRQs.
|
||||
|
|
@ -175,7 +176,8 @@ Writes to cartridge addresses return errors instead of modeling cartridge hardwa
|
|||
The desktop runner still uses only the original BIOS. Keep protected reads image-derived for all supplied images.
|
||||
2. Validate the active nominal Game Pak prefetch queue against independent timing tests.
|
||||
The [cancellation probe](research/prefetch-cancellation.md) now matches after [ordered timer accesses](research/ordered-timer-access.md).
|
||||
Validate absolute timer startup/control edges and extend ordered observations to other devices without duplicate clocks or lost rollback.
|
||||
The [shared timer prescaler](research/timer-prescaler.md) now retains a common clock origin across enable and clock-source changes.
|
||||
Validate delayed timer startup/control edges and extend ordered observations to other devices without duplicate clocks or lost rollback.
|
||||
Validate full-buffer restart, page boundaries, live WAITCNT transitions, and cold-start costs.
|
||||
See [prefetch behavior and evidence](research/gamepak-prefetch.md); exact bus arbitration and timer/IRQ delays remain separate work.
|
||||
3. Validate keypad retrigger behavior with hardware tests and add remaining device registers; extend BIOS reset coverage as sound/serial support becomes available.
|
||||
|
|
|
|||
|
|
@ -288,6 +288,10 @@ They verify timer IF reads/acknowledgements, preservation of other IF bits, defe
|
|||
Failures before fetch and after partial data work must discard staged time and requests. CPU-only APIs must not advance timer devices.
|
||||
Display-capture splits must not tick committed timers a second time. See [ordered timer accesses](research/ordered-timer-access.md).
|
||||
Bulk timer advancement is compared against an independent cycle-by-cycle reference for each prescaler and cascade configuration.
|
||||
Shared-divider tests cover every low-ten-bit phase, staggered enables, source changes, restarts, and cascade transitions.
|
||||
A separate literal edge simulation checks randomized control/reload/clock sequences without a per-timer phase accumulator.
|
||||
Maximum clock batches, HALT bounds, STOP/keypad retention, and CPU/DMA rollback across divider edges have original regressions.
|
||||
See [shared timer prescaler scope](research/timer-prescaler.md); delayed write and startup behavior remains unverified.
|
||||
Machine tests cover ARM/Thumb IRQ return, pending-request gating, acknowledgement, and the complete timer demo.
|
||||
Failed I/O block transfers are checked for partial register writes, hidden reload changes, and unintended IF acknowledgement.
|
||||
Status tests cover all modes, flag combinations, interrupt masks, field selection, privilege rules, and bank isolation.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue