feat(cpu): retain Thumb instruction pipeline halfwords

This commit is contained in:
0xrsydn 2026-10-06 04:10:20 +07:00
commit ea055270f4
21 changed files with 535 additions and 97 deletions

View file

@ -58,6 +58,8 @@ mod thumb_empty_tests;
#[cfg(test)]
mod thumb_open_bus_tests;
#[cfg(test)]
mod thumb_pipeline_tests;
#[cfg(test)]
mod thumb_tests;
#[cfg(test)]
mod timing_tests;
@ -165,8 +167,8 @@ impl InstructionSet {
}
}
/// ARM/Thumb interpreter with register banks, ARM instruction buffering, and nominal costs.
/// Clone/equality include buffered fetch results. Thumb instruction buffering is not modeled.
/// ARM/Thumb interpreter with register banks, instruction buffering, and nominal costs.
/// Clone/equality include buffered fetch results and their instruction-set tag.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Cpu {
registers: [u32; 16],
@ -176,7 +178,7 @@ pub struct Cpu {
irq_disabled: bool,
fiq_disabled: bool,
banks: status::Banks,
arm_pipeline: Option<pipeline::ArmPipeline>,
pipeline: Option<pipeline::Pipeline>,
}
impl Cpu {
@ -193,7 +195,7 @@ impl Cpu {
irq_disabled: false,
fiq_disabled: false,
banks: status::Banks::default(),
arm_pipeline: None,
pipeline: None,
}
}
@ -237,6 +239,7 @@ impl Cpu {
// Record the executing instruction, not an operand's pipelined PC value.
// 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 sequential_thumb = self.instruction_set == InstructionSet::Thumb && !refill;
memory.begin_cpu_access(pc, self.instruction_set);
@ -248,13 +251,13 @@ impl Cpu {
if result.is_ok() {
if refill {
memory.refill_cpu_bus_history(self.pc(), self.instruction_set);
self.refill_arm_pipeline(memory);
self.refill_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)
// 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())
{
continuation
Some(continuation)
} else {
None
};

View file

@ -208,7 +208,7 @@ fn swi_and_machine_irq_refill_arm_vectors_before_handler_execution() {
StepKind::Instruction
}
);
assert!(machine.cpu().arm_pipeline.is_some()); // Entry itself captured the ARM vector pair.
assert!(machine.cpu().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();
@ -244,21 +244,21 @@ fn explicit_exception_and_pc_discontinuity_discard_the_old_execution_sequence()
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.
assert!(cpu.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() {
fn state_changes_replace_the_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());
assert!(cpu.pipeline.is_some()); // BX captured the Thumb target pair.
bus.write32(CODE + 4, NEW).unwrap();
cpu.step(&mut bus).unwrap();
bus.write32(CODE + 4, OLD).unwrap();

View file

@ -163,7 +163,10 @@ fn completed_units_and_priority_changes_use_access_order_not_channel_number() {
#[test]
fn failed_dma_and_failed_resumed_instruction_preserve_last_completed_dma_history() {
for fail_destination in [false, true] {
let mut machine = ready(CODE);
let (mut cpu, bus) = program(CODE, 0x680a);
cpu.registers[0] = 0x0e00_0000; // Buffered PROBE will fail on unsupported save memory.
let mut machine = Machine::new(cpu, bus);
machine.step().unwrap();
machine.memory_mut().write32(EWRAM, DATA).unwrap();
transfer(&mut machine, EWRAM, IWRAM, true);
// A valid IWRAM source must not drive history if its destination is invalid.
@ -189,16 +192,15 @@ fn failed_dma_and_failed_resumed_instruction_preserve_last_completed_dma_history
.memory_mut()
.write16(DMA_BASE + 3 * DMA_STRIDE + 10, 0)
.unwrap();
machine.memory_mut().write16(CODE + 2, 0xde00).unwrap(); // Unsupported instruction.
assert!(machine.step().is_err());
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.cycles(), cycles);
machine.memory_mut().write16(CODE + 2, PROBE).unwrap();
machine.step().unwrap();
assert_eq!(
machine.cpu().registers()[6],
resumed(DATA.to_le_bytes(), CODE)
);
assert_eq!(machine.last_timing(), timing);
// Repair the data address, not the retained instruction or DMA history.
let mut cpu = machine.cpu().clone();
cpu.registers[0] = UNUSED;
cpu.step_timed(machine.memory_mut()).unwrap();
assert_eq!(cpu.registers()[6], resumed(DATA.to_le_bytes(), CODE));
}
}

View file

@ -133,7 +133,7 @@ fn known_history_supports_widths_sign_extension_and_unaligned_loads_in_all_modes
expected.registers[6] = value;
expected.registers[15] += 2;
cpu.step(&mut bus).unwrap();
assert_eq!(cpu, expected);
assert_cpu_arch_eq!(cpu, expected);
}
}
}
@ -284,6 +284,7 @@ fn failed_steps_discard_prefetch_history_before_retry() {
assert!(cpu.step_timed(&mut bus).is_err());
assert_eq!(cpu, before);
bus.write16(pc + 2, PROBE).unwrap();
cpu.invalidate_pipeline(); // Debugger repair leaves separate bus history intact.
cpu.step(&mut bus).unwrap();
assert_eq!(
cpu.registers[6],

View file

@ -134,7 +134,7 @@ fn refill_history_supports_normal_load_lanes_rotation_and_sign_extension() {
after.registers[6] = value;
after.registers[15] += 2;
cpu.step(&mut bus).unwrap();
assert_eq!(cpu, after);
assert_cpu_arch_eq!(cpu, after);
}
}
}

View file

@ -1,62 +1,67 @@
//! ARM instruction buffering only. Timing and bus ownership remain separate.
//! ARM/Thumb instruction buffering. 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 {
pub(super) struct Pipeline {
pc: u32,
instruction_set: InstructionSet,
// Deferred strict-fetch results for the next execute and decode positions.
words: [Result<u32, MemoryError>; 2],
// Thumb halfwords are zero-extended; their upper bits are not another opcode.
instructions: [Result<u32, MemoryError>; 2],
}
pub(super) struct Fetched {
pub instruction: u32,
pub continuation: Option<ArmPipeline>,
pub continuation: Pipeline,
}
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.
/// The next step fills from its current PC/state without adding nominal cycles.
pub fn invalidate_pipeline(&mut self) {
self.arm_pipeline = None;
self.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_set = self.instruction_set;
let width = instruction_set.width();
let retained = self
.pipeline
.as_ref()
.filter(|pipe| pipe.pc == pc && pipe.instruction_set == instruction_set);
let instruction = match retained {
Some(pipe) => pipe.words[0].clone(),
None => memory.fetch_arm_word(pc),
Some(pipe) => pipe.instructions[0].clone(),
None => memory.fetch_instruction(pc, instruction_set),
}?;
let decode = match retained {
Some(pipe) => pipe.words[1].clone(),
None => memory.fetch_arm_word(pc.wrapping_add(4)),
Some(pipe) => pipe.instructions[1].clone(),
None => memory.fetch_instruction(pc.wrapping_add(width), instruction_set),
};
let fetched = memory.fetch_arm_word(pc.wrapping_add(8));
let fetched = memory.fetch_instruction(pc.wrapping_add(2 * width), instruction_set);
Ok(Fetched {
instruction,
continuation: Some(ArmPipeline {
pc: pc.wrapping_add(4),
words: [decode, fetched],
}),
continuation: Pipeline {
pc: pc.wrapping_add(width),
instruction_set,
instructions: [decode, fetched],
},
})
}
/// Capture target and target+4 after a successful refill, without early errors.
/// Capture the target pair using the resulting state, 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)),
pub(crate) fn refill_pipeline(&mut self, memory: &Memory) {
let pc = self.pc();
let instruction_set = self.instruction_set;
self.pipeline = Some(Pipeline {
pc,
instruction_set,
instructions: [
memory.fetch_instruction(pc, instruction_set),
memory.fetch_instruction(pc.wrapping_add(instruction_set.width()), instruction_set),
],
});
}

View file

@ -66,8 +66,9 @@ fn empty_thumb_stores_use_pc_plus_six_for_all_bases_banks_and_alignments() {
} else {
cpu.step(&mut memory).unwrap();
}
assert_eq!(
cpu, after,
assert_cpu_arch_eq!(
cpu,
after,
"{instruction:#06x} {mode:?} code={code_low} data={data_low}"
);
assert_eq!(memory.read32(DATA).unwrap(), pc + 6);

View file

@ -127,8 +127,9 @@ fn loads_select_lanes_rotate_and_sign_extend_without_changing_flags() {
after.registers[1] = value;
after.registers[15] += 2;
cpu.step(&mut bus).unwrap();
assert_eq!(
cpu, after,
assert_cpu_arch_eq!(
cpu,
after,
"pc={pc:#x} instruction={instruction:#x} lane={lane}"
);
}

View file

@ -0,0 +1,321 @@
//! Original Thumb instruction retention tests; bus history and timing remain separate.
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: u16 = 0x46c0;
const OLD: u16 = 0x2001; // MOV r0,#1.
const NEW: u16 = 0x2002; // MOV r0,#2.
fn program(pc: u32, code: &[u16]) -> (Cpu, Memory) {
let mut bus = Memory::new(vec![]).unwrap();
for (index, half) in code.iter().enumerate() {
bus.write16(pc + index as u32 * 2, *half).unwrap();
}
let mut cpu = Cpu::new(pc);
cpu.instruction_set = InstructionSet::Thumb;
(cpu, bus)
}
#[test]
fn thumb_stores_retain_the_two_prefetched_halfwords_but_not_the_next_one() {
for instruction in [0x800a, 0x700a] {
// STRH/STRB r2,[r1].
for pc in [CODE, CODE + 2] {
for offset in [2, 4, 6] {
let (mut cpu, mut bus) = program(pc, &[instruction, OLD, OLD, OLD]);
cpu.registers[1] = pc + offset;
cpu.registers[2] = u32::from(NEW);
cpu.step(&mut bus).unwrap();
assert_eq!(bus.read16(pc + offset).unwrap(), NEW);
for _ in 0..offset / 2 {
cpu.step(&mut bus).unwrap();
}
assert_eq!(cpu.registers[0], if offset <= 4 { 1 } else { 2 });
}
}
}
}
#[test]
fn host_writes_and_memory_mirrors_do_not_replace_buffered_halfwords() {
for pc in [
CODE,
CODE + 2,
0x0300_fffe,
0x0203_fffe,
0x0500_03fe,
0x0600_fffe,
0x0700_03fe,
] {
let (mut cpu, mut bus) = program(pc, &[NOP, OLD, OLD, OLD]);
cpu.step(&mut bus).unwrap();
for offset in [2, 4, 6] {
bus.write16(pc + offset, NEW).unwrap();
}
for expected in [1, 1, 2] {
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[0], expected);
}
}
}
#[test]
fn pc_writes_to_fallthrough_refill_the_thumb_target_pair() {
for instruction in [
0xe7ff, 0xd0ff, 0x468f, 0x448f, 0x4708, 0xbd00, 0xbc00, 0xc900, 0xf800,
] {
// B, BEQ, MOV pc,r1, ADD pc,r1, BX r1, POP pc, empty POP/LDM, BL suffix.
let (mut cpu, mut bus) = program(CODE, &[NOP, instruction, OLD, OLD]);
cpu.flags.zero = true;
cpu.registers[1] = match instruction {
0x448f => u32::MAX - 1, // Visible PC=CODE+6, target=CODE+4.
0xc900 => DATA,
_ => (CODE + 4) | 1,
};
cpu.registers[13] = DATA;
cpu.registers[14] = CODE + 4;
bus.write32(DATA, CODE + 4).unwrap(); // POP keeps Thumb even with bit0 clear.
cpu.step(&mut bus).unwrap();
bus.write16(CODE + 4, NEW).unwrap();
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.pc(), CODE + 4);
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
bus.write16(CODE + 4, OLD).unwrap();
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[0], 2, "{instruction:#06x}");
}
}
#[test]
fn untaken_branch_keeps_the_old_pair() {
let (mut cpu, mut bus) = program(CODE, &[NOP, 0xd0ff, OLD]);
cpu.step(&mut bus).unwrap();
bus.write16(CODE + 4, NEW).unwrap();
cpu.step(&mut bus).unwrap();
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[0], 1);
}
#[test]
fn bl_prefix_retains_the_suffix_and_only_the_suffix_refills() {
let (mut cpu, mut bus) = program(CODE, &[0xf000, 0xf800, OLD, OLD]);
assert_eq!(cpu.step_timed(&mut bus).unwrap().code_cycles, 1);
assert_eq!(cpu.registers[14], CODE + 4);
bus.write16(CODE + 2, NOP).unwrap(); // Does not replace the retained suffix.
bus.write16(CODE + 4, NEW).unwrap();
assert_eq!(cpu.step_timed(&mut bus).unwrap().code_cycles, 3);
assert_eq!(cpu.registers[14], CODE + 5);
bus.write16(CODE + 4, OLD).unwrap(); // Suffix already captured the updated target.
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[0], 2);
}
#[test]
fn arm_to_thumb_refill_captures_target_and_plus_two_but_not_plus_four() {
for target in [TARGET, TARGET + 2] {
let (mut cpu, mut bus) = program(CODE, &[0, 0]);
cpu.instruction_set = InstructionSet::Arm;
bus.write32(CODE, 0xe12f_ff11).unwrap(); // BX r1.
cpu.registers[1] = target | 1;
for offset in [0, 2, 4] {
bus.write16(target + offset, OLD).unwrap();
}
cpu.step(&mut bus).unwrap();
for offset in [0, 2, 4] {
bus.write16(target + offset, NEW).unwrap();
}
for expected in [1, 1, 2] {
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[0], expected);
}
}
}
#[test]
fn dma_halfword_and_word_writes_do_not_replace_thumb_instruction_slots() {
for word in [false, true] {
let (cpu, mut bus) = program(CODE, &[NOP, OLD, OLD, OLD]);
for offset in [0, 2, 4] {
bus.write16(DATA + offset, 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 + if word { 4 } else { 2 })
.unwrap();
machine
.memory_mut()
.write32(dma + 8, if word { 0x8400_0001 } else { 0x8000_0003 })
.unwrap();
for _ in 0..if word { 1 } else { 3 } {
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 3 });
assert_eq!(machine.cpu(), &before);
}
for expected in [1, 1, 2] {
machine.step().unwrap();
assert_eq!(machine.cpu().registers()[0], expected);
}
}
}
#[test]
fn failed_thumb_load_rolls_back_buffer_progress_not_earlier_retention() {
let (mut cpu, mut bus) = program(CODE, &[NOP, 0x680a, OLD, OLD]); // LDR r2,[r1].
cpu.registers[1] = 0x0e00_0000;
cpu.step(&mut bus).unwrap();
let before = cpu.clone();
assert!(cpu.step_timed(&mut bus).is_err());
assert_eq!(cpu, before);
bus.write16(CODE + 2, NOP).unwrap();
bus.write16(CODE + 4, NEW).unwrap();
bus.write16(CODE + 6, NEW).unwrap();
bus.write32(DATA, 0x1234).unwrap();
cpu.registers[1] = DATA;
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[2], 0x1234);
for expected in [1, 2] {
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[0], expected);
}
}
#[test]
fn short_rom_and_refill_errors_are_deferred_and_never_use_open_bus_instructions() {
for half in [NOP, 0x4708, 0xf000] {
// NOP, BX r1, BL prefix.
let mut bus = Memory::new(half.to_le_bytes().to_vec()).unwrap();
bus.write16(TARGET, OLD).unwrap();
let mut cpu = Cpu::new(ROM_START);
cpu.instruction_set = InstructionSet::Thumb;
cpu.registers[1] = TARGET | 1;
cpu.step(&mut bus).unwrap();
if half == 0x4708 {
bus.write16(TARGET, NEW).unwrap();
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[0], 1);
} else {
let before = cpu.clone();
for _ in 0..2 {
assert_eq!(
cpu.step(&mut bus),
Err(CpuError::Memory(MemoryError::Unmapped(ROM_START + 2)))
);
assert_eq!(cpu, before);
}
}
}
let (mut cpu, mut bus) = program(CODE, &[0x4708]);
cpu.registers[1] = 0x8000_0001;
cpu.step(&mut bus).unwrap();
let before = cpu.clone();
assert_eq!(
cpu.step(&mut bus),
Err(CpuError::Memory(MemoryError::Unmapped(0x8000_0000)))
);
assert_eq!(cpu, before);
}
#[test]
fn explicit_invalidation_repairs_buffered_thumb_code_and_clones_stay_independent() {
let (mut cpu, mut bus) = program(CODE, &[NOP, 0xde00, OLD]);
cpu.step(&mut bus).unwrap();
let before = cpu.clone();
let mut fork = cpu.clone();
bus.write16(CODE + 2, NEW).unwrap();
assert!(cpu.step(&mut bus).is_err());
assert_eq!(cpu, before);
cpu.invalidate_pipeline();
assert_cpu_arch_eq!(cpu, before);
assert_ne!(cpu, before);
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[0], 2);
assert!(fork.step(&mut bus).is_err());
}
#[test]
fn instruction_state_and_pc_both_select_the_retained_buffer() {
let (mut cpu, mut bus) = program(CODE, &[NOP, OLD, OLD]);
cpu.step(&mut bus).unwrap();
// A PC discontinuity starts a new Thumb sequence from mapped bytes.
cpu.registers[15] = TARGET + 2;
bus.write16(TARGET + 2, NOP).unwrap();
bus.write16(TARGET + 4, OLD).unwrap();
cpu.step(&mut bus).unwrap();
// Expected next Thumb PC is now TARGET+4. Keep that PC but change state without a refill.
cpu.instruction_set = InstructionSet::Arm;
bus.write32(TARGET + 4, 0xe3a0_0007).unwrap();
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[0], 7); // Must not decode the old Thumb halfword as ARM.
cpu.registers[15] = CODE + 1;
cpu.instruction_set = InstructionSet::Thumb;
let before = cpu.clone();
assert_eq!(
cpu.step(&mut bus),
Err(CpuError::Memory(MemoryError::Unaligned(CODE + 1)))
);
assert_eq!(cpu, before);
}
#[test]
fn swi_and_irq_returns_refill_thumb_targets_using_saved_state() {
for irq in [false, true] {
let mut bios = vec![0; BIOS_SIZE];
let vector = if irq { 0x18 } else { 8 };
let instruction: u32 = if irq { 0xe25e_f004 } else { 0xe1b0_f00e };
bios[vector..vector + 4].copy_from_slice(&instruction.to_le_bytes());
let mut bus = Memory::with_bios(vec![], bios).unwrap();
bus.write16(CODE, if irq { NOP } else { 0xdf00 }).unwrap();
for offset in [2, 4] {
bus.write16(CODE + offset, OLD).unwrap();
}
let mut cpu = Cpu::new(CODE);
cpu.instruction_set = InstructionSet::Thumb;
let mut machine = Machine::new(cpu, bus);
machine.step().unwrap();
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(), StepKind::IrqEntry);
machine.memory_mut().write16(IME, 0).unwrap();
}
machine.memory_mut().write16(CODE + 2, NEW).unwrap();
machine.step().unwrap(); // ARM exception return captures updated Thumb target.
machine.memory_mut().write16(CODE + 2, OLD).unwrap();
machine.step().unwrap();
assert_eq!(machine.cpu().registers()[0], 2);
assert_eq!(machine.cpu().instruction_set(), InstructionSet::Thumb);
}
}
#[test]
fn timed_and_untimed_thumb_buffering_has_no_additional_nominal_costs() {
let code = [NOP, 0xe7ff, OLD, NOP];
let (mut timed, mut timed_bus) = program(CODE, &code);
let (mut untimed, mut bus) = program(CODE, &code);
for cost in [1, 3, 1] {
let timing = timed.step_timed(&mut timed_bus).unwrap();
untimed.step(&mut bus).unwrap();
assert_eq!(timed, untimed);
assert_eq!(timing.code_cycles, cost);
assert_eq!(timing.data_cycles, 0);
assert_eq!(timing.internal_cycles, 0);
assert_eq!(timed_bus.cycles(), 0);
}
}

View file

@ -179,7 +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);
self.cpu.refill_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.
/// CPU-owned ARM instruction buffering does not yet drive complete bus history or timing.
/// CPU-owned ARM/Thumb instruction buffering does not yet drive complete bus history or timing.
pub struct Memory {
external_ram: Vec<u8>,
internal_ram: Vec<u8>,
@ -408,13 +408,22 @@ 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 {
/// Strict instruction fetch without data context, timing, or bus-history changes.
/// Thumb reads exactly two bytes, even on a region with a wider data-bus latch.
pub(crate) fn fetch_instruction(
&self,
address: u32,
state: InstructionSet,
) -> Result<u32, MemoryError> {
let width = match state {
InstructionSet::Arm => 4,
InstructionSet::Thumb => 2,
};
if address & (width as u32 - 1) != 0 {
return Err(MemoryError::Unaligned(address));
}
let mut bytes = [0; 4];
for (offset, byte) in bytes.iter_mut().enumerate() {
for (offset, byte) in bytes[..width].iter_mut().enumerate() {
*byte = self.read_mapped_byte(address.wrapping_add(offset as u32))?;
}
Ok(u32::from_le_bytes(bytes))