feat(cpu): retain fetch access kinds across instruction execution
This commit is contained in:
parent
2d7eb6e4eb
commit
86321f0071
21 changed files with 637 additions and 61 deletions
|
|
@ -42,6 +42,8 @@ mod fetch_boundary_tests;
|
|||
#[cfg(test)]
|
||||
mod fetch_history_tests;
|
||||
#[cfg(test)]
|
||||
mod fetch_sequence_tests;
|
||||
#[cfg(test)]
|
||||
mod fetch_timing_tests;
|
||||
#[cfg(test)]
|
||||
mod instruction_tests;
|
||||
|
|
@ -176,7 +178,7 @@ impl InstructionSet {
|
|||
}
|
||||
|
||||
/// ARM/Thumb interpreter with register banks, instruction buffering, and nominal costs.
|
||||
/// Clone/equality include buffered fetch results and their instruction-set tag.
|
||||
/// Clone/equality include buffered fetch results, instruction state, and next fetch kind.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Cpu {
|
||||
registers: [u32; 16],
|
||||
|
|
@ -242,7 +244,7 @@ impl Cpu {
|
|||
) -> Result<(), CpuError> {
|
||||
let pipeline::Fetched {
|
||||
instruction,
|
||||
continuation,
|
||||
mut continuation,
|
||||
lookahead,
|
||||
fill,
|
||||
} = fetched;
|
||||
|
|
@ -250,14 +252,15 @@ impl Cpu {
|
|||
// Always clear the access context, including on diagnostic errors.
|
||||
let pc = self.pc();
|
||||
let instruction_set = self.instruction_set;
|
||||
let refill = self.instruction_refills(instruction);
|
||||
let summary = self.instruction_summary(instruction);
|
||||
memory.begin_cpu_access(pc, self.instruction_set, fill.as_ref(), &lookahead);
|
||||
let result = self.execute_instruction(instruction, memory);
|
||||
memory.end_cpu_access(result.is_ok());
|
||||
if result.is_ok() {
|
||||
if refill {
|
||||
if summary.refill {
|
||||
self.refill_pipeline(memory);
|
||||
} else {
|
||||
continuation.next_access = summary.next_fetch_kind();
|
||||
// A state change without an ordinary refill starts a cold sequence.
|
||||
self.pipeline = if self.instruction_set == instruction_set
|
||||
&& self.pc() == pc.wrapping_add(instruction_set.width())
|
||||
|
|
|
|||
|
|
@ -165,6 +165,8 @@ fn failed_cpu_fetch_and_data_access_preserve_resume_until_success() {
|
|||
assert_eq!(timing.code_cycles, 8);
|
||||
assert_eq!(timing.data_cycles, 6);
|
||||
assert_eq!(timing.internal_cycles, 1);
|
||||
// The successful load creates a new sequence break after consuming DMA resume.
|
||||
assert_eq!(cpu.step_timed(&mut bus).unwrap().code_cycles, 8);
|
||||
assert_eq!(cpu.step_timed(&mut bus).unwrap().code_cycles, 6);
|
||||
}
|
||||
|
||||
|
|
@ -191,7 +193,7 @@ fn host_accesses_and_clock_advances_do_not_consume_resume_but_untimed_cpu_does()
|
|||
fn skipped_conditions_stores_and_boundaries_consume_resume_without_double_charging() {
|
||||
for (pc, instruction, data) in [
|
||||
(PC, 0x0591_0000, 0), // LDREQ skipped: Z is clear.
|
||||
(PC, 0xe581_0000, 6), // STR already has an N code access.
|
||||
(PC, 0xe581_0000, 6), // STR uses DMA's N, then breaks the following fetch.
|
||||
(ROM_START + 0x20000 - 8, 0xe1a0_0000, 0), // PC+8 fetch boundary already forces N.
|
||||
] {
|
||||
let (mut cpu, mut bus) = program(false, pc, instruction);
|
||||
|
|
@ -199,6 +201,10 @@ fn skipped_conditions_stores_and_boundaries_consume_resume_without_double_chargi
|
|||
let timing = cpu.step_timed(&mut bus).unwrap();
|
||||
assert_eq!(timing.code_cycles, 8);
|
||||
assert_eq!(timing.data_cycles, data);
|
||||
assert_eq!(
|
||||
cpu.step_timed(&mut bus).unwrap().code_cycles,
|
||||
if data == 6 { 8 } else { 6 }
|
||||
);
|
||||
assert_eq!(cpu.step_timed(&mut bus).unwrap().code_cycles, 6);
|
||||
}
|
||||
}
|
||||
|
|
@ -210,6 +216,7 @@ fn waitcnt_cpu_store_uses_old_code_cost_but_dma_store_applies_before_resume() {
|
|||
cpu.registers[1] = WAITCNT;
|
||||
dma(&mut bus);
|
||||
assert_eq!(cpu.step_timed(&mut bus).unwrap().code_cycles, 8);
|
||||
assert_eq!(cpu.step_timed(&mut bus).unwrap().code_cycles, 5); // Store breaks the next fetch; new N=3, S=2.
|
||||
assert_eq!(cpu.step_timed(&mut bus).unwrap().code_cycles, 4);
|
||||
|
||||
let (mut cpu, mut bus) = program(false, PC, 0xe1a0_0000);
|
||||
|
|
|
|||
381
crates/gba-core/src/cpu/fetch_sequence_tests.rs
Normal file
381
crates/gba-core/src/cpu/fetch_sequence_tests.rs
Normal file
|
|
@ -0,0 +1,381 @@
|
|||
//! Original instruction-pair tests. Expected access kinds do not use CPU summaries.
|
||||
use super::*;
|
||||
use crate::{
|
||||
dma::{DMA_BASE, DMA_STRIDE},
|
||||
io::{HALTCNT, IE, IME, TIMER_BASE, WAITCNT},
|
||||
machine::{Machine, StepKind},
|
||||
memory::{BIOS_SIZE, ROM_CAPACITY, ROM_START},
|
||||
timing::AccessKind,
|
||||
};
|
||||
|
||||
const PC: u32 = ROM_START + 0x100;
|
||||
const DATA: u32 = 0x0300_1000;
|
||||
const ARM_NOP: u32 = 0xe1a0_0000;
|
||||
const THUMB_NOP: u32 = 0x46c0;
|
||||
|
||||
fn program(thumb: bool, pc: u32, instructions: &[u32]) -> (Cpu, Memory) {
|
||||
let width = if thumb { 2 } else { 4 };
|
||||
let pattern = if thumb { 0x46c0_46c0 } else { ARM_NOP };
|
||||
let offset = (pc & 0x01ff_ffff) as usize;
|
||||
let length = (offset + 4 * width).clamp(0x20020, ROM_CAPACITY);
|
||||
let mut rom = pattern.to_le_bytes().repeat(length.div_ceil(4));
|
||||
for (index, instruction) in instructions.iter().enumerate() {
|
||||
rom[offset + index * width..offset + (index + 1) * width]
|
||||
.copy_from_slice(&instruction.to_le_bytes()[..width]);
|
||||
}
|
||||
let mut cpu = Cpu::new(pc);
|
||||
if thumb {
|
||||
cpu.instruction_set = InstructionSet::Thumb;
|
||||
}
|
||||
cpu.registers[0] = 7;
|
||||
cpu.registers[1] = DATA;
|
||||
cpu.registers[13] = DATA + 64;
|
||||
(
|
||||
cpu,
|
||||
Memory::with_bios(rom, ARM_NOP.to_le_bytes().repeat(BIOS_SIZE / 4)).unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arm_instruction_effects_select_the_following_fetch_not_their_own() {
|
||||
for (instruction, breaks) in [
|
||||
(ARM_NOP, false),
|
||||
(0xe1a0_0082, false), // Immediate shift.
|
||||
(0xe10f_0000, false),
|
||||
(0xe321_f01f, false), // MRS / MSR.
|
||||
(0x0591_0000, false),
|
||||
(0x0581_0000, false), // Skipped load/store.
|
||||
(0x0000_0291, false),
|
||||
(0x01a0_0312, false), // Skipped multiply/shift.
|
||||
(0xe591_0000, true),
|
||||
(0xe581_0000, true), // Word load/store.
|
||||
(0xe5d1_0000, true),
|
||||
(0xe5c1_0000, true), // Byte load/store.
|
||||
(0xe1d1_00b0, true),
|
||||
(0xe1c1_00b0, true), // Halfword load/store.
|
||||
(0xe1d1_00d0, true),
|
||||
(0xe1d1_00f0, true), // Signed byte/halfword.
|
||||
(0xe891_0005, true),
|
||||
(0xe881_0005, true), // Block load/store.
|
||||
(0xe881_0000, true), // Empty block store.
|
||||
(0xe101_0092, true),
|
||||
(0xe141_0092, true), // SWP/SWPB.
|
||||
(0xe1a0_0312, true), // Register shift, zero shift amount.
|
||||
(0xe000_0392, true),
|
||||
(0xe020_4392, true), // MUL/MLA.
|
||||
(0xe085_4392, true),
|
||||
(0xe0a5_4392, true), // UMULL/UMLAL.
|
||||
(0xe0c5_4392, true),
|
||||
(0xe0e5_4392, true), // SMULL/SMLAL.
|
||||
] {
|
||||
check_pair(false, instruction, breaks);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thumb_instruction_effects_select_the_following_fetch_not_their_own() {
|
||||
for (instruction, breaks) in [
|
||||
(THUMB_NOP, false),
|
||||
(0x0040, false),
|
||||
(0x3001, false),
|
||||
(0x4008, false),
|
||||
(0xb001, false),
|
||||
(0xf000, false), // AND, SP adjust, BL prefix.
|
||||
(0xd000, false), // Skipped BEQ (Z clear).
|
||||
(0x4800, true), // Literal load.
|
||||
(0x6008, true),
|
||||
(0x6808, true),
|
||||
(0x7008, true),
|
||||
(0x7808, true),
|
||||
(0x8008, true),
|
||||
(0x8808, true),
|
||||
(0x9000, true),
|
||||
(0x9800, true),
|
||||
(0x5088, true),
|
||||
(0x5288, true),
|
||||
(0x5488, true),
|
||||
(0x5688, true),
|
||||
(0x5888, true),
|
||||
(0x5a88, true),
|
||||
(0x5c88, true),
|
||||
(0x5e88, true),
|
||||
(0xb405, true),
|
||||
(0xbc05, true),
|
||||
(0xc105, true),
|
||||
(0xc905, true),
|
||||
(0xb400, true),
|
||||
(0xc100, true), // Empty stores.
|
||||
(0x4090, true),
|
||||
(0x40d0, true),
|
||||
(0x4110, true),
|
||||
(0x41d0, true), // Register shifts by r2=0.
|
||||
(0x4348, true), // MUL.
|
||||
] {
|
||||
check_pair(true, instruction, breaks);
|
||||
}
|
||||
}
|
||||
|
||||
fn check_pair(thumb: bool, instruction: u32, breaks: bool) {
|
||||
for window in 0..3 {
|
||||
let (cpu, mut memory) = program(thumb, PC + window as u32 * 0x0200_0000, &[instruction]);
|
||||
for first in 0..4 {
|
||||
for second in 0..2 {
|
||||
for prefetch in [0, 0x4000] {
|
||||
memory
|
||||
.write16(
|
||||
WAITCNT,
|
||||
(first << [2, 5, 8][window])
|
||||
| (second << [4, 7, 10][window])
|
||||
| prefetch,
|
||||
)
|
||||
.unwrap();
|
||||
let n = [5, 4, 3, 9][first as usize];
|
||||
let s = if second == 1 { 2 } else { [3, 5, 9][window] };
|
||||
let extra = if thumb { 0 } else { s };
|
||||
let mut cpu = cpu.clone();
|
||||
// Cold startup keeps the existing nominal S policy.
|
||||
assert_eq!(
|
||||
cpu.step_timed(&mut memory).unwrap().code_cycles,
|
||||
s + extra,
|
||||
"first fetch for {instruction:08x}"
|
||||
);
|
||||
assert_eq!(
|
||||
cpu.step_timed(&mut memory).unwrap().code_cycles,
|
||||
if breaks { n + extra } else { s + extra },
|
||||
"following fetch for {instruction:08x}"
|
||||
);
|
||||
assert_eq!(cpu.step_timed(&mut memory).unwrap().code_cycles, s + extra);
|
||||
assert_eq!(memory.cycles(), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timed_and_untimed_execution_and_clones_retain_the_same_next_fetch_kind() {
|
||||
for thumb in [false, true] {
|
||||
let instruction = if thumb { 0x6808 } else { 0xe591_0000 };
|
||||
let (mut timed, mut a) = program(thumb, PC, &[instruction]);
|
||||
let (mut plain, mut b) = program(thumb, PC, &[instruction]);
|
||||
timed.step_timed(&mut a).unwrap();
|
||||
plain.step(&mut b).unwrap();
|
||||
assert_eq!(timed, plain);
|
||||
let mut copy = plain.clone();
|
||||
for expected in [if thumb { 5 } else { 8 }, if thumb { 3 } else { 6 }] {
|
||||
assert_eq!(timed.step_timed(&mut a).unwrap().code_cycles, expected);
|
||||
assert_eq!(copy.step_timed(&mut b).unwrap().code_cycles, expected);
|
||||
assert_eq!(timed, copy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn following_transfers_branches_and_skipped_conditions_consume_the_prior_sequence_break() {
|
||||
for thumb in [false, true] {
|
||||
let load = if thumb { 0x6808 } else { 0xe591_0000 };
|
||||
let store = if thumb { 0x6008 } else { 0xe581_0000 };
|
||||
let branch = if thumb { 0xe000 } else { 0xea00_0000 };
|
||||
let skipped = if thumb { 0xd000 } else { 0x0581_0000 };
|
||||
for second in [load, store, branch, skipped] {
|
||||
let (mut cpu, mut memory) = program(thumb, PC, &[load, second]);
|
||||
cpu.step(&mut memory).unwrap();
|
||||
let cost = cpu.step_timed(&mut memory).unwrap();
|
||||
let n = if thumb { 5 } else { 8 };
|
||||
let s = if thumb { 3 } else { 6 };
|
||||
assert_eq!(
|
||||
cost.code_cycles,
|
||||
if second == branch { n + n + s } else { n }
|
||||
);
|
||||
assert_eq!(
|
||||
cpu.step_timed(&mut memory).unwrap().code_cycles,
|
||||
if second == branch || second == skipped {
|
||||
s
|
||||
} else {
|
||||
n
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pc_load_refills_restore_sequential_fetches_after_data_and_internal_cycles() {
|
||||
for thumb in [false, true] {
|
||||
let instruction = if thumb { 0xbd00 } else { 0xe591_f000 };
|
||||
let (mut cpu, mut memory) = program(thumb, PC, &[instruction]);
|
||||
memory
|
||||
.write32(if thumb { DATA + 64 } else { DATA }, PC + 0x20)
|
||||
.unwrap();
|
||||
let cost = cpu.step_timed(&mut memory).unwrap();
|
||||
assert_eq!(cost.code_cycles, if thumb { 11 } else { 20 });
|
||||
assert_eq!(cost.data_cycles, 1);
|
||||
assert_eq!(cost.internal_cycles, 1);
|
||||
assert_eq!(
|
||||
cpu.step_timed(&mut memory).unwrap().code_cycles,
|
||||
if thumb { 3 } else { 6 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dma_override_coalesces_with_cpu_history_and_does_not_replay_the_penalty() {
|
||||
for thumb in [false, true] {
|
||||
for instruction in [
|
||||
if thumb { 0x6808 } else { 0xe591_0000 },
|
||||
if thumb { THUMB_NOP } else { ARM_NOP },
|
||||
] {
|
||||
let (mut cpu, mut memory) = program(thumb, PC, &[instruction]);
|
||||
cpu.step(&mut memory).unwrap();
|
||||
let dma = DMA_BASE + 3 * DMA_STRIDE;
|
||||
memory.write32(dma, DATA).unwrap();
|
||||
memory.write32(dma + 4, DATA + 128).unwrap();
|
||||
memory.write32(dma + 8, 0x8400_0001).unwrap();
|
||||
memory.step_dma().unwrap().unwrap();
|
||||
assert_eq!(
|
||||
cpu.step_timed(&mut memory).unwrap().code_cycles,
|
||||
if thumb { 5 } else { 8 }
|
||||
);
|
||||
assert_eq!(
|
||||
cpu.step_timed(&mut memory).unwrap().code_cycles,
|
||||
if thumb { 3 } else { 6 }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn irq_uses_retained_incoming_kind_before_invalidating_the_interrupted_buffer() {
|
||||
for thumb in [false, true] {
|
||||
for instruction in [
|
||||
if thumb { 0x6808 } else { 0xe591_0000 },
|
||||
if thumb { THUMB_NOP } else { ARM_NOP },
|
||||
] {
|
||||
let (mut cpu, mut memory) = program(thumb, PC, &[instruction]);
|
||||
cpu.step(&mut memory).unwrap();
|
||||
memory.write16(IE, 8).unwrap();
|
||||
memory.write16(IME, 1).unwrap();
|
||||
memory.write32(TIMER_BASE, 0x00c0_ffff).unwrap();
|
||||
memory.advance_cycles(1);
|
||||
let n = instruction != if thumb { THUMB_NOP } else { ARM_NOP };
|
||||
let expected = match (thumb, n) {
|
||||
(true, true) => 7,
|
||||
(true, false) => 5,
|
||||
(false, true) => 10,
|
||||
(false, false) => 8,
|
||||
};
|
||||
let mut machine = Machine::new(cpu, memory);
|
||||
let start = machine.cycles();
|
||||
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
|
||||
assert_eq!(machine.last_timing().code_cycles, expected);
|
||||
assert_eq!(machine.cycles() - start, u64::from(expected));
|
||||
assert_eq!(machine.cpu().next_fetch_kind(), AccessKind::Sequential);
|
||||
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
|
||||
assert_eq!(machine.last_timing().code_cycles, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_execution_retains_next_kind_and_retry_does_not_consume_it() {
|
||||
for thumb in [false, true] {
|
||||
let load = if thumb { 0x6808 } else { 0xe591_0000 };
|
||||
let (mut cpu, mut memory) = program(thumb, PC, &[load, load]);
|
||||
cpu.step(&mut memory).unwrap();
|
||||
cpu.registers[1] = 0x0e00_0000;
|
||||
let before = cpu.clone();
|
||||
for _ in 0..2 {
|
||||
assert!(cpu.step_timed(&mut memory).is_err());
|
||||
assert_eq!(cpu, before);
|
||||
assert_eq!(cpu.next_fetch_kind(), AccessKind::NonSequential);
|
||||
}
|
||||
cpu.registers[1] = DATA;
|
||||
assert_eq!(
|
||||
cpu.step_timed(&mut memory).unwrap().code_cycles,
|
||||
if thumb { 5 } else { 8 }
|
||||
);
|
||||
assert_eq!(
|
||||
cpu.step_timed(&mut memory).unwrap().code_cycles,
|
||||
if thumb { 5 } else { 8 }
|
||||
);
|
||||
assert_eq!(memory.cycles(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_current_slot_and_masked_irq_preserve_the_pending_cpu_sequence() {
|
||||
for thumb in [false, true] {
|
||||
let instruction: u32 = if thumb { 0x6808 } else { 0xe591_0000 };
|
||||
let width = if thumb { 2 } else { 4 };
|
||||
let mut memory = Memory::new(instruction.to_le_bytes()[..width].to_vec()).unwrap();
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
if thumb {
|
||||
cpu.instruction_set = InstructionSet::Thumb;
|
||||
}
|
||||
cpu.registers[1] = DATA;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.next_fetch_kind(), AccessKind::NonSequential);
|
||||
let before = cpu.clone();
|
||||
assert!(cpu.step_timed(&mut memory).is_err());
|
||||
assert_eq!(cpu, before);
|
||||
memory.write16(IE, 8).unwrap();
|
||||
memory.write16(IME, 1).unwrap();
|
||||
memory.write32(TIMER_BASE, 0x00c0_ffff).unwrap();
|
||||
memory.advance_cycles(1);
|
||||
cpu.irq_disabled = true;
|
||||
let before = cpu.clone();
|
||||
assert_eq!(cpu.take_irq_timed(&mut memory), None);
|
||||
assert_eq!(cpu, before);
|
||||
// A debugger PC discontinuity cannot reuse the old sequence's access kind.
|
||||
cpu.registers[15] = PC;
|
||||
assert_eq!(cpu.next_fetch_kind(), AccessKind::Sequential);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_accesses_and_halt_idle_preserve_kind_but_debugger_invalidation_starts_cold() {
|
||||
let (mut cpu, mut memory) = program(false, PC, &[0xe591_0000]);
|
||||
cpu.step(&mut memory).unwrap();
|
||||
memory.read32(DATA).unwrap();
|
||||
memory.write32(DATA, 1).unwrap();
|
||||
memory.advance_cycles(7);
|
||||
memory.write8(HALTCNT, 0).unwrap();
|
||||
let mut machine = Machine::new(cpu, memory);
|
||||
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
|
||||
assert_eq!(machine.cpu().next_fetch_kind(), AccessKind::NonSequential);
|
||||
let mut cpu = machine.cpu().clone();
|
||||
cpu.invalidate_pipeline();
|
||||
assert_eq!(cpu.next_fetch_kind(), AccessKind::Sequential);
|
||||
assert_eq!(cpu.step_timed(machine.memory_mut()).unwrap().code_cycles, 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_and_wait_window_crossings_apply_the_retained_kind_at_the_new_fetch_address() {
|
||||
for thumb in [false, true] {
|
||||
let width = if thumb { 2 } else { 4 };
|
||||
for (boundary, waitcnt, n, s) in [
|
||||
(
|
||||
ROM_START + 0x20000,
|
||||
0,
|
||||
if thumb { 5 } else { 8 },
|
||||
if thumb { 3 } else { 6 },
|
||||
),
|
||||
(
|
||||
0x0a00_0000,
|
||||
0x80,
|
||||
if thumb { 5 } else { 7 },
|
||||
if thumb { 2 } else { 4 },
|
||||
),
|
||||
] {
|
||||
let (mut cpu, mut memory) = program(
|
||||
thumb,
|
||||
boundary - 3 * width,
|
||||
&[if thumb { 0x6808 } else { 0xe591_0000 }],
|
||||
);
|
||||
memory.write16(WAITCNT, waitcnt).unwrap();
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.step_timed(&mut memory).unwrap().code_cycles, n);
|
||||
assert_eq!(cpu.step_timed(&mut memory).unwrap().code_cycles, s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -164,9 +164,10 @@ fn a_boundary_waitcnt_store_uses_old_fetch_settings_and_the_next_fetch_uses_new_
|
|||
cpu.registers[0] = 0xc0; // WS1 N=3, S=2.
|
||||
cpu.registers[1] = WAITCNT;
|
||||
let cost = cpu.step_timed(&mut memory).unwrap();
|
||||
assert_eq!(cost.code_cycles, 10); // Old WS1 N=5, S=5 at lookahead 0x0a000004.
|
||||
assert_eq!(cost.code_cycles, 10); // Old WS1 S+S=10 at lookahead 0x0a000004.
|
||||
assert_eq!(cost.data_cycles, 1);
|
||||
assert_eq!(memory.waitcnt(), 0xc0);
|
||||
assert_eq!(cpu.step_timed(&mut memory).unwrap().code_cycles, 5); // Store's next fetch: new N+S.
|
||||
assert_eq!(cpu.step_timed(&mut memory).unwrap().code_cycles, 4);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
//! ARM/Thumb instruction buffering. Timing and bus ownership remain separate.
|
||||
//! ARM/Thumb instruction buffering and the next CPU fetch access kind.
|
||||
use super::{Cpu, CpuError, InstructionSet};
|
||||
use crate::memory::{InstructionFetch, Memory, MemoryError};
|
||||
use crate::{
|
||||
memory::{InstructionFetch, Memory, MemoryError},
|
||||
timing::AccessKind,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(super) struct Pipeline {
|
||||
|
|
@ -9,6 +12,7 @@ pub(super) struct Pipeline {
|
|||
// Deferred strict-fetch results for the next execute and decode positions.
|
||||
// Thumb halfwords are zero-extended; their upper bits are not another opcode.
|
||||
instructions: [Result<u32, MemoryError>; 2],
|
||||
pub(super) next_access: AccessKind,
|
||||
}
|
||||
|
||||
pub(super) struct Fetched {
|
||||
|
|
@ -21,11 +25,21 @@ pub(super) struct Fetched {
|
|||
impl Cpu {
|
||||
/// Discard buffered instructions after debugger code edits or rebinding memory.
|
||||
/// Ordinary CPU/DMA stores and host inspection must not call this automatically.
|
||||
/// The next step fills from its current PC/state without adding nominal cycles.
|
||||
/// The next step uses nominal S startup with no separate fill charge.
|
||||
/// This also discards the prior CPU fetch kind, but not Memory's DMA override.
|
||||
pub fn invalidate_pipeline(&mut self) {
|
||||
self.pipeline = None;
|
||||
}
|
||||
|
||||
/// Cold/debugger entry keeps the existing nominal S startup policy.
|
||||
/// A mismatched PC/state cannot reuse another sequence's access kind.
|
||||
pub(super) fn next_fetch_kind(&self) -> AccessKind {
|
||||
self.pipeline
|
||||
.as_ref()
|
||||
.filter(|pipe| pipe.pc == self.pc() && pipe.instruction_set == self.instruction_set)
|
||||
.map_or(AccessKind::Sequential, |pipe| pipe.next_access)
|
||||
}
|
||||
|
||||
pub(super) fn fetch(&self, memory: &Memory) -> Result<Fetched, CpuError> {
|
||||
let pc = self.pc();
|
||||
let instruction_set = self.instruction_set;
|
||||
|
|
@ -53,6 +67,7 @@ impl Cpu {
|
|||
pc: pc.wrapping_add(width),
|
||||
instruction_set,
|
||||
instructions: [decode, fetched.instruction.clone()],
|
||||
next_access: AccessKind::Sequential, // Set from execution only on success.
|
||||
},
|
||||
lookahead: fetched,
|
||||
fill,
|
||||
|
|
@ -73,6 +88,7 @@ impl Cpu {
|
|||
pc,
|
||||
instruction_set,
|
||||
instructions: fetches.map(|fetch| fetch.instruction),
|
||||
next_access: AccessKind::Sequential,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
//! Nominal ARM7 instruction-cycle summaries, separate from instruction semantics.
|
||||
//! Code costs use source-fetch and target-pair addresses; data costs use actual bus accesses.
|
||||
//! Source access kinds remain instruction-local nominal summaries.
|
||||
//! Successful execution records the next fetch kind in the instruction buffer.
|
||||
|
||||
use super::{Cpu, CpuError, InstructionSet};
|
||||
use crate::{
|
||||
|
|
@ -9,13 +9,23 @@ use crate::{
|
|||
};
|
||||
|
||||
#[derive(Default)]
|
||||
struct Summary {
|
||||
code_kind: AccessKind,
|
||||
refill: bool,
|
||||
pub(super) struct Summary {
|
||||
data: bool,
|
||||
pub(super) refill: bool,
|
||||
internal: u32,
|
||||
}
|
||||
|
||||
impl Summary {
|
||||
/// Data accesses and internal cycles break the following code sequence.
|
||||
/// A refill ends with a sequential target fetch and starts a new sequence.
|
||||
pub(super) fn next_fetch_kind(&self) -> AccessKind {
|
||||
if !self.refill && (self.data || self.internal != 0) {
|
||||
AccessKind::NonSequential
|
||||
} else {
|
||||
AccessKind::Sequential
|
||||
}
|
||||
}
|
||||
|
||||
fn branch() -> Self {
|
||||
Self {
|
||||
refill: true,
|
||||
|
|
@ -25,11 +35,7 @@ impl Summary {
|
|||
|
||||
fn transfer(load: bool, pc: bool) -> Self {
|
||||
Self {
|
||||
code_kind: if load {
|
||||
AccessKind::Sequential
|
||||
} else {
|
||||
AccessKind::NonSequential
|
||||
},
|
||||
data: true,
|
||||
refill: load && pc,
|
||||
internal: u32::from(load),
|
||||
}
|
||||
|
|
@ -52,15 +58,12 @@ impl Cpu {
|
|||
pub fn step_timed(&mut self, memory: &mut Memory) -> Result<StepTiming, CpuError> {
|
||||
let fetched = self.fetch(memory)?;
|
||||
let instruction = fetched.instruction;
|
||||
let summary = match self.instruction_set {
|
||||
InstructionSet::Arm => self.arm_summary(instruction),
|
||||
InstructionSet::Thumb => self.thumb_summary(instruction),
|
||||
};
|
||||
let summary = self.instruction_summary(instruction);
|
||||
let fetch_address = fetched.lookahead.address();
|
||||
let width = self.instruction_set.access_width();
|
||||
// A WAITCNT store affects subsequent instructions, not this code access.
|
||||
let waitcnt = memory.waitcnt();
|
||||
let code_kind = memory.cpu_code_kind(summary.code_kind);
|
||||
let code_kind = memory.cpu_code_kind(self.next_fetch_kind());
|
||||
memory.begin_data_timing();
|
||||
let result = self.execute_fetched(fetched, memory);
|
||||
let data_cycles = memory.end_data_timing();
|
||||
|
|
@ -82,11 +85,11 @@ impl Cpu {
|
|||
pub(crate) fn take_irq_timed(&mut self, memory: &mut Memory) -> Option<StepTiming> {
|
||||
let state = self.instruction_set;
|
||||
let address = self.pc().wrapping_add(2 * state.width());
|
||||
let kind = memory.cpu_code_kind(self.next_fetch_kind());
|
||||
if !self.take_interrupt(memory.irq_pending(), false) {
|
||||
return None;
|
||||
}
|
||||
let waitcnt = memory.waitcnt();
|
||||
let kind = memory.cpu_code_kind(AccessKind::Sequential);
|
||||
let fetch = memory.fetch_instruction(address, state);
|
||||
memory.discarded_cpu_fetch(&fetch);
|
||||
self.refill_pipeline(memory);
|
||||
|
|
@ -122,6 +125,7 @@ impl Cpu {
|
|||
}
|
||||
if instruction & 0x0fb0_0ff0 == 0x0100_0090 {
|
||||
return Summary {
|
||||
data: true,
|
||||
internal: 1,
|
||||
..Summary::default()
|
||||
}; // SWP: two N data accesses
|
||||
|
|
@ -158,12 +162,12 @@ impl Cpu {
|
|||
}
|
||||
}
|
||||
|
||||
// Share the existing control-flow classification with bounded bus history.
|
||||
// Classify before instruction effects in both timed and untimed execution.
|
||||
// A PC write can refill even when its target equals the sequential address.
|
||||
pub(super) fn instruction_refills(&self, instruction: u32) -> bool {
|
||||
pub(super) fn instruction_summary(&self, instruction: u32) -> Summary {
|
||||
match self.instruction_set {
|
||||
InstructionSet::Arm => self.arm_summary(instruction).refill,
|
||||
InstructionSet::Thumb => self.thumb_summary(instruction).refill,
|
||||
InstructionSet::Arm => self.arm_summary(instruction),
|
||||
InstructionSet::Thumb => self.thumb_summary(instruction),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -338,9 +338,9 @@ fn waitcnt_store_charges_old_code_settings_and_new_settings_apply_next_step() {
|
|||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = WAITCNT;
|
||||
cpu.registers[1] = 0x18; // N=3 and S=2 per halfword
|
||||
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(8, 1, 0));
|
||||
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(6, 1, 0)); // Incoming S at old WAITCNT.
|
||||
assert_eq!(memory.waitcnt(), 0x18);
|
||||
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(4, 0, 0));
|
||||
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(5, 0, 0)); // Following N at new WAITCNT.
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue