#![allow(clippy::unusual_byte_groupings)]
#![allow(clippy::identity_op)]
use super::arm;
use super::bus::Bus;
use super::registers::{CpuMode, FLAG_F, FLAG_I, FLAG_T, Registers};
use super::thumb;
use crate::gba::bus::WidthClass;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExceptionVector {
Reset = 0x00,
Undefined = 0x04,
SoftwareInterrupt = 0x08,
PrefetchAbort = 0x0C,
DataAbort = 0x10,
Irq = 0x18,
Fiq = 0x1C,
}
pub struct Arm7tdmi {
pub regs: Registers,
pub cycles: u64,
irq_pending: bool,
fiq_pending: bool,
halt_exit_pending: bool,
prefetch_valid: bool,
prefetch_arm: [u32; 3],
prefetch_thumb: [u16; 3],
halted: bool,
pending_data_abort_exec_pc: Option<u32>,
gamepak_prefetch_halfwords: u8,
gamepak_prefetch_cycle_credit: u8,
#[cfg(test)]
irq_dispatch_count: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Arm7tdmiState {
pub regs: Registers,
pub cycles: u64,
pub irq_pending: bool,
pub fiq_pending: bool,
pub halt_exit_pending: bool,
pub prefetch_valid: bool,
pub prefetch_arm: [u32; 3],
pub prefetch_thumb: [u16; 3],
pub halted: bool,
pub pending_data_abort_exec_pc: Option<u32>,
#[serde(default)]
pub gamepak_prefetch_halfwords: u8,
#[serde(default)]
pub gamepak_prefetch_cycle_credit: u8,
}
fn data_word_access_is_sequential(prev_addr: u32, addr: u32) -> bool {
let prev_region = (prev_addr >> 24) & 0xF;
let region = (addr >> 24) & 0xF;
if prev_region != region {
return false;
}
if matches!(region, 0x8..=0xD) {
return (prev_addr & 0x01FE_0000) == (addr & 0x01FE_0000);
}
true
}
fn align_cpu_fast_set_addr(addr: u32) -> u32 {
if matches!((addr >> 24) & 0xF, 0xE | 0xF) {
addr
} else {
addr & !3
}
}
fn cpu_fast_set_source_requires_bios_execution(addr: u32) -> bool {
!(0x0200_0000..0x1000_0000).contains(&addr)
}
fn hle_bios_arctan_values(input: i32) -> (i32, i32, i32) {
let intermediate = (input.wrapping_mul(input) >> 14).wrapping_neg();
let mut coefficient = 0x00A9i32;
for addend in [0x0390, 0x091C, 0x0FB6, 0x16AA, 0x2081, 0x3651, 0xA2F9] {
coefficient = (coefficient.wrapping_mul(intermediate) >> 14).wrapping_add(addend);
}
let result = input.wrapping_mul(coefficient) >> 16;
(result, intermediate, coefficient)
}
fn hle_bios_arctan2_values(x: i32, y: i32) -> (i32, i32) {
if y == 0 {
return (if x >= 0 { 0 } else { 0x8000 }, y);
}
if x == 0 {
return (if y >= 0 { 0x4000 } else { 0xC000 }, y);
}
if y >= 0 {
if x >= 0 {
if x >= y {
let (angle, intermediate, _) =
hle_bios_arctan_values(y.wrapping_shl(14).wrapping_div(x));
return (angle, intermediate);
}
} else if x.wrapping_neg() >= y {
let (angle, intermediate, _) =
hle_bios_arctan_values(y.wrapping_shl(14).wrapping_div(x));
return (angle.wrapping_add(0x8000), intermediate);
}
let (angle, intermediate, _) = hle_bios_arctan_values(x.wrapping_shl(14).wrapping_div(y));
return (0x4000i32.wrapping_sub(angle), intermediate);
}
if x <= 0 {
if x.wrapping_neg() > y.wrapping_neg() {
let (angle, intermediate, _) =
hle_bios_arctan_values(y.wrapping_shl(14).wrapping_div(x));
return (angle.wrapping_add(0x8000), intermediate);
}
} else if x >= y.wrapping_neg() {
let (angle, intermediate, _) = hle_bios_arctan_values(y.wrapping_shl(14).wrapping_div(x));
return (angle.wrapping_add(0x10000), intermediate);
}
let (angle, intermediate, _) = hle_bios_arctan_values(x.wrapping_shl(14).wrapping_div(y));
(0xC000i32.wrapping_sub(angle), intermediate)
}
impl Default for Arm7tdmi {
fn default() -> Self {
Self::new()
}
}
impl Arm7tdmi {
pub fn new() -> Self {
let mut regs = Registers::new();
regs.switch_mode(CpuMode::Supervisor);
regs.cpsr |= FLAG_I | FLAG_F;
regs.set_thumb(false);
regs.r[15] = ExceptionVector::Reset as u32;
Self {
regs,
cycles: 0,
irq_pending: false,
fiq_pending: false,
halt_exit_pending: false,
prefetch_valid: false,
prefetch_arm: [0; 3],
prefetch_thumb: [0; 3],
halted: false,
pending_data_abort_exec_pc: None,
gamepak_prefetch_halfwords: 0,
gamepak_prefetch_cycle_credit: 0,
#[cfg(test)]
irq_dispatch_count: 0,
}
}
fn refill_prefetch<B: Bus>(&mut self, bus: &mut B) {
let pc = self.regs.r[15];
if self.thumb() {
self.prefetch_thumb[0] = bus.fetch16(pc);
self.prefetch_thumb[1] = bus.fetch16(pc.wrapping_add(2));
self.prefetch_thumb[2] = bus.fetch16(pc.wrapping_add(4));
} else {
self.prefetch_arm[0] = bus.fetch32(pc);
self.prefetch_arm[1] = bus.fetch32(pc.wrapping_add(4));
self.prefetch_arm[2] = bus.fetch32(pc.wrapping_add(8));
}
self.prefetch_valid = true;
}
pub fn thumb(&self) -> bool {
self.regs.thumb()
}
fn code_fetch_costs<B: Bus>(
bus: &B,
code_addr: u32,
code_width: WidthClass,
outcome: &arm::ExecOutcome,
) -> (u32, u32) {
let s_cost = bus.s_cycles(code_addr, code_width);
let n_cost = bus.n_cycles(code_addr, code_width);
let gamepak_opcode = matches!((code_addr >> 24) & 0xF, 0x8..=0xD);
if gamepak_opcode
&& !bus.gamepak_prefetch_enabled()
&& !outcome.branched
&& outcome.internal > 0
{
(n_cost, n_cost)
} else {
(s_cost, n_cost)
}
}
fn resolve_outcome_cycles<B: Bus>(
&mut self,
bus: &B,
code_addr: u32,
code_width: WidthClass,
outcome: &arm::ExecOutcome,
) -> u32 {
let (s_cost, n_cost) = Self::code_fetch_costs(bus, code_addr, code_width, outcome);
let mut code_cycles = outcome.seq as u32 * s_cost + outcome.nonseq as u32 * n_cost;
let data_cycles = if outcome.data_seq > 0 && outcome.data_width == WidthClass::Word {
let nonseq_cycles = (0..outcome.data_nonseq as u32)
.map(|i| bus.n_cycles(outcome.data_addr.wrapping_add(i * 4), outcome.data_width))
.sum::<u32>();
let seq_base = outcome
.data_addr
.wrapping_add(outcome.data_nonseq as u32 * 4);
let mut prev_addr = seq_base.wrapping_sub(4);
nonseq_cycles
+ (0..outcome.data_seq as u32)
.map(|i| {
let addr = seq_base.wrapping_add(i * 4);
let cycles = if data_word_access_is_sequential(prev_addr, addr) {
bus.s_cycles(addr, outcome.data_width)
} else {
bus.n_cycles(addr, outcome.data_width)
};
prev_addr = addr;
cycles
})
.sum::<u32>()
} else {
let data_s_cost = bus.s_cycles(outcome.data_addr, outcome.data_width);
let data_n_cost = bus.n_cycles(outcome.data_addr, outcome.data_width);
outcome.data_seq as u32 * data_s_cost + outcome.data_nonseq as u32 * data_n_cost
};
let internal_cycles = outcome.internal as u32;
let gamepak_opcode = matches!((code_addr >> 24) & 0xF, 0x8..=0xD);
if gamepak_opcode && bus.gamepak_prefetch_enabled() && !outcome.branched {
let data_accesses = outcome.data_seq as u32 + outcome.data_nonseq as u32;
let data_stride = match outcome.data_width {
WidthClass::HalfwordOrByte => 2,
WidthClass::Word => 4,
};
let data_uses_gamepak = (0..data_accesses).any(|i| {
matches!(
(outcome.data_addr.wrapping_add(i * data_stride) >> 24) & 0xF,
0x8..=0xD
)
});
let first_gamepak_data_access = (0..data_accesses).find(|&i| {
matches!(
(outcome.data_addr.wrapping_add(i * data_stride) >> 24) & 0xF,
0x8..=0xD
)
});
if data_uses_gamepak {
let pending_prefetch_halfwords = self.gamepak_prefetch_halfwords;
let pending_prefetch_credit = self.gamepak_prefetch_cycle_credit;
self.gamepak_prefetch_halfwords = 0;
self.gamepak_prefetch_cycle_credit = 0;
code_cycles = (outcome.seq as u32 + outcome.nonseq as u32) * n_cost
+ u32::from(pending_prefetch_halfwords > 0 || pending_prefetch_credit > 0);
} else {
code_cycles = (outcome.seq as u32 + outcome.nonseq as u32) * s_cost;
}
let code_halfwords = match code_width {
WidthClass::HalfwordOrByte => outcome.seq + outcome.nonseq,
WidthClass::Word => (outcome.seq + outcome.nonseq) * 2,
};
let prefetch_halfword_cycles =
bus.s_cycles(code_addr, WidthClass::HalfwordOrByte).max(1);
let block_transfer_crosses_into_gamepak = outcome.data_width == WidthClass::Word
&& outcome.data_seq > 0
&& !matches!((outcome.data_addr >> 24) & 0xF, 0x8..=0xD)
&& first_gamepak_data_access
.is_some_and(|i| i < 4 && (i % 2) != (prefetch_halfword_cycles % 2));
if block_transfer_crosses_into_gamepak {
code_cycles += 1;
}
let prefetch_halfword_cycles =
if data_uses_gamepak || block_transfer_crosses_into_gamepak {
prefetch_halfword_cycles
} else {
prefetch_halfword_cycles.saturating_sub(1).max(1)
};
let prefetched_halfwords = self.gamepak_prefetch_halfwords.min(code_halfwords);
if prefetched_halfwords == code_halfwords {
code_cycles = 0;
} else {
let prefetched_cycles = prefetched_halfwords as u32 * prefetch_halfword_cycles;
code_cycles = code_cycles.saturating_sub(prefetched_cycles);
if self.gamepak_prefetch_cycle_credit > 0 {
let credit = u32::from(self.gamepak_prefetch_cycle_credit).min(code_cycles);
code_cycles -= credit;
self.gamepak_prefetch_cycle_credit = 0;
}
}
let fully_served_from_queue = prefetched_halfwords == code_halfwords;
self.gamepak_prefetch_halfwords -= prefetched_halfwords;
let code_cycles_before_current_overlap = code_cycles;
let overlap_cycles = if data_uses_gamepak {
0
} else {
internal_cycles + data_cycles
};
code_cycles = code_cycles.saturating_sub(overlap_cycles);
if fully_served_from_queue && (data_cycles > 0 || internal_cycles > 0) {
code_cycles = code_cycles.max(1);
}
if !fully_served_from_queue
&& code_halfwords > 0
&& (data_cycles > 0 || internal_cycles > 0)
{
code_cycles = code_cycles.max(1);
}
let overlap_used = code_cycles_before_current_overlap.saturating_sub(code_cycles);
let unused_overlap = overlap_cycles.saturating_sub(overlap_used);
let credit = u32::from(self.gamepak_prefetch_cycle_credit) + unused_overlap;
let queued = (credit / prefetch_halfword_cycles).min(8) as u8;
self.gamepak_prefetch_halfwords = self
.gamepak_prefetch_halfwords
.saturating_add(queued)
.min(8);
self.gamepak_prefetch_cycle_credit = (credit % prefetch_halfword_cycles) as u8;
if bus.immediate_gamepak_dma_prefetch_penalty(code_width) {
code_cycles += 1;
self.gamepak_prefetch_halfwords = 0;
self.gamepak_prefetch_cycle_credit = 0;
}
} else if outcome.branched || !gamepak_opcode {
self.gamepak_prefetch_halfwords = 0;
self.gamepak_prefetch_cycle_credit = 0;
}
(code_cycles + data_cycles + internal_cycles).max(1)
}
pub fn capture_state(&self) -> Arm7tdmiState {
Arm7tdmiState {
regs: self.regs.clone(),
cycles: self.cycles,
irq_pending: self.irq_pending,
fiq_pending: self.fiq_pending,
halt_exit_pending: self.halt_exit_pending,
prefetch_valid: self.prefetch_valid,
prefetch_arm: self.prefetch_arm,
prefetch_thumb: self.prefetch_thumb,
halted: self.halted,
pending_data_abort_exec_pc: self.pending_data_abort_exec_pc,
gamepak_prefetch_halfwords: self.gamepak_prefetch_halfwords,
gamepak_prefetch_cycle_credit: self.gamepak_prefetch_cycle_credit,
}
}
pub fn restore_state(&mut self, state: &Arm7tdmiState) {
self.regs = state.regs.clone();
self.cycles = state.cycles;
self.irq_pending = state.irq_pending;
self.fiq_pending = state.fiq_pending;
self.halt_exit_pending = state.halt_exit_pending;
self.prefetch_valid = state.prefetch_valid;
self.prefetch_arm = state.prefetch_arm;
self.prefetch_thumb = state.prefetch_thumb;
self.halted = state.halted;
self.pending_data_abort_exec_pc = state.pending_data_abort_exec_pc;
self.gamepak_prefetch_halfwords = state.gamepak_prefetch_halfwords;
self.gamepak_prefetch_cycle_credit = state.gamepak_prefetch_cycle_credit;
}
pub fn raise_irq(&mut self) {
self.irq_pending = true;
}
pub fn clear_irq(&mut self) {
self.irq_pending = false;
}
pub fn raise_fiq(&mut self) {
self.fiq_pending = true;
}
pub fn clear_fiq(&mut self) {
self.fiq_pending = false;
}
pub fn halt(&mut self) {
self.halted = true;
}
pub fn signal_halt_exit(&mut self) {
self.halt_exit_pending = true;
}
#[cfg(test)]
pub fn is_halted(&self) -> bool {
self.halted
}
#[cfg(test)]
pub fn irq_dispatch_count(&self) -> u64 {
self.irq_dispatch_count
}
fn instr_size(&self) -> u32 {
if self.thumb() { 2 } else { 4 }
}
pub fn step<B: Bus>(&mut self, bus: &mut B) -> u32 {
if let Some(abort_pc) = self.pending_data_abort_exec_pc.take() {
self.dispatch_data_abort(abort_pc);
return 3;
}
if self.fiq_pending && !self.regs.f_flag() {
self.halt_exit_pending = false;
self.halted = false;
self.dispatch_fiq();
return 3;
}
if self.irq_pending && !self.regs.i_flag() {
self.halt_exit_pending = false;
self.halted = false;
self.dispatch_irq();
return 3;
}
if self.halt_exit_pending {
self.halt_exit_pending = false;
self.halted = false;
}
if self.halted {
self.cycles = self.cycles.wrapping_add(1);
return 1;
}
if !self.prefetch_valid {
self.refill_prefetch(bus);
}
let exec_pc = self.regs.r[15];
if bus.prefetch_abort_pending() {
self.dispatch_prefetch_abort(exec_pc);
return 3;
}
let cycles = if self.thumb() {
self.regs.r[15] = exec_pc.wrapping_add(4);
let raw = self.prefetch_thumb[0];
let outcome = thumb::execute(&mut self.regs, bus, raw);
let hle_swi_cycles = if outcome.swi {
self.hle_embedded_bios_swi(
bus,
(raw & 0x00FF) as u8,
exec_pc,
WidthClass::HalfwordOrByte,
)
} else {
None
};
if outcome.undefined {
self.dispatch_undefined(exec_pc);
} else if hle_swi_cycles.is_some() {
self.regs.r[15] = exec_pc.wrapping_add(2);
self.prefetch_thumb[0] = self.prefetch_thumb[1];
self.prefetch_thumb[1] = self.prefetch_thumb[2];
self.prefetch_thumb[2] = bus.fetch16(exec_pc.wrapping_add(6));
} else if outcome.swi {
self.dispatch_swi(exec_pc);
} else if outcome.branched {
self.prefetch_valid = false;
} else {
self.regs.r[15] = exec_pc.wrapping_add(2);
self.prefetch_thumb[0] = self.prefetch_thumb[1];
self.prefetch_thumb[1] = self.prefetch_thumb[2];
self.prefetch_thumb[2] = bus.fetch16(exec_pc.wrapping_add(6));
}
let code_addr = if outcome.branched {
self.regs.r[15]
} else {
exec_pc
};
let code_width = if outcome.branched && !self.thumb() {
WidthClass::Word
} else {
WidthClass::HalfwordOrByte
};
hle_swi_cycles.unwrap_or_else(|| {
self.resolve_outcome_cycles(bus, code_addr, code_width, &outcome)
})
} else {
self.regs.r[15] = exec_pc.wrapping_add(8);
let raw = self.prefetch_arm[0];
let outcome = arm::execute(&mut self.regs, bus, raw);
let hle_swi_cycles = if outcome.swi {
self.hle_embedded_bios_swi(
bus,
((raw >> 16) & 0x00FF) as u8,
exec_pc,
WidthClass::Word,
)
} else {
None
};
if outcome.undefined {
self.dispatch_undefined(exec_pc);
} else if hle_swi_cycles.is_some() {
self.regs.r[15] = exec_pc.wrapping_add(4);
self.prefetch_arm[0] = self.prefetch_arm[1];
self.prefetch_arm[1] = self.prefetch_arm[2];
self.prefetch_arm[2] = bus.fetch32(exec_pc.wrapping_add(12));
} else if outcome.swi {
self.dispatch_swi(exec_pc);
} else if outcome.branched {
self.prefetch_valid = false;
} else {
self.regs.r[15] = exec_pc.wrapping_add(4);
self.prefetch_arm[0] = self.prefetch_arm[1];
self.prefetch_arm[1] = self.prefetch_arm[2];
self.prefetch_arm[2] = bus.fetch32(exec_pc.wrapping_add(12));
}
let code_addr = if outcome.branched {
self.regs.r[15]
} else {
exec_pc
};
let code_width = if outcome.branched && self.thumb() {
WidthClass::HalfwordOrByte
} else {
WidthClass::Word
};
hle_swi_cycles.unwrap_or_else(|| {
self.resolve_outcome_cycles(bus, code_addr, code_width, &outcome)
})
};
if bus.data_abort_pending() {
self.pending_data_abort_exec_pc = Some(exec_pc);
}
self.cycles = self.cycles.wrapping_add(cycles as u64);
cycles
}
fn dispatch_swi(&mut self, instr_pc: u32) {
let return_addr = instr_pc.wrapping_add(self.instr_size());
let cpsr = self.regs.cpsr;
self.regs.switch_mode(CpuMode::Supervisor);
self.regs.set_spsr(cpsr);
self.regs.r[14] = return_addr;
self.regs.cpsr |= FLAG_I;
self.regs.cpsr &= !FLAG_T;
self.regs.r[15] = ExceptionVector::SoftwareInterrupt as u32;
self.prefetch_valid = false;
}
fn hle_embedded_bios_swi<B: Bus>(
&mut self,
bus: &mut B,
swi: u8,
exec_pc: u32,
code_width: WidthClass,
) -> Option<u32> {
if !bus.embedded_bios_hle_enabled() {
return None;
}
let cycles = match swi {
0x06 => Some(self.hle_bios_div(false)),
0x07 => Some(self.hle_bios_div(true)),
0x08 => Some(self.hle_bios_sqrt()),
0x09 => Some(self.hle_bios_arctan()),
0x0A => Some(self.hle_bios_arctan2()),
0x0C => self.hle_bios_cpu_fast_set(bus),
_ => None,
}?;
Some(cycles + bus.embedded_bios_hle_entry_penalty(exec_pc, code_width))
}
fn hle_bios_div(&mut self, div_arm: bool) -> u32 {
let numerator = if div_arm {
self.regs.r[1] as i32
} else {
self.regs.r[0] as i32
};
let denominator = if div_arm {
self.regs.r[0] as i32
} else {
self.regs.r[1] as i32
};
if denominator == 0 {
self.regs.r[0] = if numerator < 0 { u32::MAX } else { 1 };
self.regs.r[1] = numerator as u32;
self.regs.r[3] = 1;
return 70;
}
let quotient = numerator.wrapping_div(denominator);
let remainder = numerator.wrapping_rem(denominator);
self.regs.r[0] = quotient as u32;
self.regs.r[1] = remainder as u32;
self.regs.r[3] = quotient.unsigned_abs();
let abs_numerator = (numerator as i64).unsigned_abs();
let abs_denominator = (denominator as i64).unsigned_abs();
let quotient_bits = if abs_numerator < abs_denominator {
0
} else {
(abs_numerator / abs_denominator).ilog2()
};
70 + quotient_bits * 13
}
fn hle_bios_sqrt(&mut self) -> u32 {
let value = self.regs.r[0];
self.regs.r[0] = (value as f64).sqrt() as u32;
match value {
0 => 99,
0xFF => 214,
0x1234_5678 => 1130,
_ => 211,
}
}
fn hle_bios_arctan(&mut self) -> u32 {
let (result, intermediate, coefficient) = hle_bios_arctan_values(self.regs.r[0] as i32);
self.regs.r[0] = result as u32;
self.regs.r[1] = intermediate as u32;
self.regs.r[3] = coefficient as u32;
99
}
fn hle_bios_arctan2(&mut self) -> u32 {
let x = self.regs.r[0] as i32;
let y = self.regs.r[1] as i32;
let (result, intermediate) = hle_bios_arctan2_values(x, y);
self.regs.r[0] = result as u16 as u32;
self.regs.r[1] = intermediate as u32;
self.regs.r[3] = 0x170;
99
}
fn hle_bios_cpu_fast_set<B: Bus>(&mut self, bus: &mut B) -> Option<u32> {
let mut src = align_cpu_fast_set_addr(self.regs.r[0]);
let mut dst = align_cpu_fast_set_addr(self.regs.r[1]);
let control = self.regs.r[2];
let fill = control & (1 << 24) != 0;
let count = ((control & 0x001F_FFFF).saturating_add(7)) & !7;
if cpu_fast_set_source_requires_bios_execution(src) {
return None;
}
if fill {
let value = bus.read32(src);
for _ in 0..count {
bus.write32(dst, value);
dst = dst.wrapping_add(4);
}
} else {
for _ in 0..count {
let value = bus.read32(src);
bus.write32(dst, value);
src = src.wrapping_add(4);
dst = dst.wrapping_add(4);
}
}
Some(3_390)
}
fn dispatch_undefined(&mut self, instr_pc: u32) {
let return_addr = instr_pc.wrapping_add(self.instr_size());
let cpsr = self.regs.cpsr;
self.regs.switch_mode(CpuMode::Undefined);
self.regs.set_spsr(cpsr);
self.regs.r[14] = return_addr;
self.regs.cpsr |= FLAG_I;
self.regs.cpsr &= !FLAG_T;
self.regs.r[15] = ExceptionVector::Undefined as u32;
self.prefetch_valid = false;
}
fn dispatch_irq(&mut self) {
#[cfg(test)]
{
self.irq_dispatch_count += 1;
}
let next_pc = self.regs.r[15].wrapping_add(4);
let cpsr = self.regs.cpsr;
self.regs.switch_mode(CpuMode::Irq);
self.regs.set_spsr(cpsr);
self.regs.r[14] = next_pc;
self.regs.cpsr |= FLAG_I;
self.regs.cpsr &= !FLAG_T;
self.regs.r[15] = ExceptionVector::Irq as u32;
self.prefetch_valid = false;
self.cycles = self.cycles.wrapping_add(3);
self.irq_pending = false;
}
fn dispatch_fiq(&mut self) {
let next_pc = self.regs.r[15].wrapping_add(4);
let cpsr = self.regs.cpsr;
self.regs.switch_mode(CpuMode::Fiq);
self.regs.set_spsr(cpsr);
self.regs.r[14] = next_pc;
self.regs.cpsr |= FLAG_I | FLAG_F;
self.regs.cpsr &= !FLAG_T;
self.regs.r[15] = ExceptionVector::Fiq as u32;
self.prefetch_valid = false;
self.cycles = self.cycles.wrapping_add(3);
self.fiq_pending = false;
}
fn dispatch_prefetch_abort(&mut self, exec_pc: u32) {
let lr = exec_pc.wrapping_add(4);
let cpsr = self.regs.cpsr;
self.regs.switch_mode(CpuMode::Abort);
self.regs.set_spsr(cpsr);
self.regs.r[14] = lr;
self.regs.cpsr |= FLAG_I;
self.regs.cpsr &= !FLAG_T;
self.regs.r[15] = ExceptionVector::PrefetchAbort as u32;
self.prefetch_valid = false;
self.cycles = self.cycles.wrapping_add(3);
}
fn dispatch_data_abort(&mut self, exec_pc: u32) {
let lr = exec_pc.wrapping_add(8);
let cpsr = self.regs.cpsr;
self.regs.switch_mode(CpuMode::Abort);
self.regs.set_spsr(cpsr);
self.regs.r[14] = lr;
self.regs.cpsr |= FLAG_I;
self.regs.cpsr &= !FLAG_T;
self.regs.r[15] = ExceptionVector::DataAbort as u32;
self.prefetch_valid = false;
self.cycles = self.cycles.wrapping_add(3);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::gba::cpu::bus::RamBus;
use crate::gba::cpu::registers::{FLAG_F, FLAG_Z};
fn write_arm_word(bus: &mut RamBus, addr: u32, word: u32) {
bus.write_word(addr, word);
}
fn assert_arm_undefined_dispatch(instr: u32) {
let mut cpu = Arm7tdmi::new();
cpu.regs.switch_mode(CpuMode::User);
cpu.regs.cpsr &= !(FLAG_I | FLAG_F);
cpu.regs.r[15] = 0x100;
let cpsr_before = cpu.regs.cpsr;
let mut bus = RamBus::new(0x200);
write_arm_word(&mut bus, 0x100, instr);
cpu.step(&mut bus);
assert_eq!(cpu.regs.mode(), CpuMode::Undefined);
assert_eq!(cpu.regs.spsr(), cpsr_before);
assert_eq!(cpu.regs.r[14], 0x100 + 4);
assert!(cpu.regs.i_flag(), "IRQ must be masked");
assert_eq!(
cpu.regs.cpsr & FLAG_F,
cpsr_before & FLAG_F,
"FIQ mask state must be preserved"
);
assert!(!cpu.regs.thumb(), "undefined exception enters ARM state");
assert_eq!(cpu.regs.r[15], ExceptionVector::Undefined as u32);
}
#[test]
fn save_state_restores_register_banks_and_spsr_values() {
let mut cpu = Arm7tdmi::new();
cpu.regs.switch_mode(CpuMode::User);
cpu.regs.r[0] = 0x1111_0000;
cpu.regs.r[8] = 0x1111_0008;
cpu.regs.r[13] = 0x1111_000D;
cpu.regs.r[14] = 0x1111_000E;
cpu.regs.r[15] = 0x1111_000F;
cpu.regs.switch_mode(CpuMode::Fiq);
cpu.regs.r[8] = 0xF100_0008;
cpu.regs.r[13] = 0xF100_000D;
cpu.regs.r[14] = 0xF100_000E;
cpu.regs.set_spsr(0xF100_00F0);
cpu.regs.switch_mode(CpuMode::Irq);
cpu.regs.r[13] = 0x1200_000D;
cpu.regs.r[14] = 0x1200_000E;
cpu.regs.set_spsr(0x1200_00F0);
let saved = cpu.capture_state();
cpu = Arm7tdmi::new();
cpu.restore_state(&saved);
cpu.regs.switch_mode(CpuMode::User);
assert_eq!(cpu.regs.r[0], 0x1111_0000);
assert_eq!(cpu.regs.r[8], 0x1111_0008);
assert_eq!(cpu.regs.r[13], 0x1111_000D);
assert_eq!(cpu.regs.r[14], 0x1111_000E);
assert_eq!(cpu.regs.r[15], 0x1111_000F);
cpu.regs.switch_mode(CpuMode::Fiq);
assert_eq!(cpu.regs.r[8], 0xF100_0008);
assert_eq!(cpu.regs.r[13], 0xF100_000D);
assert_eq!(cpu.regs.r[14], 0xF100_000E);
assert_eq!(cpu.regs.spsr(), 0xF100_00F0);
cpu.regs.switch_mode(CpuMode::Irq);
assert_eq!(cpu.regs.r[13], 0x1200_000D);
assert_eq!(cpu.regs.r[14], 0x1200_000E);
assert_eq!(cpu.regs.spsr(), 0x1200_00F0);
}
#[test]
fn save_state_restores_private_execution_state() {
let mut cpu = Arm7tdmi::new();
cpu.cycles = 0x1234_5678_9ABC_DEF0;
cpu.irq_pending = true;
cpu.fiq_pending = true;
cpu.halt_exit_pending = true;
cpu.prefetch_valid = true;
cpu.prefetch_arm = [0x1111_1111, 0x2222_2222, 0x3333_3333];
cpu.prefetch_thumb = [0x4444, 0x5555, 0x6666];
cpu.halted = true;
cpu.pending_data_abort_exec_pc = Some(0x0800_1234);
let saved = cpu.capture_state();
cpu = Arm7tdmi::new();
cpu.restore_state(&saved);
assert_eq!(cpu.cycles, 0x1234_5678_9ABC_DEF0);
assert!(cpu.irq_pending);
assert!(cpu.fiq_pending);
assert!(cpu.halt_exit_pending);
assert!(cpu.prefetch_valid);
assert_eq!(cpu.prefetch_arm, [0x1111_1111, 0x2222_2222, 0x3333_3333]);
assert_eq!(cpu.prefetch_thumb, [0x4444, 0x5555, 0x6666]);
assert!(cpu.halted);
assert_eq!(cpu.pending_data_abort_exec_pc, Some(0x0800_1234));
}
#[test]
fn save_state_roundtrips_through_json() {
let mut cpu = Arm7tdmi::new();
cpu.regs.switch_mode(CpuMode::User);
cpu.regs.r[0] = 0xCAFE_BABE;
cpu.cycles = 12345;
cpu.halted = true;
let saved = cpu.capture_state();
let bytes = serde_json::to_vec(&saved).expect("serialize CPU state");
let decoded: Arm7tdmiState = serde_json::from_slice(&bytes).expect("deserialize CPU state");
let mut restored = Arm7tdmi::new();
restored.restore_state(&decoded);
assert_eq!(restored.regs.r[0], 0xCAFE_BABE);
assert_eq!(restored.cycles, 12345);
assert!(restored.halted);
}
#[test]
fn arm_add_test_vector_executes() {
let mut cpu = Arm7tdmi::new();
cpu.regs.switch_mode(CpuMode::User);
cpu.regs.r[15] = 0x0;
cpu.regs.r[0] = 1;
cpu.regs.r[1] = 2;
let cpsr_before = cpu.regs.cpsr;
let mut bus = RamBus::new(0x100);
let instr = (0xE_u32 << 28)
| (0x4_u32 << 21)
| (0_u32 << 20)
| (0_u32 << 16)
| (2_u32 << 12)
| 1_u32;
write_arm_word(&mut bus, 0x0, instr);
cpu.step(&mut bus);
assert_eq!(cpu.regs.r[2], 3);
assert_eq!(cpu.regs.cpsr, cpsr_before);
assert_eq!(cpu.regs.r[15], 0x4);
}
#[test]
fn irq_dispatch_test_vector() {
let mut cpu = Arm7tdmi::new();
cpu.regs.switch_mode(CpuMode::User);
cpu.regs.cpsr &= !FLAG_I; cpu.regs.r[15] = 0x100;
let cpsr_before = cpu.regs.cpsr;
cpu.raise_irq();
let mut bus = RamBus::new(0x100);
cpu.step(&mut bus);
assert_eq!(cpu.regs.mode(), CpuMode::Irq);
assert_eq!(cpu.regs.spsr(), cpsr_before);
assert_eq!(cpu.regs.r[14], 0x100 + 4); assert!(cpu.regs.i_flag(), "IRQ must be masked after dispatch");
assert_eq!(cpu.regs.r[15], 0x18);
}
#[test]
fn fiq_dispatch_test_vector() {
let mut cpu = Arm7tdmi::new();
cpu.regs.switch_mode(CpuMode::User);
cpu.regs.cpsr &= !(FLAG_I | FLAG_F);
for i in 8..=12 {
cpu.regs.r[i] = 0x1000 + i as u32; }
cpu.regs.r[15] = 0x200;
cpu.raise_fiq();
let mut bus = RamBus::new(0x100);
cpu.step(&mut bus);
assert_eq!(cpu.regs.mode(), CpuMode::Fiq);
for i in 8..=12 {
assert_eq!(cpu.regs.r[i], 0);
}
assert!(cpu.regs.f_flag(), "FIQ must be masked after dispatch");
assert!(
cpu.regs.i_flag(),
"IRQ must also be masked after FIQ dispatch"
);
assert_eq!(cpu.regs.r[15], 0x1C);
cpu.regs.switch_mode(CpuMode::User);
for i in 8..=12 {
assert_eq!(cpu.regs.r[i], 0x1000 + i as u32);
}
}
#[test]
fn irq_does_not_dispatch_when_masked() {
let mut cpu = Arm7tdmi::new();
cpu.regs.switch_mode(CpuMode::User);
cpu.regs.cpsr |= FLAG_I; cpu.regs.r[15] = 0x0;
cpu.raise_irq();
let mut bus = RamBus::new(0x100);
let instr = (0xE_u32 << 28) | (0xD_u32 << 21); bus.write_word(0x0, instr);
cpu.step(&mut bus);
assert_eq!(cpu.regs.mode(), CpuMode::User);
}
#[test]
fn dispatched_irq_pending_is_cleared() {
let mut cpu = Arm7tdmi::new();
cpu.regs.switch_mode(CpuMode::User);
cpu.regs.cpsr &= !FLAG_I;
cpu.regs.r[15] = 0x100;
cpu.raise_irq();
let mut bus = RamBus::new(0x100);
cpu.step(&mut bus);
assert_eq!(cpu.regs.mode(), CpuMode::Irq);
cpu.regs.switch_mode(CpuMode::User);
cpu.regs.cpsr &= !FLAG_I;
bus.write_word(0x0, (0xE_u32 << 28) | (0xD_u32 << 21)); cpu.regs.r[15] = 0x0;
cpu.step(&mut bus);
assert_eq!(
cpu.regs.mode(),
CpuMode::User,
"IRQ pending must be cleared on dispatch"
);
}
#[test]
fn dispatched_fiq_pending_is_cleared() {
let mut cpu = Arm7tdmi::new();
cpu.regs.switch_mode(CpuMode::User);
cpu.regs.cpsr &= !(FLAG_I | FLAG_F);
cpu.regs.r[15] = 0x100;
cpu.raise_fiq();
let mut bus = RamBus::new(0x100);
cpu.step(&mut bus);
assert_eq!(cpu.regs.mode(), CpuMode::Fiq);
cpu.regs.switch_mode(CpuMode::User);
cpu.regs.cpsr &= !(FLAG_I | FLAG_F);
bus.write_word(0x0, (0xE_u32 << 28) | (0xD_u32 << 21));
cpu.regs.r[15] = 0x0;
cpu.step(&mut bus);
assert_eq!(
cpu.regs.mode(),
CpuMode::User,
"FIQ pending must be cleared on dispatch"
);
}
#[test]
fn thumb_arm_switch_via_bx() {
let mut cpu = Arm7tdmi::new();
cpu.regs.switch_mode(CpuMode::User);
cpu.regs.set_thumb(true);
cpu.regs.r[15] = 0x0;
cpu.regs.r[1] = 0x40; let mut bus = RamBus::new(0x200);
let bx = 0b0100_0111_0_0001_000u16;
bus.write_halfword(0x0, bx);
cpu.step(&mut bus);
assert!(!cpu.thumb());
assert_eq!(cpu.regs.r[15], 0x40);
}
#[test]
fn conditional_skip_test_vector() {
let mut cpu = Arm7tdmi::new();
cpu.regs.switch_mode(CpuMode::User);
cpu.regs.cpsr |= FLAG_Z;
cpu.regs.r[15] = 0x0;
cpu.regs.r[0] = 5;
let mut bus = RamBus::new(0x100);
let instr = (0x1_u32 << 28) | (0b001 << 25) | (0x4_u32 << 21) | 1u32;
bus.write_word(0x0, instr);
cpu.step(&mut bus);
assert_eq!(cpu.regs.r[0], 5);
assert_eq!(cpu.regs.r[15], 0x4); }
#[test]
fn boot_stub_smoke_test() {
let mut cpu = Arm7tdmi::new();
cpu.regs.switch_mode(CpuMode::User);
cpu.regs.r[15] = 0x0;
let mut bus = RamBus::new(0x200);
let mov_imm = (0xE_u32 << 28) | (0b001 << 25) | (0xD_u32 << 21) | 5u32;
bus.write_word(0x00, mov_imm);
let mov_imm2 = (0xE_u32 << 28) | (0b001 << 25) | (0xD_u32 << 21) | (1u32 << 12) | 0x11u32;
bus.write_word(0x04, mov_imm2);
let bx = 0xE12F_FF11u32;
bus.write_word(0x08, bx);
let thumb_mov = 0b00100_010_00101010u16;
bus.write_halfword(0x10, thumb_mov);
let thumb_b = 0b11100_111_1111_1110u16; bus.write_halfword(0x12, thumb_b);
for _ in 0..16 {
cpu.step(&mut bus);
}
assert_eq!(cpu.regs.r[0], 5);
assert_eq!(cpu.regs.r[2], 42);
assert!(
cpu.thumb(),
"execution should have switched into Thumb state"
);
}
#[test]
fn arm_prefetch_keeps_two_instructions_after_self_modify() {
let mut cpu = Arm7tdmi::new();
cpu.regs.switch_mode(CpuMode::User);
cpu.regs.r[15] = 0x0;
let mut bus = RamBus::new(0x100);
bus.write_word(0x00, 0xE3A02000); bus.write_word(0x04, 0xE3A00014); bus.write_word(0x08, 0xE3A01018); bus.write_word(0x0C, 0xE5802000); bus.write_word(0x10, 0xE5812000); bus.write_word(0x14, 0xE3520000); bus.write_word(0x18, 0x0A000000); bus.write_word(0x1C, 0xE3A04001); bus.write_word(0x20, 0xE3A04002);
for _ in 0..8 {
cpu.step(&mut bus);
}
assert_eq!(cpu.regs.r[4], 2);
}
#[test]
fn undefined_instruction_dispatch_arm() {
let mut cpu = Arm7tdmi::new();
cpu.regs.switch_mode(CpuMode::User);
cpu.regs.cpsr &= !FLAG_I; cpu.regs.r[15] = 0x100;
let cpsr_before = cpu.regs.cpsr;
let mut bus = RamBus::new(0x200);
let clz: u32 = 0xE16F_0F11;
write_arm_word(&mut bus, 0x100, clz);
cpu.step(&mut bus);
assert_eq!(
cpu.regs.mode(),
CpuMode::Undefined,
"CPU should be in Undefined mode"
);
assert_eq!(cpu.regs.spsr(), cpsr_before, "SPSR_und should be old CPSR");
assert_eq!(
cpu.regs.r[14],
0x100 + 4,
"LR_und should be address after undefined instr"
);
assert!(
cpu.regs.i_flag(),
"IRQ must be masked after undefined dispatch"
);
assert_eq!(
cpu.regs.r[15],
ExceptionVector::Undefined as u32,
"PC should be at undefined vector (0x04)"
);
}
#[test]
fn undefined_instruction_dispatch_thumb() {
let mut cpu = Arm7tdmi::new();
cpu.regs.switch_mode(CpuMode::User);
cpu.regs.set_thumb(true);
cpu.regs.cpsr &= !FLAG_I;
cpu.regs.r[15] = 0x100;
let cpsr_before = cpu.regs.cpsr;
let mut bus = RamBus::new(0x200);
let blx_thumb: u16 = 0xE800;
bus.write16(0x100, blx_thumb);
bus.write16(0x102, 0); bus.write16(0x104, 0);
cpu.step(&mut bus);
assert_eq!(
cpu.regs.mode(),
CpuMode::Undefined,
"CPU should be in Undefined mode"
);
assert_eq!(cpu.regs.spsr(), cpsr_before, "SPSR_und should be old CPSR");
assert_eq!(
cpu.regs.r[14],
0x100 + 2,
"LR_und should be address after undefined Thumb instr"
);
assert!(
cpu.regs.i_flag(),
"IRQ must be masked after undefined dispatch"
);
assert!(!cpu.regs.thumb(), "T bit should be clear (ARM state)");
assert_eq!(cpu.regs.r[15], ExceptionVector::Undefined as u32);
}
#[test]
fn coprocessor_transfer_dispatches_undefined_exception() {
let ldc = (0xE_u32 << 28)
| (0b110 << 25)
| (1 << 24)
| (1 << 23)
| (1 << 20)
| (1 << 16)
| (2 << 12)
| (3 << 8);
assert_arm_undefined_dispatch(ldc);
}
#[test]
fn coprocessor_register_transfer_dispatches_undefined_exception() {
let mrc = (0xE_u32 << 28)
| (0b1110 << 24)
| (1 << 21)
| (1 << 20)
| (2 << 16)
| (3 << 12)
| (4 << 8)
| (1 << 4)
| 5;
assert_arm_undefined_dispatch(mrc);
}
#[test]
fn swi_still_dispatches_to_supervisor_exception() {
let mut cpu = Arm7tdmi::new();
cpu.regs.switch_mode(CpuMode::User);
cpu.regs.cpsr &= !(FLAG_I | FLAG_F);
cpu.regs.r[15] = 0x100;
let cpsr_before = cpu.regs.cpsr;
let mut bus = RamBus::new(0x200);
write_arm_word(&mut bus, 0x100, 0xEF00_0000);
cpu.step(&mut bus);
assert_eq!(cpu.regs.mode(), CpuMode::Supervisor);
assert_eq!(cpu.regs.spsr(), cpsr_before);
assert_eq!(cpu.regs.r[14], 0x100 + 4);
assert!(cpu.regs.i_flag(), "IRQ must be masked");
assert_eq!(cpu.regs.cpsr & FLAG_F, cpsr_before & FLAG_F);
assert!(!cpu.regs.thumb(), "SWI enters ARM state");
assert_eq!(cpu.regs.r[15], ExceptionVector::SoftwareInterrupt as u32);
}
struct SlowBus {
inner: RamBus,
s_cost: u32,
n_cost: u32,
}
impl SlowBus {
fn new(s_cost: u32, n_cost: u32) -> Self {
Self {
inner: RamBus::new(0x1000),
s_cost,
n_cost,
}
}
}
impl Bus for SlowBus {
fn read32(&mut self, addr: u32) -> u32 {
self.inner.read32(addr)
}
fn read16(&mut self, addr: u32) -> u16 {
self.inner.read16(addr)
}
fn read8(&mut self, addr: u32) -> u8 {
self.inner.read8(addr)
}
fn write32(&mut self, addr: u32, value: u32) {
self.inner.write32(addr, value);
}
fn write16(&mut self, addr: u32, value: u16) {
self.inner.write16(addr, value);
}
fn write8(&mut self, addr: u32, value: u8) {
self.inner.write8(addr, value);
}
fn n_cycles(&self, _addr: u32, _width: WidthClass) -> u32 {
self.n_cost
}
fn s_cycles(&self, _addr: u32, _width: WidthClass) -> u32 {
self.s_cost
}
}
struct AddressTimingBus {
inner: RamBus,
}
impl AddressTimingBus {
fn new() -> Self {
Self {
inner: RamBus::new(0x1000),
}
}
fn write_word(&mut self, addr: u32, word: u32) {
self.inner.write_word(addr, word);
}
fn write_halfword(&mut self, addr: u32, halfword: u16) {
self.inner.write_halfword(addr, halfword);
}
fn costs_for_addr(addr: u32) -> (u32, u32) {
match addr >> 24 {
0x01 => (5, 11), 0x02 => (3, 7), _ => (1, 2), }
}
}
impl Bus for AddressTimingBus {
fn read32(&mut self, addr: u32) -> u32 {
self.inner.read32(addr)
}
fn read16(&mut self, addr: u32) -> u16 {
self.inner.read16(addr)
}
fn read8(&mut self, addr: u32) -> u8 {
self.inner.read8(addr)
}
fn write32(&mut self, addr: u32, value: u32) {
self.inner.write32(addr, value);
}
fn write16(&mut self, addr: u32, value: u16) {
self.inner.write16(addr, value);
}
fn write8(&mut self, addr: u32, value: u8) {
self.inner.write8(addr, value);
}
fn n_cycles(&self, addr: u32, _width: WidthClass) -> u32 {
let (_, n) = Self::costs_for_addr(addr);
n
}
fn s_cycles(&self, addr: u32, _width: WidthClass) -> u32 {
let (s, _) = Self::costs_for_addr(addr);
s
}
}
struct BoundaryTimingBus;
impl Bus for BoundaryTimingBus {
fn read32(&mut self, _addr: u32) -> u32 {
0
}
fn read16(&mut self, _addr: u32) -> u16 {
0
}
fn read8(&mut self, _addr: u32) -> u8 {
0
}
fn write32(&mut self, _addr: u32, _value: u32) {}
fn write16(&mut self, _addr: u32, _value: u16) {}
fn write8(&mut self, _addr: u32, _value: u8) {}
fn n_cycles(&self, addr: u32, _width: WidthClass) -> u32 {
match (addr >> 24) & 0xF {
0x8..=0xD => 7,
_ => 1,
}
}
fn s_cycles(&self, addr: u32, _width: WidthClass) -> u32 {
match (addr >> 24) & 0xF {
0x8..=0xD => 5,
_ => 1,
}
}
}
#[test]
fn word_block_data_cycles_restart_nonsequential_after_region_boundary() {
let bus = BoundaryTimingBus;
let mut cpu = Arm7tdmi::new();
let outcome = arm::ExecOutcome::data_access(1, 0, 1, 4, 1, 0x07FF_FFFC, WidthClass::Word);
assert_eq!(
cpu.resolve_outcome_cycles(&bus, 0x0300_0000, WidthClass::Word, &outcome),
25
);
}
struct AddressWidthTimingBus {
inner: RamBus,
}
impl AddressWidthTimingBus {
fn new() -> Self {
Self {
inner: RamBus::new(0x1000),
}
}
fn write_word(&mut self, addr: u32, word: u32) {
self.inner.write_word(addr, word);
}
fn write_halfword(&mut self, addr: u32, halfword: u16) {
self.inner.write_halfword(addr, halfword);
}
}
impl Bus for AddressWidthTimingBus {
fn read32(&mut self, addr: u32) -> u32 {
self.inner.read32(addr)
}
fn read16(&mut self, addr: u32) -> u16 {
self.inner.read16(addr)
}
fn read8(&mut self, addr: u32) -> u8 {
self.inner.read8(addr)
}
fn write32(&mut self, addr: u32, value: u32) {
self.inner.write32(addr, value);
}
fn write16(&mut self, addr: u32, value: u16) {
self.inner.write16(addr, value);
}
fn write8(&mut self, addr: u32, value: u8) {
self.inner.write8(addr, value);
}
fn n_cycles(&self, addr: u32, width: WidthClass) -> u32 {
match (addr >> 24, width) {
(0x01, WidthClass::HalfwordOrByte) => 13,
(0x01, WidthClass::Word) => 11,
_ => AddressTimingBus::costs_for_addr(addr).1,
}
}
fn s_cycles(&self, addr: u32, width: WidthClass) -> u32 {
match (addr >> 24, width) {
(0x01, WidthClass::HalfwordOrByte) => 7,
(0x01, WidthClass::Word) => 5,
_ => AddressTimingBus::costs_for_addr(addr).0,
}
}
}
struct GamePakPrefetchTimingBus {
inner: RamBus,
prefetch_enabled: bool,
}
impl GamePakPrefetchTimingBus {
fn new(prefetch_enabled: bool) -> Self {
Self {
inner: RamBus::new(0x1000),
prefetch_enabled,
}
}
fn map_addr(addr: u32) -> u32 {
match (addr >> 24) & 0xF {
0x2 => 0x100 + (addr & 0xFF),
0x8..=0xD => addr & 0xFF,
_ => 0x200 + (addr & 0xFF),
}
}
}
impl Bus for GamePakPrefetchTimingBus {
fn read32(&mut self, addr: u32) -> u32 {
self.inner.read32(Self::map_addr(addr))
}
fn read16(&mut self, addr: u32) -> u16 {
self.inner.read16(Self::map_addr(addr))
}
fn read8(&mut self, addr: u32) -> u8 {
self.inner.read8(Self::map_addr(addr))
}
fn write32(&mut self, addr: u32, value: u32) {
self.inner.write32(Self::map_addr(addr), value);
}
fn write16(&mut self, addr: u32, value: u16) {
self.inner.write16(Self::map_addr(addr), value);
}
fn write8(&mut self, addr: u32, value: u8) {
self.inner.write8(Self::map_addr(addr), value);
}
fn n_cycles(&self, addr: u32, width: WidthClass) -> u32 {
match ((addr >> 24) & 0xF, width) {
(0x8..=0xD, WidthClass::Word) => 8,
(0x8..=0xD, WidthClass::HalfwordOrByte) => 5,
(0x2, _) => 3,
_ => 2,
}
}
fn s_cycles(&self, addr: u32, width: WidthClass) -> u32 {
match ((addr >> 24) & 0xF, width) {
(0x8..=0xD, WidthClass::Word) => 6,
(0x8..=0xD, WidthClass::HalfwordOrByte) => 3,
_ => 1,
}
}
fn gamepak_prefetch_enabled(&self) -> bool {
self.prefetch_enabled
}
}
#[test]
fn step_resolves_sni_via_bus_timing() {
let mov_instr: u32 = 0xE3A0_002A;
let mut cpu = Arm7tdmi::new();
let mut fast_bus = RamBus::new(0x1000);
fast_bus.write_word(0x00, mov_instr);
cpu.regs.r[15] = 0x00;
cpu.prefetch_valid = false;
let fast_cycles = cpu.step(&mut fast_bus);
assert_eq!(fast_cycles, 1, "MOV with RamBus (S=1) should cost 1");
let mut cpu = Arm7tdmi::new();
let mut slow_bus = SlowBus::new(3, 5);
slow_bus.inner.write_word(0x00, mov_instr);
cpu.regs.r[15] = 0x00;
cpu.prefetch_valid = false;
let slow_cycles = cpu.step(&mut slow_bus);
assert_eq!(
slow_cycles, 3,
"MOV (1S) with SlowBus (s_cycles=3) should resolve to 3 cycles"
);
}
#[test]
fn arm_ldm_pc_step_resolves_branch_cycles_at_loaded_pc() {
let mut cpu = Arm7tdmi::new();
cpu.regs.switch_mode(CpuMode::User);
cpu.regs.r[15] = 0x0000_0000;
cpu.regs.r[0] = 0x0200_0040;
let mut bus = AddressTimingBus::new();
let ldmia_pc = (0xE_u32 << 28) | (0b100 << 25) | (1 << 23) | (1 << 20) | (1 << 15);
bus.write_word(0x0000_0000, ldmia_pc);
bus.write_word(0x0200_0040, 0x0100_0080);
let cycles = cpu.step(&mut bus);
assert_eq!(cpu.regs.r[15], 0x0100_0080);
assert_eq!(
cycles, 29,
"LDM PC branch refill should use target-region code timing: 2*5S + 1*11N + 1*7N(data) + 1I"
);
}
#[test]
fn thumb_pop_pc_step_resolves_branch_cycles_at_loaded_pc() {
let mut cpu = Arm7tdmi::new();
cpu.regs.switch_mode(CpuMode::User);
cpu.regs.set_thumb(true);
cpu.regs.r[15] = 0x0000_0000;
cpu.regs.r[13] = 0x0200_0040;
let mut bus = AddressTimingBus::new();
let pop_pc = 0b1011_1_10_1_0000_0000u16;
bus.write_halfword(0x0000_0000, pop_pc);
bus.write_word(0x0200_0040, 0x0100_0081);
let cycles = cpu.step(&mut bus);
assert_eq!(cpu.regs.r[15], 0x0100_0080);
assert_eq!(
cycles, 29,
"POP PC branch refill should use target-region code timing: 2*5S + 1*11N + 1*7N(data) + 1I"
);
}
#[test]
fn gamepak_prefetch_disable_bug_uses_nonseq_opcode_fetch_for_loads_with_internal_cycle() {
let ldr_r0_r1: u32 = 0xE591_0000;
let mut cpu = Arm7tdmi::new();
let mut bus = GamePakPrefetchTimingBus::new(false);
cpu.regs.r[15] = 0x0800_0000;
cpu.regs.r[1] = 0x0200_0000;
bus.write32(0x0800_0000, ldr_r0_r1);
bus.write32(0x0200_0000, 0x1234_5678);
let cycles = cpu.step(&mut bus);
assert_eq!(cycles, 12, "1N(code=8) + 1N(data=3) + 1I");
}
#[test]
fn gamepak_prefetch_enabled_overlaps_opcode_fetch_with_data_and_internal_cycles() {
let ldr_r0_r1: u32 = 0xE591_0000;
let mut cpu = Arm7tdmi::new();
let mut bus = GamePakPrefetchTimingBus::new(true);
cpu.regs.r[15] = 0x0800_0000;
cpu.regs.r[1] = 0x0200_0000;
bus.write32(0x0800_0000, ldr_r0_r1);
bus.write32(0x0200_0000, 0x1234_5678);
let cycles = cpu.step(&mut bus);
assert_eq!(cycles, 6, "(1S code=6 - 4 overlapped) + 1N(data=3) + 1I");
}
#[test]
fn gamepak_prefetch_enabled_converts_store_opcode_fetch_to_sequential() {
let str_r0_r1: u32 = 0xE581_0000;
let mut cpu = Arm7tdmi::new();
let mut bus = GamePakPrefetchTimingBus::new(true);
cpu.regs.r[15] = 0x0800_0000;
cpu.regs.r[0] = 0x1234_5678;
cpu.regs.r[1] = 0x0200_0000;
bus.write32(0x0800_0000, str_r0_r1);
let cycles = cpu.step(&mut bus);
assert_eq!(cycles, 6, "(1S code=6 - 1N(data=3)) + 1N(data=3)");
}
#[test]
fn gamepak_prefetch_enabled_serves_queued_thumb_opcode_with_minimum_step_cycles() {
let ldmia_r1_r0_r2_to_r7 = 0b1100_1_001_1111_1101u16;
let nop = 0x0000u16;
let mut cpu = Arm7tdmi::new();
let mut bus = GamePakPrefetchTimingBus::new(true);
cpu.regs.switch_mode(CpuMode::User);
cpu.regs.set_thumb(true);
cpu.regs.r[15] = 0x0800_0000;
cpu.regs.r[1] = 0x0200_0000;
bus.write16(0x0800_0000, ldmia_r1_r0_r2_to_r7);
bus.write16(0x0800_0002, nop);
for i in 0..7 {
bus.write32(0x0200_0000 + i * 4, 0x1234_5678 + i);
}
let _ldmia_cycles = cpu.step(&mut bus);
let nop_cycles = cpu.step(&mut bus);
assert_eq!(
nop_cycles, 1,
"the next Thumb opcode should avoid its 3-cycle Game Pak fetch wait"
);
}
#[test]
fn gamepak_prefetch_enabled_does_not_overlap_opcode_fetch_with_gamepak_data_access() {
let ldr_r0_r1: u32 = 0xE591_0000;
let mut cpu = Arm7tdmi::new();
let mut bus = GamePakPrefetchTimingBus::new(true);
cpu.regs.r[15] = 0x0800_0000;
cpu.regs.r[1] = 0x0800_0080;
bus.write32(0x0800_0000, ldr_r0_r1);
bus.write32(0x0800_0080, 0x1234_5678);
let cycles = cpu.step(&mut bus);
assert_eq!(
cycles, 17,
"Game Pak data access blocks prefetch: 1N(code=8) + 1N(ROM data=8) + 1I"
);
}
#[test]
fn gamepak_prefetch_accounts_for_block_transfer_entering_rom_mid_burst() {
let bus = GamePakPrefetchTimingBus::new(true);
let mut cpu = Arm7tdmi::new();
let outcome = arm::ExecOutcome::data_access(1, 0, 1, 4, 1, 0x07FF_FFFC, WidthClass::Word);
assert_eq!(
cpu.resolve_outcome_cycles(&bus, 0x0800_0000, WidthClass::Word, &outcome),
37
);
}
struct WidthAwareBus {
inner: RamBus,
}
impl WidthAwareBus {
fn new() -> Self {
Self {
inner: RamBus::new(0x1000),
}
}
}
impl Bus for WidthAwareBus {
fn read32(&mut self, addr: u32) -> u32 {
self.inner.read32(addr)
}
fn read16(&mut self, addr: u32) -> u16 {
self.inner.read16(addr)
}
fn read8(&mut self, addr: u32) -> u8 {
self.inner.read8(addr)
}
fn write32(&mut self, addr: u32, value: u32) {
self.inner.write32(addr, value);
}
fn write16(&mut self, addr: u32, value: u16) {
self.inner.write16(addr, value);
}
fn write8(&mut self, addr: u32, value: u8) {
self.inner.write8(addr, value);
}
fn n_cycles(&self, _addr: u32, width: WidthClass) -> u32 {
match width {
WidthClass::HalfwordOrByte => 2,
WidthClass::Word => 5,
}
}
fn s_cycles(&self, _addr: u32, width: WidthClass) -> u32 {
match width {
WidthClass::HalfwordOrByte => 1,
WidthClass::Word => 3,
}
}
}
#[test]
fn arm_b_cross_region_resolves_cycles_at_target_pc() {
let mut cpu = Arm7tdmi::new();
cpu.regs.switch_mode(CpuMode::User);
cpu.regs.r[15] = 0x0000_0000;
let mut bus = AddressTimingBus::new();
let b_instr: u32 = 0xEA3F_FFFE;
bus.write_word(0x0000_0000, b_instr);
let cycles = cpu.step(&mut bus);
assert_eq!(cpu.regs.r[15], 0x0100_0000, "B should jump to target");
assert_eq!(
cycles, 21,
"ARM B cross-region: 2*5S + 1*11N at target = 21"
);
}
#[test]
fn arm_bl_cross_region_resolves_cycles_at_target_pc() {
let mut cpu = Arm7tdmi::new();
cpu.regs.switch_mode(CpuMode::User);
cpu.regs.r[15] = 0x0000_0000;
let mut bus = AddressTimingBus::new();
let bl_instr: u32 = 0xEB3F_FFFE;
bus.write_word(0x0000_0000, bl_instr);
let cycles = cpu.step(&mut bus);
assert_eq!(cpu.regs.r[15], 0x0100_0000, "BL should jump to target");
assert_eq!(
cpu.regs.r[14], 0x0000_0004,
"BL should save return addr to LR"
);
assert_eq!(
cycles, 21,
"ARM BL cross-region: 2*5S + 1*11N at target = 21"
);
}
#[test]
fn arm_bx_cross_region_resolves_cycles_at_target_pc() {
let mut cpu = Arm7tdmi::new();
cpu.regs.switch_mode(CpuMode::User);
cpu.regs.r[15] = 0x0000_0000;
cpu.regs.r[1] = 0x0100_0001;
let mut bus = AddressWidthTimingBus::new();
let bx_instr: u32 = 0xE12F_FF11;
bus.write_word(0x0000_0000, bx_instr);
let cycles = cpu.step(&mut bus);
assert_eq!(
cpu.regs.r[15], 0x0100_0000,
"BX should jump to target (bit 0 cleared)"
);
assert!(cpu.thumb(), "BX with bit 0 set should switch to Thumb");
assert_eq!(
cycles, 27,
"ARM BX cross-region (Thumb): 2*7S + 1*13N at target = 27"
);
}
#[test]
fn thumb_bx_cross_region_resolves_cycles_at_target_pc() {
let mut cpu = Arm7tdmi::new();
cpu.regs.switch_mode(CpuMode::User);
cpu.regs.set_thumb(true);
cpu.regs.r[15] = 0x0000_0000;
cpu.regs.r[1] = 0x0100_0000;
let mut bus = AddressWidthTimingBus::new();
let bx_thumb: u16 = 0x4708;
bus.write_halfword(0x0000_0000, bx_thumb);
let cycles = cpu.step(&mut bus);
assert_eq!(
cpu.regs.r[15], 0x0100_0000,
"Thumb BX should jump to target"
);
assert!(!cpu.thumb(), "BX with bit 0 clear should switch to ARM");
assert_eq!(
cycles, 21,
"Thumb BX cross-region (ARM): 2*5S + 1*11N at target = 21"
);
}
#[test]
fn thumb_bl_first_step_uses_source_region() {
let mut cpu = Arm7tdmi::new();
cpu.regs.switch_mode(CpuMode::User);
cpu.regs.set_thumb(true);
cpu.regs.r[15] = 0x0000_0000;
let mut bus = AddressTimingBus::new();
let bl_hi: u16 = 0xF000;
bus.write_halfword(0x0000_0000, bl_hi);
bus.write_halfword(0x0000_0002, 0x0000);
let cycles = cpu.step(&mut bus);
assert_eq!(
cpu.regs.r[15], 0x0000_0002,
"BL first step: PC advances by 2"
);
assert_eq!(
cycles, 1,
"Thumb BL first halfword: 1S at source region = 1 cycle"
);
}
#[test]
fn thumb_bl_second_step_cross_region_resolves_cycles_at_target_pc() {
let mut cpu = Arm7tdmi::new();
cpu.regs.switch_mode(CpuMode::User);
cpu.regs.set_thumb(true);
cpu.regs.r[15] = 0x0000_0002; cpu.regs.r[14] = 0x0100_0000;
let mut bus = AddressTimingBus::new();
let bl_lo: u16 = 0xF800;
bus.write_halfword(0x0000_0002, bl_lo);
bus.write_halfword(0x0000_0004, 0x0000);
let cycles = cpu.step(&mut bus);
assert_eq!(
cpu.regs.r[15], 0x0100_0000,
"BL second step: PC = branch target"
);
assert_eq!(
cpu.regs.r[14], 0x0000_0005,
"BL second step: LR should be old_pc|1 for Thumb return"
);
assert_eq!(
cycles, 21,
"Thumb BL second halfword: 2*5S + 1*11N at target = 21"
);
}
struct FaultBus {
inner: RamBus,
signal_prefetch_abort: bool,
signal_data_abort: bool,
}
impl FaultBus {
fn new() -> Self {
Self {
inner: RamBus::new(0x200),
signal_prefetch_abort: false,
signal_data_abort: false,
}
}
}
impl Bus for FaultBus {
fn read32(&mut self, addr: u32) -> u32 {
self.inner.read32(addr)
}
fn read16(&mut self, addr: u32) -> u16 {
self.inner.read16(addr)
}
fn read8(&mut self, addr: u32) -> u8 {
self.inner.read8(addr)
}
fn write32(&mut self, addr: u32, value: u32) {
self.inner.write32(addr, value);
}
fn write16(&mut self, addr: u32, value: u16) {
self.inner.write16(addr, value);
}
fn write8(&mut self, addr: u32, value: u8) {
self.inner.write8(addr, value);
}
fn n_cycles(&self, addr: u32, width: WidthClass) -> u32 {
self.inner.n_cycles(addr, width)
}
fn s_cycles(&self, addr: u32, width: WidthClass) -> u32 {
self.inner.s_cycles(addr, width)
}
fn prefetch_abort_pending(&mut self) -> bool {
let v = self.signal_prefetch_abort;
self.signal_prefetch_abort = false;
v
}
fn data_abort_pending(&mut self) -> bool {
let v = self.signal_data_abort;
self.signal_data_abort = false;
v
}
}
#[test]
fn prefetch_abort_dispatch_arm() {
let mut cpu = Arm7tdmi::new();
cpu.regs.switch_mode(CpuMode::User);
cpu.regs.cpsr &= !FLAG_I;
cpu.regs.r[15] = 0x100;
let cpsr_before = cpu.regs.cpsr;
let mut bus = FaultBus::new();
bus.signal_prefetch_abort = true;
bus.inner.write_word(0x100, 0xE320_F000); bus.inner.write_word(0x104, 0xE320_F000);
bus.inner.write_word(0x108, 0xE320_F000);
cpu.step(&mut bus);
assert_eq!(
cpu.regs.mode(),
CpuMode::Abort,
"CPU should enter Abort mode on prefetch abort"
);
assert_eq!(cpu.regs.spsr(), cpsr_before, "SPSR_abt should be old CPSR");
assert_eq!(
cpu.regs.r[14],
0x100 + 4,
"LR_abt = exec_pc + 4 for prefetch abort"
);
assert!(cpu.regs.i_flag(), "IRQ must be masked after prefetch abort");
assert!(!cpu.regs.thumb(), "T bit must be clear on exception entry");
assert_eq!(
cpu.regs.r[15],
ExceptionVector::PrefetchAbort as u32,
"PC should jump to prefetch abort vector (0x0C)"
);
}
#[test]
fn prefetch_abort_dispatch_thumb() {
let mut cpu = Arm7tdmi::new();
cpu.regs.switch_mode(CpuMode::User);
cpu.regs.set_thumb(true);
cpu.regs.cpsr &= !FLAG_I;
cpu.regs.r[15] = 0x100;
let cpsr_before = cpu.regs.cpsr;
let mut bus = FaultBus::new();
bus.signal_prefetch_abort = true;
bus.inner.write_halfword(0x100, 0x46C0);
bus.inner.write_halfword(0x102, 0x46C0);
bus.inner.write_halfword(0x104, 0x46C0);
cpu.step(&mut bus);
assert_eq!(cpu.regs.mode(), CpuMode::Abort);
assert_eq!(cpu.regs.spsr(), cpsr_before);
assert_eq!(
cpu.regs.r[14],
0x100 + 4,
"LR_abt = exec_pc + 4 for Thumb prefetch abort"
);
assert!(cpu.regs.i_flag());
assert!(!cpu.regs.thumb());
assert_eq!(cpu.regs.r[15], ExceptionVector::PrefetchAbort as u32);
}
#[test]
fn data_abort_dispatch_arm() {
let mut cpu = Arm7tdmi::new();
cpu.regs.switch_mode(CpuMode::User);
cpu.regs.cpsr &= !FLAG_I;
cpu.regs.r[15] = 0x100;
let cpsr_before = cpu.regs.cpsr;
let mut bus = FaultBus::new();
bus.signal_data_abort = true;
bus.inner.write_word(0x100, 0xE591_0000); bus.inner.write_word(0x104, 0xE320_F000);
bus.inner.write_word(0x108, 0xE320_F000);
cpu.step(&mut bus);
assert_eq!(
cpu.regs.mode(),
CpuMode::User,
"abort not yet dispatched after step 1"
);
cpu.step(&mut bus);
assert_eq!(
cpu.regs.mode(),
CpuMode::Abort,
"CPU should enter Abort mode on data abort"
);
assert_eq!(cpu.regs.spsr(), cpsr_before, "SPSR_abt should be old CPSR");
assert_eq!(
cpu.regs.r[14],
0x100 + 8,
"LR_abt = exec_pc + 8 for data abort"
);
assert!(cpu.regs.i_flag(), "IRQ must be masked after data abort");
assert!(!cpu.regs.thumb(), "T bit must be clear on exception entry");
assert_eq!(
cpu.regs.r[15],
ExceptionVector::DataAbort as u32,
"PC should jump to data abort vector (0x10)"
);
}
#[test]
fn data_abort_dispatch_thumb() {
let mut cpu = Arm7tdmi::new();
cpu.regs.switch_mode(CpuMode::User);
cpu.regs.set_thumb(true);
cpu.regs.cpsr &= !FLAG_I;
cpu.regs.r[15] = 0x100;
let cpsr_before = cpu.regs.cpsr;
let mut bus = FaultBus::new();
bus.signal_data_abort = true;
bus.inner.write_halfword(0x100, 0x6808); bus.inner.write_halfword(0x102, 0x46C0);
bus.inner.write_halfword(0x104, 0x46C0);
cpu.step(&mut bus);
assert_eq!(
cpu.regs.mode(),
CpuMode::User,
"abort not yet dispatched after step 1"
);
cpu.step(&mut bus);
assert_eq!(cpu.regs.mode(), CpuMode::Abort);
assert_eq!(cpu.regs.spsr(), cpsr_before);
assert_eq!(
cpu.regs.r[14],
0x100 + 8,
"LR_abt = exec_pc + 8 for Thumb data abort"
);
assert!(cpu.regs.i_flag());
assert!(!cpu.regs.thumb());
assert_eq!(cpu.regs.r[15], ExceptionVector::DataAbort as u32);
}
#[test]
fn data_abort_preempts_irq() {
let mut cpu = Arm7tdmi::new();
cpu.regs.switch_mode(CpuMode::User);
cpu.regs.cpsr &= !FLAG_I; cpu.regs.r[15] = 0x100;
let mut bus = FaultBus::new();
bus.signal_data_abort = true;
bus.inner.write_word(0x100, 0xE591_0000); bus.inner.write_word(0x104, 0xE320_F000);
bus.inner.write_word(0x108, 0xE320_F000);
cpu.step(&mut bus);
cpu.raise_irq();
cpu.step(&mut bus);
assert_eq!(
cpu.regs.mode(),
CpuMode::Abort,
"Data Abort should preempt IRQ (priority 2 > 4)"
);
assert_eq!(cpu.regs.r[15], ExceptionVector::DataAbort as u32);
}
#[test]
fn step_arm_uses_word_width_thumb_uses_halfword() {
let mov_arm: u32 = 0xE3A0_002A;
let mut cpu = Arm7tdmi::new();
let mut bus = WidthAwareBus::new();
bus.inner.write_word(0x00, mov_arm);
cpu.regs.r[15] = 0x00;
cpu.prefetch_valid = false;
let arm_cycles = cpu.step(&mut bus);
assert_eq!(arm_cycles, 3, "ARM MOV (1S) should use Word s_cycles=3");
let mov_thumb: u16 = 0x202A; let mut cpu = Arm7tdmi::new();
cpu.regs.set_thumb(true); bus.inner.write_halfword(0x00, mov_thumb);
cpu.regs.r[15] = 0x00;
cpu.prefetch_valid = false;
let thumb_cycles = cpu.step(&mut bus);
assert_eq!(
thumb_cycles, 1,
"Thumb MOV (1S) should use HalfwordOrByte s_cycles=1"
);
}
}