feat(cpu): implement ARM7TDMI ARM and Thumb execution

Add ALU, transfers, banked status, exceptions, instruction timing, and CPU regression tests.
This commit is contained in:
0xrsydn 2026-10-05 12:55:35 +07:00
commit 535c69e09f
17 changed files with 4838 additions and 0 deletions

254
src/cpu.rs Normal file
View file

@ -0,0 +1,254 @@
use std::{error::Error, fmt};
use crate::memory::{Memory, MemoryError};
mod alu;
mod arm;
mod exception;
mod status;
mod thumb;
mod timing;
pub use exception::Exception;
pub use status::Mode;
mod transfer;
#[cfg(test)]
mod exception_tests;
#[cfg(test)]
mod instruction_tests;
#[cfg(test)]
mod status_tests;
#[cfg(test)]
mod tests;
#[cfg(test)]
mod thumb_tests;
#[cfg(test)]
mod timing_tests;
#[cfg(test)]
mod transfer_tests;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CpuError {
Memory(MemoryError),
UnsupportedInstruction { address: u32, instruction: u32 },
UnsupportedThumbInstruction { address: u32, instruction: u16 },
}
impl fmt::Display for CpuError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Memory(error) => write!(f, "{error}"),
Self::UnsupportedThumbInstruction {
address,
instruction,
} => {
write!(
f,
"unsupported Thumb instruction {instruction:#06x} at {address:#010x}"
)
}
Self::UnsupportedInstruction {
address,
instruction,
} => {
write!(
f,
"unsupported instruction {instruction:#010x} at {address:#010x}"
)
}
}
}
}
impl Error for CpuError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Memory(error) => Some(error),
Self::UnsupportedInstruction { .. } | Self::UnsupportedThumbInstruction { .. } => None,
}
}
}
impl From<MemoryError> for CpuError {
fn from(error: MemoryError) -> Self {
Self::Memory(error)
}
}
/// Arithmetic status flags from the current program status register.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct Flags {
pub negative: bool,
pub zero: bool,
/// For subtraction, carry means no unsigned borrow occurred.
pub carry: bool,
pub overflow: bool,
}
impl Flags {
fn condition_passed(self, condition: u32) -> bool {
match condition {
0x0 => self.zero, // EQ
0x1 => !self.zero, // NE
0x2 => self.carry, // CS/HS
0x3 => !self.carry, // CC/LO
0x4 => self.negative, // MI
0x5 => !self.negative, // PL
0x6 => self.overflow, // VS
0x7 => !self.overflow, // VC
0x8 => self.carry && !self.zero, // HI
0x9 => !self.carry || self.zero, // LS
0xa => self.negative == self.overflow, // GE
0xb => self.negative != self.overflow, // LT
0xc => !self.zero && self.negative == self.overflow, // GT
0xd => self.zero || self.negative != self.overflow, // LE
0xe => true, // AL
_ => false, // Reserved condition; rejected by Cpu::step.
}
}
}
/// Instruction encoding selected by the processor's T bit.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InstructionSet {
Arm,
Thumb,
}
impl InstructionSet {
fn width(self) -> u32 {
match self {
Self::Arm => 4,
Self::Thumb => 2,
}
}
fn align(self, address: u32) -> u32 {
address & !(self.width() - 1)
}
}
/// ARM/Thumb interpreter with register banks and optional nominal cycle costs.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Cpu {
registers: [u32; 16],
flags: Flags,
instruction_set: InstructionSet,
mode: Mode,
irq_disabled: bool,
fiq_disabled: bool,
banks: status::Banks,
}
impl Cpu {
/// Start directly at ARM-state test code in System mode, with interrupts enabled.
/// This does not perform a GBA reset.
pub fn new(entry_point: u32) -> Self {
let mut registers = [0; 16];
registers[15] = entry_point;
Self {
registers,
flags: Flags::default(),
instruction_set: InstructionSet::Arm,
mode: Mode::System,
irq_disabled: false,
fiq_disabled: false,
banks: status::Banks::default(),
}
}
/// r15 stores the next instruction address, not the pipeline-adjusted value.
pub fn registers(&self) -> &[u32; 16] {
&self.registers
}
pub fn pc(&self) -> u32 {
self.registers[15]
}
pub fn flags(&self) -> Flags {
self.flags
}
pub fn instruction_set(&self) -> InstructionSet {
self.instruction_set
}
/// Execute one ARM instruction or one 16-bit Thumb instruction.
/// Each half of a Thumb BL is a separate step.
/// This CPU-only API does not execute DMA, advance clocks, or sample device interrupts.
/// It records HALTCNT writes but does not enforce HALT. Use Machine::step for integration.
/// Unsupported operations and memory errors leave CPU state unchanged.
/// These errors are development diagnostics, not emulated CPU exceptions.
pub fn step(&mut self, memory: &mut Memory) -> Result<(), CpuError> {
let instruction = self.fetch(memory)?;
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> {
// Record the executing instruction, not an operand's pipelined PC value.
// Always clear the access context, including on diagnostic errors.
memory.begin_cpu_access(self.pc());
let result = self.execute_instruction(instruction, memory);
memory.end_cpu_access();
result
}
fn execute_instruction(
&mut self,
instruction: u32,
memory: &mut Memory,
) -> Result<(), CpuError> {
if self.instruction_set == InstructionSet::Thumb {
return self.execute_thumb(instruction as u16, memory);
}
let condition = instruction >> 28;
// Reserved in ARMv4. Do not decode ARMv5 unconditional extensions.
if condition == 0xf {
return Err(self.unsupported(instruction));
}
if !self.flags.condition_passed(condition) {
self.registers[15] = self.pc().wrapping_add(4);
return Ok(());
}
self.execute_arm(instruction, memory)
}
fn unsupported(&self, instruction: u32) -> CpuError {
match self.instruction_set {
InstructionSet::Arm => CpuError::UnsupportedInstruction {
address: self.pc(),
instruction,
},
InstructionSet::Thumb => CpuError::UnsupportedThumbInstruction {
address: self.pc(),
instruction: instruction as u16,
},
}
}
fn branch_exchange(&mut self, target: u32) {
self.instruction_set = if target & 1 == 0 {
InstructionSet::Arm
} else {
InstructionSet::Thumb
};
self.registers[15] = self.instruction_set.align(target);
}
fn operand_register(&self, register: usize, pc_offset: u32) -> u32 {
if register == 15 {
self.pc().wrapping_add(pc_offset)
} else {
self.registers[register]
}
}
}

108
src/cpu/alu.rs Normal file
View file

@ -0,0 +1,108 @@
use super::Flags;
#[derive(Clone, Copy, Debug)]
pub(super) enum Shift {
Lsl,
Lsr,
Asr,
Ror,
}
impl Shift {
pub(super) fn decode(bits: u32) -> Self {
match bits & 3 {
0 => Self::Lsl,
1 => Self::Lsr,
2 => Self::Asr,
_ => Self::Ror,
}
}
}
/// ARM barrel shifter. Register shifts use only the low eight amount bits.
pub(super) fn shift(
value: u32,
kind: Shift,
amount: u32,
carry: bool,
by_register: bool,
) -> (u32, bool) {
let mut amount = if by_register { amount & 0xff } else { amount };
if amount == 0 {
if by_register || matches!(kind, Shift::Lsl) {
return (value, carry);
}
if matches!(kind, Shift::Ror) {
return ((u32::from(carry) << 31) | (value >> 1), value & 1 != 0); // RRX
}
amount = 32; // Immediate LSR/ASR #0 encodes a shift by 32.
}
match kind {
Shift::Lsl => match amount {
1..=31 => (value << amount, (value >> (32 - amount)) & 1 != 0),
32 => (0, value & 1 != 0),
_ => (0, false),
},
Shift::Lsr => match amount {
1..=31 => (value >> amount, (value >> (amount - 1)) & 1 != 0),
32 => (0, value >> 31 != 0),
_ => (0, false),
},
Shift::Asr => {
let sign = value >> 31 != 0;
if amount >= 32 {
(if sign { u32::MAX } else { 0 }, sign)
} else {
(
((value as i32) >> amount) as u32,
(value >> (amount - 1)) & 1 != 0,
)
}
}
Shift::Ror => {
let result = value.rotate_right(amount);
(result, result >> 31 != 0)
}
}
}
/// Addition is also used for subtraction: a - b = a + !b + 1.
fn add_with_carry(a: u32, b: u32, carry: bool) -> (u32, bool, bool) {
let wide = u64::from(a) + u64::from(b) + u64::from(carry);
let result = wide as u32;
let overflow = (!(a ^ b) & (a ^ result)) >> 31 != 0;
(result, wide > u64::from(u32::MAX), overflow)
}
pub(super) fn execute(
opcode: u32,
a: u32,
b: u32,
shifter_carry: bool,
previous: Flags,
) -> (u32, Flags) {
let (result, carry, overflow) = match opcode {
0x0 | 0x8 => (a & b, shifter_carry, previous.overflow),
0x1 | 0x9 => (a ^ b, shifter_carry, previous.overflow),
0x2 | 0xa => add_with_carry(a, !b, true),
0x3 => add_with_carry(b, !a, true),
0x4 | 0xb => add_with_carry(a, b, false),
0x5 => add_with_carry(a, b, previous.carry),
0x6 => add_with_carry(a, !b, previous.carry),
0x7 => add_with_carry(b, !a, previous.carry),
0xc => (a | b, shifter_carry, previous.overflow),
0xd => (b, shifter_carry, previous.overflow),
0xe => (a & !b, shifter_carry, previous.overflow),
0xf => (!b, shifter_carry, previous.overflow),
_ => unreachable!("opcode comes from a four-bit instruction field"),
};
(
result,
Flags {
negative: result >> 31 != 0,
zero: result == 0,
carry,
overflow,
},
)
}

305
src/cpu/arm.rs Normal file
View file

@ -0,0 +1,305 @@
use super::{
alu::{self, Shift},
Cpu, CpuError,
};
use crate::memory::Memory;
impl Cpu {
pub(super) fn execute_arm(
&mut self,
instruction: u32,
memory: &mut Memory,
) -> Result<(), CpuError> {
if instruction & 0x0f00_0000 == 0x0f00_0000 {
self.enter_exception(super::Exception::SoftwareInterrupt);
return Ok(());
}
if instruction & 0x0fbf_0fff == 0x010f_0000 {
return self.read_status(instruction);
}
if instruction & 0x0fb0_fff0 == 0x0120_f000 || instruction & 0x0fb0_f000 == 0x0320_f000 {
return self.write_status(instruction);
}
if instruction & 0x0fff_fff0 == 0x012f_ff10 {
let target = self.operand_register((instruction & 15) as usize, 8);
self.branch_exchange(target);
return Ok(());
}
if instruction & 0x0e00_0000 == 0x0a00_0000 {
let address = self.pc();
let offset = (((instruction & 0x00ff_ffff) << 8) as i32 >> 6) as u32;
if instruction & (1 << 24) != 0 {
self.registers[14] = address.wrapping_add(4); // BL return address
}
self.registers[15] = address.wrapping_add(8).wrapping_add(offset);
return Ok(());
}
if instruction & 0x0e00_0000 == 0x0800_0000 {
return self.block_transfer(instruction, memory);
}
// SWP shares the 1001 low bits with multiply and halfword transfers.
if instruction & 0x0fb0_0ff0 == 0x0100_0090 {
return self.swap(instruction, memory);
}
if instruction & 0x0f80_00f0 == 0x0080_0090 {
return self.multiply_long(instruction);
}
if instruction & 0x0fc0_00f0 == 0x0000_0090 {
return self.multiply(instruction);
}
if instruction & 0x0e00_0090 == 0x0000_0090 {
return self.halfword_transfer(instruction, memory);
}
if instruction & 0x0c00_0000 == 0x0400_0000 {
return self.single_transfer(instruction, memory);
}
if instruction & 0x0c00_0000 == 0 {
return self.data_processing(instruction);
}
Err(self.unsupported(instruction))
}
fn data_processing(&mut self, instruction: u32) -> Result<(), CpuError> {
let opcode = (instruction >> 21) & 15;
let set_flags = instruction & (1 << 20) != 0;
let test = (8..=11).contains(&opcode);
let source = ((instruction >> 16) & 15) as usize;
let destination = ((instruction >> 12) & 15) as usize;
// S=0 in the test/compare opcode range is PSR transfer or an extension.
if test && !set_flags {
return Err(self.unsupported(instruction));
}
let restore = if !test && destination == 15 && set_flags {
Some(self.return_status(instruction)?)
} else {
None
};
let immediate = instruction & (1 << 25) != 0;
let register_shift = !immediate && instruction & (1 << 4) != 0;
let pc_offset = if register_shift { 12 } else { 8 };
let (operand, carry) = if immediate {
let rotation = ((instruction >> 8) & 15) * 2;
let value = (instruction & 255).rotate_right(rotation);
(
value,
if rotation == 0 {
self.flags.carry
} else {
value >> 31 != 0
},
)
} else {
let amount = if register_shift {
let shift_register = ((instruction >> 8) & 15) as usize;
if instruction & (1 << 7) != 0 || shift_register == 15 {
return Err(self.unsupported(instruction));
}
self.registers[shift_register] & 255
} else {
(instruction >> 7) & 31
};
alu::shift(
self.operand_register((instruction & 15) as usize, pc_offset),
Shift::decode(instruction >> 5),
amount,
self.flags.carry,
register_shift,
)
};
let (result, flags) = alu::execute(
opcode,
self.operand_register(source, pc_offset),
operand,
carry,
self.flags,
);
if let Some((status, mode)) = restore {
self.apply_status(status, mode);
} else if set_flags {
self.flags = flags;
}
let next = if !test && destination == 15 {
self.instruction_set.align(result)
} else {
self.pc().wrapping_add(4)
};
if !test && destination != 15 {
self.registers[destination] = result;
}
self.registers[15] = next;
Ok(())
}
fn single_transfer(&mut self, instruction: u32, memory: &mut Memory) -> Result<(), CpuError> {
let base_register = ((instruction >> 16) & 15) as usize;
let destination = ((instruction >> 12) & 15) as usize;
let pre = instruction & (1 << 24) != 0;
let write_bit = instruction & (1 << 21) != 0;
let write_back = !pre || write_bit;
let byte = instruction & (1 << 22) != 0;
let load = instruction & (1 << 20) != 0;
if (!pre && write_bit) // LDRT/STRT access semantics are not implemented.
|| (write_back && (base_register == 15 || (load && base_register == destination)))
|| (byte && destination == 15)
{
return Err(self.unsupported(instruction));
}
let offset = if instruction & (1 << 25) != 0 {
let rm = (instruction & 15) as usize;
if instruction & (1 << 4) != 0 || rm == 15 {
return Err(self.unsupported(instruction));
}
alu::shift(
self.registers[rm],
Shift::decode(instruction >> 5),
(instruction >> 7) & 31,
self.flags.carry,
false,
)
.0
} else {
instruction & 0xfff
};
let base = self.operand_register(base_register, 8);
let adjusted = offset_address(base, offset, instruction);
let address = if pre { adjusted } else { base };
let next = self.pc().wrapping_add(4);
// All validation precedes memory writes. CPU changes follow successful access.
let value = if load {
if byte {
u32::from(memory.read8(address)?)
} else {
memory.read32(address & !3)?.rotate_right((address & 3) * 8)
}
} else {
let value = self.operand_register(destination, 12);
if byte {
memory.write8(address, value as u8)?;
} else {
memory.write32(address & !3, value)?;
}
value
};
if write_back {
self.registers[base_register] = adjusted;
}
if load && destination != 15 {
self.registers[destination] = value;
}
// ARMv4 LDR PC does not switch to Thumb, even if bit zero is set.
self.registers[15] = if load && destination == 15 {
value & !3
} else {
next
};
Ok(())
}
fn halfword_transfer(&mut self, instruction: u32, memory: &mut Memory) -> Result<(), CpuError> {
let kind = (instruction >> 5) & 3;
let load = instruction & (1 << 20) != 0;
let pre = instruction & (1 << 24) != 0;
let write_bit = instruction & (1 << 21) != 0;
let write_back = !pre || write_bit;
let base_register = ((instruction >> 16) & 15) as usize;
let destination = ((instruction >> 12) & 15) as usize;
if kind == 0
|| (!load && kind != 1)
|| (!pre && write_bit)
|| destination == 15
|| (write_back && (base_register == 15 || (load && base_register == destination)))
{
return Err(self.unsupported(instruction));
}
let offset = if instruction & (1 << 22) != 0 {
((instruction >> 4) & 0xf0) | (instruction & 15)
} else {
let rm = (instruction & 15) as usize;
if instruction & 0xf00 != 0 || rm == 15 {
return Err(self.unsupported(instruction));
}
self.registers[rm]
};
let base = self.operand_register(base_register, 8);
let adjusted = offset_address(base, offset, instruction);
let address = if pre { adjusted } else { base };
let value = if load {
match kind {
1 => u32::from(memory.read16(address & !1)?).rotate_right((address & 1) * 8),
2 => memory.read8(address)? as i8 as i32 as u32,
_ if address & 1 != 0 => memory.read8(address)? as i8 as i32 as u32,
_ => memory.read16(address)? as i16 as i32 as u32,
}
} else {
memory.write16(address & !1, self.registers[destination] as u16)?;
0
};
if write_back {
self.registers[base_register] = adjusted;
}
if load {
self.registers[destination] = value;
}
self.registers[15] = self.pc().wrapping_add(4);
Ok(())
}
fn multiply(&mut self, instruction: u32) -> Result<(), CpuError> {
let rd = ((instruction >> 16) & 15) as usize;
let rn = ((instruction >> 12) & 15) as usize;
let rs = ((instruction >> 8) & 15) as usize;
let rm = (instruction & 15) as usize;
let accumulate = instruction & (1 << 21) != 0;
if [rd, rn, rs, rm].contains(&15) || rd == rm || (!accumulate && rn != 0) {
return Err(self.unsupported(instruction));
}
let mut result = self.registers[rm].wrapping_mul(self.registers[rs]);
if accumulate {
result = result.wrapping_add(self.registers[rn]);
}
self.registers[rd] = result;
if instruction & (1 << 20) != 0 {
self.flags.negative = result >> 31 != 0;
self.flags.zero = result == 0;
// ARM7 carry is unspecified after multiply; preserve it deterministically.
}
self.registers[15] = self.pc().wrapping_add(4);
Ok(())
}
fn multiply_long(&mut self, instruction: u32) -> Result<(), CpuError> {
let hi = ((instruction >> 16) & 15) as usize;
let lo = ((instruction >> 12) & 15) as usize;
let rs = ((instruction >> 8) & 15) as usize;
let rm = (instruction & 15) as usize;
if [hi, lo, rs, rm].contains(&15) || hi == lo || hi == rm || lo == rm {
return Err(self.unsupported(instruction));
}
let mut result = if instruction & (1 << 22) != 0 {
(i64::from(self.registers[rm] as i32) * i64::from(self.registers[rs] as i32)) as u64
} else {
u64::from(self.registers[rm]) * u64::from(self.registers[rs])
};
if instruction & (1 << 21) != 0 {
let accumulator = (u64::from(self.registers[hi]) << 32) | u64::from(self.registers[lo]);
result = result.wrapping_add(accumulator);
}
self.registers[hi] = (result >> 32) as u32;
self.registers[lo] = result as u32;
if instruction & (1 << 20) != 0 {
self.flags.negative = result >> 63 != 0;
self.flags.zero = result == 0;
// Unspecified multiply C/V outputs are preserved, not hardware-verified.
}
self.registers[15] = self.pc().wrapping_add(4);
Ok(())
}
}
fn offset_address(base: u32, offset: u32, instruction: u32) -> u32 {
if instruction & (1 << 23) != 0 {
base.wrapping_add(offset)
} else {
base.wrapping_sub(offset)
}
}

67
src/cpu/exception.rs Normal file
View file

@ -0,0 +1,67 @@
//! Exception entry. Memory errors remain diagnostics rather than hardware aborts.
use super::{Cpu, InstructionSet, Mode};
/// ARM7 exception sources. GBA hardware does not expose every source.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Exception {
UndefinedInstruction,
SoftwareInterrupt,
PrefetchAbort,
DataAbort,
Irq,
Fiq,
}
impl Cpu {
/// Start at the reset vector in ARM Supervisor mode, with IRQ/FIQ disabled.
/// Registers and RAM are not initialized as a Nintendo BIOS would initialize them.
pub fn at_reset() -> Self {
let mut cpu = Self::new(0);
cpu.mode = Mode::Supervisor;
cpu.irq_disabled = true;
cpu.fiq_disabled = true;
cpu
}
/// Enter an exception explicitly, regardless of interrupt masks.
/// For synchronous exceptions, PC must identify the faulting instruction.
/// 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) {
let (mode, vector, offset) = match exception {
Exception::UndefinedInstruction => {
(Mode::Undefined, 0x04, self.instruction_set.width())
}
Exception::SoftwareInterrupt => (Mode::Supervisor, 0x08, self.instruction_set.width()),
Exception::PrefetchAbort => (Mode::Abort, 0x0c, 4),
Exception::DataAbort => (Mode::Abort, 0x10, 8),
Exception::Irq => (Mode::Irq, 0x18, 4),
Exception::Fiq => (Mode::Fiq, 0x1c, 4),
};
let previous = self.cpsr();
let link = self.pc().wrapping_add(offset);
self.switch_mode(mode);
self.set_spsr(previous);
self.registers[14] = link;
self.registers[15] = vector;
self.instruction_set = InstructionSet::Arm;
self.irq_disabled = true;
if exception == Exception::Fiq {
self.fiq_disabled = true;
}
}
/// Sample external interrupt lines between instructions. FIQ has priority.
/// Returns true when an unmasked interrupt was taken. Machine connects the GBA IRQ line.
pub fn take_interrupt(&mut self, irq: bool, fiq: bool) -> bool {
if fiq && !self.fiq_disabled {
self.enter_exception(Exception::Fiq);
} else if irq && !self.irq_disabled {
self.enter_exception(Exception::Irq);
} else {
return false;
}
true
}
}

327
src/cpu/exception_tests.rs Normal file
View file

@ -0,0 +1,327 @@
use super::*;
use crate::memory::{BIOS_SIZE, ROM_START};
fn program(instruction: u32) -> Memory {
Memory::new(instruction.to_le_bytes().to_vec()).unwrap()
}
fn vectors(instruction: u32) -> Memory {
let bios = instruction.to_le_bytes().repeat(BIOS_SIZE / 4);
Memory::with_bios(vec![], bios).unwrap()
}
#[test]
fn exception_entry_sets_vector_link_masks_and_saved_status() {
for (exception, mode, vector, arm_offset, thumb_offset) in [
(Exception::UndefinedInstruction, Mode::Undefined, 4, 4, 2),
(Exception::SoftwareInterrupt, Mode::Supervisor, 8, 4, 2),
(Exception::PrefetchAbort, Mode::Abort, 12, 4, 4),
(Exception::DataAbort, Mode::Abort, 16, 8, 8),
(Exception::Irq, Mode::Irq, 24, 4, 4),
(Exception::Fiq, Mode::Fiq, 28, 4, 4),
] {
for thumb in [false, true] {
for fiq_mask in [0, 0x40] {
let mut cpu = Cpu::new(ROM_START);
let status = 0xb000_001f | if thumb { 0x20 } else { 0 } | fiq_mask;
cpu.apply_status(status, Mode::System);
cpu.registers[13] = 0x0300_0100;
cpu.registers[14] = 123;
cpu.enter_exception(exception);
assert_eq!(cpu.mode(), mode);
assert_eq!(cpu.pc(), vector);
assert_eq!(
cpu.registers[14],
ROM_START + if thumb { thumb_offset } else { arm_offset }
);
assert_eq!(cpu.registers[13], 0);
assert_eq!(cpu.spsr(), Some(status));
assert_eq!(
cpu.cpsr(),
0xb000_0080
| mode as u32
| if exception == Exception::Fiq {
0x40
} else {
fiq_mask
}
);
cpu.switch_mode(Mode::System);
assert_eq!(cpu.registers[13..15], [0x0300_0100, 123]);
}
}
}
}
#[test]
fn arm_and_thumb_swi_execute_and_movs_pc_restores_caller() {
for thumb in [false, true] {
for comment in [0, 1, 255] {
let rom = if thumb {
(0xdf00_u16 | comment).to_le_bytes().to_vec()
} else {
(0xefab_0000_u32 | u32::from(comment))
.to_le_bytes()
.to_vec()
};
let mut memory =
Memory::with_bios(rom, 0xe1b0_f00e_u32.to_le_bytes().repeat(BIOS_SIZE / 4))
.unwrap();
let mut cpu = Cpu::new(ROM_START);
let status = 0x6000_001f | if thumb { 0x20 } else { 0 };
cpu.apply_status(status, Mode::System);
cpu.registers[14] = 0x1234;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.pc(), 8);
assert_eq!(cpu.mode(), Mode::Supervisor);
assert_eq!(cpu.instruction_set(), InstructionSet::Arm);
// MOVS pc, lr restores the saved flags, not ALU-result flags.
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.pc(), ROM_START + if thumb { 2 } else { 4 });
assert_eq!(cpu.cpsr(), status);
assert_eq!(cpu.registers[14], 0x1234);
}
}
}
#[test]
fn conditional_swi_can_be_skipped_without_entering_supervisor() {
let mut cpu = Cpu::new(ROM_START);
let mut expected = cpu.clone();
expected.registers[15] += 4;
cpu.step(&mut program(0x0fff_ffff)).unwrap();
assert_eq!(cpu, expected);
}
#[test]
fn swi_without_vector_image_enters_then_reports_fetch_error() {
let mut cpu = Cpu::new(ROM_START);
let mut memory = program(0xef00_0000);
cpu.step(&mut memory).unwrap();
let before = cpu.clone();
assert_eq!(
cpu.step(&mut memory),
Err(CpuError::Memory(MemoryError::Unmapped(8)))
);
assert_eq!(cpu, before);
}
#[test]
fn interrupt_masks_and_fiq_priority_are_respected() {
for irq_mask in [false, true] {
for fiq_mask in [false, true] {
for irq in [false, true] {
for fiq in [false, true] {
let mut cpu = Cpu::new(ROM_START);
cpu.irq_disabled = irq_mask;
cpu.fiq_disabled = fiq_mask;
let before = cpu.clone();
let expected = if fiq && !fiq_mask {
Some(Mode::Fiq)
} else if irq && !irq_mask {
Some(Mode::Irq)
} else {
None
};
assert_eq!(cpu.take_interrupt(irq, fiq), expected.is_some());
if let Some(mode) = expected {
assert_eq!(cpu.mode(), mode);
assert_eq!(cpu.spsr(), Some(before.cpsr()));
} else {
assert_eq!(cpu, before);
}
}
}
}
}
}
#[test]
fn subs_exception_returns_resume_or_retry_the_correct_instruction() {
for (exception, subtract) in [
(Exception::Irq, 4),
(Exception::Fiq, 4),
(Exception::PrefetchAbort, 4),
(Exception::DataAbort, 8),
] {
for thumb in [false, true] {
let mut cpu = Cpu::new(ROM_START + 2 * u32::from(thumb));
let status = 0xa000_001f | if thumb { 0x20 } else { 0 };
cpu.apply_status(status, Mode::System);
let before = cpu.clone();
cpu.enter_exception(exception);
cpu.step(&mut vectors(0xe25e_f000 | subtract)).unwrap(); // SUBS pc, lr, #offset
assert_eq!(cpu.registers, before.registers);
assert_eq!(cpu.cpsr(), before.cpsr());
}
}
}
#[test]
fn nested_fiq_preserves_irq_saved_status_and_banked_registers() {
let mut cpu = Cpu::new(ROM_START);
cpu.registers[8] = 88;
cpu.enter_exception(Exception::Irq);
cpu.registers[13] = 0x0300_0200;
let irq_status = cpu.cpsr();
let irq_spsr = cpu.spsr();
let irq_link = cpu.registers[14];
assert!(cpu.take_interrupt(false, true));
cpu.registers[8] = 888;
cpu.step(&mut vectors(0xe25e_f004)).unwrap();
assert_eq!(cpu.pc(), 0x18);
assert_eq!(cpu.cpsr(), irq_status);
assert_eq!(cpu.spsr(), irq_spsr);
assert_eq!(cpu.registers[13..15], [0x0300_0200, irq_link]);
assert_eq!(cpu.registers[8], 88);
cpu.step(&mut vectors(0xe25e_f004)).unwrap();
assert_eq!(cpu.pc(), ROM_START);
assert_eq!(cpu.cpsr(), 0x1f);
cpu.switch_mode(Mode::Fiq);
assert_eq!(cpu.registers[8], 888);
}
#[test]
fn same_mode_exception_overwrites_link_and_saved_status() {
let mut cpu = Cpu::new(ROM_START);
cpu.enter_exception(Exception::SoftwareInterrupt);
cpu.registers[15] = 0x100;
let previous = cpu.cpsr();
cpu.enter_exception(Exception::SoftwareInterrupt);
assert_eq!(cpu.spsr(), Some(previous));
assert_eq!(cpu.registers[14], 0x104);
}
#[test]
fn return_alignment_uses_saved_state_not_target_bit_zero() {
for (status, expected) in [(0x1f, ROM_START), (0x3f, ROM_START + 2)] {
let mut cpu = Cpu::at_reset();
cpu.registers[15] = ROM_START;
cpu.registers[14] = ROM_START + 3;
cpu.set_spsr(status);
cpu.step(&mut program(0xe1b0_f00e)).unwrap();
assert_eq!(cpu.pc(), expected);
assert_eq!(cpu.cpsr(), status);
}
}
#[test]
fn invalid_saved_mode_rejects_return_without_partial_changes() {
for instruction in [0xe1b0_f00e, 0xe8fd_8001] {
let mut cpu = Cpu::at_reset();
cpu.registers[15] = ROM_START;
cpu.registers[13] = 0x0200_0000;
cpu.set_spsr(0); // An invalid mode
let before = cpu.clone();
assert!(cpu.step(&mut program(instruction)).is_err());
assert_eq!(cpu, before);
}
}
#[test]
fn ldm_exception_return_loads_outgoing_bank_before_restoring_status() {
let mut cpu = Cpu::new(ROM_START);
cpu.registers[13] = 0x0300_0100;
cpu.registers[14] = 123;
cpu.enter_exception(Exception::SoftwareInterrupt);
cpu.registers[15] = ROM_START;
cpu.registers[13] = 0x0200_0000;
cpu.set_spsr(0x9000_003f);
let mut memory = program(0xe8fd_c001); // LDMIA sp!, {r0, lr, pc}^
for (index, value) in [42, 456, ROM_START + 7].into_iter().enumerate() {
memory
.write32(0x0200_0000 + index as u32 * 4, value)
.unwrap();
}
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.cpsr(), 0x9000_003f);
assert_eq!(cpu.pc(), ROM_START + 6);
assert_eq!(cpu.registers[0], 42);
assert_eq!(cpu.registers[13..15], [0x0300_0100, 123]);
cpu.switch_mode(Mode::Supervisor);
assert_eq!(cpu.registers[13..15], [0x0200_000c, 456]);
}
#[test]
fn failed_ldm_return_preserves_every_bank_and_status() {
let mut cpu = Cpu::at_reset();
cpu.registers[15] = ROM_START;
cpu.registers[13] = 0x0400_0054;
cpu.set_spsr(0x3f);
let mut memory = program(0xe8fd_8001);
memory.write32(0x0400_0054, 42).unwrap();
let before = cpu.clone();
assert!(matches!(cpu.step(&mut memory), Err(CpuError::Memory(_))));
assert_eq!(cpu, before);
}
#[test]
fn user_bank_transfers_use_current_base_and_user_values() {
for mode in [Mode::System, Mode::Supervisor, Mode::Fiq] {
let mut cpu = Cpu::new(ROM_START);
for register in 0..15 {
cpu.registers[register] = 100 + register as u32;
}
cpu.switch_mode(mode);
cpu.registers[13] = 0x0200_0100;
let original = cpu.clone();
let mut memory = program(0xe8cd_7fff); // STMIA sp, {r0-r14}^
cpu.step(&mut memory).unwrap();
for register in 0..15 {
assert_eq!(
memory.read32(0x0200_0100 + register as u32 * 4).unwrap(),
original.user_register(register)
);
memory
.write32(0x0200_0100 + register as u32 * 4, 200 + register as u32)
.unwrap();
}
// Use RAM for the next instruction without changing the transfer base.
memory.write32(0x0300_0000, 0xe8dd_7fff).unwrap(); // LDMIA sp, {r0-r14}^
cpu.registers[15] = 0x0300_0000;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.cpsr(), original.cpsr());
if mode != Mode::System {
assert_eq!(cpu.registers[13..15], original.registers[13..15]);
}
if mode == Mode::Fiq {
assert_eq!(cpu.registers[8..13], original.registers[8..13]);
}
cpu.switch_mode(Mode::System);
for register in 0..15 {
assert_eq!(cpu.registers[register], 200 + register as u32);
}
}
}
#[test]
fn user_bank_store_pc_uses_pipeline_offset() {
let mut cpu = Cpu::at_reset();
cpu.registers[15] = ROM_START;
cpu.registers[0] = 0x0200_0000;
let mut memory = program(0xe8c0_8000); // STMIA r0, {pc}^
cpu.step(&mut memory).unwrap();
assert_eq!(memory.read32(0x0200_0000).unwrap(), ROM_START + 12);
}
#[test]
fn user_mode_cannot_request_user_bank_transfers_or_status_return() {
for instruction in [0xe8c0_0002, 0xe8d0_0002, 0xe8d0_8000, 0xe1b0_f00e] {
let mut cpu = Cpu::new(ROM_START);
cpu.switch_mode(Mode::User);
cpu.registers[0] = 0x0200_0000;
let before = cpu.clone();
assert!(cpu.step(&mut program(instruction)).is_err());
assert_eq!(cpu, before);
}
}
#[test]
fn failed_user_bank_load_preserves_hidden_registers() {
let mut cpu = Cpu::at_reset();
cpu.registers[15] = ROM_START;
cpu.registers[0] = 0x0400_0054;
let before = cpu.clone();
assert!(cpu.step(&mut program(0xe8d0_6000)).is_err()); // LDMIA r0, {sp, lr}^
assert_eq!(cpu, before);
}

View file

@ -0,0 +1,560 @@
use super::{
alu::{self, Shift},
*,
};
use crate::memory::ROM_START;
const RAM: u32 = 0x0200_0000;
fn program(words: &[u32]) -> Memory {
Memory::new(words.iter().flat_map(|word| word.to_le_bytes()).collect()).unwrap()
}
#[test]
fn register_shifts_match_a_bit_at_a_time_reference() {
for value in [0, 1, 0x8000_0000, 0x8000_0001, 0x1234_5678, u32::MAX] {
for kind in [Shift::Lsl, Shift::Lsr, Shift::Asr, Shift::Ror] {
for incoming_carry in [false, true] {
for amount in 0..=255 {
let mut expected = value;
let mut carry = incoming_carry;
for _ in 0..amount {
match kind {
Shift::Lsl => {
carry = expected >> 31 != 0;
expected <<= 1;
}
Shift::Lsr => {
carry = expected & 1 != 0;
expected >>= 1;
}
Shift::Asr => {
carry = expected & 1 != 0;
expected = ((expected as i32) >> 1) as u32;
}
Shift::Ror => {
carry = expected & 1 != 0;
expected = (expected >> 1) | (u32::from(carry) << 31);
}
}
}
assert_eq!(
alu::shift(value, kind, amount, incoming_carry, true),
(expected, carry),
"value={value:08x} kind={kind:?} amount={amount}"
);
}
}
}
}
}
#[test]
fn immediate_zero_shifts_have_arm_special_meanings() {
for carry in [false, true] {
let value = 0x8000_0001;
assert_eq!(
alu::shift(value, Shift::Lsl, 0, carry, false),
(value, carry)
);
assert_eq!(alu::shift(value, Shift::Lsr, 0, carry, false), (0, true));
assert_eq!(
alu::shift(value, Shift::Asr, 0, carry, false),
(u32::MAX, true)
);
assert_eq!(
alu::shift(value, Shift::Ror, 0, carry, false),
(0x4000_0000 | (u32::from(carry) << 31), true)
);
}
assert_eq!(alu::shift(1, Shift::Asr, 0, true, false), (0, false));
}
#[test]
fn register_shift_amount_uses_only_the_low_byte() {
assert_eq!(alu::shift(3, Shift::Lsl, 256, true, true), (3, true));
assert_eq!(alu::shift(3, Shift::Lsl, 257, true, true), (6, false));
let mut memory = program(&[0xe1b0_0311]); // MOVS r0,r1,LSL r3
let mut cpu = Cpu::new(ROM_START);
cpu.registers[1] = 3;
cpu.registers[3] = 257;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[0], 6);
}
#[test]
fn all_data_processing_opcodes_decode_and_preserve_test_destinations() {
let expected = [
0x10,
0x26,
0x22,
0xffff_ffde,
0x46,
0x47,
0x22,
0xffff_ffde,
0x10,
0x26,
0x22,
0x46,
0x36,
0x12,
0x24,
0xffff_ffed,
];
for (opcode, result) in expected.into_iter().enumerate() {
// opcode{S} r2,r0,r1. Test instructions ignore the destination.
let instruction = 0xe010_2001 | ((opcode as u32) << 21);
let mut memory = program(&[instruction]);
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = 0x34;
cpu.registers[1] = 0x12;
cpu.registers[2] = 0xfeed_face;
cpu.flags.carry = true;
cpu.step(&mut memory).unwrap();
assert_eq!(
cpu.registers[2],
if (8..=11).contains(&opcode) {
0xfeed_face
} else {
result
}
);
assert_eq!(cpu.flags.negative, result >> 31 != 0);
assert_eq!(cpu.flags.zero, result == 0);
assert_eq!(cpu.pc(), ROM_START + 4);
}
}
#[test]
fn arithmetic_with_carry_matches_wide_signed_and_unsigned_reference() {
let values = [0, 1, 0x7fff_ffff, 0x8000_0000, 0x8000_0001, u32::MAX];
for opcode in [2, 3, 4, 5, 6, 7, 10, 11] {
let mut memory = program(&[0xe010_2001 | (opcode << 21)]);
for a in values {
for b in values {
for carry in [false, true] {
let subtract = matches!(opcode, 2 | 3 | 6 | 7 | 10);
let reverse = matches!(opcode, 3 | 7);
let (a, b) = if reverse { (b, a) } else { (a, b) };
let extra = match opcode {
5 => i64::from(carry),
6 | 7 => i64::from(carry) - 1,
_ => 0,
};
let wide = if subtract {
i64::from(a) - i64::from(b) + extra
} else {
i64::from(a) + i64::from(b) + extra
};
let signed = if subtract {
i64::from(a as i32) - i64::from(b as i32) + extra
} else {
i64::from(a as i32) + i64::from(b as i32) + extra
};
let mut cpu = Cpu::new(ROM_START);
(cpu.registers[0], cpu.registers[1]) = if reverse { (b, a) } else { (a, b) };
cpu.flags.carry = carry;
cpu.step(&mut memory).unwrap();
if opcode < 8 {
assert_eq!(cpu.registers[2], wide as u32);
}
assert_eq!(
cpu.flags,
Flags {
negative: (wide as u32) >> 31 != 0,
zero: wide as u32 == 0,
carry: if subtract {
wide >= 0
} else {
wide > i64::from(u32::MAX)
},
overflow: i32::try_from(signed).is_err(),
},
"opcode={opcode:x} a={a:08x} b={b:08x} carry={carry}"
);
}
}
}
}
}
#[test]
fn logical_ops_use_shifter_carry_and_preserve_overflow() {
for opcode in [0, 1, 8, 9, 12, 13, 14, 15] {
let mut memory = program(&[0xe010_2081 | (opcode << 21)]); // r1 LSL #1
let mut cpu = Cpu::new(ROM_START);
cpu.registers[1] = 0x8000_0001;
cpu.flags.overflow = true;
cpu.step(&mut memory).unwrap();
assert!(cpu.flags.carry);
assert!(cpu.flags.overflow);
}
}
#[test]
fn arithmetic_uses_old_carry_not_shifter_carry() {
let mut memory = program(&[0xe0b0_2081]); // ADCS r2,r0,r1,LSL #1
let mut cpu = Cpu::new(ROM_START);
cpu.registers[1] = 0x8000_0000; // shift produces carry=1 and value=0
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[2], 0);
assert!(!cpu.flags.carry);
assert!(cpu.flags.zero);
}
#[test]
fn non_flag_setting_register_ops_preserve_flags() {
for opcode in [0, 1, 2, 3, 4, 5, 6, 7, 12, 13, 14, 15] {
let mut memory = program(&[0xe000_2081 | (opcode << 21)]);
let mut cpu = Cpu::new(ROM_START);
cpu.flags = Flags {
negative: true,
zero: true,
carry: true,
overflow: true,
};
let before = cpu.flags;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.flags, before);
}
}
#[test]
fn register_specified_shifts_read_pc_plus_twelve_for_both_operands() {
for (instruction, expected) in [
(0xe1a0_021f, ROM_START + 12), // MOV r0,pc,LSL r2
(0xe08f_0211, ROM_START + 12), // ADD r0,pc,r1,LSL r2
(0xe1a0_000f, ROM_START + 8), // MOV r0,pc (immediate zero shift)
] {
let mut memory = program(&[instruction]);
let mut cpu = Cpu::new(ROM_START);
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[0], expected);
}
}
#[test]
fn data_processing_pc_destination_aligns_without_switching_state() {
let mut memory = program(&[0xe1a0_f000]); // MOV pc,r0
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = RAM + 3;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.pc(), RAM);
}
#[test]
fn branch_link_and_bx_return_execute_a_subroutine() {
let mut memory = program(&[
0xe3a0_0006, // MOV r0,#6
0xe3a0_1007, // MOV r1,#7
0xeb00_0001, // BL subroutine at +20
0xeaff_fffe, // B .
0xe1a0_0000, // NOP, skipped
0xe002_0190, // MUL r2,r0,r1
0xe12f_ff1e, // BX lr
]);
let mut cpu = Cpu::new(ROM_START);
for _ in 0..6 {
cpu.step(&mut memory).unwrap();
}
assert_eq!(cpu.registers[2], 42);
assert_eq!(cpu.registers[14], ROM_START + 12);
assert_eq!(cpu.pc(), ROM_START + 12);
}
#[test]
fn bx_thumb_target_switches_state_and_clears_only_bit_zero() {
let mut memory = program(&[0xe12f_ff10]);
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = ROM_START + 11;
let mut expected = cpu.registers;
expected[15] = ROM_START + 10;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
assert_eq!(cpu.registers, expected);
assert_eq!(cpu.flags, Flags::default());
}
#[test]
fn skipped_branch_link_does_not_change_lr() {
let mut memory = program(&[0x0b00_0000]); // BLEQ with Z clear
let mut cpu = Cpu::new(ROM_START);
cpu.registers[14] = 123;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[14], 123);
assert_eq!(cpu.pc(), ROM_START + 4);
}
#[test]
fn word_transfers_cover_pre_post_up_down_and_writeback() {
for pre in [false, true] {
for up in [false, true] {
for explicit_writeback in [false, true] {
if !pre && explicit_writeback {
continue;
} // T form is intentionally unsupported.
let fields = (u32::from(pre) << 24)
| (u32::from(up) << 23)
| (u32::from(explicit_writeback) << 21);
let mut memory = program(&[0xe400_1004 | fields, 0xe410_2004 | fields]);
let mut cpu = Cpu::new(ROM_START);
let base = RAM + 16;
let adjusted = if up { base + 4 } else { base - 4 };
let address = if pre { adjusted } else { base };
cpu.registers[0] = base;
cpu.registers[1] = 0x1234_5678;
cpu.step(&mut memory).unwrap();
assert_eq!(memory.read32(address).unwrap(), 0x1234_5678);
assert_eq!(
cpu.registers[0],
if !pre || explicit_writeback {
adjusted
} else {
base
}
);
cpu.registers[0] = base;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[2], 0x1234_5678);
assert_eq!(
cpu.registers[0],
if !pre || explicit_writeback {
adjusted
} else {
base
}
);
}
}
}
}
#[test]
fn byte_transfers_zero_extend_and_preserve_neighbors() {
let mut memory = program(&[0xe5c0_1001, 0xe5d0_2001]); // STRB r1,[r0,#1]; LDRB r2,[r0,#1]
memory.write32(RAM, 0xffff_ffff).unwrap();
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = RAM;
cpu.registers[1] = 0x1234_5680;
cpu.step(&mut memory).unwrap();
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[2], 0x80);
assert_eq!(memory.read32(RAM).unwrap(), 0xffff_80ff);
}
#[test]
fn word_loads_rotate_and_stores_align_down() {
for offset in 0..4 {
let mut memory = program(&[0xe590_1000, 0xe580_2000]);
memory.write32(RAM, 0x1234_5678).unwrap();
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = RAM + offset;
cpu.registers[2] = 0xaabb_ccdd;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[1], 0x1234_5678_u32.rotate_right(offset * 8));
cpu.step(&mut memory).unwrap();
assert_eq!(memory.read32(RAM).unwrap(), 0xaabb_ccdd);
}
}
#[test]
fn register_offset_load_uses_shifter_without_changing_flags() {
let mut memory = program(&[0xe790_1102]); // LDR r1,[r0,r2,LSL #2]
memory.write32(RAM + 12, 42).unwrap();
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = RAM;
cpu.registers[2] = 3;
cpu.flags.carry = true;
let flags = cpu.flags;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[1], 42);
assert_eq!(cpu.flags, flags);
}
#[test]
fn pc_relative_load_and_pc_store_use_different_pipeline_offsets() {
let mut memory = program(&[0xe59f_1000, 0xe580_f000, 42]);
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = RAM;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[1], 42);
cpu.step(&mut memory).unwrap();
assert_eq!(memory.read32(RAM).unwrap(), ROM_START + 16);
}
#[test]
fn ldr_pc_aligns_target_without_thumb_exchange() {
let mut memory = program(&[0xe590_f000]);
memory.write32(RAM, ROM_START + 15).unwrap();
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = RAM;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.pc(), ROM_START + 12);
}
#[test]
fn store_base_with_writeback_stores_original_base_value() {
let mut memory = program(&[0xe480_0004]); // STR r0,[r0],#4
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = RAM;
cpu.step(&mut memory).unwrap();
assert_eq!(memory.read32(RAM).unwrap(), RAM);
assert_eq!(cpu.registers[0], RAM + 4);
}
#[test]
fn halfword_and_signed_loads_handle_arm7_odd_addresses() {
for (instruction, offset, expected) in [
(0xe1d0_10b0, 0, 0x80fe), // LDRH
(0xe1d0_10b0, 1, 0xfe00_0080), // odd LDRH rotates in 32 bits
(0xe1d0_10d0, 0, 0xffff_fffe), // LDRSB
(0xe1d0_10f0, 0, 0xffff_80fe), // LDRSH
(0xe1d0_10f0, 1, 0xffff_ff80), // odd LDRSH becomes LDRSB
] {
let mut memory = program(&[instruction]);
memory.write16(RAM, 0x80fe).unwrap();
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = RAM + offset;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[1], expected);
}
}
#[test]
fn halfword_store_aligns_down_and_updates_the_unaligned_base() {
let mut memory = program(&[0xe0c0_10b2]); // STRH r1,[r0],#2
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = RAM + 1;
cpu.registers[1] = 0x1234_abcd;
cpu.step(&mut memory).unwrap();
assert_eq!(memory.read16(RAM).unwrap(), 0xabcd);
assert_eq!(cpu.registers[0], RAM + 3);
}
#[test]
fn halfword_register_offsets_and_split_immediate_are_decoded() {
for (instruction, offset) in [(0xe190_10b2, 6), (0xe1d0_12b4, 0x24)] {
let mut memory = program(&[instruction]);
memory.write16(RAM + offset, 0x1234).unwrap();
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = RAM;
cpu.registers[2] = 6;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[1], 0x1234);
}
}
#[test]
fn multiply_and_accumulate_wrap_and_set_only_defined_flags() {
for (instruction, expected) in [(0xe012_0190, u32::MAX - 1), (0xe032_3190, 3)] {
let mut memory = program(&[instruction]); // MULS/MLAS r2,r0,r1[,r3]
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = u32::MAX;
cpu.registers[1] = 2;
cpu.registers[3] = 5;
cpu.flags.carry = true;
cpu.flags.overflow = true;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[2], expected);
assert_eq!(cpu.flags.negative, expected >> 31 != 0);
assert_eq!(cpu.flags.zero, expected == 0);
assert!(cpu.flags.carry); // Deterministic policy for unspecified output.
assert!(cpu.flags.overflow);
}
}
#[test]
fn long_multiply_supports_signed_unsigned_and_accumulation() {
for (instruction, expected) in [
(0xe093_2190, 0x0000_0001_ffff_fffe_u64), // UMULLS r2,r3,r0,r1
(0xe0b3_2190, 0x0000_0002_0000_0003), // UMLALS (+5)
(0xe0d3_2190, 0xffff_ffff_ffff_fffe), // SMULLS (-1 * 2)
(0xe0f3_2190, 3), // SMLALS (-2 + 5)
] {
let mut memory = program(&[instruction]);
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = u32::MAX;
cpu.registers[1] = 2;
cpu.registers[2] = 5;
cpu.step(&mut memory).unwrap();
assert_eq!(
(u64::from(cpu.registers[3]) << 32) | u64::from(cpu.registers[2]),
expected
);
assert_eq!(cpu.flags.negative, expected >> 63 != 0);
assert_eq!(cpu.flags.zero, expected == 0);
}
}
#[test]
fn long_multiply_zero_flag_uses_both_halves() {
let mut memory = program(&[0xe093_2190]);
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = 0x8000_0000;
cpu.registers[1] = 2;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[2], 0);
assert_eq!(cpu.registers[3], 1);
assert!(!cpu.flags.zero);
}
#[test]
fn invalid_or_unimplemented_encodings_do_not_change_state_or_memory() {
for instruction in [
0xe1a0_0f11, // Rs=pc (register shift)
0xe3b0_f001, // status restore via MOVS pc
0xe490_0004, // LDR r0,[r0],#4 (base/destination overlap)
0xe5bf_1000, // LDR with PC writeback
0xe4b0_1004, // LDRT user-mode transfer
0xe5d0_f000, // LDRB pc
0xe790_1012, // illegal register-specified memory shift
0xe790_100f, // memory offset register PC
0xe1c0_10d0, // ARMv5 doubleword load, not an ARM7 signed store
0xe000_0190, // MUL Rd=Rm
0xe082_2190, // UMULL RdHi=RdLo
0xe8f0_0001, // LDM user-bank transfer with writeback
0xe100_f090, // SWP with PC destination
0xe12f_ff30, // BLX, ARMv5 only
] {
let mut memory = program(&[instruction]);
memory.write32(RAM, 0xdead_beef).unwrap();
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = RAM;
cpu.registers[1] = 123;
let before = cpu.registers;
let flags = cpu.flags;
assert!(
matches!(
cpu.step(&mut memory),
Err(CpuError::UnsupportedInstruction { .. })
),
"{instruction:08x}"
);
assert_eq!(cpu.registers, before);
assert_eq!(cpu.flags, flags);
assert_eq!(memory.read32(RAM).unwrap(), 0xdead_beef);
}
}
#[test]
fn failed_memory_access_does_not_commit_writeback_or_destination() {
for instruction in [0xe490_1004, 0xe480_1004, 0xe0d0_10b2, 0xe0c0_10b2] {
let mut memory = program(&[instruction]);
let mut cpu = Cpu::new(ROM_START);
cpu.registers[1] = 123;
let before = cpu.registers;
assert!(matches!(cpu.step(&mut memory), Err(CpuError::Memory(_))));
assert_eq!(cpu.registers, before);
}
}
#[test]
fn conditional_store_is_skipped_without_touching_memory() {
let mut memory = program(&[0x0580_1000]); // STREQ with Z clear
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = RAM;
cpu.registers[1] = 123;
cpu.step(&mut memory).unwrap();
assert_eq!(memory.read32(RAM).unwrap(), 0);
}

183
src/cpu/status.rs Normal file
View file

@ -0,0 +1,183 @@
//! ARM7 processor modes, register banks, and status-register transfers.
use super::{Cpu, CpuError, Flags, InstructionSet};
/// ARMv4T processor mode. User and System share a register bank.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum Mode {
User = 0x10,
Fiq = 0x11,
Irq = 0x12,
Supervisor = 0x13,
Abort = 0x17,
Undefined = 0x1b,
System = 0x1f,
}
impl Mode {
fn decode(status: u32) -> Option<Self> {
Some(match status & 31 {
0x10 => Self::User,
0x11 => Self::Fiq,
0x12 => Self::Irq,
0x13 => Self::Supervisor,
0x17 => Self::Abort,
0x1b => Self::Undefined,
0x1f => Self::System,
_ => return None,
})
}
fn bank(self) -> usize {
match self {
Self::User | Self::System => 0,
Self::Fiq => 1,
Self::Irq => 2,
Self::Supervisor => 3,
Self::Abort => 4,
Self::Undefined => 5,
}
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub(super) struct Banks {
high: [[u32; 5]; 2], // r8-r12: shared and FIQ
sp_lr: [[u32; 2]; 6],
spsr: [u32; 5],
}
impl Cpu {
pub fn mode(&self) -> Mode {
self.mode
}
/// Current program status register. Reserved bits read as zero.
pub fn cpsr(&self) -> u32 {
(u32::from(self.flags.negative) << 31)
| (u32::from(self.flags.zero) << 30)
| (u32::from(self.flags.carry) << 29)
| (u32::from(self.flags.overflow) << 28)
| (u32::from(self.irq_disabled) << 7)
| (u32::from(self.fiq_disabled) << 6)
| (u32::from(self.instruction_set == InstructionSet::Thumb) << 5)
| self.mode as u32
}
/// Saved program status register. User and System have no SPSR.
pub fn spsr(&self) -> Option<u32> {
self.mode
.bank()
.checked_sub(1)
.map(|bank| self.banks.spsr[bank])
}
pub(super) fn set_spsr(&mut self, value: u32) {
self.banks.spsr[self.mode.bank() - 1] = value;
}
pub(super) fn switch_mode(&mut self, mode: Mode) {
// Save the active bank before loading the new one. User/System share it.
self.banks.high[usize::from(self.mode == Mode::Fiq)]
.copy_from_slice(&self.registers[8..13]);
self.banks.sp_lr[self.mode.bank()].copy_from_slice(&self.registers[13..15]);
self.registers[8..13].copy_from_slice(&self.banks.high[usize::from(mode == Mode::Fiq)]);
self.registers[13..15].copy_from_slice(&self.banks.sp_lr[mode.bank()]);
self.mode = mode;
}
// Callers validate the mode before committing any CPU or memory changes.
pub(super) fn apply_status(&mut self, value: u32, mode: Mode) {
self.switch_mode(mode);
self.flags = Flags {
negative: value & (1 << 31) != 0,
zero: value & (1 << 30) != 0,
carry: value & (1 << 29) != 0,
overflow: value & (1 << 28) != 0,
};
self.irq_disabled = value & (1 << 7) != 0;
self.fiq_disabled = value & (1 << 6) != 0;
self.instruction_set = if value & (1 << 5) == 0 {
InstructionSet::Arm
} else {
InstructionSet::Thumb
};
}
pub(super) fn return_status(&self, instruction: u32) -> Result<(u32, Mode), CpuError> {
let value = self.spsr().ok_or_else(|| self.unsupported(instruction))?;
let mode = Mode::decode(value).ok_or_else(|| self.unsupported(instruction))?;
Ok((value, mode))
}
pub(super) fn user_register(&self, register: usize) -> u32 {
match register {
8..=12 if self.mode == Mode::Fiq => self.banks.high[0][register - 8],
13..=14 if self.mode.bank() != 0 => self.banks.sp_lr[0][register - 13],
_ => self.operand_register(register, 12),
}
}
pub(super) fn set_user_register(&mut self, register: usize, value: u32) {
match register {
8..=12 if self.mode == Mode::Fiq => self.banks.high[0][register - 8] = value,
13..=14 if self.mode.bank() != 0 => self.banks.sp_lr[0][register - 13] = value,
_ => self.registers[register] = value,
}
}
pub(super) fn read_status(&mut self, instruction: u32) -> Result<(), CpuError> {
let destination = ((instruction >> 12) & 15) as usize;
if destination == 15 {
return Err(self.unsupported(instruction));
}
let value = if instruction & (1 << 22) == 0 {
self.cpsr()
} else {
self.spsr().ok_or_else(|| self.unsupported(instruction))?
};
self.registers[destination] = value;
self.registers[15] = self.pc().wrapping_add(4);
Ok(())
}
pub(super) fn write_status(&mut self, instruction: u32) -> Result<(), CpuError> {
let value = if instruction & (1 << 25) != 0 {
(instruction & 255).rotate_right(((instruction >> 8) & 15) * 2)
} else {
let source = (instruction & 15) as usize;
if source == 15 {
return Err(self.unsupported(instruction));
}
self.registers[source]
};
let saved = instruction & (1 << 22) != 0;
let previous = if saved {
self.spsr().ok_or_else(|| self.unsupported(instruction))?
} else {
self.cpsr()
};
let mut mask = 0;
if instruction & (1 << 19) != 0 {
mask |= 0xf000_0000;
}
if instruction & (1 << 16) != 0 && (saved || self.mode != Mode::User) {
mask |= 0xff;
}
let next = (previous & !mask) | (value & mask);
if saved {
// Invalid mode bits can be stored, but cannot be used for a return.
self.set_spsr(next);
} else {
let mode = Mode::decode(next).ok_or_else(|| self.unsupported(instruction))?;
// ARM7 forbids changing T through MSR CPSR. Diagnose that form.
if (previous ^ next) & (1 << 5) != 0 {
return Err(self.unsupported(instruction));
}
self.apply_status(next, mode);
}
self.registers[15] = self.pc().wrapping_add(4);
Ok(())
}
}

195
src/cpu/status_tests.rs Normal file
View file

@ -0,0 +1,195 @@
use super::*;
use crate::memory::ROM_START;
const MODES: [Mode; 7] = [
Mode::User,
Mode::Fiq,
Mode::Irq,
Mode::Supervisor,
Mode::Abort,
Mode::Undefined,
Mode::System,
];
fn program(instruction: u32) -> Memory {
Memory::new(instruction.to_le_bytes().to_vec()).unwrap()
}
#[test]
fn direct_start_and_reset_have_explicit_status() {
let cpu = Cpu::new(ROM_START);
assert_eq!(cpu.mode(), Mode::System);
assert_eq!(cpu.cpsr(), 0x1f);
assert_eq!(cpu.spsr(), None);
let cpu = Cpu::at_reset();
assert_eq!(cpu.pc(), 0);
assert_eq!(cpu.cpsr(), 0xd3);
assert_eq!(cpu.spsr(), Some(0));
}
#[test]
fn every_mode_has_the_correct_shared_and_private_registers() {
let mut cpu = Cpu::new(ROM_START);
for register in 0..15 {
cpu.registers[register] = register as u32 + 100;
}
// Initialize all five exception banks independently.
for (index, mode) in MODES[1..6].iter().copied().enumerate() {
cpu.switch_mode(mode);
assert_eq!(
cpu.registers[0..8],
[100, 101, 102, 103, 104, 105, 106, 107]
);
if mode == Mode::Fiq {
assert_eq!(cpu.registers[8..13], [0; 5]);
cpu.registers[8..13].copy_from_slice(&[208, 209, 210, 211, 212]);
} else {
assert_eq!(cpu.registers[8..13], [108, 109, 110, 111, 112]);
}
assert_eq!(cpu.registers[13..15], [0, 0]);
cpu.registers[13] = 300 + index as u32;
cpu.registers[14] = 400 + index as u32;
cpu.set_spsr(500 + index as u32);
}
for (index, mode) in MODES[1..6].iter().copied().enumerate() {
cpu.switch_mode(mode);
assert_eq!(
cpu.registers[13..15],
[300 + index as u32, 400 + index as u32]
);
assert_eq!(cpu.spsr(), Some(500 + index as u32));
}
cpu.switch_mode(Mode::User);
assert_eq!(cpu.registers[8..15], [108, 109, 110, 111, 112, 113, 114]);
cpu.registers[13] = 999;
cpu.switch_mode(Mode::System);
assert_eq!(cpu.registers[13], 999);
assert_eq!(cpu.spsr(), None);
cpu.switch_mode(Mode::Fiq);
assert_eq!(cpu.registers[8..13], [208, 209, 210, 211, 212]);
}
#[test]
fn cpsr_round_trips_all_modes_flags_masks_and_states() {
let mut cpu = Cpu::new(ROM_START);
for mode in MODES {
for flags in 0..16 {
for control in 0..8 {
let value = (flags << 28) | (control << 5) | mode as u32;
cpu.apply_status(value, mode);
assert_eq!(cpu.cpsr(), value);
assert_eq!(cpu.mode(), mode);
}
}
}
}
#[test]
fn mrs_reads_current_and_saved_status() {
let mut cpu = Cpu::new(ROM_START);
cpu.apply_status(0xa000_00d3, Mode::Supervisor);
cpu.set_spsr(0x5000_0030);
let previous = cpu.cpsr();
cpu.step(&mut program(0xe10f_0000)).unwrap(); // MRS r0, CPSR
assert_eq!(cpu.registers[0], previous);
assert_eq!(cpu.cpsr(), previous);
cpu.registers[15] = ROM_START;
cpu.step(&mut program(0xe14f_1000)).unwrap(); // MRS r1, SPSR
assert_eq!(cpu.registers[1], 0x5000_0030);
assert_eq!(cpu.pc(), ROM_START + 4);
}
#[test]
fn msr_register_and_immediate_write_selected_fields_only() {
for fields in 0..16 {
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = 0xffff_ffd3; // reserved bits are ignored
cpu.step(&mut program(0xe120_f000 | (fields << 16)))
.unwrap();
let expected = if fields & 1 != 0 { 0xd3 } else { 0x1f }
| if fields & 8 != 0 { 0xf000_0000 } else { 0 };
assert_eq!(cpu.cpsr(), expected);
}
let mut cpu = Cpu::new(ROM_START);
cpu.step(&mut program(0xe328_f480)).unwrap(); // MSR CPSR_f, #0x80000000
assert_eq!(cpu.cpsr(), 0x8000_001f);
cpu.registers[15] = ROM_START;
cpu.step(&mut program(0xe321_f0d3)).unwrap(); // MSR CPSR_c, #0xd3
assert_eq!(cpu.cpsr(), 0x8000_00d3);
}
#[test]
fn msr_reads_old_banked_operand_before_switching_mode() {
let mut cpu = Cpu::new(ROM_START);
cpu.registers[13] = 0xd3;
cpu.step(&mut program(0xe121_f00d)).unwrap(); // MSR CPSR_c, sp
assert_eq!(cpu.mode(), Mode::Supervisor);
assert_eq!(cpu.registers[13], 0);
cpu.switch_mode(Mode::System);
assert_eq!(cpu.registers[13], 0xd3);
}
#[test]
fn user_msr_changes_flags_but_cannot_change_control() {
let mut cpu = Cpu::new(ROM_START);
cpu.switch_mode(Mode::User);
cpu.registers[0] = 0xf000_00ff; // Includes T, invalid mode, and interrupt masks
cpu.step(&mut program(0xe12f_f000)).unwrap();
assert_eq!(cpu.cpsr(), 0xf000_0010);
}
#[test]
fn msr_spsr_accepts_thumb_and_preserves_unselected_fields() {
for fields in 0..16 {
let mut cpu = Cpu::new(ROM_START);
cpu.switch_mode(Mode::Supervisor);
cpu.set_spsr(0x2000_001f);
cpu.registers[0] = 0x9fff_fff0;
cpu.step(&mut program(0xe160_f000 | (fields << 16)))
.unwrap();
assert_eq!(cpu.cpsr(), 0x13);
let expected = if fields & 1 != 0 { 0xf0 } else { 0x1f }
| if fields & 8 != 0 {
0x9000_0000
} else {
0x2000_0000
};
assert_eq!(cpu.spsr(), Some(expected));
}
let mut cpu = Cpu::at_reset();
cpu.registers[15] = ROM_START;
cpu.step(&mut program(0xe361_f03f)).unwrap(); // MSR SPSR_c, #0x3f
assert_eq!(cpu.spsr(), Some(0x3f));
}
#[test]
fn invalid_status_transfers_preserve_all_banks() {
for instruction in [
0xe10f_f000, // MRS pc, CPSR
0xe14f_0000, // MRS r0, SPSR without an SPSR
0xe161_f000, // MSR SPSR_c, r0 without an SPSR
0xe121_f00f, // MSR CPSR_c, pc
0xe321_f000, // Invalid mode
0xe321_f03f, // Attempt to change T
] {
let mut cpu = Cpu::new(ROM_START);
let before = cpu.clone();
assert!(
cpu.step(&mut program(instruction)).is_err(),
"{instruction:08x}"
);
assert_eq!(cpu, before);
}
}
#[test]
fn conditional_status_transfers_do_not_execute_when_condition_fails() {
let mut cpu = Cpu::new(ROM_START);
for instruction in [0x010f_0000, 0x0121_f000, 0x0161_f000] {
cpu.registers[15] = ROM_START;
let mut expected = cpu.clone();
expected.registers[15] += 4;
cpu.step(&mut program(instruction)).unwrap();
assert_eq!(cpu, expected);
}
}

210
src/cpu/tests.rs Normal file
View file

@ -0,0 +1,210 @@
use super::*;
use crate::memory::ROM_START;
fn program(words: &[u32]) -> Memory {
Memory::new(words.iter().flat_map(|word| word.to_le_bytes()).collect()).unwrap()
}
fn flags_from_bits(bits: u32) -> Flags {
Flags {
negative: bits & 8 != 0,
zero: bits & 4 != 0,
carry: bits & 2 != 0,
overflow: bits & 1 != 0,
}
}
#[test]
fn all_conditions_execute_or_skip_for_every_flag_combination() {
// Each bit marks a passing NZCV combination, indexed as N*8+Z*4+C*2+V.
// Conditions in order: EQ NE CS CC MI PL VS VC HI LS GE LT GT LE AL.
let truth_tables = [
0xf0f0_u16, 0x0f0f, 0xcccc, 0x3333, 0xff00, 0x00ff, 0xaaaa, 0x5555, 0x0c0c, 0xf3f3, 0xaa55,
0x55aa, 0x0a05, 0xf5fa, 0xffff,
];
for (condition, truth_table) in truth_tables.into_iter().enumerate() {
for instruction in [0x03a0_002a, 0x0a00_0000] {
// MOV r0, #42; B +8
let mut memory = program(&[instruction | ((condition as u32) << 28)]);
for bits in 0..16 {
let mut cpu = Cpu::new(ROM_START);
cpu.flags = flags_from_bits(bits);
let before = cpu.flags();
let passes = truth_table & (1 << bits) != 0;
cpu.step(&mut memory).unwrap();
let branch = instruction == 0x0a00_0000;
assert_eq!(cpu.registers()[0], if passes && !branch { 42 } else { 0 });
assert_eq!(cpu.pc(), ROM_START + if passes && branch { 8 } else { 4 });
assert_eq!(cpu.flags(), before);
}
}
}
}
#[test]
fn arithmetic_flags_match_wide_integer_reference() {
// Exercise every rotated-immediate encoding against boundary operands.
let operands = [
0,
1,
127,
255,
0x7fff_fffe,
0x7fff_ffff,
0x8000_0000,
0x8000_0001,
u32::MAX,
];
let address = 0x0200_0000;
let mut memory = Memory::new(vec![]).unwrap();
for opcode in [0x2, 0x4, 0xa] {
for encoding in 0..4096_u32 {
let immediate = (encoding & 0xff).rotate_right(((encoding >> 8) & 15) * 2);
// CMP has no destination; ADD/SUB write r1 from r0.
let destination = if opcode == 0xa { 0 } else { 1 };
let instruction = 0xe210_0000 | (opcode << 21) | (destination << 12) | encoding;
for (offset, byte) in instruction.to_le_bytes().into_iter().enumerate() {
memory.write8(address + offset as u32, byte).unwrap();
}
for operand in operands {
let mut cpu = Cpu::new(address);
cpu.registers[0] = operand;
cpu.registers[1] = 0x1234_5678;
cpu.flags = flags_from_bits(15);
let mut expected_registers = *cpu.registers();
let (unsigned_result, signed_result, carry) = if opcode == 0x4 {
let wide = u64::from(operand) + u64::from(immediate);
(
wide as u32,
i64::from(operand as i32) + i64::from(immediate as i32),
wide > u64::from(u32::MAX),
)
} else {
let wide = i64::from(operand) - i64::from(immediate);
(
wide as u32,
i64::from(operand as i32) - i64::from(immediate as i32),
operand >= immediate,
)
};
if opcode != 0xa {
expected_registers[1] = unsigned_result;
}
expected_registers[15] += 4;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers(), &expected_registers);
assert_eq!(
cpu.flags(),
Flags {
negative: unsigned_result >> 31 != 0,
zero: unsigned_result == 0,
carry,
overflow: i32::try_from(signed_result).is_err(),
},
"opcode={opcode:x} operand={operand:08x} encoding={encoding:03x}"
);
}
}
}
}
#[test]
fn movs_uses_shifter_carry_and_preserves_overflow() {
for (instruction, result, shifted_carry) in [
(0xe3b0_0000, 0, None), // MOVS r0, #0, no rotation
(0xe3b0_00ff, 255, None), // MOVS r0, #255, no rotation
(0xe3b0_0480, 0x8000_0000, Some(true)), // rotated immediate
(0xe3b0_0401, 0x0100_0000, Some(false)),
(0xe3b0_0400, 0, Some(false)), // rotated zero still clears carry
] {
let mut memory = program(&[instruction]);
for bits in 0..16 {
let mut cpu = Cpu::new(ROM_START);
cpu.flags = flags_from_bits(bits);
let before = cpu.flags();
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers()[0], result);
assert_eq!(
cpu.flags(),
Flags {
negative: result >> 31 != 0,
zero: result == 0,
carry: shifted_carry.unwrap_or(before.carry),
overflow: before.overflow,
}
);
}
}
}
#[test]
fn instructions_without_s_preserve_all_flags() {
for instruction in [0xe3a0_0480, 0xe280_0001, 0xe240_0001] {
let mut memory = program(&[instruction]);
for bits in 0..16 {
let mut cpu = Cpu::new(ROM_START);
cpu.flags = flags_from_bits(bits);
let before = cpu.flags();
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.flags(), before);
}
}
}
#[test]
fn failed_condition_skips_flag_updates_and_unsupported_opcodes() {
for instruction in [0x03b0_0000, 0x0f00_0000, 0x0350_0000] {
// EQ is false, so MOVS, SWI, and CMP must all be skipped.
let mut memory = program(&[instruction]);
let mut cpu = Cpu::new(ROM_START);
cpu.flags = flags_from_bits(11); // N=1 Z=0 C=1 V=1
let before = cpu.flags();
let mut registers = *cpu.registers();
registers[15] += 4;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers(), &registers);
assert_eq!(cpu.flags(), before);
}
}
#[test]
fn errors_preserve_nonzero_flags_and_registers() {
for instruction in [0xe3b0_f001, 0xe10f_f000, 0xf3a0_0001, 0xe340_0001] {
let mut memory = program(&[instruction]);
let mut cpu = Cpu::new(ROM_START);
cpu.flags = flags_from_bits(15);
cpu.registers[0] = 123;
let before = *cpu.registers();
let flags = cpu.flags();
assert!(matches!(
cpu.step(&mut memory),
Err(CpuError::UnsupportedInstruction { .. })
));
assert_eq!(cpu.registers(), &before);
assert_eq!(cpu.flags(), flags);
}
}
#[test]
fn compare_controls_a_conditional_move_without_changing_source() {
let mut memory = program(&[
0xe3a0_0005, // MOV r0, #5
0xe350_0005, // CMP r0, #5
0x03a0_102a, // MOVEQ r1, #42
0x13a0_1063, // MOVNE r1, #99, skipped
]);
let mut cpu = Cpu::new(ROM_START);
for _ in 0..4 {
cpu.step(&mut memory).unwrap();
}
assert_eq!(&cpu.registers()[..2], &[5, 42]);
assert_eq!(
cpu.flags(),
Flags {
negative: false,
zero: true,
carry: true,
overflow: false
}
);
}

314
src/cpu/thumb.rs Normal file
View file

@ -0,0 +1,314 @@
//! ARMv4T Thumb instructions, including software interrupts.
use super::{
alu::{self, Shift},
Cpu, CpuError,
};
use crate::memory::Memory;
#[derive(Clone, Copy)]
enum Transfer {
StoreWord,
StoreHalf,
StoreByte,
LoadSignedByte,
LoadWord,
LoadHalf,
LoadByte,
LoadSignedHalf,
}
impl Cpu {
pub(super) fn execute_thumb(
&mut self,
instruction: u16,
memory: &mut Memory,
) -> Result<(), CpuError> {
let instruction = u32::from(instruction);
let next_pc = self.pc().wrapping_add(2);
match instruction {
0x0000..=0x17ff => {
let source = ((instruction >> 3) & 7) as usize;
let destination = (instruction & 7) as usize;
let (value, carry) = alu::shift(
self.registers[source],
Shift::decode(instruction >> 11),
(instruction >> 6) & 31,
self.flags.carry,
false,
);
self.thumb_result(destination, 0xd, 0, value, carry);
}
0x1800..=0x1fff => {
let source = ((instruction >> 3) & 7) as usize;
let destination = (instruction & 7) as usize;
let operand = (instruction >> 6) & 7;
let operand = if instruction & (1 << 10) != 0 {
operand
} else {
self.registers[operand as usize]
};
let opcode = if instruction & (1 << 9) != 0 { 2 } else { 4 };
self.thumb_result(
destination,
opcode,
self.registers[source],
operand,
self.flags.carry,
);
}
0x2000..=0x3fff => {
let destination = ((instruction >> 8) & 7) as usize;
let opcode = [0xd, 0xa, 4, 2][((instruction >> 11) & 3) as usize];
self.thumb_result(
destination,
opcode,
self.registers[destination],
instruction & 255,
self.flags.carry,
);
}
0x4000..=0x43ff => self.thumb_register_alu(instruction),
0x4400..=0x47ff => return self.thumb_high_register(instruction),
0x4800..=0x4fff => {
let address =
(self.pc().wrapping_add(4) & !3).wrapping_add((instruction & 255) * 4);
self.thumb_transfer(
memory,
Transfer::LoadWord,
((instruction >> 8) & 7) as usize,
address,
)?;
}
0x5000..=0x5fff => {
let base = self.registers[((instruction >> 3) & 7) as usize];
let offset = self.registers[((instruction >> 6) & 7) as usize];
let kind = [
Transfer::StoreWord,
Transfer::StoreHalf,
Transfer::StoreByte,
Transfer::LoadSignedByte,
Transfer::LoadWord,
Transfer::LoadHalf,
Transfer::LoadByte,
Transfer::LoadSignedHalf,
][((instruction >> 9) & 7) as usize];
self.thumb_transfer(
memory,
kind,
(instruction & 7) as usize,
base.wrapping_add(offset),
)?;
}
0x6000..=0x7fff => {
let byte = instruction & (1 << 12) != 0;
let load = instruction & (1 << 11) != 0;
let offset = ((instruction >> 6) & 31) * if byte { 1 } else { 4 };
let base = self.registers[((instruction >> 3) & 7) as usize];
let kind = match (load, byte) {
(false, false) => Transfer::StoreWord,
(false, true) => Transfer::StoreByte,
(true, false) => Transfer::LoadWord,
(true, true) => Transfer::LoadByte,
};
self.thumb_transfer(
memory,
kind,
(instruction & 7) as usize,
base.wrapping_add(offset),
)?;
}
0x8000..=0x8fff => {
let kind = if instruction & (1 << 11) != 0 {
Transfer::LoadHalf
} else {
Transfer::StoreHalf
};
let base = self.registers[((instruction >> 3) & 7) as usize];
let address = base.wrapping_add(((instruction >> 6) & 31) * 2);
self.thumb_transfer(memory, kind, (instruction & 7) as usize, address)?;
}
0x9000..=0x9fff => {
let kind = if instruction & (1 << 11) != 0 {
Transfer::LoadWord
} else {
Transfer::StoreWord
};
let address = self.registers[13].wrapping_add((instruction & 255) * 4);
self.thumb_transfer(memory, kind, ((instruction >> 8) & 7) as usize, address)?;
}
0xa000..=0xafff => {
let base = if instruction & (1 << 11) != 0 {
self.registers[13]
} else {
self.pc().wrapping_add(4) & !3
};
self.registers[((instruction >> 8) & 7) as usize] =
base.wrapping_add((instruction & 255) * 4);
}
0xb000..=0xb0ff => {
let offset = (instruction & 127) * 4;
self.registers[13] = if instruction & 128 != 0 {
self.registers[13].wrapping_sub(offset)
} else {
self.registers[13].wrapping_add(offset)
};
}
0xb400..=0xb5ff | 0xbc00..=0xbdff => {
let pop = instruction & (1 << 11) != 0;
let extra = if instruction & (1 << 8) != 0 {
1 << if pop { 15 } else { 14 }
} else {
0
};
// Shared block logic uses Thumb PC alignment and pipeline offsets.
let arm = if pop { 0xe8bd_0000 } else { 0xe92d_0000 };
return self.block_transfer(arm | extra | (instruction & 255), memory);
}
0xc000..=0xcfff => {
let arm = 0xe8a0_0000
| ((instruction & 0x800) << 9)
| (((instruction >> 8) & 7) << 16)
| (instruction & 255);
return self.block_transfer(arm, memory);
}
0xdf00..=0xdfff => {
self.enter_exception(super::Exception::SoftwareInterrupt);
return Ok(());
}
0xd000..=0xddff => {
if self.flags.condition_passed((instruction >> 8) & 15) {
let offset = (instruction as u8 as i8 as i32 * 2) as u32;
self.registers[15] = self.pc().wrapping_add(4).wrapping_add(offset);
return Ok(());
}
}
0xe000..=0xe7ff => {
let offset = (((instruction & 0x7ff) << 21) as i32 >> 20) as u32;
self.registers[15] = self.pc().wrapping_add(4).wrapping_add(offset);
return Ok(());
}
0xf000..=0xf7ff => {
// BL prefix: sign-extend eleven bits, then shift left twelve.
let offset = (((instruction & 0x7ff) << 21) as i32 >> 9) as u32;
self.registers[14] = self.pc().wrapping_add(4).wrapping_add(offset);
}
0xf800..=0xffff => {
// A standalone suffix is legal on ARM7; it uses the existing LR.
let target = self.registers[14].wrapping_add((instruction & 0x7ff) * 2);
self.registers[14] = next_pc | 1;
self.registers[15] = target & !1;
return Ok(());
}
// Includes undefined condition 0xE and ARMv5+ extensions.
_ => return Err(self.unsupported(instruction)),
}
self.registers[15] = next_pc;
Ok(())
}
fn thumb_result(&mut self, destination: usize, opcode: u32, a: u32, b: u32, carry: bool) {
let (result, flags) = alu::execute(opcode, a, b, carry, self.flags);
self.flags = flags;
if !(8..=11).contains(&opcode) {
self.registers[destination] = result;
}
}
fn thumb_register_alu(&mut self, instruction: u32) {
let destination = (instruction & 7) as usize;
let source = ((instruction >> 3) & 7) as usize;
let opcode = (instruction >> 6) & 15;
let a = self.registers[destination];
let b = self.registers[source];
match opcode {
2 | 3 | 4 | 7 => {
let kind = match opcode {
2 => Shift::Lsl,
3 => Shift::Lsr,
4 => Shift::Asr,
_ => Shift::Ror,
};
let (value, carry) = alu::shift(a, kind, b, self.flags.carry, true);
self.thumb_result(destination, 0xd, 0, value, carry);
}
9 => self.thumb_result(destination, 2, 0, b, self.flags.carry), // NEG
13 => {
let result = a.wrapping_mul(b);
self.registers[destination] = result;
self.flags.negative = result >> 31 != 0;
self.flags.zero = result == 0;
// Preserve unspecified multiply carry, matching the ARM core policy.
}
_ => self.thumb_result(destination, opcode, a, b, self.flags.carry),
}
}
fn thumb_high_register(&mut self, instruction: u32) -> Result<(), CpuError> {
let opcode = (instruction >> 8) & 3;
let source = ((instruction >> 3) & 15) as usize;
let destination = ((instruction & 7) | ((instruction >> 4) & 8)) as usize;
if opcode == 3 {
// Bit 7 selects BLX on ARMv5; low destination bits must be zero.
if instruction & 0x87 != 0 {
return Err(self.unsupported(instruction));
}
self.branch_exchange(self.operand_register(source, 4));
return Ok(());
}
// ARMv4T requires at least one high register in this instruction format.
if instruction & 0xc0 == 0 {
return Err(self.unsupported(instruction));
}
let a = self.operand_register(destination, 4);
let b = self.operand_register(source, 4);
let value = match opcode {
0 => a.wrapping_add(b),
1 => {
self.flags = alu::execute(0xa, a, b, self.flags.carry, self.flags).1;
self.registers[15] = self.pc().wrapping_add(2);
return Ok(());
}
_ => b,
};
if destination == 15 {
self.registers[15] = value & !1; // ADD/MOV PC stay in Thumb state.
} else {
self.registers[destination] = value;
self.registers[15] = self.pc().wrapping_add(2);
}
Ok(())
}
fn thumb_transfer(
&mut self,
memory: &mut Memory,
kind: Transfer,
register: usize,
address: u32,
) -> Result<(), CpuError> {
let value = match kind {
Transfer::StoreWord => {
return Ok(memory.write32(address & !3, self.registers[register])?)
}
Transfer::StoreHalf => {
return Ok(memory.write16(address & !1, self.registers[register] as u16)?)
}
Transfer::StoreByte => {
return Ok(memory.write8(address, self.registers[register] as u8)?)
}
Transfer::LoadWord => memory.read32(address & !3)?.rotate_right((address & 3) * 8),
Transfer::LoadHalf => {
u32::from(memory.read16(address & !1)?).rotate_right((address & 1) * 8)
}
Transfer::LoadByte => u32::from(memory.read8(address)?),
Transfer::LoadSignedByte => memory.read8(address)? as i8 as i32 as u32,
Transfer::LoadSignedHalf if address & 1 != 0 => {
memory.read8(address)? as i8 as i32 as u32
}
Transfer::LoadSignedHalf => memory.read16(address)? as i16 as i32 as u32,
};
self.registers[register] = value;
Ok(())
}
}

821
src/cpu/thumb_tests.rs Normal file
View file

@ -0,0 +1,821 @@
use super::*;
use crate::memory::ROM_START;
const RAM: u32 = 0x0200_0000;
fn program(instructions: &[u16]) -> Memory {
Memory::new(
instructions
.iter()
.flat_map(|word| word.to_le_bytes())
.collect(),
)
.unwrap()
}
fn thumb_cpu() -> Cpu {
let mut cpu = Cpu::new(ROM_START);
cpu.instruction_set = InstructionSet::Thumb;
cpu
}
fn flags() -> Flags {
Flags {
negative: true,
zero: true,
carry: true,
overflow: true,
}
}
#[test]
fn fetches_one_halfword_and_advances_two_bytes() {
let mut memory = program(&[0x202a]); // MOV r0,#42, no trailing halfword
let mut cpu = thumb_cpu();
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[0], 42);
assert_eq!(cpu.pc(), ROM_START + 2);
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
}
#[test]
fn immediate_shift_formats_use_arm7_zero_amount_rules() {
for (instruction, expected, carry) in [
(0x0008, 0x8000_0001, false), // LSL r0,r1,#0
(0x0048, 2, true), // LSL #1
(0x0808, 0, true), // LSR #32
(0x0848, 0x4000_0000, true), // LSR #1
(0x1008, u32::MAX, true), // ASR #32
(0x1048, 0xc000_0000, true), // ASR #1
] {
let mut memory = program(&[instruction]);
let mut cpu = thumb_cpu();
cpu.registers[1] = 0x8000_0001;
cpu.flags.overflow = true;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[0], expected);
assert_eq!(
cpu.flags,
Flags {
negative: expected >> 31 != 0,
zero: expected == 0,
carry,
overflow: true
}
);
}
}
#[test]
fn three_register_and_small_immediate_add_subtract_formats() {
for (instruction, expected) in [
(0x1888, 12), // ADD r0,r1,r2
(0x1a88, 8), // SUB r0,r1,r2
(0x1cc8, 13), // ADD r0,r1,#3
(0x1ec8, 7), // SUB r0,r1,#3
] {
let mut memory = program(&[instruction]);
let mut cpu = thumb_cpu();
cpu.registers[1] = 10;
cpu.registers[2] = 2;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[0], expected);
assert!(!cpu.flags.overflow);
assert_eq!(cpu.flags.carry, instruction & 0x200 != 0);
}
}
#[test]
fn add_subtract_and_compare_set_signed_overflow_and_borrow() {
for (instruction, initial, expected, expected_flags) in [
(
0x3001,
0x7fff_ffff,
0x8000_0000,
Flags {
negative: true,
zero: false,
carry: false,
overflow: true,
},
),
(
0x3801,
0x8000_0000,
0x7fff_ffff,
Flags {
negative: false,
zero: false,
carry: true,
overflow: true,
},
),
(
0x3801,
0,
u32::MAX,
Flags {
negative: true,
zero: false,
carry: false,
overflow: false,
},
),
(
0x2801,
1,
1,
Flags {
negative: false,
zero: true,
carry: true,
overflow: false,
},
),
] {
let mut memory = program(&[instruction]);
let mut cpu = thumb_cpu();
cpu.registers[0] = initial;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[0], expected);
assert_eq!(cpu.flags, expected_flags);
}
}
#[test]
fn immediate_mov_preserves_carry_and_overflow() {
let mut memory = program(&[0x2700]); // MOV r7,#0
let mut cpu = thumb_cpu();
cpu.flags = flags();
cpu.step(&mut memory).unwrap();
assert_eq!(
cpu.flags,
Flags {
negative: false,
zero: true,
carry: true,
overflow: true
}
);
}
#[test]
fn all_sixteen_register_alu_opcodes_decode_correctly() {
let results = [
0,
0x36,
0xd0,
0xd,
0xd,
0x37,
0x32,
0xd,
0,
0xffff_fffe,
0x32,
0x36,
0x36,
0x68,
0x34,
0xffff_fffd,
];
for (opcode, result) in results.into_iter().enumerate() {
let mut memory = program(&[0x4008 | ((opcode as u16) << 6)]); // op r0,r1
let mut cpu = thumb_cpu();
cpu.registers[0] = 0x34;
cpu.registers[1] = 2;
cpu.flags = flags();
cpu.step(&mut memory).unwrap();
assert_eq!(
cpu.registers[0],
if [8, 10, 11].contains(&opcode) {
0x34
} else {
result
},
"opcode {opcode}"
);
assert_eq!(cpu.flags.negative, result >> 31 != 0);
assert_eq!(cpu.flags.zero, result == 0);
assert_eq!(cpu.flags.overflow, ![5, 6, 9, 10, 11].contains(&opcode));
assert_eq!(cpu.flags.carry, ![2, 3, 4, 5, 7, 9, 11].contains(&opcode));
}
}
#[test]
fn register_shift_amounts_include_zero_32_and_255_and_use_low_byte() {
for (opcode, amount, expected, carry) in [
(2, 0, 0x8000_0001, true),
(2, 32, 0, true),
(2, 33, 0, false),
(3, 32, 0, true),
(3, 255, 0, false),
(4, 255, u32::MAX, true),
(7, 32, 0x8000_0001, true),
(7, 256, 0x8000_0001, true),
(2, 257, 2, true),
] {
let mut memory = program(&[0x4008 | (opcode << 6)]);
let mut cpu = thumb_cpu();
cpu.registers[0] = 0x8000_0001;
cpu.registers[1] = amount;
cpu.flags.carry = true;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[0], expected);
assert_eq!(cpu.flags.carry, carry);
}
}
#[test]
fn adc_sbc_and_neg_handle_boundary_values() {
for (instruction, a, b, carry, expected, out_carry, overflow) in [
(0x4148, u32::MAX, 0, true, 0, true, false),
(0x4148, 0x7fff_ffff, 0, true, 0x8000_0000, false, true),
(0x4188, 0, 0, false, u32::MAX, false, false),
(0x4188, 0x8000_0000, 0, false, 0x7fff_ffff, true, true),
(0x4248, 123, 0x8000_0000, false, 0x8000_0000, false, true),
] {
let mut memory = program(&[instruction]);
let mut cpu = thumb_cpu();
cpu.registers[0] = a;
cpu.registers[1] = b;
cpu.flags.carry = carry;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[0], expected);
assert_eq!(cpu.flags.carry, out_carry);
assert_eq!(cpu.flags.overflow, overflow);
}
}
#[test]
fn multiply_wraps_and_preserves_the_unspecified_carry_policy() {
let mut memory = program(&[0x4348]); // MUL r0,r1
let mut cpu = thumb_cpu();
cpu.registers[0] = 0x8000_0000;
cpu.registers[1] = 2;
cpu.flags = flags();
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[0], 0);
assert_eq!(
cpu.flags,
Flags {
negative: false,
zero: true,
carry: true,
overflow: true
}
);
}
#[test]
fn high_register_add_and_mov_preserve_flags_while_cmp_updates_them() {
let mut memory = program(&[
0x4680, // MOV r8,r0
0x4448, // ADD r0,r9
0x4540, // CMP r0,r8
]);
let mut cpu = thumb_cpu();
cpu.registers[0] = 10;
cpu.registers[9] = 5;
cpu.flags = flags();
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[8], 10);
assert_eq!(cpu.flags, flags());
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[0], 15);
assert_eq!(cpu.flags, flags());
cpu.step(&mut memory).unwrap();
assert_eq!(
cpu.flags,
Flags {
negative: false,
zero: false,
carry: true,
overflow: false
}
);
}
#[test]
fn high_register_pc_reads_use_pc_plus_four_without_word_alignment() {
for instruction in [0x4678, 0x4478] {
// MOV/ADD r0,pc
let mut memory = program(&[0x46c0, instruction]);
let mut cpu = thumb_cpu();
cpu.registers[15] += 2;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[0], ROM_START + 6);
}
}
#[test]
fn high_register_pc_writes_remain_thumb_and_keep_bit_one() {
for instruction in [0x4687, 0x4487] {
// MOV/ADD pc,r0
let mut memory = program(&[instruction]);
let mut cpu = thumb_cpu();
cpu.registers[0] = if instruction == 0x4687 {
ROM_START + 11
} else {
7
};
cpu.flags = flags();
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.pc(), ROM_START + 10);
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
assert_eq!(cpu.flags, flags());
}
}
#[test]
fn thumb_bx_selects_instruction_set_from_bit_zero() {
for (target, expected, state) in [
(ROM_START + 11, ROM_START + 10, InstructionSet::Thumb),
(ROM_START + 10, ROM_START + 8, InstructionSet::Arm),
] {
let mut memory = program(&[0x4700]); // BX r0
let mut cpu = thumb_cpu();
cpu.registers[0] = target;
cpu.flags = flags();
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.pc(), expected);
assert_eq!(cpu.instruction_set(), state);
assert_eq!(cpu.flags, flags());
}
}
#[test]
fn bx_pc_switches_to_arm_with_word_alignment() {
let mut memory = program(&[0x46c0, 0x4778]); // BX pc at halfword boundary
let mut cpu = thumb_cpu();
cpu.registers[15] += 2;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.pc(), ROM_START + 4);
assert_eq!(cpu.instruction_set(), InstructionSet::Arm);
}
#[test]
fn literal_load_and_adr_align_pc_plus_four() {
for instruction in [0x4801, 0xa001] {
let mut memory = program(&[0x46c0, instruction, 0, 0, 0x5678, 0x1234]);
let mut cpu = thumb_cpu();
cpu.registers[15] += 2;
cpu.flags = flags();
cpu.step(&mut memory).unwrap();
assert_eq!(
cpu.registers[0],
if instruction == 0x4801 {
0x1234_5678
} else {
ROM_START + 8
}
);
assert_eq!(cpu.flags, flags());
}
}
#[test]
fn all_register_offset_memory_formats_decode_and_handle_odd_addresses() {
let loaded = [
0,
0,
0,
0xffff_ff80,
0xfe12_3480,
0xfe00_0080,
0x80,
0xffff_ff80,
];
let stored = [0xaabb_ccdd, 0x1234_ccdd, 0x1234_ddfe];
for opcode in 0..8 {
let mut memory = program(&[0x5081 | (opcode << 9)]); // Rd=r1,Rb=r0,Ro=r2
memory.write32(RAM, 0x1234_80fe).unwrap();
let mut cpu = thumb_cpu();
cpu.registers[0] = RAM;
cpu.registers[1] = 0xaabb_ccdd;
cpu.registers[2] = 1;
cpu.flags = flags();
cpu.step(&mut memory).unwrap();
if opcode < 3 {
assert_eq!(memory.read32(RAM).unwrap(), stored[opcode as usize]);
assert_eq!(cpu.registers[1], 0xaabb_ccdd);
} else {
assert_eq!(cpu.registers[1], loaded[opcode as usize]);
assert_eq!(memory.read32(RAM).unwrap(), 0x1234_80fe);
}
assert_eq!(cpu.flags, flags());
}
}
#[test]
fn immediate_memory_offsets_scale_by_transfer_size() {
for (store, load, address, expected) in [
(0x6041, 0x6842, RAM + 4, 0x1234_abcd),
(0x7041, 0x7842, RAM + 1, 0xcd),
(0x8041, 0x8842, RAM + 2, 0xabcd),
] {
let mut memory = program(&[store, load]);
let mut cpu = thumb_cpu();
cpu.registers[0] = RAM;
cpu.registers[1] = 0x1234_abcd;
cpu.step(&mut memory).unwrap();
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[2], expected);
assert_eq!(memory.read8(address).unwrap(), 0xcd);
}
}
#[test]
fn sp_relative_memory_and_address_operations_preserve_flags() {
let mut memory = program(&[
0x9001, // STR r0,[sp,#4]
0x9901, // LDR r1,[sp,#4]
0xaa02, // ADD r2,sp,#8
0xb001, // ADD sp,#4
0xb081, // SUB sp,#4
]);
let mut cpu = thumb_cpu();
cpu.registers[13] = RAM;
cpu.registers[0] = 42;
cpu.flags = flags();
for _ in 0..5 {
cpu.step(&mut memory).unwrap();
}
assert_eq!(cpu.registers[1], 42);
assert_eq!(cpu.registers[2], RAM + 8);
assert_eq!(cpu.registers[13], RAM);
assert_eq!(cpu.flags, flags());
}
#[test]
fn memory_offsets_wrap_and_sp_addition_wraps_without_panicking() {
let mut memory = program(&[0x5081, 0xb001]); // STR r1,[r0,r2]; ADD sp,#4
let mut cpu = thumb_cpu();
cpu.registers[0] = 0xffff_fffc;
cpu.registers[1] = 42;
cpu.registers[2] = RAM + 4;
cpu.registers[13] = u32::MAX - 3;
cpu.step(&mut memory).unwrap();
assert_eq!(memory.read32(RAM).unwrap(), 42);
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[13], 0);
}
#[test]
fn signed_halfword_aligned_load_sign_extends_sixteen_bits() {
let mut memory = program(&[0x5e81]);
memory.write16(RAM, 0x8001).unwrap();
let mut cpu = thumb_cpu();
cpu.registers[0] = RAM;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[1], 0xffff_8001);
}
#[test]
fn push_and_pop_restore_registers_and_pc_without_arm_exchange() {
for return_address in [ROM_START + 10, ROM_START + 11] {
let mut memory = program(&[0xb505, 0x2000, 0x2200, 0xbd05]); // PUSH/POP {r0,r2,lr/pc}
let mut cpu = thumb_cpu();
cpu.registers[0] = 11;
cpu.registers[2] = 22;
cpu.registers[13] = RAM + 0x100;
cpu.registers[14] = return_address;
for _ in 0..4 {
cpu.step(&mut memory).unwrap();
}
assert_eq!(cpu.registers[0], 11);
assert_eq!(cpu.registers[2], 22);
assert_eq!(cpu.registers[13], RAM + 0x100);
assert_eq!(cpu.pc(), ROM_START + 10);
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
assert_eq!(memory.read32(RAM + 0xf4).unwrap(), 11);
assert_eq!(memory.read32(RAM + 0xf8).unwrap(), 22);
assert_eq!(memory.read32(RAM + 0xfc).unwrap(), return_address);
}
}
#[test]
fn push_pop_without_lr_or_pc_advance_by_two() {
let mut memory = program(&[0xb401, 0xbc02]); // PUSH {r0}; POP {r1}
let mut cpu = thumb_cpu();
cpu.registers[0] = 123;
cpu.registers[13] = RAM + 16;
cpu.flags = flags();
cpu.step(&mut memory).unwrap();
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[1], 123);
assert_eq!(cpu.registers[13], RAM + 16);
assert_eq!(cpu.pc(), ROM_START + 4);
assert_eq!(cpu.flags, flags());
}
#[test]
fn thumb_multiple_transfers_use_low_register_base_and_ascending_addresses() {
let mut memory = program(&[0xc305, 0xcb30]); // STMIA r3!,{r0,r2}; LDMIA r3!,{r4,r5}
let mut cpu = thumb_cpu();
cpu.registers[0] = 11;
cpu.registers[2] = 22;
cpu.registers[3] = RAM + 1;
cpu.flags = flags();
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[3], RAM + 9);
cpu.registers[3] = RAM + 1;
cpu.step(&mut memory).unwrap();
assert_eq!(&cpu.registers[4..6], &[11, 22]);
assert_eq!(cpu.registers[3], RAM + 9);
assert_eq!(cpu.pc(), ROM_START + 4);
assert_eq!(cpu.flags, flags());
}
#[test]
fn thumb_multiple_transfer_base_overlap_matches_arm7() {
for (instruction, old_base_stored) in [(0xc103, false), (0xc106, true)] {
let mut memory = program(&[instruction]);
let mut cpu = thumb_cpu();
cpu.registers[1] = RAM;
cpu.step(&mut memory).unwrap();
assert_eq!(
memory
.read32(if old_base_stored { RAM } else { RAM + 4 })
.unwrap(),
if old_base_stored { RAM } else { RAM + 8 }
);
}
let mut memory = program(&[0xc903]); // LDMIA r1!,{r0,r1}
memory.write32(RAM, 11).unwrap();
memory.write32(RAM + 4, 22).unwrap();
let mut cpu = thumb_cpu();
cpu.registers[1] = RAM;
cpu.step(&mut memory).unwrap();
assert_eq!(&cpu.registers[..2], &[11, 22]);
}
#[test]
fn empty_thumb_multiple_lists_transfer_pc_with_sixty_four_byte_writeback() {
for instruction in [0xc000, 0xc800] {
let mut memory = program(&[instruction]);
memory.write32(RAM, ROM_START + 11).unwrap();
let mut cpu = thumb_cpu();
cpu.registers[0] = RAM;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[0], RAM + 64);
if instruction == 0xc000 {
assert_eq!(memory.read32(RAM).unwrap(), ROM_START + 4);
assert_eq!(cpu.pc(), ROM_START + 2);
} else {
assert_eq!(cpu.pc(), ROM_START + 10);
}
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
}
}
#[test]
fn empty_push_pop_follow_arm7_block_transfer_rules() {
let mut memory = program(&[0xb400, 0xbc00]);
let mut cpu = thumb_cpu();
cpu.registers[13] = RAM + 0x100;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[13], RAM + 0xc0);
assert_eq!(memory.read32(RAM + 0xc0).unwrap(), ROM_START + 4);
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[13], RAM + 0x100);
assert_eq!(cpu.pc(), ROM_START + 4);
}
#[test]
fn conditional_branches_cover_every_condition_and_flag_combination() {
let tables = [
0xf0f0_u16, 0x0f0f, 0xcccc, 0x3333, 0xff00, 0x00ff, 0xaaaa, 0x5555, 0x0c0c, 0xf3f3, 0xaa55,
0x55aa, 0x0a05, 0xf5fa,
];
for (condition, table) in tables.into_iter().enumerate() {
let mut memory = program(&[0xd001 | ((condition as u16) << 8)]);
for bits in 0..16 {
let mut cpu = thumb_cpu();
cpu.flags = Flags {
negative: bits & 8 != 0,
zero: bits & 4 != 0,
carry: bits & 2 != 0,
overflow: bits & 1 != 0,
};
let before = cpu.flags;
cpu.step(&mut memory).unwrap();
assert_eq!(
cpu.pc(),
ROM_START + if table & (1 << bits) != 0 { 6 } else { 2 }
);
assert_eq!(cpu.flags, before);
}
}
}
#[test]
fn branch_offsets_are_signed_and_relative_to_pc_plus_four() {
for (instruction, target) in [
(0xe3ff, ROM_START + 2050),
(0xe400, ROM_START - 2044),
(0xe7fe, ROM_START),
(0xd080, ROM_START - 252),
(0xd07f, ROM_START + 258),
] {
let mut memory = program(&[instruction]);
let mut cpu = thumb_cpu();
cpu.flags.zero = true;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.pc(), target);
}
}
#[test]
fn bl_prefix_and_suffix_execute_as_separate_halfwords() {
let mut memory = program(&[0xf000, 0xf802, 0xe7fe, 0x46c0, 0x202a, 0x4770]);
let mut cpu = thumb_cpu();
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[14], ROM_START + 4);
assert_eq!(cpu.pc(), ROM_START + 2);
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.pc(), ROM_START + 8);
assert_eq!(cpu.registers[14], ROM_START + 5);
cpu.step(&mut memory).unwrap();
cpu.step(&mut memory).unwrap(); // BX lr
assert_eq!(cpu.registers[0], 42);
assert_eq!(cpu.pc(), ROM_START + 4);
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
}
#[test]
fn bl_prefix_sign_extends_and_suffix_can_execute_alone() {
for (instruction, expected) in [
(0xf3ff, ROM_START + 4 + 0x3ff000),
(0xf400, ROM_START + 4 - 0x400000),
(0xf7ff, ROM_START + 4 - 0x1000),
] {
let mut memory = program(&[instruction]);
let mut cpu = thumb_cpu();
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[14], expected);
}
let mut memory = program(&[0xf801]);
let mut cpu = thumb_cpu();
cpu.registers[14] = ROM_START + 9;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.pc(), ROM_START + 10);
assert_eq!(cpu.registers[14], ROM_START + 3);
}
#[test]
fn arm_thumb_arm_round_trip_fetches_the_correct_instruction_width() {
let mut bytes: Vec<u8> = [0xe12f_ff10_u32, 0xe3a0_2063]
.into_iter()
.flat_map(u32::to_le_bytes)
.collect();
bytes.extend([0x212a_u16, 0x4718].into_iter().flat_map(u16::to_le_bytes)); // MOV r1,#42; BX r3
let mut memory = Memory::new(bytes).unwrap();
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = ROM_START + 9;
cpu.registers[3] = ROM_START + 4;
for _ in 0..4 {
cpu.step(&mut memory).unwrap();
}
assert_eq!(cpu.instruction_set(), InstructionSet::Arm);
assert_eq!(cpu.registers[1], 42);
assert_eq!(cpu.registers[2], 99);
assert_eq!(cpu.pc(), ROM_START + 8);
}
#[test]
fn unsupported_thumb_instructions_preserve_registers_flags_and_state() {
for instruction in [
0xde00, 0xdeff, 0xbe00, 0xe800, 0x4780, 0x4701, 0x4400, 0x4500, 0x4600, 0xb100, 0xb200,
0xb600, 0xbf00,
] {
let mut memory = program(&[instruction]);
let mut cpu = thumb_cpu();
cpu.flags = flags();
let before = cpu.registers;
assert_eq!(
cpu.step(&mut memory),
Err(CpuError::UnsupportedThumbInstruction {
address: ROM_START,
instruction
})
);
assert_eq!(cpu.registers, before);
assert_eq!(cpu.flags, flags());
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
}
}
#[test]
fn thumb_memory_errors_leave_state_unchanged() {
for instruction in [
0x6801, 0x6001, 0x8801, 0x8001, 0x7801, 0x7001, 0x5e81, 0x9801, 0x9001, 0x4801,
] {
let mut memory = program(&[instruction]);
let mut cpu = thumb_cpu();
cpu.flags = flags();
cpu.registers[1] = 123;
let before = cpu.registers;
assert!(
matches!(cpu.step(&mut memory), Err(CpuError::Memory(_))),
"{instruction:04x}"
);
assert_eq!(cpu.registers, before);
assert_eq!(cpu.flags, flags());
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
}
}
#[test]
fn failed_thumb_store_to_rom_does_not_modify_rom_or_cpu() {
let mut memory = program(&[0x6001]);
let mut cpu = thumb_cpu();
cpu.registers[0] = ROM_START;
let before = cpu.registers;
assert_eq!(
cpu.step(&mut memory),
Err(CpuError::Memory(MemoryError::ReadOnly(ROM_START)))
);
assert_eq!(cpu.registers, before);
assert_eq!(memory.read16(ROM_START).unwrap(), 0x6001);
}
#[test]
fn late_thumb_stack_and_block_errors_do_not_partially_commit() {
for instruction in [0xb403, 0xbc03, 0xc003, 0xc803] {
let mut memory = Memory::new(vec![]).unwrap();
memory.write16(0x0300_0000, instruction).unwrap();
memory.write32(0x07ff_fffc, 0x1234_5678).unwrap();
let mut cpu = thumb_cpu();
cpu.registers[15] = 0x0300_0000;
cpu.registers[0] = 0x07ff_fffc;
cpu.registers[13] = if instruction == 0xb403 {
0x0800_0004
} else {
0x07ff_fffc
};
let before = cpu.registers;
assert!(matches!(cpu.step(&mut memory), Err(CpuError::Memory(_))));
assert_eq!(cpu.registers, before);
assert_eq!(memory.read32(0x07ff_fffc).unwrap(), 0x1234_5678);
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
}
}
#[test]
fn skipped_arm_bx_does_not_switch_to_thumb() {
let mut memory = Memory::new(0x012f_ff10_u32.to_le_bytes().to_vec()).unwrap(); // BXEQ r0
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = ROM_START + 9;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.instruction_set(), InstructionSet::Arm);
assert_eq!(cpu.pc(), ROM_START + 4);
}
#[test]
fn failing_fetch_after_bl_prefix_preserves_the_completed_prefix() {
let mut memory = program(&[0xf000]);
let mut cpu = thumb_cpu();
cpu.step(&mut memory).unwrap();
let before = cpu.registers;
assert!(matches!(cpu.step(&mut memory), Err(CpuError::Memory(_))));
assert_eq!(cpu.registers, before);
assert_eq!(cpu.registers[14], ROM_START + 4);
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
}
#[test]
fn every_thumb_encoding_executes_or_returns_a_diagnostic_without_panicking() {
let mut memory = Memory::new(vec![]).unwrap();
for instruction in 0..=u16::MAX {
memory.write16(RAM, instruction).unwrap();
let mut cpu = thumb_cpu();
for register in 0..15 {
cpu.registers[register] = RAM + 0x100 + register as u32 * 4;
}
cpu.registers[15] = RAM;
cpu.flags = flags();
let before = cpu.clone();
match cpu.step(&mut memory) {
Ok(()) => assert_eq!(cpu.pc() & (cpu.instruction_set.width() - 1), 0),
Err(_) => assert_eq!(cpu, before, "encoding {instruction:04x}"),
}
}
}
#[test]
fn truncated_or_misaligned_thumb_fetches_do_not_advance_pc() {
let mut memory = Memory::new(vec![0x20]).unwrap();
let mut cpu = thumb_cpu();
assert_eq!(
cpu.step(&mut memory),
Err(CpuError::Memory(MemoryError::Unmapped(ROM_START + 1)))
);
assert_eq!(cpu.pc(), ROM_START);
cpu.registers[15] += 1;
assert_eq!(
cpu.step(&mut memory),
Err(CpuError::Memory(MemoryError::Unaligned(ROM_START + 1)))
);
assert_eq!(cpu.pc(), ROM_START + 1);
}

205
src/cpu/timing.rs Normal file
View file

@ -0,0 +1,205 @@
//! Nominal ARM7 instruction-cycle summaries, separate from instruction semantics.
//! Data costs come from actual bus accesses. No speculative pipeline reads occur.
//! PC writes use target-region code costs, following GBATEK's cycle summary.
use super::{Cpu, CpuError, InstructionSet};
use crate::{
memory::Memory,
timing::{bus_cycles, refill_cycles, AccessKind, AccessWidth, StepTiming},
};
#[derive(Default)]
struct Summary {
code_kind: AccessKind,
refill: bool,
internal: u32,
}
impl Summary {
fn branch() -> Self {
Self {
refill: true,
..Self::default()
}
}
fn transfer(load: bool, pc: bool) -> Self {
Self {
code_kind: if load {
AccessKind::Sequential
} else {
AccessKind::NonSequential
},
refill: load && pc,
internal: u32::from(load),
}
}
}
impl InstructionSet {
fn access_width(self) -> AccessWidth {
match self {
Self::Arm => AccessWidth::Word,
Self::Thumb => AccessWidth::Halfword,
}
}
}
impl Cpu {
/// Execute one instruction and return its nominal cycle cost. Like Cpu::step,
/// 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 summary = match self.instruction_set {
InstructionSet::Arm => self.arm_summary(instruction),
InstructionSet::Thumb => self.thumb_summary(instruction),
};
let pc = self.pc();
let width = self.instruction_set.access_width();
// A WAITCNT store affects subsequent instructions, not this code access.
let waitcnt = memory.waitcnt();
memory.begin_data_timing();
let result = self.execute_fetched(instruction, memory);
let data_cycles = memory.end_data_timing();
result?;
let code_cycles = if summary.refill {
refill_cycles(waitcnt, self.pc(), self.instruction_set.access_width())
} else {
bus_cycles(waitcnt, pc, width, summary.code_kind)
};
Ok(StepTiming {
code_cycles,
data_cycles,
internal_cycles: summary.internal,
idle_cycles: 0,
})
}
/// Nominal refill cost after explicit exception entry. No vector bytes are read.
pub(crate) fn exception_timing(&self, memory: &Memory) -> StepTiming {
StepTiming {
code_cycles: refill_cycles(
memory.waitcnt(),
self.pc(),
self.instruction_set.access_width(),
),
..StepTiming::default()
}
}
fn arm_summary(&self, instruction: u32) -> Summary {
if !self.flags.condition_passed(instruction >> 28) {
return Summary::default();
}
if instruction & 0x0f00_0000 == 0x0f00_0000
|| instruction & 0x0fff_fff0 == 0x012f_ff10
|| instruction & 0x0e00_0000 == 0x0a00_0000
{
return Summary::branch();
}
if instruction & 0x0fbf_0fff == 0x010f_0000
|| instruction & 0x0fb0_fff0 == 0x0120_f000
|| instruction & 0x0fb0_f000 == 0x0320_f000
{
return Summary::default(); // MRS/MSR overlap the data-processing space.
}
if instruction & 0x0e00_0000 == 0x0800_0000 {
let list = instruction & 0xffff;
return Summary::transfer(
instruction & (1 << 20) != 0,
list == 0 || list & 0x8000 != 0,
);
}
if instruction & 0x0fb0_0ff0 == 0x0100_0090 {
return Summary {
internal: 1,
..Summary::default()
}; // SWP: two N data accesses
}
if instruction & 0x0f80_00f0 == 0x0080_0090 {
let signed = instruction & (1 << 22) != 0;
let multiplier = self.registers[((instruction >> 8) & 15) as usize];
return Summary {
internal: multiply_cycles(multiplier, signed)
+ 1
+ u32::from(instruction & (1 << 21) != 0),
..Summary::default()
};
}
if instruction & 0x0fc0_00f0 == 0x0000_0090 {
let multiplier = self.registers[((instruction >> 8) & 15) as usize];
return Summary {
internal: multiply_cycles(multiplier, true)
+ u32::from(instruction & (1 << 21) != 0),
..Summary::default()
};
}
if instruction & 0x0e00_0090 == 0x0000_0090 {
return Summary::transfer(instruction & (1 << 20) != 0, false);
}
if instruction & 0x0c00_0000 == 0x0400_0000 {
return Summary::transfer(instruction & (1 << 20) != 0, (instruction >> 12) & 15 == 15);
}
let test = (8..=11).contains(&((instruction >> 21) & 15));
Summary {
refill: !test && (instruction >> 12) & 15 == 15,
internal: u32::from(instruction & (1 << 25) == 0 && instruction & (1 << 4) != 0),
..Summary::default()
}
}
fn thumb_summary(&self, instruction: u32) -> Summary {
match instruction {
0x4000..=0x43ff => {
let opcode = (instruction >> 6) & 15;
let internal = match opcode {
2 | 3 | 4 | 7 => 1,
// Thumb MUL maps the incoming destination to ARM's Rs.
13 => multiply_cycles(self.registers[(instruction & 7) as usize], true),
_ => 0,
};
Summary {
internal,
..Summary::default()
}
}
0x4400..=0x47ff => {
let opcode = (instruction >> 8) & 3;
let destination = (instruction & 7) | ((instruction >> 4) & 8);
if opcode == 3 || (opcode != 1 && destination == 15) {
Summary::branch()
} else {
Summary::default()
}
}
0x4800..=0x4fff => Summary::transfer(true, false),
0x5000..=0x5fff => Summary::transfer((instruction >> 9) & 7 >= 3, false),
0x6000..=0x9fff => Summary::transfer(instruction & (1 << 11) != 0, false),
0xb400..=0xb5ff | 0xbc00..=0xbdff => Summary::transfer(
instruction & (1 << 11) != 0,
instruction & 0x100 != 0 || instruction & 0xff == 0,
),
0xc000..=0xcfff => {
Summary::transfer(instruction & (1 << 11) != 0, instruction & 255 == 0)
}
0xd000..=0xddff if self.flags.condition_passed((instruction >> 8) & 15) => {
Summary::branch()
}
0xdf00..=0xdfff | 0xe000..=0xe7ff | 0xf800..=0xffff => Summary::branch(),
// Thumb BL prefix is 1S in the old region; its suffix is 2S+1N at the target.
_ => Summary::default(),
}
}
}
fn multiply_cycles(value: u32, signed: bool) -> u32 {
for bytes in 1..=3 {
let mask = u32::MAX << (bytes * 8);
let high = value & mask;
if high == 0 || signed && high == mask {
return bytes;
}
}
4
}

383
src/cpu/timing_tests.rs Normal file
View file

@ -0,0 +1,383 @@
use super::*;
use crate::{io::WAITCNT, memory::ROM_START, timing::StepTiming};
const CODE: u32 = 0x0300_0000;
const DATA: u32 = 0x0200_0100;
fn arm(instruction: u32) -> (Cpu, Memory) {
let mut memory = Memory::new(vec![0; 0x40000]).unwrap();
memory.write32(CODE, instruction).unwrap();
(Cpu::new(CODE), memory)
}
fn thumb(instruction: u16) -> (Cpu, Memory) {
let (mut cpu, mut memory) = arm(0);
cpu.instruction_set = InstructionSet::Thumb;
memory.write16(CODE, instruction).unwrap();
(cpu, memory)
}
fn expected(code: u32, data: u32, internal: u32) -> StepTiming {
StepTiming {
code_cycles: code,
data_cycles: data,
internal_cycles: internal,
idle_cycles: 0,
}
}
#[test]
fn arm_alu_immediate_shifts_register_shifts_and_status_transfers() {
for opcode in 0..16 {
for operand in [0x0200_0001, 0x0000_0082, 0x0000_0312] {
// r0 destination, r1 first operand; S set makes TST/TEQ/CMP/CMN valid.
let instruction = 0xe011_0000 | (opcode << 21) | operand;
let (mut cpu, mut memory) = arm(instruction);
cpu.registers[1] = 123;
cpu.registers[2] = 12;
cpu.registers[3] = 0; // Register shift still costs I when amount is zero.
let cycles = cpu.step_timed(&mut memory).unwrap();
assert_eq!(cycles, expected(1, 0, u32::from(operand == 0x312)));
assert_eq!(memory.cycles(), 0); // Timed CPU API returns costs; it does not run devices.
}
}
for instruction in [0xe10f_0000, 0xe328_f480, 0xe128_f000, 0xe321_f013] {
let (mut cpu, mut memory) = arm(instruction);
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(1, 0, 0));
}
}
#[test]
fn conditional_skips_do_not_charge_memory_multiply_or_branch_work() {
for instruction in [
0x0590_1000,
0x0580_1000,
0x0a00_0000,
0x0f00_0000,
0x0000_0291,
] {
let (mut cpu, mut memory) = arm(instruction);
cpu.registers[2] = 0x1234_5678;
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(1, 0, 0));
assert_eq!(cpu.pc(), CODE + 4);
}
}
#[test]
fn branch_and_pc_write_refill_even_when_target_equals_fallthrough() {
for instruction in [0xeaff_ffff, 0xe1a0_f000, 0xe1a0_f110] {
let (mut cpu, mut memory) = arm(instruction);
cpu.registers[0] = CODE + 4;
let timing = cpu.step_timed(&mut memory).unwrap();
assert_eq!(
timing,
expected(3, 0, u32::from(instruction == 0xe1a0_f110))
);
assert_eq!(cpu.pc(), CODE + 4);
}
}
#[test]
fn cross_region_bx_uses_destination_width_and_wait_states() {
let (mut cpu, mut memory) = arm(0xe12f_ff10);
cpu.registers[0] = 0x0a00_0101; // Thumb in WS1: 5N + 5S + 5S
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(15, 0, 0));
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
let (mut cpu, mut memory) = thumb(0x4700);
cpu.registers[0] = ROM_START + 0x100; // ARM in WS0: 8N + 6S + 6S
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(20, 0, 0));
}
#[test]
fn arm_single_load_store_widths_charge_one_actual_bus_access() {
for (instruction, data_cycles, internal) in [
(0xe590_1000, 6, 1), // LDR
(0xe580_1000, 6, 0), // STR
(0xe5d0_1000, 3, 1), // LDRB
(0xe5c0_1000, 3, 0), // STRB
(0xe1d0_10b0, 3, 1), // LDRH
(0xe1c0_10b0, 3, 0), // STRH
(0xe1d0_10d0, 3, 1), // LDRSB
(0xe1d0_10f0, 3, 1), // LDRSH
] {
for offset in 0..4 {
let (mut cpu, mut memory) = arm(instruction);
cpu.registers[0] = DATA + offset;
assert_eq!(
cpu.step_timed(&mut memory).unwrap(),
expected(1, data_cycles, internal)
);
}
}
}
#[test]
fn actual_pre_post_and_shifted_addresses_determine_load_costs() {
for (instruction, base, offset) in [
(0xe790_1102, 0x0100_0100, 0x0080_0000), // LDR r1,[r0,r2,LSL #2] -> IWRAM
(0xe490_1004, 0x0300_0100, 0), // post-index uses old base
(0xe510_1100, 0x0300_0200, 0), // negative immediate -> IWRAM
] {
let (mut cpu, mut memory) = arm(instruction);
cpu.registers[0] = base;
cpu.registers[2] = offset;
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(1, 1, 1));
}
}
#[test]
fn load_pc_adds_refill_and_does_not_count_fetch_as_data() {
let (mut cpu, mut memory) = arm(0xe590_f000);
cpu.registers[0] = DATA;
memory.write32(DATA, ROM_START + 0x100).unwrap();
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(20, 6, 1));
}
#[test]
fn block_transfers_count_words_and_apply_refill_only_for_load_pc() {
for list in [0, 1, 0x000f, 0x1fff, 0xffff] {
for load in [false, true] {
let (mut cpu, mut memory) = arm(0xe880_0000 | (u32::from(load) << 20) | list);
cpu.registers[0] = CODE + 0x100;
let count = if list == 0 { 1 } else { list.count_ones() };
let pc = load && (list == 0 || list & 0x8000 != 0);
assert_eq!(
cpu.step_timed(&mut memory).unwrap(),
expected(if pc { 3 } else { 1 }, count, u32::from(load))
);
}
}
}
#[test]
fn rom_block_reads_use_n_then_s_and_restart_at_128_kib_boundary() {
let (mut cpu, mut memory) = arm(0xe890_001e); // LDMIA r0, {r1-r4}
cpu.registers[0] = ROM_START + 0x1fff8;
// Word accesses: N+S=8, S+S=6, forced N+S=8, S+S=6.
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(1, 28, 1));
let (mut cpu, mut memory) = arm(0xe590_1000);
cpu.registers[0] = ROM_START + 0x100;
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(1, 8, 1));
}
#[test]
fn swap_uses_two_nonsequential_accesses_and_one_internal_cycle() {
for (instruction, data_cycles) in [(0xe100_1092, 12), (0xe140_1092, 6)] {
let (mut cpu, mut memory) = arm(instruction);
cpu.registers[0] = DATA;
cpu.registers[2] = 123;
assert_eq!(
cpu.step_timed(&mut memory).unwrap(),
expected(1, data_cycles, 1)
);
}
}
#[test]
fn multiply_early_termination_uses_rs_and_signedness_before_execution() {
for (value, signed_m, unsigned_m) in [
(0, 1, 1),
(255, 1, 1),
(256, 2, 2),
(65535, 2, 2),
(65536, 3, 3),
(0x00ff_ffff, 3, 3),
(0x0100_0000, 4, 4),
(0xffff_ffff, 1, 4),
(0xffff_ff00, 1, 4),
(0xffff_0000, 2, 4),
(0xff00_0000, 3, 4),
(0x8000_0000, 4, 4),
] {
for (instruction, internal) in [
(0xe000_0291, signed_m), // MUL r0,r1,r2
(0xe020_3291, signed_m + 1), // MLA r0,r1,r2,r3
(0xe081_0392, unsigned_m + 1), // UMULL r0,r1,r2,r3
(0xe0a1_0392, unsigned_m + 2), // UMLAL
(0xe0c1_0392, signed_m + 1), // SMULL
(0xe0e1_0392, signed_m + 2), // SMLAL
] {
let (mut cpu, mut memory) = arm(instruction);
cpu.registers[1] = 0x1234_5678;
cpu.registers[2] = value;
cpu.registers[3] = value;
assert_eq!(
cpu.step_timed(&mut memory).unwrap(),
expected(1, 0, internal),
"{instruction:08x}, {value:08x}"
);
}
}
}
#[test]
fn thumb_alu_shifts_and_multiply_use_correct_internal_cycles() {
for opcode in 0..16 {
for value in [7, 0x1234_5678, 0xffff_ffff] {
let (mut cpu, mut memory) = thumb(0x4008 | opcode << 6);
cpu.registers[0] = value;
cpu.registers[1] = 0; // Multiplication timing must not use this source.
let internal = match opcode {
2 | 3 | 4 | 7 => 1,
13 if value == 0x1234_5678 => 4,
13 => 1,
_ => 0,
};
assert_eq!(
cpu.step_timed(&mut memory).unwrap(),
expected(1, 0, internal)
);
}
}
}
#[test]
fn thumb_transfers_literals_sp_and_register_offsets_use_actual_bus_widths() {
for (instruction, data_cycles, internal) in [
(0x4800, 1, 1), // literal LDR from IWRAM
(0x5081, 6, 0),
(0x5281, 3, 0),
(0x5481, 3, 0),
(0x5681, 3, 1),
(0x5881, 6, 1),
(0x5a81, 3, 1),
(0x5c81, 3, 1),
(0x5e81, 3, 1),
(0x6001, 6, 0),
(0x6801, 6, 1),
(0x7001, 3, 0),
(0x7801, 3, 1),
(0x8001, 3, 0),
(0x8801, 3, 1),
(0x9000, 6, 0),
(0x9800, 6, 1),
] {
let (mut cpu, mut memory) = thumb(instruction);
cpu.registers[0] = DATA;
cpu.registers[13] = DATA;
assert_eq!(
cpu.step_timed(&mut memory).unwrap(),
expected(1, data_cycles, internal),
"{instruction:04x}"
);
}
}
#[test]
fn thumb_branches_bl_halves_and_high_pc_writes_refill_only_when_taken() {
for (instruction, code) in [
(0xd000, 1),
(0xd100, 3),
(0xe000, 3),
(0x4687, 3),
(0x4487, 3),
(0x4587, 1),
] {
let (mut cpu, mut memory) = thumb(instruction);
cpu.registers[0] = CODE + 4;
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(code, 0, 0));
}
let (mut cpu, mut memory) = thumb(0xf000);
memory.write16(CODE + 2, 0xf800).unwrap();
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(1, 0, 0));
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(3, 0, 0));
}
#[test]
fn thumb_push_pop_and_multiple_transfers_charge_each_word() {
for (instruction, data, internal, code) in [
(0xb403, 12, 0, 1),
(0xb503, 18, 0, 1),
(0xbc03, 12, 1, 1),
(0xbd03, 18, 1, 3),
(0xc003, 12, 0, 1),
(0xc803, 12, 1, 1),
(0xc800, 6, 1, 3),
(0xbc00, 6, 1, 3),
] {
let (mut cpu, mut memory) = thumb(instruction);
cpu.registers[0] = DATA;
cpu.registers[13] = DATA;
assert_eq!(
cpu.step_timed(&mut memory).unwrap(),
expected(code, data, internal)
);
}
}
#[test]
fn software_interrupts_and_exception_returns_use_new_code_region_and_state() {
for thumb_state in [false, true] {
let (mut cpu, mut memory) = if thumb_state {
thumb(0xdf00)
} else {
arm(0xef00_0000)
};
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(3, 0, 0));
assert_eq!(cpu.mode(), Mode::Supervisor);
// Return via MOVS pc,lr, executing from IWRAM without a BIOS image.
memory.write32(CODE + 0x100, 0xe1b0_f00e).unwrap();
cpu.registers[15] = CODE + 0x100;
cpu.registers[14] = ROM_START + 0x100;
assert_eq!(
cpu.step_timed(&mut memory).unwrap(),
expected(if thumb_state { 11 } else { 20 }, 0, 0)
);
}
}
#[test]
fn waitcnt_store_charges_old_code_settings_and_new_settings_apply_next_step() {
let mut memory = Memory::new(
[0xe580_1000_u32, 0xe1a0_0000]
.into_iter()
.flat_map(u32::to_le_bytes)
.collect(),
)
.unwrap();
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = WAITCNT;
cpu.registers[1] = 0x18; // N=3 and S=2 per halfword
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(8, 1, 0));
assert_eq!(memory.waitcnt(), 0x18);
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(4, 0, 0));
}
#[test]
fn failed_data_access_discards_trace_before_retry() {
let (mut cpu, mut memory) = arm(0xe8b0_0006); // LDMIA r0!,{r1,r2}
cpu.registers[0] = 0x0400_0054;
let before = cpu.clone();
assert!(cpu.step_timed(&mut memory).is_err());
assert_eq!(cpu, before);
assert_eq!(memory.cycles(), 0);
cpu.registers[0] = DATA;
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(1, 12, 1));
}
#[test]
fn timing_preserves_semantics_for_every_thumb_encoding() {
let mut timed_memory = Memory::new(vec![]).unwrap();
let mut plain_memory = Memory::new(vec![]).unwrap();
for instruction in 0..=u16::MAX {
timed_memory.write16(CODE, instruction).unwrap();
plain_memory.write16(CODE, instruction).unwrap();
let mut plain = Cpu::new(CODE);
plain.instruction_set = InstructionSet::Thumb;
for register in 0..15 {
plain.registers[register] = DATA + register as u32 * 4;
}
let mut timed = plain.clone();
let cost = timed.step_timed(&mut timed_memory);
if let Ok(cost) = cost {
assert!(cost.total() > 0);
}
assert_eq!(
cost.map(|_| ()),
plain.step(&mut plain_memory),
"{instruction:04x}"
);
assert_eq!(timed, plain, "{instruction:04x}");
assert_eq!(timed_memory.cycles(), 0);
}
}

145
src/cpu/transfer.rs Normal file
View file

@ -0,0 +1,145 @@
//! Block transfers shared by ARM and Thumb, plus ARM swaps.
//! Data aborts and per-access device event timing are not modeled.
use super::{Cpu, CpuError, InstructionSet};
use crate::memory::Memory;
impl Cpu {
pub(super) fn block_transfer(
&mut self,
instruction: u32,
memory: &mut Memory,
) -> Result<(), CpuError> {
let base_register = ((instruction >> 16) & 15) as usize;
if base_register == 15 {
return Err(self.unsupported(instruction));
}
let encoded_list = instruction & 0xffff;
// ARM7's empty list transfers PC, but calculates addresses/writeback
// as if sixteen registers were selected.
let (list, span) = if encoded_list == 0 {
(1 << 15, 64)
} else {
(encoded_list, encoded_list.count_ones() * 4)
};
let pre = instruction & (1 << 24) != 0;
let up = instruction & (1 << 23) != 0;
let write_back = instruction & (1 << 21) != 0;
let load = instruction & (1 << 20) != 0;
let special = instruction & (1 << 22) != 0;
let user_bank = special && !(load && list & (1 << 15) != 0);
// Diagnose unpredictable user-bank forms and the unverified S+empty case.
if special && (self.mode == super::Mode::User || encoded_list == 0)
|| user_bank && write_back
{
return Err(self.unsupported(instruction));
}
let restore = if special && !user_bank {
Some(self.return_status(instruction)?)
} else {
None
};
let base = self.registers[base_register];
let adjusted = if up {
base.wrapping_add(span)
} else {
base.wrapping_sub(span)
};
let start = match (up, pre) {
(true, false) => base, // IA
(true, true) => base.wrapping_add(4), // IB
(false, false) => adjusted.wrapping_add(4), // DA
(false, true) => adjusted, // DB
};
// Unlike LDR, block loads align down without rotating the loaded word.
// The low base bits are retained in writeback.
let mut address = start & !3;
let mut next = self.registers;
next[15] = self.pc().wrapping_add(self.instruction_set.width());
let base_in_list = list & (1 << base_register) != 0;
if write_back && !(load && base_in_list) {
next[base_register] = adjusted;
}
let mut writes = [(0, 0); 16];
let mut write_count = 0;
for (register, destination) in next.iter_mut().enumerate() {
if list & (1 << register) == 0 {
continue;
}
if load {
let value = memory.read32(address)?;
*destination = if register == 15 && restore.is_none() {
self.instruction_set.align(value)
} else {
value
};
} else {
let value = if write_back
&& register == base_register
&& register as u32 != list.trailing_zeros()
{
// ARM7 stores the updated base when it is not first in Rlist.
adjusted
} else if user_bank {
self.user_register(register)
} else {
let pc_offset = if self.instruction_set == InstructionSet::Arm {
12
} else {
4
};
self.operand_register(register, pc_offset)
};
writes[write_count] = (address, value);
write_count += 1;
}
address = address.wrapping_add(4);
}
if !load {
memory.write_words(&writes[..write_count])?;
}
// Commit only after every access succeeds. This keeps diagnostics retryable.
if user_bank && load {
for (register, value) in next.into_iter().enumerate() {
if list & (1 << register) != 0 {
self.set_user_register(register, value);
}
}
self.registers[15] = next[15];
} else {
self.registers = next;
}
if let Some((status, mode)) = restore {
// Save loaded banked registers and writeback in the outgoing bank.
self.apply_status(status, mode);
self.registers[15] = self.instruction_set.align(self.pc());
}
Ok(())
}
pub(super) fn swap(&mut self, instruction: u32, memory: &mut Memory) -> Result<(), CpuError> {
let base_register = ((instruction >> 16) & 15) as usize;
let destination = ((instruction >> 12) & 15) as usize;
let source = (instruction & 15) as usize;
if [base_register, destination, source].contains(&15) {
return Err(self.unsupported(instruction));
}
// Snapshot operands so source/destination/base aliases use old values.
let address = self.registers[base_register];
let value = self.registers[source];
let previous = if instruction & (1 << 22) != 0 {
let previous = u32::from(memory.read8(address)?);
memory.write8(address, value as u8)?;
previous
} else {
let previous = memory.read32(address & !3)?.rotate_right((address & 3) * 8);
memory.write32(address & !3, value)?;
previous
};
self.registers[destination] = previous;
self.registers[15] = self.pc().wrapping_add(4);
Ok(())
}
}

493
src/cpu/transfer_tests.rs Normal file
View file

@ -0,0 +1,493 @@
use super::*;
use crate::memory::ROM_START;
const RAM: u32 = 0x0200_0000;
const BASE: u32 = RAM + 0x100;
fn program(words: &[u32]) -> Memory {
Memory::new(words.iter().flat_map(|word| word.to_le_bytes()).collect()).unwrap()
}
fn flags() -> Flags {
Flags {
negative: true,
zero: true,
carry: true,
overflow: true,
}
}
fn block(mode: u32, load: bool, write_back: bool, base: u32, list: u32) -> u32 {
0xe800_0000
| mode
| (u32::from(load) << 20)
| (u32::from(write_back) << 21)
| (base << 16)
| list
}
// Explicit address fixtures for five-register transfers with BASE as Rn.
const MODES: [(u32, u32, u32); 4] = [
(0x0080_0000, BASE, BASE + 20), // IA
(0x0180_0000, BASE + 4, BASE + 20), // IB
(0x0000_0000, BASE - 16, BASE - 20), // DA
(0x0100_0000, BASE - 20, BASE - 20), // DB
];
#[test]
fn stm_all_addressing_modes_store_sparse_registers_in_ascending_order() {
let list = (1 << 0) | (1 << 3) | (1 << 7) | (1 << 14) | (1 << 15);
for (mode, first, updated) in MODES {
for write_back in [false, true] {
let mut memory = program(&[block(mode, false, write_back, 10, list)]);
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = 0x11;
cpu.registers[3] = 0x22;
cpu.registers[7] = 0x33;
cpu.registers[14] = 0x44;
cpu.registers[10] = BASE;
cpu.flags = flags();
memory.write32(first - 4, 0xdead_beef).unwrap();
memory.write32(first + 20, 0xcafe_babe).unwrap();
cpu.step(&mut memory).unwrap();
for (offset, value) in [0x11, 0x22, 0x33, 0x44, ROM_START + 12]
.into_iter()
.enumerate()
{
assert_eq!(memory.read32(first + offset as u32 * 4).unwrap(), value);
}
assert_eq!(memory.read32(first - 4).unwrap(), 0xdead_beef);
assert_eq!(memory.read32(first + 20).unwrap(), 0xcafe_babe);
assert_eq!(cpu.registers[10], if write_back { updated } else { BASE });
assert_eq!(cpu.flags, flags());
assert_eq!(cpu.pc(), ROM_START + 4);
}
}
}
#[test]
fn ldm_all_addressing_modes_load_sparse_registers_and_align_pc() {
let registers = [0, 3, 7, 14, 15];
let list = registers
.iter()
.fold(0, |list, register| list | (1 << register));
for (mode, first, updated) in MODES {
for write_back in [false, true] {
let mut memory = program(&[block(mode, true, write_back, 10, list)]);
for (offset, value) in [11, 22, 33, 44, ROM_START + 0x47].into_iter().enumerate() {
memory.write32(first + offset as u32 * 4, value).unwrap();
}
let mut cpu = Cpu::new(ROM_START);
cpu.registers[10] = BASE;
cpu.flags = flags();
cpu.step(&mut memory).unwrap();
for (register, value) in registers
.into_iter()
.zip([11, 22, 33, 44, ROM_START + 0x44])
{
assert_eq!(cpu.registers[register], value);
}
assert_eq!(cpu.registers[10], if write_back { updated } else { BASE });
assert_eq!(cpu.flags, flags());
}
}
}
#[test]
fn stm_base_in_list_stores_old_base_only_when_first_with_writeback() {
// The five-entry list is r1,r3,r5,r7,r9. Exercise each possible base position.
let registers = [1, 3, 5, 7, 9];
let list = registers
.iter()
.fold(0, |list, register| list | (1 << register));
for (mode, first, updated) in MODES {
for write_back in [false, true] {
for (position, base_register) in registers.into_iter().enumerate() {
let mut memory = program(&[block(mode, false, write_back, base_register, list)]);
let mut cpu = Cpu::new(ROM_START);
cpu.registers[base_register as usize] = BASE;
cpu.step(&mut memory).unwrap();
let stored = memory.read32(first + position as u32 * 4).unwrap();
assert_eq!(
stored,
if write_back && position != 0 {
updated
} else {
BASE
}
);
assert_eq!(
cpu.registers[base_register as usize],
if write_back { updated } else { BASE }
);
}
}
}
}
#[test]
fn ldm_base_in_list_keeps_loaded_value_instead_of_writeback() {
let registers = [1, 3, 5, 7, 9];
let list = registers
.iter()
.fold(0, |list, register| list | (1 << register));
for (mode, first, _) in MODES {
for write_back in [false, true] {
for base_register in registers {
let mut memory = program(&[block(mode, true, write_back, base_register, list)]);
for (offset, register) in registers.into_iter().enumerate() {
memory
.write32(first + offset as u32 * 4, 100 + register)
.unwrap();
}
let mut cpu = Cpu::new(ROM_START);
cpu.registers[base_register as usize] = BASE;
cpu.step(&mut memory).unwrap();
for register in registers {
assert_eq!(cpu.registers[register as usize], 100 + register);
}
}
}
}
}
#[test]
fn a_single_base_register_in_the_list_uses_arm7_writeback_rules() {
for load in [false, true] {
let mut memory = program(&[block(0x0080_0000, load, true, 2, 1 << 2)]);
memory.write32(BASE, 0x1234_5678).unwrap();
let mut cpu = Cpu::new(ROM_START);
cpu.registers[2] = BASE;
cpu.step(&mut memory).unwrap();
if load {
assert_eq!(cpu.registers[2], 0x1234_5678);
} else {
assert_eq!(cpu.registers[2], BASE + 4);
assert_eq!(memory.read32(BASE).unwrap(), BASE);
}
}
}
#[test]
fn full_register_list_loads_all_registers_including_the_base_and_pc() {
let mut memory = program(&[block(0x0080_0000, true, true, 10, 0xffff)]);
for register in 0..16 {
memory
.write32(BASE + register * 4, 0x1000 + register * 4)
.unwrap();
}
let mut cpu = Cpu::new(ROM_START);
cpu.registers[10] = BASE;
cpu.step(&mut memory).unwrap();
for register in 0..16 {
assert_eq!(cpu.registers[register], 0x1000 + register as u32 * 4);
}
}
#[test]
fn full_register_list_stores_pc_and_updated_base_without_array_overflow() {
let mut memory = program(&[block(0x0080_0000, false, true, 10, 0xffff)]);
let mut cpu = Cpu::new(ROM_START);
for register in 0..15 {
cpu.registers[register] = register as u32;
}
cpu.registers[10] = BASE;
cpu.step(&mut memory).unwrap();
for register in 0..16 {
let expected = match register {
10 => BASE + 64,
15 => ROM_START + 12,
_ => register,
};
assert_eq!(memory.read32(BASE + register * 4).unwrap(), expected);
}
}
#[test]
fn empty_list_transfers_only_pc_with_a_sixty_four_byte_span() {
let modes = [
(0x0080_0000, BASE, BASE + 64),
(0x0180_0000, BASE + 4, BASE + 64),
(0x0000_0000, BASE - 60, BASE - 64),
(0x0100_0000, BASE - 64, BASE - 64),
];
for (mode, address, updated) in modes {
for write_back in [false, true] {
for load in [false, true] {
let mut memory = program(&[block(mode, load, write_back, 1, 0)]);
memory.write32(address - 4, 0xfeed_face).unwrap();
memory.write32(address, ROM_START + 0x47).unwrap();
memory.write32(address + 4, 0xdead_beef).unwrap();
let mut cpu = Cpu::new(ROM_START);
cpu.registers[1] = BASE;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[1], if write_back { updated } else { BASE });
assert_eq!(cpu.pc(), ROM_START + if load { 0x44 } else { 4 });
assert_eq!(
memory.read32(address).unwrap(),
if load {
ROM_START + 0x47
} else {
ROM_START + 12
}
);
assert_eq!(memory.read32(address - 4).unwrap(), 0xfeed_face);
assert_eq!(memory.read32(address + 4).unwrap(), 0xdead_beef);
}
}
}
}
#[test]
fn unaligned_block_transfers_align_words_without_rotation_and_preserve_base_low_bits() {
for offset in 1..4 {
let mut memory = program(&[
block(0x0080_0000, false, true, 0, 0x6),
block(0x0100_0000, true, true, 0, 0x18),
]);
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = BASE + offset;
cpu.registers[1] = 0x1234_5678;
cpu.registers[2] = 0x90ab_cdef;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[0], BASE + offset + 8);
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[0], BASE + offset);
assert_eq!(cpu.registers[3], 0x1234_5678);
assert_eq!(cpu.registers[4], 0x90ab_cdef);
}
}
#[test]
fn stack_push_and_pop_restore_registers_and_return_through_pc() {
let mut memory = program(&[
0xe92d_4010, // STMDB sp!,{r4,lr}
0xe3a0_4000, // MOV r4,#0
0xe8bd_8010, // LDMIA sp!,{r4,pc}
]);
let mut cpu = Cpu::new(ROM_START);
cpu.registers[4] = 42;
cpu.registers[13] = BASE;
cpu.registers[14] = ROM_START + 20;
for _ in 0..3 {
cpu.step(&mut memory).unwrap();
}
assert_eq!(cpu.registers[4], 42);
assert_eq!(cpu.registers[13], BASE);
assert_eq!(cpu.pc(), ROM_START + 20);
assert_eq!(memory.read32(BASE - 8).unwrap(), 42);
assert_eq!(memory.read32(BASE - 4).unwrap(), ROM_START + 20);
}
#[test]
fn block_transfer_can_cross_ram_regions() {
let mut memory = program(&[block(0x0080_0000, false, true, 0, 0x6)]);
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = 0x02ff_fffc;
cpu.registers[1] = 11;
cpu.registers[2] = 22;
cpu.step(&mut memory).unwrap();
assert_eq!(memory.read32(0x0203_fffc).unwrap(), 11); // External RAM mirror
assert_eq!(memory.read32(0x0300_0000).unwrap(), 22); // Internal RAM
assert_eq!(cpu.registers[0], 0x0300_0004);
}
#[test]
fn block_diagnostics_do_not_commit_partial_register_or_ram_changes() {
for load in [false, true] {
let mut memory = Memory::new(vec![]).unwrap();
memory
.write32(0x0300_0000, block(0x0080_0000, load, true, 2, 0x8001))
.unwrap();
memory.write32(0x07ff_fffc, 0xdead_beef).unwrap();
let mut cpu = Cpu::new(0x0300_0000);
cpu.registers[0] = 42;
cpu.registers[2] = 0x07ff_fffc; // Second transfer reaches absent/read-only ROM.
cpu.flags = flags();
let before = cpu.registers;
assert_eq!(
cpu.step(&mut memory),
Err(CpuError::Memory(if load {
MemoryError::Unmapped(ROM_START)
} else {
MemoryError::ReadOnly(ROM_START)
}))
);
assert_eq!(cpu.registers, before);
assert_eq!(cpu.flags, flags());
assert_eq!(memory.read32(0x07ff_fffc).unwrap(), 0xdead_beef);
}
}
#[test]
fn block_load_can_read_rom_and_store_reports_read_only_memory() {
for load in [false, true] {
let mut memory = program(&[block(0x0080_0000, load, true, 0, 0x6), 11, 22]);
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = ROM_START + 4;
if load {
cpu.step(&mut memory).unwrap();
assert_eq!(&cpu.registers[1..3], &[11, 22]);
assert_eq!(cpu.registers[0], ROM_START + 12);
} else {
let before = cpu.registers;
assert_eq!(
cpu.step(&mut memory),
Err(CpuError::Memory(MemoryError::ReadOnly(ROM_START + 4)))
);
assert_eq!(cpu.registers, before);
assert_eq!(memory.read32(ROM_START + 4).unwrap(), 11);
}
}
}
#[test]
fn truncated_rom_does_not_partially_load_a_register_list() {
let mut memory = program(&[block(0x0080_0000, true, true, 0, 0x6), 11]);
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = ROM_START + 4;
let before = cpu.registers;
assert!(matches!(cpu.step(&mut memory), Err(CpuError::Memory(_))));
assert_eq!(cpu.registers, before);
}
#[test]
fn swap_word_rotates_loads_and_aligns_stores_at_all_offsets() {
for offset in 0..4 {
let mut memory = program(&[0xe100_1092]); // SWP r1,r2,[r0]
memory.write32(BASE, 0x1234_5678).unwrap();
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = BASE + offset;
cpu.registers[2] = 0xaabb_ccdd;
cpu.flags = flags();
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[1], 0x1234_5678_u32.rotate_right(offset * 8));
assert_eq!(memory.read32(BASE).unwrap(), 0xaabb_ccdd);
assert_eq!(cpu.registers[0], BASE + offset);
assert_eq!(cpu.registers[2], 0xaabb_ccdd);
assert_eq!(cpu.flags, flags());
assert_eq!(cpu.pc(), ROM_START + 4);
}
}
#[test]
fn swap_byte_zero_extends_and_preserves_neighboring_bytes() {
let mut memory = program(&[0xe140_1092]); // SWPB r1,r2,[r0]
memory.write32(BASE, 0x1234_8078).unwrap();
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = BASE + 1;
cpu.registers[2] = 0xaabb_ccdd;
cpu.flags = flags();
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[1], 0x80);
assert_eq!(memory.read32(BASE).unwrap(), 0x1234_dd78);
assert_eq!(cpu.flags, flags());
}
#[test]
fn swap_snapshots_aliased_source_destination_and_base_registers() {
for byte in [false, true] {
for (destination, source) in [(1, 2), (1, 1), (1, 0), (0, 1), (0, 0)] {
let instruction = 0xe100_0090 | (u32::from(byte) << 22) | (destination << 12) | source;
let mut memory = program(&[instruction]);
memory.write32(BASE, 0x1234_5678).unwrap();
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = BASE;
cpu.registers[1] = 0x1122_3344;
cpu.registers[2] = 0x5566_7788;
let old_source = cpu.registers[source as usize];
cpu.step(&mut memory).unwrap();
assert_eq!(
cpu.registers[destination as usize],
if byte { 0x78 } else { 0x1234_5678 }
);
assert_eq!(
memory.read32(BASE).unwrap(),
if byte {
0x1234_5600 | (old_source & 255)
} else {
old_source
}
);
}
}
}
#[test]
fn swap_write_failure_preserves_destination_and_memory() {
for byte in [false, true] {
let mut memory = program(&[0xe100_1092 | (u32::from(byte) << 22), 0x1234_5678]);
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = ROM_START + 4;
cpu.registers[1] = 123;
cpu.registers[2] = 456;
let before = cpu.registers;
assert_eq!(
cpu.step(&mut memory),
Err(CpuError::Memory(MemoryError::ReadOnly(ROM_START + 4)))
);
assert_eq!(cpu.registers, before);
assert_eq!(memory.read32(ROM_START + 4).unwrap(), 0x1234_5678);
}
}
#[test]
fn swap_read_failure_preserves_cpu_state() {
let mut memory = program(&[0xe100_1092]);
let mut cpu = Cpu::new(ROM_START);
cpu.registers[1] = 42;
let before = cpu.registers;
assert_eq!(
cpu.step(&mut memory),
Err(CpuError::Memory(MemoryError::Unmapped(0)))
);
assert_eq!(cpu.registers, before);
}
#[test]
fn skipped_block_transfers_and_swaps_do_not_touch_unmapped_memory() {
for instruction in [0x08a0_0006, 0x08b0_0006, 0x0100_1092, 0x0140_1092] {
let mut memory = program(&[instruction]); // EQ while Z is clear
let mut cpu = Cpu::new(ROM_START);
cpu.registers[1] = 42;
let mut expected = cpu.registers;
expected[15] += 4;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers, expected);
}
}
#[test]
fn unsupported_transfer_variants_preserve_registers_and_memory() {
for instruction in [
0xe8ef_0001, // PC base plus S bit
0xe89f_0001, // PC base without S bit
0xe8e0_0001, // STM user-bank transfer with writeback
0xe8f0_0001, // LDM user-bank transfer with writeback
0xe8fd_8000, // LDM with status restore
0xe8c0_0000, // Empty-list STM with S bit
0xe10f_1092, // SWP PC base
0xe100_f092, // SWP PC destination
0xe100_109f, // SWP PC source
0xe110_1092, // Reserved SWP bit 20
0xe100_1192, // Reserved SWP bits 11..8
] {
let mut memory = program(&[instruction]);
memory.write32(BASE, 0xdead_beef).unwrap();
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = BASE;
cpu.registers[13] = BASE;
cpu.flags = flags();
let before = cpu.registers;
assert_eq!(
cpu.step(&mut memory),
Err(CpuError::UnsupportedInstruction {
address: ROM_START,
instruction
})
);
assert_eq!(cpu.registers, before);
assert_eq!(cpu.flags, flags());
assert_eq!(memory.read32(BASE).unwrap(), 0xdead_beef);
}
}

178
tests/core.rs Normal file
View file

@ -0,0 +1,178 @@
use gba_rust::{
cpu::{Cpu, CpuError, InstructionSet},
demo_program,
memory::{Memory, MemoryError, ROM_START},
DEMO_STEPS,
};
fn program(words: &[u32]) -> Memory {
Memory::new(words.iter().flat_map(|word| word.to_le_bytes()).collect()).unwrap()
}
#[test]
fn demo_computes_values_and_loops() {
let mut memory = Memory::new(demo_program()).unwrap();
let mut cpu = Cpu::new(ROM_START);
let mut saw_thumb = false;
for _ in 0..DEMO_STEPS + 5 {
saw_thumb |= cpu.instruction_set() == InstructionSet::Thumb;
cpu.step(&mut memory).unwrap();
}
assert!(saw_thumb);
assert_eq!(cpu.instruction_set(), InstructionSet::Arm);
assert_eq!(&cpu.registers()[..3], &[42, 7, 42]);
assert_eq!(cpu.pc(), ROM_START + 96);
assert_eq!(cpu.registers()[3], 0x0200_0000);
assert_eq!(&cpu.registers()[4..8], &[42, 84, 3528, 42]);
assert_eq!(memory.read32(0x0200_0000).unwrap(), 3528);
assert_eq!(memory.read32(0x0200_0004).unwrap(), 42);
assert_eq!(cpu.registers()[13], 0x0300_0100);
assert_eq!(cpu.registers()[14], ROM_START + 109);
assert_eq!(memory.read32(0x0300_00f8).unwrap(), 42);
assert_eq!(memory.read32(0x0300_00fc).unwrap(), ROM_START + 109);
assert!(cpu.flags().zero);
assert!(cpu.flags().carry);
}
#[test]
fn memory_is_little_endian() {
let memory = program(&[0x1234_5678]);
assert_eq!(memory.read8(ROM_START).unwrap(), 0x78);
assert_eq!(memory.read32(ROM_START).unwrap(), 0x1234_5678);
}
#[test]
fn ram_is_zeroed_writable_and_mirrored() {
let mut memory = Memory::new(vec![]).unwrap();
for (base, size) in [(0x0200_0000, 0x40000), (0x0300_0000, 0x8000)] {
assert_eq!(memory.read32(base).unwrap(), 0);
memory.write8(base, 0x12).unwrap();
memory.write8(base + 1, 0x34).unwrap();
assert_eq!(memory.read32(base + size).unwrap(), 0x3412);
memory.write8(base + size - 1, 0xab).unwrap();
assert_eq!(memory.read8(base + size * 2 - 1).unwrap(), 0xab);
}
}
#[test]
fn cartridge_has_three_windows() {
let memory = program(&[0x1234_5678]);
for address in [0x0800_0000, 0x0a00_0000, 0x0c00_0000] {
assert_eq!(memory.read32(address).unwrap(), 0x1234_5678);
}
}
#[test]
fn invalid_memory_accesses_return_errors() {
let mut memory = program(&[0]);
assert_eq!(memory.read8(0), Err(MemoryError::Unmapped(0)));
assert_eq!(
memory.read32(ROM_START + 1),
Err(MemoryError::Unaligned(ROM_START + 1))
);
assert_eq!(
memory.read8(ROM_START + 4),
Err(MemoryError::Unmapped(ROM_START + 4))
);
assert_eq!(
memory.write8(ROM_START, 1),
Err(MemoryError::ReadOnly(ROM_START))
);
assert_eq!(memory.write8(0, 1), Err(MemoryError::Unmapped(0)));
assert_eq!(
memory.read32(0xffff_fffc),
Err(MemoryError::Unmapped(0xffff_fffc))
);
}
#[test]
fn oversized_rom_is_rejected() {
assert!(matches!(
Memory::new(vec![0; 32 * 1024 * 1024 + 1]),
Err(MemoryError::RomTooLarge(_))
));
}
#[test]
fn rotated_immediate_is_decoded() {
let mut memory = program(&[0xe3a0_0480]); // MOV r0, #0x80000000
let mut cpu = Cpu::new(ROM_START);
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers()[0], 0x8000_0000);
}
#[test]
fn arithmetic_wraps_at_32_bits() {
let mut memory = program(&[
0xe240_0001, // SUB r0, r0, #1
0xe280_1001, // ADD r1, r0, #1
]);
let mut cpu = Cpu::new(ROM_START);
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers()[0], u32::MAX);
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers()[1], 0);
}
#[test]
fn reading_pc_as_operand_includes_pipeline_offset() {
let mut memory = program(&[0xe28f_0004]); // ADD r0, pc, #4
let mut cpu = Cpu::new(ROM_START);
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers()[0], ROM_START + 12);
}
#[test]
fn branches_use_signed_displacement_and_pipeline_offset() {
let mut memory = program(&[
0xea00_0000, // B +8
0xe3a0_0063, // skipped
0xeaff_fffc, // B -8, back to start
]);
let mut cpu = Cpu::new(ROM_START);
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.pc(), ROM_START + 8);
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.pc(), ROM_START);
assert_eq!(cpu.registers()[0], 0);
}
#[test]
fn unsupported_instructions_do_not_change_cpu() {
for instruction in [
0xf3a0_0001, // reserved condition
0xe340_0001, // CMP encoding with S=0
0xe3b0_f001, // MOVS pc, #1 (status restore unsupported)
0xe10f_f000, // MRS pc, CPSR
0xe8f0_0001, // LDM user-bank transfer with writeback
0xe100_f090, // SWP with PC destination
0xe14f_0000, // MRS SPSR in System mode
0xe16f_0f10, // CLZ (ARMv5)
0xe12f_ff30, // BLX (ARMv5)
] {
let mut memory = program(&[instruction]);
let mut cpu = Cpu::new(ROM_START);
let before = *cpu.registers();
let flags_before = cpu.flags();
assert_eq!(
cpu.step(&mut memory),
Err(CpuError::UnsupportedInstruction {
address: ROM_START,
instruction
}),
);
assert_eq!(cpu.registers(), &before);
assert_eq!(cpu.flags(), flags_before);
}
}
#[test]
fn fetch_errors_do_not_advance_pc() {
let mut memory = Memory::new(vec![]).unwrap();
let mut cpu = Cpu::new(ROM_START);
assert_eq!(
cpu.step(&mut memory),
Err(CpuError::Memory(MemoryError::Unmapped(ROM_START)))
);
assert_eq!(cpu.pc(), ROM_START);
}

90
tests/exceptions.rs Normal file
View file

@ -0,0 +1,90 @@
use gba_rust::{
cpu::{Cpu, InstructionSet, Mode},
demo_bios, exception_demo_program,
memory::{Memory, MemoryError, BIOS_SIZE, ROM_START},
EXCEPTION_DEMO_STEPS,
};
#[test]
fn original_demo_enters_supervisor_and_returns_to_arm_and_thumb() {
let mut memory = Memory::with_bios(exception_demo_program(), demo_bios()).unwrap();
let mut cpu = Cpu::new(ROM_START);
let expected = [
(8, Mode::Supervisor, InstructionSet::Arm),
(0x40, Mode::Supervisor, InstructionSet::Arm),
(0x44, Mode::Supervisor, InstructionSet::Arm),
(ROM_START + 4, Mode::System, InstructionSet::Arm),
(ROM_START + 8, Mode::System, InstructionSet::Arm),
(ROM_START + 12, Mode::System, InstructionSet::Thumb),
(8, Mode::Supervisor, InstructionSet::Arm),
(0x40, Mode::Supervisor, InstructionSet::Arm),
(0x44, Mode::Supervisor, InstructionSet::Arm),
(ROM_START + 14, Mode::System, InstructionSet::Thumb),
(ROM_START + 14, Mode::System, InstructionSet::Thumb),
];
assert_eq!(expected.len(), EXCEPTION_DEMO_STEPS);
for (pc, mode, state) in expected {
cpu.step(&mut memory).unwrap();
assert_eq!(
(cpu.pc(), cpu.mode(), cpu.instruction_set()),
(pc, mode, state)
);
}
assert_eq!(cpu.registers()[10], 2);
assert_eq!(cpu.registers()[14], 0); // Supervisor LR did not replace System LR
assert_eq!(cpu.cpsr(), 0x3f);
for _ in 0..5 {
cpu.step(&mut memory).unwrap();
}
assert_eq!(cpu.pc(), ROM_START + 14);
assert_eq!(cpu.registers()[10], 2);
}
#[test]
fn optional_bios_image_is_exactly_16_kib_read_only_and_not_mirrored() {
for size in [0, BIOS_SIZE - 1, BIOS_SIZE + 1] {
assert!(matches!(
Memory::with_bios(vec![], vec![0; size]),
Err(MemoryError::InvalidBiosSize(actual)) if actual == size
));
}
let bios: Vec<u8> = (0..BIOS_SIZE).map(|index| index as u8).collect();
let mut memory = Memory::with_bios(vec![], bios).unwrap();
assert_eq!(memory.read32(0).unwrap(), 0x0302_0100);
assert_eq!(memory.read16(0x3ffe).unwrap(), 0xfffe);
assert_eq!(memory.read8(0x3fff).unwrap(), 255);
for address in [0x4000, 0x0100_0000] {
assert_eq!(memory.read8(address), Err(MemoryError::Unmapped(address)));
assert_eq!(
memory.write8(address, 1),
Err(MemoryError::Unmapped(address))
);
}
assert_eq!(memory.write8(0x3fff, 0), Err(MemoryError::ReadOnly(0x3fff)));
assert_eq!(
memory.write16(0x3ffe, 0),
Err(MemoryError::ReadOnly(0x3ffe))
);
assert_eq!(memory.write32(0, 0), Err(MemoryError::ReadOnly(0)));
assert_eq!(memory.read32(0).unwrap(), 0x0302_0100);
assert_eq!(memory.read16(0x3ffe).unwrap(), 0xfffe);
}
#[test]
fn no_bios_is_mapped_by_default() {
let mut memory = Memory::new(vec![]).unwrap();
assert_eq!(memory.read32(0), Err(MemoryError::Unmapped(0)));
assert_eq!(memory.write32(0, 0), Err(MemoryError::Unmapped(0)));
}
#[test]
fn reset_can_fetch_caller_supplied_vector_code() {
let mut bios = vec![0; BIOS_SIZE];
bios[0..4].copy_from_slice(&0xe3a0_002a_u32.to_le_bytes()); // MOV r0, #42
let mut memory = Memory::with_bios(vec![], bios).unwrap();
let mut cpu = Cpu::at_reset();
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers()[0], 42);
assert_eq!(cpu.pc(), 4);
assert_eq!(cpu.cpsr(), 0xd3);
}