diff --git a/src/cpu.rs b/src/cpu.rs new file mode 100644 index 0000000..1c0a9be --- /dev/null +++ b/src/cpu.rs @@ -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 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 { + 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] + } + } +} diff --git a/src/cpu/alu.rs b/src/cpu/alu.rs new file mode 100644 index 0000000..cdb893e --- /dev/null +++ b/src/cpu/alu.rs @@ -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, + }, + ) +} diff --git a/src/cpu/arm.rs b/src/cpu/arm.rs new file mode 100644 index 0000000..579b4c9 --- /dev/null +++ b/src/cpu/arm.rs @@ -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) + } +} diff --git a/src/cpu/exception.rs b/src/cpu/exception.rs new file mode 100644 index 0000000..6fa5198 --- /dev/null +++ b/src/cpu/exception.rs @@ -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 + } +} diff --git a/src/cpu/exception_tests.rs b/src/cpu/exception_tests.rs new file mode 100644 index 0000000..9685a7e --- /dev/null +++ b/src/cpu/exception_tests.rs @@ -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); +} diff --git a/src/cpu/instruction_tests.rs b/src/cpu/instruction_tests.rs new file mode 100644 index 0000000..d545c52 --- /dev/null +++ b/src/cpu/instruction_tests.rs @@ -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); +} diff --git a/src/cpu/status.rs b/src/cpu/status.rs new file mode 100644 index 0000000..6081815 --- /dev/null +++ b/src/cpu/status.rs @@ -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 { + 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 { + 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(()) + } +} diff --git a/src/cpu/status_tests.rs b/src/cpu/status_tests.rs new file mode 100644 index 0000000..890f2a2 --- /dev/null +++ b/src/cpu/status_tests.rs @@ -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); + } +} diff --git a/src/cpu/tests.rs b/src/cpu/tests.rs new file mode 100644 index 0000000..6db4ba8 --- /dev/null +++ b/src/cpu/tests.rs @@ -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(), ®isters); + 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 + } + ); +} diff --git a/src/cpu/thumb.rs b/src/cpu/thumb.rs new file mode 100644 index 0000000..700bca2 --- /dev/null +++ b/src/cpu/thumb.rs @@ -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(()) + } +} diff --git a/src/cpu/thumb_tests.rs b/src/cpu/thumb_tests.rs new file mode 100644 index 0000000..1d53e9b --- /dev/null +++ b/src/cpu/thumb_tests.rs @@ -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 = [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); +} diff --git a/src/cpu/timing.rs b/src/cpu/timing.rs new file mode 100644 index 0000000..6a29445 --- /dev/null +++ b/src/cpu/timing.rs @@ -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 { + 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 +} diff --git a/src/cpu/timing_tests.rs b/src/cpu/timing_tests.rs new file mode 100644 index 0000000..9e4bcac --- /dev/null +++ b/src/cpu/timing_tests.rs @@ -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); + } +} diff --git a/src/cpu/transfer.rs b/src/cpu/transfer.rs new file mode 100644 index 0000000..5b05ae3 --- /dev/null +++ b/src/cpu/transfer.rs @@ -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(()) + } +} diff --git a/src/cpu/transfer_tests.rs b/src/cpu/transfer_tests.rs new file mode 100644 index 0000000..2753b2c --- /dev/null +++ b/src/cpu/transfer_tests.rs @@ -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); + } +} diff --git a/tests/core.rs b/tests/core.rs new file mode 100644 index 0000000..2c9bcad --- /dev/null +++ b/tests/core.rs @@ -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); +} diff --git a/tests/exceptions.rs b/tests/exceptions.rs new file mode 100644 index 0000000..0bbef68 --- /dev/null +++ b/tests/exceptions.rs @@ -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 = (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); +}