refactor(cpu): account for timing in bus-access order
This commit is contained in:
parent
86321f0071
commit
7f3ebd3a5d
14 changed files with 626 additions and 65 deletions
|
|
@ -257,6 +257,7 @@ impl Cpu {
|
|||
let result = self.execute_instruction(instruction, memory);
|
||||
memory.end_cpu_access(result.is_ok());
|
||||
if result.is_ok() {
|
||||
memory.record_internal_cycles(summary.internal);
|
||||
if summary.refill {
|
||||
self.refill_pipeline(memory);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -83,6 +83,12 @@ impl Cpu {
|
|||
memory.fetch_instruction(pc, instruction_set),
|
||||
memory.fetch_instruction(pc.wrapping_add(instruction_set.width()), instruction_set),
|
||||
];
|
||||
for (sample, kind) in fetches
|
||||
.iter()
|
||||
.zip([AccessKind::NonSequential, AccessKind::Sequential])
|
||||
{
|
||||
memory.record_code_access(sample.address(), instruction_set.access_width(), kind);
|
||||
}
|
||||
memory.refill_cpu_bus_history(&fetches);
|
||||
self.pipeline = Some(Pipeline {
|
||||
pc,
|
||||
|
|
|
|||
|
|
@ -5,14 +5,14 @@
|
|||
use super::{Cpu, CpuError, InstructionSet};
|
||||
use crate::{
|
||||
memory::Memory,
|
||||
timing::{bus_cycles, refill_cycles, AccessKind, AccessWidth, StepTiming},
|
||||
timing::{AccessKind, AccessWidth, StepTiming},
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct Summary {
|
||||
data: bool,
|
||||
pub(super) refill: bool,
|
||||
internal: u32,
|
||||
pub(super) internal: u32,
|
||||
}
|
||||
|
||||
impl Summary {
|
||||
|
|
@ -43,7 +43,7 @@ impl Summary {
|
|||
}
|
||||
|
||||
impl InstructionSet {
|
||||
fn access_width(self) -> AccessWidth {
|
||||
pub(super) fn access_width(self) -> AccessWidth {
|
||||
match self {
|
||||
Self::Arm => AccessWidth::Word,
|
||||
Self::Thumb => AccessWidth::Halfword,
|
||||
|
|
@ -57,27 +57,18 @@ impl Cpu {
|
|||
/// Failed instructions discard their timing trace and retain existing diagnostics.
|
||||
pub fn step_timed(&mut self, memory: &mut Memory) -> Result<StepTiming, CpuError> {
|
||||
let fetched = self.fetch(memory)?;
|
||||
let instruction = fetched.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(self.next_fetch_kind());
|
||||
memory.begin_data_timing();
|
||||
// Source timing precedes data accesses and their WAITCNT writes.
|
||||
memory.begin_cpu_timing();
|
||||
memory.record_code_access(
|
||||
fetched.lookahead.address(),
|
||||
self.instruction_set.access_width(),
|
||||
code_kind,
|
||||
);
|
||||
let result = self.execute_fetched(fetched, memory);
|
||||
let data_cycles = memory.end_data_timing();
|
||||
let timing = memory.end_cpu_timing(result.is_ok());
|
||||
result?;
|
||||
let mut code_cycles = bus_cycles(waitcnt, fetch_address, width, code_kind);
|
||||
if summary.refill {
|
||||
code_cycles += refill_cycles(waitcnt, self.pc(), self.instruction_set.access_width());
|
||||
}
|
||||
Ok(StepTiming {
|
||||
code_cycles,
|
||||
data_cycles,
|
||||
internal_cycles: summary.internal,
|
||||
idle_cycles: 0,
|
||||
})
|
||||
Ok(timing)
|
||||
}
|
||||
|
||||
/// Accept IRQ between instructions, discard the old-state fetch, then refill ARM.
|
||||
|
|
@ -89,15 +80,12 @@ impl Cpu {
|
|||
if !self.take_interrupt(memory.irq_pending(), false) {
|
||||
return None;
|
||||
}
|
||||
let waitcnt = memory.waitcnt();
|
||||
memory.begin_cpu_timing();
|
||||
let fetch = memory.fetch_instruction(address, state);
|
||||
memory.record_code_access(fetch.address(), state.access_width(), kind);
|
||||
memory.discarded_cpu_fetch(&fetch);
|
||||
self.refill_pipeline(memory);
|
||||
Some(StepTiming {
|
||||
code_cycles: bus_cycles(waitcnt, fetch.address(), state.access_width(), kind)
|
||||
+ refill_cycles(waitcnt, self.pc(), self.instruction_set.access_width()),
|
||||
..StepTiming::default()
|
||||
})
|
||||
Some(memory.end_cpu_timing(true))
|
||||
}
|
||||
|
||||
fn arm_summary(&self, instruction: u32) -> Summary {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ mod irq_fetch_tests;
|
|||
mod iwram_bus;
|
||||
#[cfg(test)]
|
||||
mod iwram_history_tests;
|
||||
#[cfg(test)]
|
||||
mod timing_event_tests;
|
||||
pub(crate) use fetch::InstructionFetch;
|
||||
use iwram_bus::IwramBus;
|
||||
|
||||
|
|
@ -17,7 +19,7 @@ use crate::{
|
|||
dma::{DmaError, DMA_BASE, DMA_END},
|
||||
input::Buttons,
|
||||
io::{Io, HALTCNT, KEYINPUT, POSTFLG},
|
||||
timing::{bus_cycles, AccessKind, AccessWidth, DataTiming, StepTiming},
|
||||
timing::{bus_cycles, AccessKind, AccessWidth, CpuTiming, StepTiming},
|
||||
video::{self, sprites::pipeline, Framebuffer, VideoError},
|
||||
};
|
||||
|
||||
|
|
@ -89,7 +91,9 @@ pub struct Memory {
|
|||
bios: Option<Vec<u8>>,
|
||||
io: Io,
|
||||
cycles: u64,
|
||||
data_timing: Cell<Option<DataTiming>>,
|
||||
cpu_timing: Cell<Option<CpuTiming>>,
|
||||
#[cfg(test)]
|
||||
last_cpu_timing: Option<CpuTiming>,
|
||||
// A completed DMA unit breaks the CPU's next nominal code-access sequence.
|
||||
cpu_resume_nonsequential: bool,
|
||||
cpu_access: Option<CpuAccess>,
|
||||
|
|
@ -114,7 +118,9 @@ impl Memory {
|
|||
bios: None,
|
||||
io: Io::default(),
|
||||
cycles: 0,
|
||||
data_timing: Cell::new(None),
|
||||
cpu_timing: Cell::new(None),
|
||||
#[cfg(test)]
|
||||
last_cpu_timing: None,
|
||||
cpu_resume_nonsequential: false,
|
||||
cpu_access: None,
|
||||
bios_prefetch: None,
|
||||
|
|
@ -509,19 +515,47 @@ impl Memory {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn begin_data_timing(&self) {
|
||||
debug_assert!(self.data_timing.get().is_none());
|
||||
self.data_timing.set(Some(DataTiming::default()));
|
||||
pub(crate) fn begin_cpu_timing(&self) {
|
||||
debug_assert!(self.cpu_timing.get().is_none());
|
||||
self.cpu_timing.set(Some(CpuTiming::new(self.waitcnt())));
|
||||
}
|
||||
|
||||
pub(crate) fn end_data_timing(&self) -> u32 {
|
||||
self.data_timing.take().map_or(0, |trace| trace.cycles)
|
||||
pub(crate) fn end_cpu_timing(&mut self, succeeded: bool) -> StepTiming {
|
||||
let trace = self
|
||||
.cpu_timing
|
||||
.take()
|
||||
.expect("CPU timing transaction is active");
|
||||
if !succeeded {
|
||||
return StepTiming::default();
|
||||
}
|
||||
#[cfg(test)]
|
||||
{
|
||||
self.last_cpu_timing = Some(trace);
|
||||
}
|
||||
trace.total
|
||||
}
|
||||
|
||||
pub(crate) fn record_code_access(&self, address: u32, width: AccessWidth, kind: AccessKind) {
|
||||
if let Some(mut trace) = self.cpu_timing.get() {
|
||||
trace.code(address, width, kind);
|
||||
self.cpu_timing.set(Some(trace));
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn record_internal_cycles(&self, cycles: u32) {
|
||||
if cycles == 0 {
|
||||
return;
|
||||
}
|
||||
if let Some(mut trace) = self.cpu_timing.get() {
|
||||
trace.internal(cycles);
|
||||
self.cpu_timing.set(Some(trace));
|
||||
}
|
||||
}
|
||||
|
||||
fn record_access(&self, address: u32, width: AccessWidth) {
|
||||
if let Some(mut trace) = self.data_timing.get() {
|
||||
trace.access(self.waitcnt(), address, width);
|
||||
self.data_timing.set(Some(trace));
|
||||
if let Some(mut trace) = self.cpu_timing.get() {
|
||||
trace.data(self.waitcnt(), address, width);
|
||||
self.cpu_timing.set(Some(trace));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -142,10 +142,10 @@ fn sampling_has_no_timing_or_history_side_effects_and_never_uses_data_fallback()
|
|||
bios[8..12].copy_from_slice(&WORD.to_le_bytes());
|
||||
let mut memory = Memory::with_bios(vec![], bios).unwrap();
|
||||
memory.cpu_resume_nonsequential = true;
|
||||
memory.begin_data_timing();
|
||||
memory.begin_cpu_timing();
|
||||
let fetch = memory.fetch_instruction(8, InstructionSet::Arm);
|
||||
memory.fetch_instruction(0x0300_0000, InstructionSet::Thumb);
|
||||
assert_eq!(memory.end_data_timing(), 0);
|
||||
assert_eq!(memory.end_cpu_timing(true), StepTiming::default());
|
||||
assert_eq!(memory.bios_prefetch, None);
|
||||
assert!(memory.cpu_resume_nonsequential);
|
||||
assert_eq!(memory.cycles(), 0);
|
||||
|
|
|
|||
321
crates/gba-core/src/memory/timing_event_tests.rs
Normal file
321
crates/gba-core/src/memory/timing_event_tests.rs
Normal file
|
|
@ -0,0 +1,321 @@
|
|||
//! Original checks of timing-event order, independent of aggregate cycle totals.
|
||||
use super::*;
|
||||
use crate::{
|
||||
cpu::Cpu,
|
||||
dma::DMA_STRIDE,
|
||||
io::{IE, IME, TIMER_BASE, WAITCNT},
|
||||
machine::{Machine, StepKind},
|
||||
timing::TimingEvent,
|
||||
};
|
||||
|
||||
const BASE: u32 = ROM_START + 0x100;
|
||||
const THUMB: u32 = ROM_START + 0x180;
|
||||
const DATA: u32 = 0x0300_1000;
|
||||
const ARM_NOP: u32 = 0xe1a0_0000;
|
||||
|
||||
fn prepared(thumb: bool, instructions: &[u32], operands: &[(usize, u32)]) -> (Cpu, Memory) {
|
||||
let mut operands = operands.to_vec();
|
||||
if thumb {
|
||||
operands.push((12, THUMB | 1));
|
||||
}
|
||||
let mut rom = ARM_NOP.to_le_bytes().repeat(256);
|
||||
for (index, &(register, value)) in operands.iter().enumerate() {
|
||||
let at = 0x100 + 4 * index;
|
||||
let literal = 0x200 + 4 * index;
|
||||
let ldr = 0xe59f_0000 | (register as u32) << 12 | (literal - at - 8) as u32;
|
||||
rom[at..at + 4].copy_from_slice(&ldr.to_le_bytes());
|
||||
rom[literal..literal + 4].copy_from_slice(&value.to_le_bytes());
|
||||
}
|
||||
// A NOP after operand setup establishes an incoming S kind.
|
||||
let entry = 0x100 + 4 * (operands.len() + 1);
|
||||
let (entry, width) = if thumb {
|
||||
rom[entry..entry + 4].copy_from_slice(&0xe12f_ff1c_u32.to_le_bytes());
|
||||
rom[0x180..0x1c0].copy_from_slice(&0x46c0_u16.to_le_bytes().repeat(32));
|
||||
(0x180, 2)
|
||||
} else {
|
||||
(entry, 4)
|
||||
};
|
||||
for (index, instruction) in instructions.iter().enumerate() {
|
||||
rom[entry + index * width..entry + (index + 1) * width]
|
||||
.copy_from_slice(&instruction.to_le_bytes()[..width]);
|
||||
}
|
||||
let mut memory = Memory::with_bios(rom, ARM_NOP.to_le_bytes().repeat(BIOS_SIZE / 4)).unwrap();
|
||||
let mut cpu = Cpu::new(BASE);
|
||||
for _ in 0..operands.len() + 1 + usize::from(thumb) {
|
||||
cpu.step(&mut memory).unwrap();
|
||||
}
|
||||
assert!(memory.last_cpu_timing.is_none()); // Untimed setup does not create a trace.
|
||||
(cpu, memory)
|
||||
}
|
||||
|
||||
fn code(address: u32, width: AccessWidth, kind: AccessKind, cycles: u32) -> TimingEvent {
|
||||
TimingEvent::Code {
|
||||
address,
|
||||
width,
|
||||
kind,
|
||||
waitcnt: 0,
|
||||
cycles,
|
||||
}
|
||||
}
|
||||
|
||||
fn data(address: u32, width: AccessWidth, kind: AccessKind, cycles: u32) -> TimingEvent {
|
||||
TimingEvent::Data {
|
||||
address,
|
||||
width,
|
||||
kind,
|
||||
waitcnt: 0,
|
||||
cycles,
|
||||
}
|
||||
}
|
||||
|
||||
fn events(memory: &Memory) -> Vec<TimingEvent> {
|
||||
assert!(memory.cpu_timing.get().is_none());
|
||||
memory.last_cpu_timing.unwrap().events()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pc_load_records_source_data_internal_and_target_pair_in_order() {
|
||||
let target = 0x0200_0100;
|
||||
let (mut cpu, mut memory) = prepared(false, &[0xe591_f000], &[(1, DATA)]);
|
||||
memory.write32(DATA, target).unwrap();
|
||||
let source = cpu.pc() + 8;
|
||||
let timing = cpu.step_timed(&mut memory).unwrap();
|
||||
assert_eq!(
|
||||
events(&memory),
|
||||
vec![
|
||||
code(source, AccessWidth::Word, AccessKind::Sequential, 6),
|
||||
data(DATA, AccessWidth::Word, AccessKind::NonSequential, 1),
|
||||
TimingEvent::Internal { cycles: 1 },
|
||||
code(target, AccessWidth::Word, AccessKind::NonSequential, 6),
|
||||
code(target + 4, AccessWidth::Word, AccessKind::Sequential, 6),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
timing,
|
||||
StepTiming {
|
||||
code_cycles: 18,
|
||||
data_cycles: 1,
|
||||
internal_cycles: 1,
|
||||
idle_cycles: 0
|
||||
}
|
||||
);
|
||||
assert_eq!(memory.cycles(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn largest_block_load_records_all_data_before_internal_and_refill_events() {
|
||||
let target = 0x0200_0200;
|
||||
let (mut cpu, mut memory) = prepared(false, &[0xe890_ffff], &[(0, DATA)]);
|
||||
memory.write32(DATA + 60, target).unwrap();
|
||||
let mut expected = vec![code(
|
||||
cpu.pc() + 8,
|
||||
AccessWidth::Word,
|
||||
AccessKind::Sequential,
|
||||
6,
|
||||
)];
|
||||
for index in 0..16 {
|
||||
expected.push(data(
|
||||
DATA + index * 4,
|
||||
AccessWidth::Word,
|
||||
if index == 0 {
|
||||
AccessKind::NonSequential
|
||||
} else {
|
||||
AccessKind::Sequential
|
||||
},
|
||||
1,
|
||||
));
|
||||
}
|
||||
expected.extend([
|
||||
TimingEvent::Internal { cycles: 1 },
|
||||
code(target, AccessWidth::Word, AccessKind::NonSequential, 6),
|
||||
code(target + 4, AccessWidth::Word, AccessKind::Sequential, 6),
|
||||
]);
|
||||
let timing = cpu.step_timed(&mut memory).unwrap();
|
||||
assert_eq!(events(&memory), expected);
|
||||
assert_eq!(timing.total(), 35);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thumb_pop_records_word_data_then_halfword_targets_without_duplicate_fetch_costs() {
|
||||
let target = 0x0300_2000;
|
||||
let (mut cpu, mut memory) = prepared(true, &[0xbd03], &[(13, DATA)]);
|
||||
memory.write32(DATA + 8, target).unwrap();
|
||||
let timing = cpu.step_timed(&mut memory).unwrap();
|
||||
assert_eq!(
|
||||
events(&memory),
|
||||
vec![
|
||||
code(THUMB + 4, AccessWidth::Halfword, AccessKind::Sequential, 3),
|
||||
data(DATA, AccessWidth::Word, AccessKind::NonSequential, 1),
|
||||
data(DATA + 4, AccessWidth::Word, AccessKind::Sequential, 1),
|
||||
data(DATA + 8, AccessWidth::Word, AccessKind::Sequential, 1),
|
||||
TimingEvent::Internal { cycles: 1 },
|
||||
code(target, AccessWidth::Halfword, AccessKind::NonSequential, 1),
|
||||
code(target + 2, AccessWidth::Halfword, AccessKind::Sequential, 1),
|
||||
]
|
||||
);
|
||||
assert_eq!(timing.total(), 9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_change_and_taken_or_skipped_branches_have_only_the_required_code_events() {
|
||||
let target = 0x0200_0100;
|
||||
let (mut cpu, mut memory) = prepared(false, &[0xe12f_ff10], &[(0, target | 1)]);
|
||||
let source = cpu.pc() + 8;
|
||||
cpu.step_timed(&mut memory).unwrap();
|
||||
assert_eq!(
|
||||
events(&memory),
|
||||
vec![
|
||||
code(source, AccessWidth::Word, AccessKind::Sequential, 6),
|
||||
code(target, AccessWidth::Halfword, AccessKind::NonSequential, 3),
|
||||
code(target + 2, AccessWidth::Halfword, AccessKind::Sequential, 3),
|
||||
]
|
||||
);
|
||||
for (instruction, taken) in [(0xeaff_ffff, true), (0x0aff_ffff, false)] {
|
||||
let (mut cpu, mut memory) = prepared(false, &[instruction], &[]);
|
||||
let pc = cpu.pc();
|
||||
let mut expected = vec![code(pc + 8, AccessWidth::Word, AccessKind::Sequential, 6)];
|
||||
if taken {
|
||||
expected.extend([
|
||||
code(pc + 4, AccessWidth::Word, AccessKind::NonSequential, 8),
|
||||
code(pc + 8, AccessWidth::Word, AccessKind::Sequential, 6),
|
||||
]);
|
||||
}
|
||||
cpu.step_timed(&mut memory).unwrap();
|
||||
assert_eq!(events(&memory), expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swaps_and_register_shifts_place_internal_cycles_after_their_bus_work() {
|
||||
for (instruction, swap) in [(0xe101_0092, true), (0xe1a0_0312, false)] {
|
||||
let (mut cpu, mut memory) = prepared(false, &[instruction], &[(1, DATA)]);
|
||||
let mut expected = vec![code(
|
||||
cpu.pc() + 8,
|
||||
AccessWidth::Word,
|
||||
AccessKind::Sequential,
|
||||
6,
|
||||
)];
|
||||
if swap {
|
||||
expected.extend([
|
||||
data(DATA, AccessWidth::Word, AccessKind::NonSequential, 1),
|
||||
data(DATA, AccessWidth::Word, AccessKind::NonSequential, 1),
|
||||
]);
|
||||
}
|
||||
expected.push(TimingEvent::Internal { cycles: 1 });
|
||||
cpu.step_timed(&mut memory).unwrap();
|
||||
assert_eq!(events(&memory), expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn waitcnt_store_records_old_source_settings_and_next_step_records_new_settings() {
|
||||
for waitcnt in [0x18, 0x4018] {
|
||||
// Prefetch enable is still stored only.
|
||||
let (mut cpu, mut memory) = prepared(
|
||||
false,
|
||||
&[0xe581_0000, ARM_NOP],
|
||||
&[(0, waitcnt), (1, WAITCNT)],
|
||||
);
|
||||
let pc = cpu.pc();
|
||||
cpu.step_timed(&mut memory).unwrap();
|
||||
assert_eq!(
|
||||
events(&memory),
|
||||
vec![
|
||||
code(pc + 8, AccessWidth::Word, AccessKind::Sequential, 6),
|
||||
data(WAITCNT, AccessWidth::Word, AccessKind::NonSequential, 1),
|
||||
]
|
||||
);
|
||||
cpu.step_timed(&mut memory).unwrap();
|
||||
assert_eq!(
|
||||
events(&memory),
|
||||
vec![TimingEvent::Code {
|
||||
address: pc + 12,
|
||||
width: AccessWidth::Word,
|
||||
kind: AccessKind::NonSequential,
|
||||
waitcnt: waitcnt as u16,
|
||||
cycles: 5,
|
||||
}]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn irq_entry_records_the_incoming_source_and_vector_pair_without_executing_code() {
|
||||
let (mut cpu, mut memory) = prepared(true, &[0x6808], &[(1, DATA)]);
|
||||
cpu.step_timed(&mut memory).unwrap(); // Load makes the next source N.
|
||||
memory.write16(IE, 8).unwrap();
|
||||
memory.write16(IME, 1).unwrap();
|
||||
memory.write32(TIMER_BASE, 0x00c0_ffff).unwrap();
|
||||
memory.advance_cycles(1);
|
||||
let mut machine = Machine::new(cpu, memory);
|
||||
let start = machine.cycles();
|
||||
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
|
||||
assert_eq!(
|
||||
events(machine.memory()),
|
||||
vec![
|
||||
code(
|
||||
THUMB + 6,
|
||||
AccessWidth::Halfword,
|
||||
AccessKind::NonSequential,
|
||||
5
|
||||
),
|
||||
code(0x18, AccessWidth::Word, AccessKind::NonSequential, 1),
|
||||
code(0x1c, AccessWidth::Word, AccessKind::Sequential, 1),
|
||||
]
|
||||
);
|
||||
assert_eq!(machine.cycles() - start, 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_instructions_discard_events_and_preserve_the_last_successful_trace() {
|
||||
// The second LDR reads an unsupported data region; the first retains normal events.
|
||||
let (mut cpu, mut memory) = prepared(false, &[0xe591_1000, 0xe591_0000], &[(1, DATA)]);
|
||||
memory.write32(DATA, 0x0e00_0000).unwrap();
|
||||
cpu.step_timed(&mut memory).unwrap();
|
||||
let previous = events(&memory);
|
||||
let before = cpu.clone();
|
||||
for _ in 0..2 {
|
||||
assert!(cpu.step_timed(&mut memory).is_err());
|
||||
assert_eq!(cpu, before);
|
||||
assert_eq!(events(&memory), previous);
|
||||
assert_eq!(memory.cycles(), 0);
|
||||
}
|
||||
// A late block-load error must also discard already recorded data accesses.
|
||||
{
|
||||
let (mut cpu, mut memory) = prepared(false, &[ARM_NOP, 0xe8b1_0005], &[(1, IME)]);
|
||||
cpu.step_timed(&mut memory).unwrap();
|
||||
let previous = events(&memory);
|
||||
let before = cpu.clone();
|
||||
assert!(cpu.step_timed(&mut memory).is_err());
|
||||
assert_eq!(cpu, before);
|
||||
assert_eq!(events(&memory), previous);
|
||||
}
|
||||
// Host reads and untimed code neither join nor replace a completed trace.
|
||||
memory.read32(DATA).unwrap();
|
||||
let mut probe = Cpu::new(0x0300_0000);
|
||||
probe.step(&mut memory).unwrap();
|
||||
assert_eq!(events(&memory), previous);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deferred_source_errors_and_dma_steps_do_not_add_hidden_cpu_events() {
|
||||
let mut memory = Memory::new(ARM_NOP.to_le_bytes().to_vec()).unwrap();
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.step_timed(&mut memory).unwrap(); // Missing P+8 is deferred but still costs a fetch.
|
||||
let previous = vec![code(
|
||||
ROM_START + 8,
|
||||
AccessWidth::Word,
|
||||
AccessKind::Sequential,
|
||||
6,
|
||||
)];
|
||||
assert_eq!(events(&memory), previous);
|
||||
assert!(cpu.step_timed(&mut memory).is_err());
|
||||
assert_eq!(events(&memory), previous);
|
||||
let dma = DMA_BASE + 3 * DMA_STRIDE;
|
||||
memory.write32(dma, DATA).unwrap();
|
||||
memory.write32(dma + 4, DATA + 4).unwrap();
|
||||
memory.write32(dma + 8, 0x8400_0001).unwrap();
|
||||
memory.step_dma().unwrap().unwrap();
|
||||
assert_eq!(events(&memory), previous);
|
||||
assert_eq!(memory.cycles(), 4); // IWRAM read/write plus DMA startup, no CPU timing.
|
||||
}
|
||||
|
|
@ -89,33 +89,103 @@ pub fn bus_cycles(waitcnt: u16, address: u32, width: AccessWidth, kind: AccessKi
|
|||
}
|
||||
}
|
||||
|
||||
/// Target/decode pair only. The caller separately charges the old-state source fetch.
|
||||
pub(crate) fn refill_cycles(waitcnt: u16, target: u32, width: AccessWidth) -> u32 {
|
||||
bus_cycles(waitcnt, target, width, AccessKind::NonSequential)
|
||||
+ bus_cycles(
|
||||
waitcnt,
|
||||
target.wrapping_add(width.bytes()),
|
||||
width,
|
||||
AccessKind::Sequential,
|
||||
)
|
||||
}
|
||||
|
||||
/// A data-only trace. Instruction fetches and branch refills are charged by the
|
||||
/// CPU timing summary, not here. Actual CPU bus calls supply addresses and widths.
|
||||
/// One speculative instruction/IRQ timing transaction. Events arrive in source,
|
||||
/// data, internal, then target-pair order. Device clocks still advance in bulk.
|
||||
/// Code uses the entry WAITCNT snapshot; data uses settings at each bus access.
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub(crate) struct DataTiming {
|
||||
pub cycles: u32,
|
||||
next: Option<u32>,
|
||||
pub(crate) struct CpuTiming {
|
||||
pub total: StepTiming,
|
||||
code_waitcnt: u16,
|
||||
next_data: Option<u32>,
|
||||
// The largest supported step is LDM with PC: source + 16 data + I + 2 targets.
|
||||
// Production builds keep only costs and sequencing state, not an event log.
|
||||
#[cfg(test)]
|
||||
events: [Option<TimingEvent>; 20],
|
||||
#[cfg(test)]
|
||||
event_count: usize,
|
||||
}
|
||||
|
||||
impl DataTiming {
|
||||
pub fn access(&mut self, waitcnt: u16, address: u32, width: AccessWidth) {
|
||||
let kind = if self.next == Some(address) {
|
||||
impl CpuTiming {
|
||||
pub fn new(code_waitcnt: u16) -> Self {
|
||||
Self {
|
||||
code_waitcnt,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn code(&mut self, address: u32, width: AccessWidth, kind: AccessKind) {
|
||||
let cycles = bus_cycles(self.code_waitcnt, address, width, kind);
|
||||
self.total.code_cycles += cycles;
|
||||
#[cfg(test)]
|
||||
self.record(TimingEvent::Code {
|
||||
address,
|
||||
width,
|
||||
kind,
|
||||
waitcnt: self.code_waitcnt,
|
||||
cycles,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn data(&mut self, waitcnt: u16, address: u32, width: AccessWidth) {
|
||||
let kind = if self.next_data == Some(address) {
|
||||
AccessKind::Sequential
|
||||
} else {
|
||||
AccessKind::NonSequential
|
||||
};
|
||||
self.cycles += bus_cycles(waitcnt, address, width, kind);
|
||||
self.next = Some(address.wrapping_add(width.bytes()));
|
||||
let cycles = bus_cycles(waitcnt, address, width, kind);
|
||||
self.total.data_cycles += cycles;
|
||||
self.next_data = Some(address.wrapping_add(width.bytes()));
|
||||
#[cfg(test)]
|
||||
self.record(TimingEvent::Data {
|
||||
address,
|
||||
width,
|
||||
kind,
|
||||
waitcnt,
|
||||
cycles,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn internal(&mut self, cycles: u32) {
|
||||
self.total.internal_cycles += cycles;
|
||||
#[cfg(test)]
|
||||
if cycles != 0 {
|
||||
self.record(TimingEvent::Internal { cycles });
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn record(&mut self, event: TimingEvent) {
|
||||
self.events[self.event_count] = Some(event);
|
||||
self.event_count += 1;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn events(&self) -> Vec<TimingEvent> {
|
||||
self.events[..self.event_count]
|
||||
.iter()
|
||||
.map(|event| event.unwrap())
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum TimingEvent {
|
||||
Code {
|
||||
address: u32,
|
||||
width: AccessWidth,
|
||||
kind: AccessKind,
|
||||
waitcnt: u16,
|
||||
cycles: u32,
|
||||
},
|
||||
Data {
|
||||
address: u32,
|
||||
width: AccessWidth,
|
||||
kind: AccessKind,
|
||||
waitcnt: u16,
|
||||
cycles: u32,
|
||||
},
|
||||
Internal {
|
||||
cycles: u32,
|
||||
},
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue