feat(cpu): retain ARM instruction pipeline words

Buffer ARM sequential fetch results and refill target pairs after branches, PC writes, and machine exception entry. Stage advances transactionally, defer speculative fetch diagnostics, and preserve buffered words across CPU/DMA stores and host edits. Add explicit CPU pipeline invalidation for debugger repair and memory rebinding.

Add 15 original instruction-buffer regressions. Keep full-state rollback equality while distinguishing architectural comparisons in semantic tests. Update the display error test for an already buffered branch. Document evidence and separate remaining Thumb, bus-history, and timing work.

Validated on Darwin arm64: 967 workspace and 898 core/demo tests in each debug/release profile, 39 Python tests, native ROM windows, ten window smoke modes, fmt, clippy, and rustdoc. Public ARM/Thumb/memory/BIOS reports and nominal cycles remain unchanged.
This commit is contained in:
0xrsydn 2026-10-06 03:57:57 +07:00
commit 47df9d05a7
24 changed files with 629 additions and 37 deletions

View file

@ -2,9 +2,23 @@ use std::{error::Error, fmt};
use crate::memory::{Memory, MemoryError};
// Semantic tests compare architectural state after successful instructions.
// Error/rollback tests keep full Cpu equality, including buffered fetch results.
#[cfg(test)]
macro_rules! assert_cpu_arch_eq {
($actual:expr, $expected:expr $(, $($message:tt)+)?) => {{
let mut actual = $actual.clone();
let mut expected = $expected.clone();
actual.invalidate_pipeline();
expected.invalidate_pipeline();
assert_eq!(actual, expected $(, $($message)+)?);
}};
}
mod alu;
mod arm;
mod exception;
mod pipeline;
mod status;
mod thumb;
mod timing;
@ -13,6 +27,8 @@ pub use exception::Exception;
pub use status::Mode;
mod transfer;
#[cfg(test)]
mod arm_pipeline_tests;
#[cfg(test)]
mod bios_access_tests;
#[cfg(test)]
@ -149,7 +165,8 @@ impl InstructionSet {
}
}
/// ARM/Thumb interpreter with register banks and optional nominal cycle costs.
/// ARM/Thumb interpreter with register banks, ARM instruction buffering, and nominal costs.
/// Clone/equality include buffered fetch results. Thumb instruction buffering is not modeled.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Cpu {
registers: [u32; 16],
@ -159,6 +176,7 @@ pub struct Cpu {
irq_disabled: bool,
fiq_disabled: bool,
banks: status::Banks,
arm_pipeline: Option<pipeline::ArmPipeline>,
}
impl Cpu {
@ -175,6 +193,7 @@ impl Cpu {
irq_disabled: false,
fiq_disabled: false,
banks: status::Banks::default(),
arm_pipeline: None,
}
}
@ -206,14 +225,15 @@ impl Cpu {
self.execute_fetched(instruction, memory)
}
fn fetch(&self, memory: &Memory) -> Result<u32, CpuError> {
Ok(match self.instruction_set {
InstructionSet::Arm => memory.read32(self.pc())?,
InstructionSet::Thumb => u32::from(memory.read16(self.pc())?),
})
}
fn execute_fetched(&mut self, instruction: u32, memory: &mut Memory) -> Result<(), CpuError> {
fn execute_fetched(
&mut self,
fetched: pipeline::Fetched,
memory: &mut Memory,
) -> Result<(), CpuError> {
let pipeline::Fetched {
instruction,
continuation,
} = fetched;
// Record the executing instruction, not an operand's pipelined PC value.
// Always clear the access context, including on diagnostic errors.
let pc = self.pc();
@ -225,8 +245,20 @@ impl Cpu {
&& self.instruction_set == InstructionSet::Thumb
&& self.pc() == pc.wrapping_add(2);
memory.end_cpu_access(result.is_ok(), sequential);
if result.is_ok() && refill {
memory.refill_cpu_bus_history(self.pc(), self.instruction_set);
if result.is_ok() {
if refill {
memory.refill_cpu_bus_history(self.pc(), self.instruction_set);
self.refill_arm_pipeline(memory);
} else {
// Never carry an ARM buffer through a Thumb-state transition.
self.arm_pipeline = if self.instruction_set == InstructionSet::Arm
&& self.pc() == pc.wrapping_add(4)
{
continuation
} else {
None
};
}
}
result
}

View file

@ -0,0 +1,321 @@
//! Original ARM instruction-buffer tests. Bus timing remains nominal.
use super::*;
use crate::{
dma::{DMA_BASE, DMA_STRIDE},
io::{IE, IME, TIMER_BASE},
machine::{Machine, StepKind},
memory::{BIOS_SIZE, ROM_START},
};
const CODE: u32 = 0x0300_0100;
const TARGET: u32 = 0x0300_0200;
const DATA: u32 = 0x0200_1000;
const NOP: u32 = 0xe1a0_0000;
const OLD: u32 = 0xe3a0_0001; // MOV r0,#1.
const NEW: u32 = 0xe3a0_0002; // MOV r0,#2.
fn program(code: &[u32]) -> (Cpu, Memory) {
let mut bus = Memory::new(vec![]).unwrap();
for (index, word) in code.iter().enumerate() {
bus.write32(CODE + index as u32 * 4, *word).unwrap();
}
(Cpu::new(CODE), bus)
}
#[test]
fn cpu_stores_do_not_replace_the_two_words_fetched_before_execution() {
for offset in [4, 8, 12] {
let (mut cpu, mut bus) = program(&[0xe581_2000, OLD, OLD, OLD]); // STR r2,[r1].
cpu.registers[1] = CODE + offset;
cpu.registers[2] = NEW;
cpu.step(&mut bus).unwrap();
assert_eq!(bus.read32(CODE + offset).unwrap(), NEW);
for _ in 0..offset / 4 {
cpu.step(&mut bus).unwrap();
}
assert_eq!(cpu.registers[0], if offset <= 8 { 1 } else { 2 });
}
}
#[test]
fn host_writes_leave_buffered_words_but_future_fetches_observe_changes() {
let (mut cpu, mut bus) = program(&[NOP, OLD, OLD, OLD]);
cpu.step(&mut bus).unwrap();
for offset in [4, 8, 12] {
bus.write32(CODE + offset, NEW).unwrap();
}
assert_eq!(bus.read32(CODE + 4).unwrap(), NEW);
for expected in [1, 1, 2] {
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[0], expected);
}
}
#[test]
fn branch_to_fallthrough_refills_even_when_the_pc_value_is_sequential() {
for instruction in [0xeaff_ffff, 0xe1a0_f001, 0xe12f_ff11, 0xe591_f000] {
// B P+4, MOV pc,r1, BX r1, LDR pc,[r1].
let (mut cpu, mut bus) = program(&[NOP, instruction, OLD, OLD]);
cpu.registers[1] = if instruction == 0xe591_f000 {
DATA
} else {
CODE + 8
};
bus.write32(DATA, CODE + 8).unwrap();
cpu.step(&mut bus).unwrap();
bus.write32(CODE + 8, NEW).unwrap(); // Was fetched by NOP.
cpu.step(&mut bus).unwrap();
bus.write32(CODE + 8, OLD).unwrap(); // Was refetched by the branch.
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[0], 2);
}
}
#[test]
fn untaken_branch_keeps_buffered_instructions() {
let (mut cpu, mut bus) = program(&[NOP, 0x0aff_ffff, OLD]); // BEQ, Z is clear.
cpu.step(&mut bus).unwrap();
bus.write32(CODE + 8, NEW).unwrap();
cpu.step(&mut bus).unwrap();
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[0], 1);
}
#[test]
fn refill_captures_target_pair_but_not_target_plus_eight() {
for thumb_source in [false, true] {
let (mut cpu, mut bus) = program(&[if thumb_source { 0x4708 } else { 0xe12f_ff11 }]);
if thumb_source {
cpu.instruction_set = InstructionSet::Thumb;
}
cpu.registers[1] = TARGET;
for offset in [0, 4, 8] {
bus.write32(TARGET + offset, OLD).unwrap();
}
cpu.step(&mut bus).unwrap();
for offset in [0, 4, 8] {
bus.write32(TARGET + offset, NEW).unwrap();
}
for expected in [1, 1, 2] {
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[0], expected);
}
}
}
#[test]
fn dma_writes_do_not_snoop_buffered_arm_instructions() {
let (cpu, mut bus) = program(&[NOP, OLD, OLD, OLD]);
bus.write32(DATA, NEW).unwrap();
bus.write32(DATA + 4, NEW).unwrap();
let mut machine = Machine::new(cpu, bus);
machine.step().unwrap();
let before = machine.cpu().clone();
let dma = DMA_BASE + 3 * DMA_STRIDE;
machine.memory_mut().write32(dma, DATA).unwrap();
machine.memory_mut().write32(dma + 4, CODE + 4).unwrap();
machine.memory_mut().write32(dma + 8, 0x8400_0002).unwrap();
for _ in 0..2 {
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 3 });
assert_eq!(machine.cpu(), &before);
}
for _ in 0..2 {
machine.step().unwrap();
assert_eq!(machine.cpu().registers()[0], 1);
}
}
#[test]
fn speculative_errors_do_not_fail_a_valid_instruction_or_an_escaping_branch() {
for first in [NOP, 0xe12f_ff11] {
let mut bus = Memory::new(first.to_le_bytes().to_vec()).unwrap();
bus.write32(TARGET, OLD).unwrap();
let mut cpu = Cpu::new(ROM_START);
cpu.registers[1] = TARGET;
cpu.step(&mut bus).unwrap();
if first == NOP {
let before = cpu.clone();
for _ in 0..2 {
assert_eq!(
cpu.step(&mut bus),
Err(CpuError::Memory(MemoryError::Unmapped(ROM_START + 4)))
);
assert_eq!(cpu, before);
}
} else {
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[0], 1);
}
}
let (mut cpu, mut bus) = program(&[0xe12f_ff11]);
cpu.registers[1] = 0x1000_0000;
cpu.step(&mut bus).unwrap(); // Refill failure is deferred until target execution.
let before = cpu.clone();
assert!(cpu.step(&mut bus).is_err());
assert_eq!(cpu, before);
}
#[test]
fn failed_data_access_keeps_the_old_buffer_and_discards_speculative_progress() {
let (mut cpu, mut bus) = program(&[NOP, 0xe591_2000, OLD, OLD]);
cpu.registers[1] = 0x0e00_0000;
cpu.step(&mut bus).unwrap();
let before = cpu.clone();
assert!(cpu.step(&mut bus).is_err());
assert_eq!(cpu, before);
// Current LDR and next MOV were buffered. P+8 sampled by the failed step was not committed.
bus.write32(CODE + 4, NOP).unwrap();
bus.write32(CODE + 8, NEW).unwrap();
bus.write32(CODE + 12, NEW).unwrap();
bus.write32(DATA, 0x1234).unwrap();
cpu.registers[1] = DATA;
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[2], 0x1234);
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[0], 1);
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[0], 2);
}
#[test]
fn swi_and_machine_irq_refill_arm_vectors_before_handler_execution() {
for irq in [false, true] {
let mut bios = vec![0; BIOS_SIZE];
let vector = if irq { 0x18 } else { 8 };
bios[vector..vector + 4].copy_from_slice(&0xe12f_ff11_u32.to_le_bytes()); // BX r1.
let rom = if irq { NOP } else { 0xef00_0000_u32 }
.to_le_bytes()
.to_vec();
let mut bus = Memory::with_bios(rom, bios).unwrap();
bus.write32(TARGET, OLD).unwrap();
let mut cpu = Cpu::new(ROM_START);
cpu.registers[1] = TARGET;
let mut machine = Machine::new(cpu, bus);
if irq {
machine.memory_mut().write16(IE, 8).unwrap();
machine.memory_mut().write16(IME, 1).unwrap();
machine
.memory_mut()
.write32(TIMER_BASE, 0x00c0_ffff)
.unwrap();
machine.memory_mut().advance_cycles(1);
}
assert_eq!(
machine.step().unwrap(),
if irq {
StepKind::IrqEntry
} else {
StepKind::Instruction
}
);
assert!(machine.cpu().arm_pipeline.is_some()); // Entry itself captured the ARM vector pair.
machine.step().unwrap(); // Vector BX refills target pair.
machine.memory_mut().write32(TARGET, NEW).unwrap();
machine.step().unwrap();
assert_eq!(machine.cpu().registers()[0], 1);
}
}
#[test]
fn explicit_invalidation_repairs_buffered_unsupported_code_without_changing_registers() {
let (mut cpu, mut bus) = program(&[NOP, 0xffff_ffff, OLD]);
cpu.step(&mut bus).unwrap();
let before = cpu.clone();
let mut fork = cpu.clone();
bus.write32(CODE + 4, NEW).unwrap();
for _ in 0..2 {
assert!(cpu.step(&mut bus).is_err());
assert_eq!(cpu, before); // Includes the retained unsupported word.
}
cpu.invalidate_pipeline();
assert_cpu_arch_eq!(cpu, before);
assert_ne!(cpu, before); // Full Cpu equality includes the buffer.
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[0], 2);
assert!(fork.step(&mut bus).is_err()); // Cloning owns an independent retained buffer.
}
#[test]
fn explicit_exception_and_pc_discontinuity_discard_the_old_execution_sequence() {
let (mut cpu, mut bus) = program(&[NOP, OLD, OLD]);
cpu.step(&mut bus).unwrap();
bus.write32(TARGET, NEW).unwrap();
cpu.registers[15] = TARGET;
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[0], 2);
cpu.enter_exception(Exception::Irq);
assert!(cpu.arm_pipeline.is_none()); // This API has no Memory to sample.
let before = cpu.clone();
assert!(cpu.step(&mut bus).is_err()); // Missing BIOS remains a strict error.
assert_eq!(cpu, before);
}
#[test]
fn leaving_arm_discards_its_buffer_and_returning_refills_from_current_memory() {
let (mut cpu, mut bus) = program(&[0xe12f_ff11, OLD, OLD]);
bus.write16(TARGET, 0x4710).unwrap(); // Thumb BX r2.
cpu.registers[1] = TARGET | 1;
cpu.registers[2] = CODE + 4;
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
assert!(cpu.arm_pipeline.is_none());
bus.write32(CODE + 4, NEW).unwrap();
cpu.step(&mut bus).unwrap();
bus.write32(CODE + 4, OLD).unwrap();
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[0], 2);
}
#[test]
fn arm_exception_return_refills_after_status_and_target_restore() {
let (mut cpu, mut bus) = program(&[0xe1b0_f00e]); // MOVS pc,lr.
cpu.enter_exception(Exception::SoftwareInterrupt);
cpu.registers[15] = CODE;
cpu.registers[14] = TARGET;
bus.write32(TARGET, OLD).unwrap();
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.mode(), Mode::System);
assert_eq!(cpu.pc(), TARGET);
bus.write32(TARGET, NEW).unwrap();
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[0], 1);
}
#[test]
fn ram_mirrors_and_word_alignment_apply_to_buffered_fetches() {
for pc in [0x0300_7ffc, 0x0300_fffc, 0x0203_fffc] {
let mut bus = Memory::new(vec![]).unwrap();
bus.write32(pc, NOP).unwrap();
bus.write32(pc + 4, OLD).unwrap();
bus.write32(pc + 8, OLD).unwrap();
let mut cpu = Cpu::new(pc);
cpu.step(&mut bus).unwrap();
bus.write32(pc + 4, NEW).unwrap();
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[0], 1);
}
let (mut cpu, mut bus) = program(&[NOP]);
cpu.registers[15] += 2;
let before = cpu.clone();
assert_eq!(
cpu.step(&mut bus),
Err(CpuError::Memory(MemoryError::Unaligned(CODE + 2)))
);
assert_eq!(cpu, before);
}
#[test]
fn timed_and_untimed_buffers_match_without_extra_fetch_or_refill_cycles() {
let code = [NOP, 0xeaff_ffff, OLD, NOP];
let (mut timed, mut bus_timed) = program(&code);
let (mut untimed, mut bus_untimed) = program(&code);
for expected_cost in [1, 3, 1] {
let timing = timed.step_timed(&mut bus_timed).unwrap();
untimed.step(&mut bus_untimed).unwrap();
assert_eq!(timed, untimed);
assert_eq!(timing.code_cycles, expected_cost);
assert_eq!(timing.data_cycles, 0);
assert_eq!(timing.internal_cycles, 0);
assert_eq!(bus_timed.cycles(), 0);
}
}

View file

@ -78,8 +78,9 @@ fn protected_bios_loads_use_latched_lanes_for_arm_and_thumb_callers() {
expected.registers[1] = value;
expected.registers[15] += if is_thumb { 2 } else { 4 };
let timing = cpu.step_timed(&mut memory).unwrap();
assert_eq!(
cpu, expected,
assert_cpu_arch_eq!(
cpu,
expected,
"arm={arm:#010x} thumb={is_thumb} address={address:#x} lane={low}"
);
assert_eq!(timing.data_cycles, 1);

View file

@ -129,7 +129,7 @@ fn failed_conditions_do_not_restore_status_or_validate_spsr() {
expected.registers[15] += 4;
cpu.step(&mut memory(instruction(opcode, 1) & 0x0fff_ffff))
.unwrap(); // EQ
assert_eq!(cpu, expected);
assert_cpu_arch_eq!(cpu, expected);
}
}

View file

@ -29,6 +29,7 @@ impl Cpu {
/// For IRQ/FIQ, PC must identify the next instruction, between CPU steps.
/// This does not execute a BIOS service or fetch the vector instruction.
pub fn enter_exception(&mut self, exception: Exception) {
self.invalidate_pipeline();
let (mode, vector, offset) = match exception {
Exception::UndefinedInstruction => {
(Mode::Undefined, 0x04, self.instruction_set.width())

View file

@ -90,7 +90,7 @@ fn conditional_swi_can_be_skipped_without_entering_supervisor() {
let mut expected = cpu.clone();
expected.registers[15] += 4;
cpu.step(&mut program(0x0fff_ffff)).unwrap();
assert_eq!(cpu, expected);
assert_cpu_arch_eq!(cpu, expected);
}
#[test]

View file

@ -99,7 +99,7 @@ fn load_aliases_keep_loaded_values_for_all_widths_offsets_and_index_modes() {
after.registers[0] = expected(kind, low);
after.registers[15] += 4;
let timing = cpu.step_timed(&mut bus).unwrap();
assert_eq!(cpu, after, "{kind:?} {index:?} up={up} register_offset={register_offset} low={low}");
assert_cpu_arch_eq!(cpu, after, "{kind:?} {index:?} up={up} register_offset={register_offset} low={low}");
assert_eq!(bus.read32(RAM).unwrap(), DATA);
assert_eq!(bus.cycles(), 0);
assert_eq!(
@ -152,7 +152,7 @@ fn load_aliases_modify_only_the_active_register_in_every_cpu_bank() {
register as u32,
)))
.unwrap();
assert_eq!(cpu, after, "{kind:?} {mode:?} r{register}");
assert_cpu_arch_eq!(cpu, after, "{kind:?} {mode:?} r{register}");
}
}
}
@ -263,7 +263,7 @@ fn skipped_alias_loads_do_not_access_memory_or_apply_writeback() {
let mut after = cpu.clone();
after.registers[15] += 4;
let timing = cpu.step_timed(&mut bus).unwrap();
assert_eq!(cpu, after);
assert_cpu_arch_eq!(cpu, after);
assert_eq!(timing.data_cycles, 0);
assert_eq!(timing.internal_cycles, 0);
}

View file

@ -38,7 +38,7 @@ fn arm_unused_ranges_return_pc_plus_eight_not_address_mirrors() {
expected.registers[1] = DATA;
expected.registers[15] += 4;
cpu.step(&mut bus).unwrap();
assert_eq!(cpu, expected, "{address:#010x}");
assert_cpu_arch_eq!(cpu, expected, "{address:#010x}");
// Inspection outside CPU execution remains strict, including every lane.
for low in 0..4 {
assert_eq!(
@ -73,7 +73,7 @@ fn open_bus_loads_select_lanes_rotate_and_sign_extend_normally() {
expected.registers[1] = value;
expected.registers[15] += 4;
let timing = cpu.step_timed(&mut bus).unwrap();
assert_eq!(cpu, expected, "{instruction:#010x}, lane {low}");
assert_cpu_arch_eq!(cpu, expected, "{instruction:#010x}, lane {low}");
assert_eq!(timing.data_cycles, 1);
assert_eq!(timing.internal_cycles, 1);
assert_eq!(timing.code_cycles, 8); // ROM boundary; no extra lookahead cost

View file

@ -0,0 +1,63 @@
//! ARM instruction buffering only. Timing and bus ownership remain separate.
use super::{Cpu, CpuError, InstructionSet};
use crate::memory::{Memory, MemoryError};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct ArmPipeline {
pc: u32,
// Deferred strict-fetch results for the next execute and decode positions.
words: [Result<u32, MemoryError>; 2],
}
pub(super) struct Fetched {
pub instruction: u32,
pub continuation: Option<ArmPipeline>,
}
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 ARM step fills from its current PC without adding nominal cycles.
pub fn invalidate_pipeline(&mut self) {
self.arm_pipeline = None;
}
pub(super) fn fetch(&self, memory: &Memory) -> Result<Fetched, CpuError> {
if self.instruction_set == InstructionSet::Thumb {
return Ok(Fetched {
instruction: u32::from(memory.read16(self.pc())?),
continuation: None,
});
}
let pc = self.pc();
let retained = self.arm_pipeline.as_ref().filter(|pipe| pipe.pc == pc);
let instruction = match retained {
Some(pipe) => pipe.words[0].clone(),
None => memory.fetch_arm_word(pc),
}?;
let decode = match retained {
Some(pipe) => pipe.words[1].clone(),
None => memory.fetch_arm_word(pc.wrapping_add(4)),
};
let fetched = memory.fetch_arm_word(pc.wrapping_add(8));
Ok(Fetched {
instruction,
continuation: Some(ArmPipeline {
pc: pc.wrapping_add(4),
words: [decode, fetched],
}),
})
}
/// Capture target and target+4 after a successful refill, without early errors.
/// Explicit exception entry without Memory leaves a cold buffer instead.
pub(crate) fn refill_arm_pipeline(&mut self, memory: &Memory) {
self.arm_pipeline = (self.instruction_set == InstructionSet::Arm).then(|| ArmPipeline {
pc: self.pc(),
words: [
memory.fetch_arm_word(self.pc()),
memory.fetch_arm_word(self.pc().wrapping_add(4)),
],
});
}
}

View file

@ -190,6 +190,6 @@ fn conditional_status_transfers_do_not_execute_when_condition_fails() {
let mut expected = cpu.clone();
expected.registers[15] += 4;
cpu.step(&mut program(instruction)).unwrap();
assert_eq!(cpu, expected);
assert_cpu_arch_eq!(cpu, expected);
}
}

View file

@ -80,7 +80,11 @@ fn thumb_exit_retains_both_word_lanes_for_each_code_alignment_and_load_width() {
after.registers[1] = value;
after.registers[15] += if target_thumb { 2 } else { 4 };
let timing = cpu.step_timed(&mut memory).unwrap();
assert_eq!(cpu, after, "pc={pc:#x} lane={lane} thumb={target_thumb}");
assert_cpu_arch_eq!(
cpu,
after,
"pc={pc:#x} lane={lane} thumb={target_thumb}"
);
assert_eq!(timing.data_cycles, 1);
assert_eq!(timing.internal_cycles, 1);
assert_eq!(memory.cycles(), 0);

View file

@ -50,7 +50,8 @@ impl Cpu {
/// this does not advance device time or sample interrupts. Machine does that.
/// Failed instructions discard their timing trace and retain existing diagnostics.
pub fn step_timed(&mut self, memory: &mut Memory) -> Result<StepTiming, CpuError> {
let instruction = self.fetch(memory)?;
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),
@ -61,7 +62,7 @@ impl Cpu {
let waitcnt = memory.waitcnt();
let code_kind = memory.cpu_code_kind(summary.code_kind);
memory.begin_data_timing();
let result = self.execute_fetched(instruction, memory);
let result = self.execute_fetched(fetched, memory);
let data_cycles = memory.end_data_timing();
result?;
let code_cycles = if summary.refill {

View file

@ -179,6 +179,7 @@ impl Machine {
}
let (kind, timing) = if self.cpu.take_interrupt(self.memory.irq_pending(), false) {
self.memory.invalidate_cpu_bus_history();
self.cpu.refill_arm_pipeline(&self.memory);
(StepKind::IrqEntry, self.cpu.exception_timing(&self.memory))
} else {
(

View file

@ -69,7 +69,7 @@ struct CpuAccess {
/// Successful refills into Thumb IWRAM sample the target pair to establish history.
/// DMA accesses drive existing Thumb IWRAM continuation lanes before the next fetch.
/// Channel data remains separate; general and sub-instruction CPU bus handoff is unmodeled.
/// A full fetch pipeline and ARM-target refill history are not modeled.
/// CPU-owned ARM instruction buffering does not yet drive complete bus history or timing.
pub struct Memory {
external_ram: Vec<u8>,
internal_ram: Vec<u8>,
@ -408,6 +408,18 @@ impl Memory {
]))
}
/// Strict ARM fetch without CPU data context, timing trace, or bus-history changes.
pub(crate) fn fetch_arm_word(&self, address: u32) -> Result<u32, MemoryError> {
if address & 3 != 0 {
return Err(MemoryError::Unaligned(address));
}
let mut bytes = [0; 4];
for (offset, byte) in bytes.iter_mut().enumerate() {
*byte = self.read_mapped_byte(address.wrapping_add(offset as u32))?;
}
Ok(u32::from_le_bytes(bytes))
}
fn snapshot_mapped_word(&self, address: u32) -> Option<u32> {
let mut bytes = [0; 4];
for (offset, byte) in bytes.iter_mut().enumerate() {