refactor: split into gameboy-rust workspace with core, demos, and desktop crates
Move the emulator into crates/gba-core, the original demo programs into crates/gba-demos, and the minifb frontend into crates/desktop. The binary is now gameboy-rust. Imports change from gba_rust to gba_core/gba_demos; no behavior changes. gba-core has no dependencies. Its integration tests use gba-demos as a development dependency. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
a8944d1d5f
commit
f8a0676748
113 changed files with 304 additions and 282 deletions
108
crates/gba-core/src/cpu/alu.rs
Normal file
108
crates/gba-core/src/cpu/alu.rs
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
use super::Flags;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(super) enum Shift {
|
||||
Lsl,
|
||||
Lsr,
|
||||
Asr,
|
||||
Ror,
|
||||
}
|
||||
|
||||
impl Shift {
|
||||
pub(super) fn decode(bits: u32) -> Self {
|
||||
match bits & 3 {
|
||||
0 => Self::Lsl,
|
||||
1 => Self::Lsr,
|
||||
2 => Self::Asr,
|
||||
_ => Self::Ror,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ARM barrel shifter. Register shifts use only the low eight amount bits.
|
||||
pub(super) fn shift(
|
||||
value: u32,
|
||||
kind: Shift,
|
||||
amount: u32,
|
||||
carry: bool,
|
||||
by_register: bool,
|
||||
) -> (u32, bool) {
|
||||
let mut amount = if by_register { amount & 0xff } else { amount };
|
||||
if amount == 0 {
|
||||
if by_register || matches!(kind, Shift::Lsl) {
|
||||
return (value, carry);
|
||||
}
|
||||
if matches!(kind, Shift::Ror) {
|
||||
return ((u32::from(carry) << 31) | (value >> 1), value & 1 != 0); // RRX
|
||||
}
|
||||
amount = 32; // Immediate LSR/ASR #0 encodes a shift by 32.
|
||||
}
|
||||
match kind {
|
||||
Shift::Lsl => match amount {
|
||||
1..=31 => (value << amount, (value >> (32 - amount)) & 1 != 0),
|
||||
32 => (0, value & 1 != 0),
|
||||
_ => (0, false),
|
||||
},
|
||||
Shift::Lsr => match amount {
|
||||
1..=31 => (value >> amount, (value >> (amount - 1)) & 1 != 0),
|
||||
32 => (0, value >> 31 != 0),
|
||||
_ => (0, false),
|
||||
},
|
||||
Shift::Asr => {
|
||||
let sign = value >> 31 != 0;
|
||||
if amount >= 32 {
|
||||
(if sign { u32::MAX } else { 0 }, sign)
|
||||
} else {
|
||||
(
|
||||
((value as i32) >> amount) as u32,
|
||||
(value >> (amount - 1)) & 1 != 0,
|
||||
)
|
||||
}
|
||||
}
|
||||
Shift::Ror => {
|
||||
let result = value.rotate_right(amount);
|
||||
(result, result >> 31 != 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Addition is also used for subtraction: a - b = a + !b + 1.
|
||||
fn add_with_carry(a: u32, b: u32, carry: bool) -> (u32, bool, bool) {
|
||||
let wide = u64::from(a) + u64::from(b) + u64::from(carry);
|
||||
let result = wide as u32;
|
||||
let overflow = (!(a ^ b) & (a ^ result)) >> 31 != 0;
|
||||
(result, wide > u64::from(u32::MAX), overflow)
|
||||
}
|
||||
|
||||
pub(super) fn execute(
|
||||
opcode: u32,
|
||||
a: u32,
|
||||
b: u32,
|
||||
shifter_carry: bool,
|
||||
previous: Flags,
|
||||
) -> (u32, Flags) {
|
||||
let (result, carry, overflow) = match opcode {
|
||||
0x0 | 0x8 => (a & b, shifter_carry, previous.overflow),
|
||||
0x1 | 0x9 => (a ^ b, shifter_carry, previous.overflow),
|
||||
0x2 | 0xa => add_with_carry(a, !b, true),
|
||||
0x3 => add_with_carry(b, !a, true),
|
||||
0x4 | 0xb => add_with_carry(a, b, false),
|
||||
0x5 => add_with_carry(a, b, previous.carry),
|
||||
0x6 => add_with_carry(a, !b, previous.carry),
|
||||
0x7 => add_with_carry(b, !a, previous.carry),
|
||||
0xc => (a | b, shifter_carry, previous.overflow),
|
||||
0xd => (b, shifter_carry, previous.overflow),
|
||||
0xe => (a & !b, shifter_carry, previous.overflow),
|
||||
0xf => (!b, shifter_carry, previous.overflow),
|
||||
_ => unreachable!("opcode comes from a four-bit instruction field"),
|
||||
};
|
||||
(
|
||||
result,
|
||||
Flags {
|
||||
negative: result >> 31 != 0,
|
||||
zero: result == 0,
|
||||
carry,
|
||||
overflow,
|
||||
},
|
||||
)
|
||||
}
|
||||
305
crates/gba-core/src/cpu/arm.rs
Normal file
305
crates/gba-core/src/cpu/arm.rs
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
use super::{
|
||||
alu::{self, Shift},
|
||||
Cpu, CpuError,
|
||||
};
|
||||
use crate::memory::Memory;
|
||||
|
||||
impl Cpu {
|
||||
pub(super) fn execute_arm(
|
||||
&mut self,
|
||||
instruction: u32,
|
||||
memory: &mut Memory,
|
||||
) -> Result<(), CpuError> {
|
||||
if instruction & 0x0f00_0000 == 0x0f00_0000 {
|
||||
self.enter_exception(super::Exception::SoftwareInterrupt);
|
||||
return Ok(());
|
||||
}
|
||||
if instruction & 0x0fbf_0fff == 0x010f_0000 {
|
||||
return self.read_status(instruction);
|
||||
}
|
||||
if instruction & 0x0fb0_fff0 == 0x0120_f000 || instruction & 0x0fb0_f000 == 0x0320_f000 {
|
||||
return self.write_status(instruction);
|
||||
}
|
||||
if instruction & 0x0fff_fff0 == 0x012f_ff10 {
|
||||
let target = self.operand_register((instruction & 15) as usize, 8);
|
||||
self.branch_exchange(target);
|
||||
return Ok(());
|
||||
}
|
||||
if instruction & 0x0e00_0000 == 0x0a00_0000 {
|
||||
let address = self.pc();
|
||||
let offset = (((instruction & 0x00ff_ffff) << 8) as i32 >> 6) as u32;
|
||||
if instruction & (1 << 24) != 0 {
|
||||
self.registers[14] = address.wrapping_add(4); // BL return address
|
||||
}
|
||||
self.registers[15] = address.wrapping_add(8).wrapping_add(offset);
|
||||
return Ok(());
|
||||
}
|
||||
if instruction & 0x0e00_0000 == 0x0800_0000 {
|
||||
return self.block_transfer(instruction, memory);
|
||||
}
|
||||
// SWP shares the 1001 low bits with multiply and halfword transfers.
|
||||
if instruction & 0x0fb0_0ff0 == 0x0100_0090 {
|
||||
return self.swap(instruction, memory);
|
||||
}
|
||||
if instruction & 0x0f80_00f0 == 0x0080_0090 {
|
||||
return self.multiply_long(instruction);
|
||||
}
|
||||
if instruction & 0x0fc0_00f0 == 0x0000_0090 {
|
||||
return self.multiply(instruction);
|
||||
}
|
||||
if instruction & 0x0e00_0090 == 0x0000_0090 {
|
||||
return self.halfword_transfer(instruction, memory);
|
||||
}
|
||||
if instruction & 0x0c00_0000 == 0x0400_0000 {
|
||||
return self.single_transfer(instruction, memory);
|
||||
}
|
||||
if instruction & 0x0c00_0000 == 0 {
|
||||
return self.data_processing(instruction);
|
||||
}
|
||||
Err(self.unsupported(instruction))
|
||||
}
|
||||
|
||||
fn data_processing(&mut self, instruction: u32) -> Result<(), CpuError> {
|
||||
let opcode = (instruction >> 21) & 15;
|
||||
let set_flags = instruction & (1 << 20) != 0;
|
||||
let test = (8..=11).contains(&opcode);
|
||||
let source = ((instruction >> 16) & 15) as usize;
|
||||
let destination = ((instruction >> 12) & 15) as usize;
|
||||
// S=0 in the test/compare opcode range is PSR transfer or an extension.
|
||||
if test && !set_flags {
|
||||
return Err(self.unsupported(instruction));
|
||||
}
|
||||
let restore = if !test && destination == 15 && set_flags {
|
||||
Some(self.return_status(instruction)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let immediate = instruction & (1 << 25) != 0;
|
||||
let register_shift = !immediate && instruction & (1 << 4) != 0;
|
||||
let pc_offset = if register_shift { 12 } else { 8 };
|
||||
let (operand, carry) = if immediate {
|
||||
let rotation = ((instruction >> 8) & 15) * 2;
|
||||
let value = (instruction & 255).rotate_right(rotation);
|
||||
(
|
||||
value,
|
||||
if rotation == 0 {
|
||||
self.flags.carry
|
||||
} else {
|
||||
value >> 31 != 0
|
||||
},
|
||||
)
|
||||
} else {
|
||||
let amount = if register_shift {
|
||||
let shift_register = ((instruction >> 8) & 15) as usize;
|
||||
if instruction & (1 << 7) != 0 || shift_register == 15 {
|
||||
return Err(self.unsupported(instruction));
|
||||
}
|
||||
self.registers[shift_register] & 255
|
||||
} else {
|
||||
(instruction >> 7) & 31
|
||||
};
|
||||
alu::shift(
|
||||
self.operand_register((instruction & 15) as usize, pc_offset),
|
||||
Shift::decode(instruction >> 5),
|
||||
amount,
|
||||
self.flags.carry,
|
||||
register_shift,
|
||||
)
|
||||
};
|
||||
let (result, flags) = alu::execute(
|
||||
opcode,
|
||||
self.operand_register(source, pc_offset),
|
||||
operand,
|
||||
carry,
|
||||
self.flags,
|
||||
);
|
||||
if let Some((status, mode)) = restore {
|
||||
self.apply_status(status, mode);
|
||||
} else if set_flags {
|
||||
self.flags = flags;
|
||||
}
|
||||
let next = if !test && destination == 15 {
|
||||
self.instruction_set.align(result)
|
||||
} else {
|
||||
self.pc().wrapping_add(4)
|
||||
};
|
||||
if !test && destination != 15 {
|
||||
self.registers[destination] = result;
|
||||
}
|
||||
self.registers[15] = next;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn single_transfer(&mut self, instruction: u32, memory: &mut Memory) -> Result<(), CpuError> {
|
||||
let base_register = ((instruction >> 16) & 15) as usize;
|
||||
let destination = ((instruction >> 12) & 15) as usize;
|
||||
let pre = instruction & (1 << 24) != 0;
|
||||
let write_bit = instruction & (1 << 21) != 0;
|
||||
let write_back = !pre || write_bit;
|
||||
let byte = instruction & (1 << 22) != 0;
|
||||
let load = instruction & (1 << 20) != 0;
|
||||
if (!pre && write_bit) // LDRT/STRT access semantics are not implemented.
|
||||
|| (write_back && (base_register == 15 || (load && base_register == destination)))
|
||||
|| (byte && destination == 15)
|
||||
{
|
||||
return Err(self.unsupported(instruction));
|
||||
}
|
||||
let offset = if instruction & (1 << 25) != 0 {
|
||||
let rm = (instruction & 15) as usize;
|
||||
if instruction & (1 << 4) != 0 || rm == 15 {
|
||||
return Err(self.unsupported(instruction));
|
||||
}
|
||||
alu::shift(
|
||||
self.registers[rm],
|
||||
Shift::decode(instruction >> 5),
|
||||
(instruction >> 7) & 31,
|
||||
self.flags.carry,
|
||||
false,
|
||||
)
|
||||
.0
|
||||
} else {
|
||||
instruction & 0xfff
|
||||
};
|
||||
let base = self.operand_register(base_register, 8);
|
||||
let adjusted = offset_address(base, offset, instruction);
|
||||
let address = if pre { adjusted } else { base };
|
||||
let next = self.pc().wrapping_add(4);
|
||||
// All validation precedes memory writes. CPU changes follow successful access.
|
||||
let value = if load {
|
||||
if byte {
|
||||
u32::from(memory.read8(address)?)
|
||||
} else {
|
||||
memory.read32(address & !3)?.rotate_right((address & 3) * 8)
|
||||
}
|
||||
} else {
|
||||
let value = self.operand_register(destination, 12);
|
||||
if byte {
|
||||
memory.write8(address, value as u8)?;
|
||||
} else {
|
||||
memory.write32(address & !3, value)?;
|
||||
}
|
||||
value
|
||||
};
|
||||
if write_back {
|
||||
self.registers[base_register] = adjusted;
|
||||
}
|
||||
if load && destination != 15 {
|
||||
self.registers[destination] = value;
|
||||
}
|
||||
// ARMv4 LDR PC does not switch to Thumb, even if bit zero is set.
|
||||
self.registers[15] = if load && destination == 15 {
|
||||
value & !3
|
||||
} else {
|
||||
next
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn halfword_transfer(&mut self, instruction: u32, memory: &mut Memory) -> Result<(), CpuError> {
|
||||
let kind = (instruction >> 5) & 3;
|
||||
let load = instruction & (1 << 20) != 0;
|
||||
let pre = instruction & (1 << 24) != 0;
|
||||
let write_bit = instruction & (1 << 21) != 0;
|
||||
let write_back = !pre || write_bit;
|
||||
let base_register = ((instruction >> 16) & 15) as usize;
|
||||
let destination = ((instruction >> 12) & 15) as usize;
|
||||
if kind == 0
|
||||
|| (!load && kind != 1)
|
||||
|| (!pre && write_bit)
|
||||
|| destination == 15
|
||||
|| (write_back && (base_register == 15 || (load && base_register == destination)))
|
||||
{
|
||||
return Err(self.unsupported(instruction));
|
||||
}
|
||||
let offset = if instruction & (1 << 22) != 0 {
|
||||
((instruction >> 4) & 0xf0) | (instruction & 15)
|
||||
} else {
|
||||
let rm = (instruction & 15) as usize;
|
||||
if instruction & 0xf00 != 0 || rm == 15 {
|
||||
return Err(self.unsupported(instruction));
|
||||
}
|
||||
self.registers[rm]
|
||||
};
|
||||
let base = self.operand_register(base_register, 8);
|
||||
let adjusted = offset_address(base, offset, instruction);
|
||||
let address = if pre { adjusted } else { base };
|
||||
let value = if load {
|
||||
match kind {
|
||||
1 => u32::from(memory.read16(address & !1)?).rotate_right((address & 1) * 8),
|
||||
2 => memory.read8(address)? as i8 as i32 as u32,
|
||||
_ if address & 1 != 0 => memory.read8(address)? as i8 as i32 as u32,
|
||||
_ => memory.read16(address)? as i16 as i32 as u32,
|
||||
}
|
||||
} else {
|
||||
memory.write16(address & !1, self.registers[destination] as u16)?;
|
||||
0
|
||||
};
|
||||
if write_back {
|
||||
self.registers[base_register] = adjusted;
|
||||
}
|
||||
if load {
|
||||
self.registers[destination] = value;
|
||||
}
|
||||
self.registers[15] = self.pc().wrapping_add(4);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn multiply(&mut self, instruction: u32) -> Result<(), CpuError> {
|
||||
let rd = ((instruction >> 16) & 15) as usize;
|
||||
let rn = ((instruction >> 12) & 15) as usize;
|
||||
let rs = ((instruction >> 8) & 15) as usize;
|
||||
let rm = (instruction & 15) as usize;
|
||||
let accumulate = instruction & (1 << 21) != 0;
|
||||
if [rd, rn, rs, rm].contains(&15) || rd == rm || (!accumulate && rn != 0) {
|
||||
return Err(self.unsupported(instruction));
|
||||
}
|
||||
let mut result = self.registers[rm].wrapping_mul(self.registers[rs]);
|
||||
if accumulate {
|
||||
result = result.wrapping_add(self.registers[rn]);
|
||||
}
|
||||
self.registers[rd] = result;
|
||||
if instruction & (1 << 20) != 0 {
|
||||
self.flags.negative = result >> 31 != 0;
|
||||
self.flags.zero = result == 0;
|
||||
// ARM7 carry is unspecified after multiply; preserve it deterministically.
|
||||
}
|
||||
self.registers[15] = self.pc().wrapping_add(4);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn multiply_long(&mut self, instruction: u32) -> Result<(), CpuError> {
|
||||
let hi = ((instruction >> 16) & 15) as usize;
|
||||
let lo = ((instruction >> 12) & 15) as usize;
|
||||
let rs = ((instruction >> 8) & 15) as usize;
|
||||
let rm = (instruction & 15) as usize;
|
||||
if [hi, lo, rs, rm].contains(&15) || hi == lo || hi == rm || lo == rm {
|
||||
return Err(self.unsupported(instruction));
|
||||
}
|
||||
let mut result = if instruction & (1 << 22) != 0 {
|
||||
(i64::from(self.registers[rm] as i32) * i64::from(self.registers[rs] as i32)) as u64
|
||||
} else {
|
||||
u64::from(self.registers[rm]) * u64::from(self.registers[rs])
|
||||
};
|
||||
if instruction & (1 << 21) != 0 {
|
||||
let accumulator = (u64::from(self.registers[hi]) << 32) | u64::from(self.registers[lo]);
|
||||
result = result.wrapping_add(accumulator);
|
||||
}
|
||||
self.registers[hi] = (result >> 32) as u32;
|
||||
self.registers[lo] = result as u32;
|
||||
if instruction & (1 << 20) != 0 {
|
||||
self.flags.negative = result >> 63 != 0;
|
||||
self.flags.zero = result == 0;
|
||||
// Unspecified multiply C/V outputs are preserved, not hardware-verified.
|
||||
}
|
||||
self.registers[15] = self.pc().wrapping_add(4);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn offset_address(base: u32, offset: u32, instruction: u32) -> u32 {
|
||||
if instruction & (1 << 23) != 0 {
|
||||
base.wrapping_add(offset)
|
||||
} else {
|
||||
base.wrapping_sub(offset)
|
||||
}
|
||||
}
|
||||
67
crates/gba-core/src/cpu/exception.rs
Normal file
67
crates/gba-core/src/cpu/exception.rs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
//! Exception entry. Memory errors remain diagnostics rather than hardware aborts.
|
||||
|
||||
use super::{Cpu, InstructionSet, Mode};
|
||||
|
||||
/// ARM7 exception sources. GBA hardware does not expose every source.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Exception {
|
||||
UndefinedInstruction,
|
||||
SoftwareInterrupt,
|
||||
PrefetchAbort,
|
||||
DataAbort,
|
||||
Irq,
|
||||
Fiq,
|
||||
}
|
||||
|
||||
impl Cpu {
|
||||
/// Start at the reset vector in ARM Supervisor mode, with IRQ/FIQ disabled.
|
||||
/// Registers and RAM are not initialized as a Nintendo BIOS would initialize them.
|
||||
pub fn at_reset() -> Self {
|
||||
let mut cpu = Self::new(0);
|
||||
cpu.mode = Mode::Supervisor;
|
||||
cpu.irq_disabled = true;
|
||||
cpu.fiq_disabled = true;
|
||||
cpu
|
||||
}
|
||||
|
||||
/// Enter an exception explicitly, regardless of interrupt masks.
|
||||
/// For synchronous exceptions, PC must identify the faulting instruction.
|
||||
/// For IRQ/FIQ, PC must identify the next instruction, between CPU steps.
|
||||
/// This does not execute a BIOS service or fetch the vector instruction.
|
||||
pub fn enter_exception(&mut self, exception: Exception) {
|
||||
let (mode, vector, offset) = match exception {
|
||||
Exception::UndefinedInstruction => {
|
||||
(Mode::Undefined, 0x04, self.instruction_set.width())
|
||||
}
|
||||
Exception::SoftwareInterrupt => (Mode::Supervisor, 0x08, self.instruction_set.width()),
|
||||
Exception::PrefetchAbort => (Mode::Abort, 0x0c, 4),
|
||||
Exception::DataAbort => (Mode::Abort, 0x10, 8),
|
||||
Exception::Irq => (Mode::Irq, 0x18, 4),
|
||||
Exception::Fiq => (Mode::Fiq, 0x1c, 4),
|
||||
};
|
||||
let previous = self.cpsr();
|
||||
let link = self.pc().wrapping_add(offset);
|
||||
self.switch_mode(mode);
|
||||
self.set_spsr(previous);
|
||||
self.registers[14] = link;
|
||||
self.registers[15] = vector;
|
||||
self.instruction_set = InstructionSet::Arm;
|
||||
self.irq_disabled = true;
|
||||
if exception == Exception::Fiq {
|
||||
self.fiq_disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Sample external interrupt lines between instructions. FIQ has priority.
|
||||
/// Returns true when an unmasked interrupt was taken. Machine connects the GBA IRQ line.
|
||||
pub fn take_interrupt(&mut self, irq: bool, fiq: bool) -> bool {
|
||||
if fiq && !self.fiq_disabled {
|
||||
self.enter_exception(Exception::Fiq);
|
||||
} else if irq && !self.irq_disabled {
|
||||
self.enter_exception(Exception::Irq);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
327
crates/gba-core/src/cpu/exception_tests.rs
Normal file
327
crates/gba-core/src/cpu/exception_tests.rs
Normal file
|
|
@ -0,0 +1,327 @@
|
|||
use super::*;
|
||||
use crate::memory::{BIOS_SIZE, ROM_START};
|
||||
|
||||
fn program(instruction: u32) -> Memory {
|
||||
Memory::new(instruction.to_le_bytes().to_vec()).unwrap()
|
||||
}
|
||||
|
||||
fn vectors(instruction: u32) -> Memory {
|
||||
let bios = instruction.to_le_bytes().repeat(BIOS_SIZE / 4);
|
||||
Memory::with_bios(vec![], bios).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exception_entry_sets_vector_link_masks_and_saved_status() {
|
||||
for (exception, mode, vector, arm_offset, thumb_offset) in [
|
||||
(Exception::UndefinedInstruction, Mode::Undefined, 4, 4, 2),
|
||||
(Exception::SoftwareInterrupt, Mode::Supervisor, 8, 4, 2),
|
||||
(Exception::PrefetchAbort, Mode::Abort, 12, 4, 4),
|
||||
(Exception::DataAbort, Mode::Abort, 16, 8, 8),
|
||||
(Exception::Irq, Mode::Irq, 24, 4, 4),
|
||||
(Exception::Fiq, Mode::Fiq, 28, 4, 4),
|
||||
] {
|
||||
for thumb in [false, true] {
|
||||
for fiq_mask in [0, 0x40] {
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
let status = 0xb000_001f | if thumb { 0x20 } else { 0 } | fiq_mask;
|
||||
cpu.apply_status(status, Mode::System);
|
||||
cpu.registers[13] = 0x0300_0100;
|
||||
cpu.registers[14] = 123;
|
||||
cpu.enter_exception(exception);
|
||||
assert_eq!(cpu.mode(), mode);
|
||||
assert_eq!(cpu.pc(), vector);
|
||||
assert_eq!(
|
||||
cpu.registers[14],
|
||||
ROM_START + if thumb { thumb_offset } else { arm_offset }
|
||||
);
|
||||
assert_eq!(cpu.registers[13], 0);
|
||||
assert_eq!(cpu.spsr(), Some(status));
|
||||
assert_eq!(
|
||||
cpu.cpsr(),
|
||||
0xb000_0080
|
||||
| mode as u32
|
||||
| if exception == Exception::Fiq {
|
||||
0x40
|
||||
} else {
|
||||
fiq_mask
|
||||
}
|
||||
);
|
||||
cpu.switch_mode(Mode::System);
|
||||
assert_eq!(cpu.registers[13..15], [0x0300_0100, 123]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arm_and_thumb_swi_execute_and_movs_pc_restores_caller() {
|
||||
for thumb in [false, true] {
|
||||
for comment in [0, 1, 255] {
|
||||
let rom = if thumb {
|
||||
(0xdf00_u16 | comment).to_le_bytes().to_vec()
|
||||
} else {
|
||||
(0xefab_0000_u32 | u32::from(comment))
|
||||
.to_le_bytes()
|
||||
.to_vec()
|
||||
};
|
||||
let mut memory =
|
||||
Memory::with_bios(rom, 0xe1b0_f00e_u32.to_le_bytes().repeat(BIOS_SIZE / 4))
|
||||
.unwrap();
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
let status = 0x6000_001f | if thumb { 0x20 } else { 0 };
|
||||
cpu.apply_status(status, Mode::System);
|
||||
cpu.registers[14] = 0x1234;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.pc(), 8);
|
||||
assert_eq!(cpu.mode(), Mode::Supervisor);
|
||||
assert_eq!(cpu.instruction_set(), InstructionSet::Arm);
|
||||
// MOVS pc, lr restores the saved flags, not ALU-result flags.
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.pc(), ROM_START + if thumb { 2 } else { 4 });
|
||||
assert_eq!(cpu.cpsr(), status);
|
||||
assert_eq!(cpu.registers[14], 0x1234);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conditional_swi_can_be_skipped_without_entering_supervisor() {
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
let mut expected = cpu.clone();
|
||||
expected.registers[15] += 4;
|
||||
cpu.step(&mut program(0x0fff_ffff)).unwrap();
|
||||
assert_eq!(cpu, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swi_without_vector_image_enters_then_reports_fetch_error() {
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
let mut memory = program(0xef00_0000);
|
||||
cpu.step(&mut memory).unwrap();
|
||||
let before = cpu.clone();
|
||||
assert_eq!(
|
||||
cpu.step(&mut memory),
|
||||
Err(CpuError::Memory(MemoryError::Unmapped(8)))
|
||||
);
|
||||
assert_eq!(cpu, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interrupt_masks_and_fiq_priority_are_respected() {
|
||||
for irq_mask in [false, true] {
|
||||
for fiq_mask in [false, true] {
|
||||
for irq in [false, true] {
|
||||
for fiq in [false, true] {
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.irq_disabled = irq_mask;
|
||||
cpu.fiq_disabled = fiq_mask;
|
||||
let before = cpu.clone();
|
||||
let expected = if fiq && !fiq_mask {
|
||||
Some(Mode::Fiq)
|
||||
} else if irq && !irq_mask {
|
||||
Some(Mode::Irq)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
assert_eq!(cpu.take_interrupt(irq, fiq), expected.is_some());
|
||||
if let Some(mode) = expected {
|
||||
assert_eq!(cpu.mode(), mode);
|
||||
assert_eq!(cpu.spsr(), Some(before.cpsr()));
|
||||
} else {
|
||||
assert_eq!(cpu, before);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subs_exception_returns_resume_or_retry_the_correct_instruction() {
|
||||
for (exception, subtract) in [
|
||||
(Exception::Irq, 4),
|
||||
(Exception::Fiq, 4),
|
||||
(Exception::PrefetchAbort, 4),
|
||||
(Exception::DataAbort, 8),
|
||||
] {
|
||||
for thumb in [false, true] {
|
||||
let mut cpu = Cpu::new(ROM_START + 2 * u32::from(thumb));
|
||||
let status = 0xa000_001f | if thumb { 0x20 } else { 0 };
|
||||
cpu.apply_status(status, Mode::System);
|
||||
let before = cpu.clone();
|
||||
cpu.enter_exception(exception);
|
||||
cpu.step(&mut vectors(0xe25e_f000 | subtract)).unwrap(); // SUBS pc, lr, #offset
|
||||
assert_eq!(cpu.registers, before.registers);
|
||||
assert_eq!(cpu.cpsr(), before.cpsr());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_fiq_preserves_irq_saved_status_and_banked_registers() {
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[8] = 88;
|
||||
cpu.enter_exception(Exception::Irq);
|
||||
cpu.registers[13] = 0x0300_0200;
|
||||
let irq_status = cpu.cpsr();
|
||||
let irq_spsr = cpu.spsr();
|
||||
let irq_link = cpu.registers[14];
|
||||
assert!(cpu.take_interrupt(false, true));
|
||||
cpu.registers[8] = 888;
|
||||
cpu.step(&mut vectors(0xe25e_f004)).unwrap();
|
||||
assert_eq!(cpu.pc(), 0x18);
|
||||
assert_eq!(cpu.cpsr(), irq_status);
|
||||
assert_eq!(cpu.spsr(), irq_spsr);
|
||||
assert_eq!(cpu.registers[13..15], [0x0300_0200, irq_link]);
|
||||
assert_eq!(cpu.registers[8], 88);
|
||||
cpu.step(&mut vectors(0xe25e_f004)).unwrap();
|
||||
assert_eq!(cpu.pc(), ROM_START);
|
||||
assert_eq!(cpu.cpsr(), 0x1f);
|
||||
cpu.switch_mode(Mode::Fiq);
|
||||
assert_eq!(cpu.registers[8], 888);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_mode_exception_overwrites_link_and_saved_status() {
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.enter_exception(Exception::SoftwareInterrupt);
|
||||
cpu.registers[15] = 0x100;
|
||||
let previous = cpu.cpsr();
|
||||
cpu.enter_exception(Exception::SoftwareInterrupt);
|
||||
assert_eq!(cpu.spsr(), Some(previous));
|
||||
assert_eq!(cpu.registers[14], 0x104);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn return_alignment_uses_saved_state_not_target_bit_zero() {
|
||||
for (status, expected) in [(0x1f, ROM_START), (0x3f, ROM_START + 2)] {
|
||||
let mut cpu = Cpu::at_reset();
|
||||
cpu.registers[15] = ROM_START;
|
||||
cpu.registers[14] = ROM_START + 3;
|
||||
cpu.set_spsr(status);
|
||||
cpu.step(&mut program(0xe1b0_f00e)).unwrap();
|
||||
assert_eq!(cpu.pc(), expected);
|
||||
assert_eq!(cpu.cpsr(), status);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_saved_mode_rejects_return_without_partial_changes() {
|
||||
for instruction in [0xe1b0_f00e, 0xe8fd_8001] {
|
||||
let mut cpu = Cpu::at_reset();
|
||||
cpu.registers[15] = ROM_START;
|
||||
cpu.registers[13] = 0x0200_0000;
|
||||
cpu.set_spsr(0); // An invalid mode
|
||||
let before = cpu.clone();
|
||||
assert!(cpu.step(&mut program(instruction)).is_err());
|
||||
assert_eq!(cpu, before);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ldm_exception_return_loads_outgoing_bank_before_restoring_status() {
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[13] = 0x0300_0100;
|
||||
cpu.registers[14] = 123;
|
||||
cpu.enter_exception(Exception::SoftwareInterrupt);
|
||||
cpu.registers[15] = ROM_START;
|
||||
cpu.registers[13] = 0x0200_0000;
|
||||
cpu.set_spsr(0x9000_003f);
|
||||
let mut memory = program(0xe8fd_c001); // LDMIA sp!, {r0, lr, pc}^
|
||||
for (index, value) in [42, 456, ROM_START + 7].into_iter().enumerate() {
|
||||
memory
|
||||
.write32(0x0200_0000 + index as u32 * 4, value)
|
||||
.unwrap();
|
||||
}
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.cpsr(), 0x9000_003f);
|
||||
assert_eq!(cpu.pc(), ROM_START + 6);
|
||||
assert_eq!(cpu.registers[0], 42);
|
||||
assert_eq!(cpu.registers[13..15], [0x0300_0100, 123]);
|
||||
cpu.switch_mode(Mode::Supervisor);
|
||||
assert_eq!(cpu.registers[13..15], [0x0200_000c, 456]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_ldm_return_preserves_every_bank_and_status() {
|
||||
let mut cpu = Cpu::at_reset();
|
||||
cpu.registers[15] = ROM_START;
|
||||
cpu.registers[13] = 0x0400_0054;
|
||||
cpu.set_spsr(0x3f);
|
||||
let mut memory = program(0xe8fd_8001);
|
||||
memory.write32(0x0400_0054, 42).unwrap();
|
||||
let before = cpu.clone();
|
||||
assert!(matches!(cpu.step(&mut memory), Err(CpuError::Memory(_))));
|
||||
assert_eq!(cpu, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_bank_transfers_use_current_base_and_user_values() {
|
||||
for mode in [Mode::System, Mode::Supervisor, Mode::Fiq] {
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
for register in 0..15 {
|
||||
cpu.registers[register] = 100 + register as u32;
|
||||
}
|
||||
cpu.switch_mode(mode);
|
||||
cpu.registers[13] = 0x0200_0100;
|
||||
let original = cpu.clone();
|
||||
let mut memory = program(0xe8cd_7fff); // STMIA sp, {r0-r14}^
|
||||
cpu.step(&mut memory).unwrap();
|
||||
for register in 0..15 {
|
||||
assert_eq!(
|
||||
memory.read32(0x0200_0100 + register as u32 * 4).unwrap(),
|
||||
original.user_register(register)
|
||||
);
|
||||
memory
|
||||
.write32(0x0200_0100 + register as u32 * 4, 200 + register as u32)
|
||||
.unwrap();
|
||||
}
|
||||
// Use RAM for the next instruction without changing the transfer base.
|
||||
memory.write32(0x0300_0000, 0xe8dd_7fff).unwrap(); // LDMIA sp, {r0-r14}^
|
||||
cpu.registers[15] = 0x0300_0000;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.cpsr(), original.cpsr());
|
||||
if mode != Mode::System {
|
||||
assert_eq!(cpu.registers[13..15], original.registers[13..15]);
|
||||
}
|
||||
if mode == Mode::Fiq {
|
||||
assert_eq!(cpu.registers[8..13], original.registers[8..13]);
|
||||
}
|
||||
cpu.switch_mode(Mode::System);
|
||||
for register in 0..15 {
|
||||
assert_eq!(cpu.registers[register], 200 + register as u32);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_bank_store_pc_uses_pipeline_offset() {
|
||||
let mut cpu = Cpu::at_reset();
|
||||
cpu.registers[15] = ROM_START;
|
||||
cpu.registers[0] = 0x0200_0000;
|
||||
let mut memory = program(0xe8c0_8000); // STMIA r0, {pc}^
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(memory.read32(0x0200_0000).unwrap(), ROM_START + 12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_mode_cannot_request_user_bank_transfers_or_status_return() {
|
||||
for instruction in [0xe8c0_0002, 0xe8d0_0002, 0xe8d0_8000, 0xe1b0_f00e] {
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.switch_mode(Mode::User);
|
||||
cpu.registers[0] = 0x0200_0000;
|
||||
let before = cpu.clone();
|
||||
assert!(cpu.step(&mut program(instruction)).is_err());
|
||||
assert_eq!(cpu, before);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_user_bank_load_preserves_hidden_registers() {
|
||||
let mut cpu = Cpu::at_reset();
|
||||
cpu.registers[15] = ROM_START;
|
||||
cpu.registers[0] = 0x0400_0054;
|
||||
let before = cpu.clone();
|
||||
assert!(cpu.step(&mut program(0xe8d0_6000)).is_err()); // LDMIA r0, {sp, lr}^
|
||||
assert_eq!(cpu, before);
|
||||
}
|
||||
560
crates/gba-core/src/cpu/instruction_tests.rs
Normal file
560
crates/gba-core/src/cpu/instruction_tests.rs
Normal file
|
|
@ -0,0 +1,560 @@
|
|||
use super::{
|
||||
alu::{self, Shift},
|
||||
*,
|
||||
};
|
||||
use crate::memory::ROM_START;
|
||||
|
||||
const RAM: u32 = 0x0200_0000;
|
||||
|
||||
fn program(words: &[u32]) -> Memory {
|
||||
Memory::new(words.iter().flat_map(|word| word.to_le_bytes()).collect()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_shifts_match_a_bit_at_a_time_reference() {
|
||||
for value in [0, 1, 0x8000_0000, 0x8000_0001, 0x1234_5678, u32::MAX] {
|
||||
for kind in [Shift::Lsl, Shift::Lsr, Shift::Asr, Shift::Ror] {
|
||||
for incoming_carry in [false, true] {
|
||||
for amount in 0..=255 {
|
||||
let mut expected = value;
|
||||
let mut carry = incoming_carry;
|
||||
for _ in 0..amount {
|
||||
match kind {
|
||||
Shift::Lsl => {
|
||||
carry = expected >> 31 != 0;
|
||||
expected <<= 1;
|
||||
}
|
||||
Shift::Lsr => {
|
||||
carry = expected & 1 != 0;
|
||||
expected >>= 1;
|
||||
}
|
||||
Shift::Asr => {
|
||||
carry = expected & 1 != 0;
|
||||
expected = ((expected as i32) >> 1) as u32;
|
||||
}
|
||||
Shift::Ror => {
|
||||
carry = expected & 1 != 0;
|
||||
expected = (expected >> 1) | (u32::from(carry) << 31);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(
|
||||
alu::shift(value, kind, amount, incoming_carry, true),
|
||||
(expected, carry),
|
||||
"value={value:08x} kind={kind:?} amount={amount}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn immediate_zero_shifts_have_arm_special_meanings() {
|
||||
for carry in [false, true] {
|
||||
let value = 0x8000_0001;
|
||||
assert_eq!(
|
||||
alu::shift(value, Shift::Lsl, 0, carry, false),
|
||||
(value, carry)
|
||||
);
|
||||
assert_eq!(alu::shift(value, Shift::Lsr, 0, carry, false), (0, true));
|
||||
assert_eq!(
|
||||
alu::shift(value, Shift::Asr, 0, carry, false),
|
||||
(u32::MAX, true)
|
||||
);
|
||||
assert_eq!(
|
||||
alu::shift(value, Shift::Ror, 0, carry, false),
|
||||
(0x4000_0000 | (u32::from(carry) << 31), true)
|
||||
);
|
||||
}
|
||||
assert_eq!(alu::shift(1, Shift::Asr, 0, true, false), (0, false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_shift_amount_uses_only_the_low_byte() {
|
||||
assert_eq!(alu::shift(3, Shift::Lsl, 256, true, true), (3, true));
|
||||
assert_eq!(alu::shift(3, Shift::Lsl, 257, true, true), (6, false));
|
||||
let mut memory = program(&[0xe1b0_0311]); // MOVS r0,r1,LSL r3
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[1] = 3;
|
||||
cpu.registers[3] = 257;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.registers[0], 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_data_processing_opcodes_decode_and_preserve_test_destinations() {
|
||||
let expected = [
|
||||
0x10,
|
||||
0x26,
|
||||
0x22,
|
||||
0xffff_ffde,
|
||||
0x46,
|
||||
0x47,
|
||||
0x22,
|
||||
0xffff_ffde,
|
||||
0x10,
|
||||
0x26,
|
||||
0x22,
|
||||
0x46,
|
||||
0x36,
|
||||
0x12,
|
||||
0x24,
|
||||
0xffff_ffed,
|
||||
];
|
||||
for (opcode, result) in expected.into_iter().enumerate() {
|
||||
// opcode{S} r2,r0,r1. Test instructions ignore the destination.
|
||||
let instruction = 0xe010_2001 | ((opcode as u32) << 21);
|
||||
let mut memory = program(&[instruction]);
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = 0x34;
|
||||
cpu.registers[1] = 0x12;
|
||||
cpu.registers[2] = 0xfeed_face;
|
||||
cpu.flags.carry = true;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(
|
||||
cpu.registers[2],
|
||||
if (8..=11).contains(&opcode) {
|
||||
0xfeed_face
|
||||
} else {
|
||||
result
|
||||
}
|
||||
);
|
||||
assert_eq!(cpu.flags.negative, result >> 31 != 0);
|
||||
assert_eq!(cpu.flags.zero, result == 0);
|
||||
assert_eq!(cpu.pc(), ROM_START + 4);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arithmetic_with_carry_matches_wide_signed_and_unsigned_reference() {
|
||||
let values = [0, 1, 0x7fff_ffff, 0x8000_0000, 0x8000_0001, u32::MAX];
|
||||
for opcode in [2, 3, 4, 5, 6, 7, 10, 11] {
|
||||
let mut memory = program(&[0xe010_2001 | (opcode << 21)]);
|
||||
for a in values {
|
||||
for b in values {
|
||||
for carry in [false, true] {
|
||||
let subtract = matches!(opcode, 2 | 3 | 6 | 7 | 10);
|
||||
let reverse = matches!(opcode, 3 | 7);
|
||||
let (a, b) = if reverse { (b, a) } else { (a, b) };
|
||||
let extra = match opcode {
|
||||
5 => i64::from(carry),
|
||||
6 | 7 => i64::from(carry) - 1,
|
||||
_ => 0,
|
||||
};
|
||||
let wide = if subtract {
|
||||
i64::from(a) - i64::from(b) + extra
|
||||
} else {
|
||||
i64::from(a) + i64::from(b) + extra
|
||||
};
|
||||
let signed = if subtract {
|
||||
i64::from(a as i32) - i64::from(b as i32) + extra
|
||||
} else {
|
||||
i64::from(a as i32) + i64::from(b as i32) + extra
|
||||
};
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
(cpu.registers[0], cpu.registers[1]) = if reverse { (b, a) } else { (a, b) };
|
||||
cpu.flags.carry = carry;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
if opcode < 8 {
|
||||
assert_eq!(cpu.registers[2], wide as u32);
|
||||
}
|
||||
assert_eq!(
|
||||
cpu.flags,
|
||||
Flags {
|
||||
negative: (wide as u32) >> 31 != 0,
|
||||
zero: wide as u32 == 0,
|
||||
carry: if subtract {
|
||||
wide >= 0
|
||||
} else {
|
||||
wide > i64::from(u32::MAX)
|
||||
},
|
||||
overflow: i32::try_from(signed).is_err(),
|
||||
},
|
||||
"opcode={opcode:x} a={a:08x} b={b:08x} carry={carry}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn logical_ops_use_shifter_carry_and_preserve_overflow() {
|
||||
for opcode in [0, 1, 8, 9, 12, 13, 14, 15] {
|
||||
let mut memory = program(&[0xe010_2081 | (opcode << 21)]); // r1 LSL #1
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[1] = 0x8000_0001;
|
||||
cpu.flags.overflow = true;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert!(cpu.flags.carry);
|
||||
assert!(cpu.flags.overflow);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arithmetic_uses_old_carry_not_shifter_carry() {
|
||||
let mut memory = program(&[0xe0b0_2081]); // ADCS r2,r0,r1,LSL #1
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[1] = 0x8000_0000; // shift produces carry=1 and value=0
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.registers[2], 0);
|
||||
assert!(!cpu.flags.carry);
|
||||
assert!(cpu.flags.zero);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_flag_setting_register_ops_preserve_flags() {
|
||||
for opcode in [0, 1, 2, 3, 4, 5, 6, 7, 12, 13, 14, 15] {
|
||||
let mut memory = program(&[0xe000_2081 | (opcode << 21)]);
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.flags = Flags {
|
||||
negative: true,
|
||||
zero: true,
|
||||
carry: true,
|
||||
overflow: true,
|
||||
};
|
||||
let before = cpu.flags;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.flags, before);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_specified_shifts_read_pc_plus_twelve_for_both_operands() {
|
||||
for (instruction, expected) in [
|
||||
(0xe1a0_021f, ROM_START + 12), // MOV r0,pc,LSL r2
|
||||
(0xe08f_0211, ROM_START + 12), // ADD r0,pc,r1,LSL r2
|
||||
(0xe1a0_000f, ROM_START + 8), // MOV r0,pc (immediate zero shift)
|
||||
] {
|
||||
let mut memory = program(&[instruction]);
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.registers[0], expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_processing_pc_destination_aligns_without_switching_state() {
|
||||
let mut memory = program(&[0xe1a0_f000]); // MOV pc,r0
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = RAM + 3;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.pc(), RAM);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn branch_link_and_bx_return_execute_a_subroutine() {
|
||||
let mut memory = program(&[
|
||||
0xe3a0_0006, // MOV r0,#6
|
||||
0xe3a0_1007, // MOV r1,#7
|
||||
0xeb00_0001, // BL subroutine at +20
|
||||
0xeaff_fffe, // B .
|
||||
0xe1a0_0000, // NOP, skipped
|
||||
0xe002_0190, // MUL r2,r0,r1
|
||||
0xe12f_ff1e, // BX lr
|
||||
]);
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
for _ in 0..6 {
|
||||
cpu.step(&mut memory).unwrap();
|
||||
}
|
||||
assert_eq!(cpu.registers[2], 42);
|
||||
assert_eq!(cpu.registers[14], ROM_START + 12);
|
||||
assert_eq!(cpu.pc(), ROM_START + 12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bx_thumb_target_switches_state_and_clears_only_bit_zero() {
|
||||
let mut memory = program(&[0xe12f_ff10]);
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = ROM_START + 11;
|
||||
let mut expected = cpu.registers;
|
||||
expected[15] = ROM_START + 10;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
|
||||
assert_eq!(cpu.registers, expected);
|
||||
assert_eq!(cpu.flags, Flags::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skipped_branch_link_does_not_change_lr() {
|
||||
let mut memory = program(&[0x0b00_0000]); // BLEQ with Z clear
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[14] = 123;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.registers[14], 123);
|
||||
assert_eq!(cpu.pc(), ROM_START + 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn word_transfers_cover_pre_post_up_down_and_writeback() {
|
||||
for pre in [false, true] {
|
||||
for up in [false, true] {
|
||||
for explicit_writeback in [false, true] {
|
||||
if !pre && explicit_writeback {
|
||||
continue;
|
||||
} // T form is intentionally unsupported.
|
||||
let fields = (u32::from(pre) << 24)
|
||||
| (u32::from(up) << 23)
|
||||
| (u32::from(explicit_writeback) << 21);
|
||||
let mut memory = program(&[0xe400_1004 | fields, 0xe410_2004 | fields]);
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
let base = RAM + 16;
|
||||
let adjusted = if up { base + 4 } else { base - 4 };
|
||||
let address = if pre { adjusted } else { base };
|
||||
cpu.registers[0] = base;
|
||||
cpu.registers[1] = 0x1234_5678;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(memory.read32(address).unwrap(), 0x1234_5678);
|
||||
assert_eq!(
|
||||
cpu.registers[0],
|
||||
if !pre || explicit_writeback {
|
||||
adjusted
|
||||
} else {
|
||||
base
|
||||
}
|
||||
);
|
||||
cpu.registers[0] = base;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.registers[2], 0x1234_5678);
|
||||
assert_eq!(
|
||||
cpu.registers[0],
|
||||
if !pre || explicit_writeback {
|
||||
adjusted
|
||||
} else {
|
||||
base
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn byte_transfers_zero_extend_and_preserve_neighbors() {
|
||||
let mut memory = program(&[0xe5c0_1001, 0xe5d0_2001]); // STRB r1,[r0,#1]; LDRB r2,[r0,#1]
|
||||
memory.write32(RAM, 0xffff_ffff).unwrap();
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = RAM;
|
||||
cpu.registers[1] = 0x1234_5680;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.registers[2], 0x80);
|
||||
assert_eq!(memory.read32(RAM).unwrap(), 0xffff_80ff);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn word_loads_rotate_and_stores_align_down() {
|
||||
for offset in 0..4 {
|
||||
let mut memory = program(&[0xe590_1000, 0xe580_2000]);
|
||||
memory.write32(RAM, 0x1234_5678).unwrap();
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = RAM + offset;
|
||||
cpu.registers[2] = 0xaabb_ccdd;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.registers[1], 0x1234_5678_u32.rotate_right(offset * 8));
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(memory.read32(RAM).unwrap(), 0xaabb_ccdd);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_offset_load_uses_shifter_without_changing_flags() {
|
||||
let mut memory = program(&[0xe790_1102]); // LDR r1,[r0,r2,LSL #2]
|
||||
memory.write32(RAM + 12, 42).unwrap();
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = RAM;
|
||||
cpu.registers[2] = 3;
|
||||
cpu.flags.carry = true;
|
||||
let flags = cpu.flags;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.registers[1], 42);
|
||||
assert_eq!(cpu.flags, flags);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pc_relative_load_and_pc_store_use_different_pipeline_offsets() {
|
||||
let mut memory = program(&[0xe59f_1000, 0xe580_f000, 42]);
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = RAM;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.registers[1], 42);
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(memory.read32(RAM).unwrap(), ROM_START + 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ldr_pc_aligns_target_without_thumb_exchange() {
|
||||
let mut memory = program(&[0xe590_f000]);
|
||||
memory.write32(RAM, ROM_START + 15).unwrap();
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = RAM;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.pc(), ROM_START + 12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn store_base_with_writeback_stores_original_base_value() {
|
||||
let mut memory = program(&[0xe480_0004]); // STR r0,[r0],#4
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = RAM;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(memory.read32(RAM).unwrap(), RAM);
|
||||
assert_eq!(cpu.registers[0], RAM + 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn halfword_and_signed_loads_handle_arm7_odd_addresses() {
|
||||
for (instruction, offset, expected) in [
|
||||
(0xe1d0_10b0, 0, 0x80fe), // LDRH
|
||||
(0xe1d0_10b0, 1, 0xfe00_0080), // odd LDRH rotates in 32 bits
|
||||
(0xe1d0_10d0, 0, 0xffff_fffe), // LDRSB
|
||||
(0xe1d0_10f0, 0, 0xffff_80fe), // LDRSH
|
||||
(0xe1d0_10f0, 1, 0xffff_ff80), // odd LDRSH becomes LDRSB
|
||||
] {
|
||||
let mut memory = program(&[instruction]);
|
||||
memory.write16(RAM, 0x80fe).unwrap();
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = RAM + offset;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.registers[1], expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn halfword_store_aligns_down_and_updates_the_unaligned_base() {
|
||||
let mut memory = program(&[0xe0c0_10b2]); // STRH r1,[r0],#2
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = RAM + 1;
|
||||
cpu.registers[1] = 0x1234_abcd;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(memory.read16(RAM).unwrap(), 0xabcd);
|
||||
assert_eq!(cpu.registers[0], RAM + 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn halfword_register_offsets_and_split_immediate_are_decoded() {
|
||||
for (instruction, offset) in [(0xe190_10b2, 6), (0xe1d0_12b4, 0x24)] {
|
||||
let mut memory = program(&[instruction]);
|
||||
memory.write16(RAM + offset, 0x1234).unwrap();
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = RAM;
|
||||
cpu.registers[2] = 6;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.registers[1], 0x1234);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiply_and_accumulate_wrap_and_set_only_defined_flags() {
|
||||
for (instruction, expected) in [(0xe012_0190, u32::MAX - 1), (0xe032_3190, 3)] {
|
||||
let mut memory = program(&[instruction]); // MULS/MLAS r2,r0,r1[,r3]
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = u32::MAX;
|
||||
cpu.registers[1] = 2;
|
||||
cpu.registers[3] = 5;
|
||||
cpu.flags.carry = true;
|
||||
cpu.flags.overflow = true;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.registers[2], expected);
|
||||
assert_eq!(cpu.flags.negative, expected >> 31 != 0);
|
||||
assert_eq!(cpu.flags.zero, expected == 0);
|
||||
assert!(cpu.flags.carry); // Deterministic policy for unspecified output.
|
||||
assert!(cpu.flags.overflow);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn long_multiply_supports_signed_unsigned_and_accumulation() {
|
||||
for (instruction, expected) in [
|
||||
(0xe093_2190, 0x0000_0001_ffff_fffe_u64), // UMULLS r2,r3,r0,r1
|
||||
(0xe0b3_2190, 0x0000_0002_0000_0003), // UMLALS (+5)
|
||||
(0xe0d3_2190, 0xffff_ffff_ffff_fffe), // SMULLS (-1 * 2)
|
||||
(0xe0f3_2190, 3), // SMLALS (-2 + 5)
|
||||
] {
|
||||
let mut memory = program(&[instruction]);
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = u32::MAX;
|
||||
cpu.registers[1] = 2;
|
||||
cpu.registers[2] = 5;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(
|
||||
(u64::from(cpu.registers[3]) << 32) | u64::from(cpu.registers[2]),
|
||||
expected
|
||||
);
|
||||
assert_eq!(cpu.flags.negative, expected >> 63 != 0);
|
||||
assert_eq!(cpu.flags.zero, expected == 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn long_multiply_zero_flag_uses_both_halves() {
|
||||
let mut memory = program(&[0xe093_2190]);
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = 0x8000_0000;
|
||||
cpu.registers[1] = 2;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.registers[2], 0);
|
||||
assert_eq!(cpu.registers[3], 1);
|
||||
assert!(!cpu.flags.zero);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_or_unimplemented_encodings_do_not_change_state_or_memory() {
|
||||
for instruction in [
|
||||
0xe1a0_0f11, // Rs=pc (register shift)
|
||||
0xe3b0_f001, // status restore via MOVS pc
|
||||
0xe490_0004, // LDR r0,[r0],#4 (base/destination overlap)
|
||||
0xe5bf_1000, // LDR with PC writeback
|
||||
0xe4b0_1004, // LDRT user-mode transfer
|
||||
0xe5d0_f000, // LDRB pc
|
||||
0xe790_1012, // illegal register-specified memory shift
|
||||
0xe790_100f, // memory offset register PC
|
||||
0xe1c0_10d0, // ARMv5 doubleword load, not an ARM7 signed store
|
||||
0xe000_0190, // MUL Rd=Rm
|
||||
0xe082_2190, // UMULL RdHi=RdLo
|
||||
0xe8f0_0001, // LDM user-bank transfer with writeback
|
||||
0xe100_f090, // SWP with PC destination
|
||||
0xe12f_ff30, // BLX, ARMv5 only
|
||||
] {
|
||||
let mut memory = program(&[instruction]);
|
||||
memory.write32(RAM, 0xdead_beef).unwrap();
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = RAM;
|
||||
cpu.registers[1] = 123;
|
||||
let before = cpu.registers;
|
||||
let flags = cpu.flags;
|
||||
assert!(
|
||||
matches!(
|
||||
cpu.step(&mut memory),
|
||||
Err(CpuError::UnsupportedInstruction { .. })
|
||||
),
|
||||
"{instruction:08x}"
|
||||
);
|
||||
assert_eq!(cpu.registers, before);
|
||||
assert_eq!(cpu.flags, flags);
|
||||
assert_eq!(memory.read32(RAM).unwrap(), 0xdead_beef);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_memory_access_does_not_commit_writeback_or_destination() {
|
||||
for instruction in [0xe490_1004, 0xe480_1004, 0xe0d0_10b2, 0xe0c0_10b2] {
|
||||
let mut memory = program(&[instruction]);
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[1] = 123;
|
||||
let before = cpu.registers;
|
||||
assert!(matches!(cpu.step(&mut memory), Err(CpuError::Memory(_))));
|
||||
assert_eq!(cpu.registers, before);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conditional_store_is_skipped_without_touching_memory() {
|
||||
let mut memory = program(&[0x0580_1000]); // STREQ with Z clear
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = RAM;
|
||||
cpu.registers[1] = 123;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(memory.read32(RAM).unwrap(), 0);
|
||||
}
|
||||
183
crates/gba-core/src/cpu/status.rs
Normal file
183
crates/gba-core/src/cpu/status.rs
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
//! ARM7 processor modes, register banks, and status-register transfers.
|
||||
|
||||
use super::{Cpu, CpuError, Flags, InstructionSet};
|
||||
|
||||
/// ARMv4T processor mode. User and System share a register bank.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u32)]
|
||||
pub enum Mode {
|
||||
User = 0x10,
|
||||
Fiq = 0x11,
|
||||
Irq = 0x12,
|
||||
Supervisor = 0x13,
|
||||
Abort = 0x17,
|
||||
Undefined = 0x1b,
|
||||
System = 0x1f,
|
||||
}
|
||||
|
||||
impl Mode {
|
||||
fn decode(status: u32) -> Option<Self> {
|
||||
Some(match status & 31 {
|
||||
0x10 => Self::User,
|
||||
0x11 => Self::Fiq,
|
||||
0x12 => Self::Irq,
|
||||
0x13 => Self::Supervisor,
|
||||
0x17 => Self::Abort,
|
||||
0x1b => Self::Undefined,
|
||||
0x1f => Self::System,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
fn bank(self) -> usize {
|
||||
match self {
|
||||
Self::User | Self::System => 0,
|
||||
Self::Fiq => 1,
|
||||
Self::Irq => 2,
|
||||
Self::Supervisor => 3,
|
||||
Self::Abort => 4,
|
||||
Self::Undefined => 5,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub(super) struct Banks {
|
||||
high: [[u32; 5]; 2], // r8-r12: shared and FIQ
|
||||
sp_lr: [[u32; 2]; 6],
|
||||
spsr: [u32; 5],
|
||||
}
|
||||
|
||||
impl Cpu {
|
||||
pub fn mode(&self) -> Mode {
|
||||
self.mode
|
||||
}
|
||||
|
||||
/// Current program status register. Reserved bits read as zero.
|
||||
pub fn cpsr(&self) -> u32 {
|
||||
(u32::from(self.flags.negative) << 31)
|
||||
| (u32::from(self.flags.zero) << 30)
|
||||
| (u32::from(self.flags.carry) << 29)
|
||||
| (u32::from(self.flags.overflow) << 28)
|
||||
| (u32::from(self.irq_disabled) << 7)
|
||||
| (u32::from(self.fiq_disabled) << 6)
|
||||
| (u32::from(self.instruction_set == InstructionSet::Thumb) << 5)
|
||||
| self.mode as u32
|
||||
}
|
||||
|
||||
/// Saved program status register. User and System have no SPSR.
|
||||
pub fn spsr(&self) -> Option<u32> {
|
||||
self.mode
|
||||
.bank()
|
||||
.checked_sub(1)
|
||||
.map(|bank| self.banks.spsr[bank])
|
||||
}
|
||||
|
||||
pub(super) fn set_spsr(&mut self, value: u32) {
|
||||
self.banks.spsr[self.mode.bank() - 1] = value;
|
||||
}
|
||||
|
||||
pub(super) fn switch_mode(&mut self, mode: Mode) {
|
||||
// Save the active bank before loading the new one. User/System share it.
|
||||
self.banks.high[usize::from(self.mode == Mode::Fiq)]
|
||||
.copy_from_slice(&self.registers[8..13]);
|
||||
self.banks.sp_lr[self.mode.bank()].copy_from_slice(&self.registers[13..15]);
|
||||
self.registers[8..13].copy_from_slice(&self.banks.high[usize::from(mode == Mode::Fiq)]);
|
||||
self.registers[13..15].copy_from_slice(&self.banks.sp_lr[mode.bank()]);
|
||||
self.mode = mode;
|
||||
}
|
||||
|
||||
// Callers validate the mode before committing any CPU or memory changes.
|
||||
pub(super) fn apply_status(&mut self, value: u32, mode: Mode) {
|
||||
self.switch_mode(mode);
|
||||
self.flags = Flags {
|
||||
negative: value & (1 << 31) != 0,
|
||||
zero: value & (1 << 30) != 0,
|
||||
carry: value & (1 << 29) != 0,
|
||||
overflow: value & (1 << 28) != 0,
|
||||
};
|
||||
self.irq_disabled = value & (1 << 7) != 0;
|
||||
self.fiq_disabled = value & (1 << 6) != 0;
|
||||
self.instruction_set = if value & (1 << 5) == 0 {
|
||||
InstructionSet::Arm
|
||||
} else {
|
||||
InstructionSet::Thumb
|
||||
};
|
||||
}
|
||||
|
||||
pub(super) fn return_status(&self, instruction: u32) -> Result<(u32, Mode), CpuError> {
|
||||
let value = self.spsr().ok_or_else(|| self.unsupported(instruction))?;
|
||||
let mode = Mode::decode(value).ok_or_else(|| self.unsupported(instruction))?;
|
||||
Ok((value, mode))
|
||||
}
|
||||
|
||||
pub(super) fn user_register(&self, register: usize) -> u32 {
|
||||
match register {
|
||||
8..=12 if self.mode == Mode::Fiq => self.banks.high[0][register - 8],
|
||||
13..=14 if self.mode.bank() != 0 => self.banks.sp_lr[0][register - 13],
|
||||
_ => self.operand_register(register, 12),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn set_user_register(&mut self, register: usize, value: u32) {
|
||||
match register {
|
||||
8..=12 if self.mode == Mode::Fiq => self.banks.high[0][register - 8] = value,
|
||||
13..=14 if self.mode.bank() != 0 => self.banks.sp_lr[0][register - 13] = value,
|
||||
_ => self.registers[register] = value,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn read_status(&mut self, instruction: u32) -> Result<(), CpuError> {
|
||||
let destination = ((instruction >> 12) & 15) as usize;
|
||||
if destination == 15 {
|
||||
return Err(self.unsupported(instruction));
|
||||
}
|
||||
let value = if instruction & (1 << 22) == 0 {
|
||||
self.cpsr()
|
||||
} else {
|
||||
self.spsr().ok_or_else(|| self.unsupported(instruction))?
|
||||
};
|
||||
self.registers[destination] = value;
|
||||
self.registers[15] = self.pc().wrapping_add(4);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn write_status(&mut self, instruction: u32) -> Result<(), CpuError> {
|
||||
let value = if instruction & (1 << 25) != 0 {
|
||||
(instruction & 255).rotate_right(((instruction >> 8) & 15) * 2)
|
||||
} else {
|
||||
let source = (instruction & 15) as usize;
|
||||
if source == 15 {
|
||||
return Err(self.unsupported(instruction));
|
||||
}
|
||||
self.registers[source]
|
||||
};
|
||||
let saved = instruction & (1 << 22) != 0;
|
||||
let previous = if saved {
|
||||
self.spsr().ok_or_else(|| self.unsupported(instruction))?
|
||||
} else {
|
||||
self.cpsr()
|
||||
};
|
||||
let mut mask = 0;
|
||||
if instruction & (1 << 19) != 0 {
|
||||
mask |= 0xf000_0000;
|
||||
}
|
||||
if instruction & (1 << 16) != 0 && (saved || self.mode != Mode::User) {
|
||||
mask |= 0xff;
|
||||
}
|
||||
let next = (previous & !mask) | (value & mask);
|
||||
if saved {
|
||||
// Invalid mode bits can be stored, but cannot be used for a return.
|
||||
self.set_spsr(next);
|
||||
} else {
|
||||
let mode = Mode::decode(next).ok_or_else(|| self.unsupported(instruction))?;
|
||||
// ARM7 forbids changing T through MSR CPSR. Diagnose that form.
|
||||
if (previous ^ next) & (1 << 5) != 0 {
|
||||
return Err(self.unsupported(instruction));
|
||||
}
|
||||
self.apply_status(next, mode);
|
||||
}
|
||||
self.registers[15] = self.pc().wrapping_add(4);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
195
crates/gba-core/src/cpu/status_tests.rs
Normal file
195
crates/gba-core/src/cpu/status_tests.rs
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
use super::*;
|
||||
use crate::memory::ROM_START;
|
||||
|
||||
const MODES: [Mode; 7] = [
|
||||
Mode::User,
|
||||
Mode::Fiq,
|
||||
Mode::Irq,
|
||||
Mode::Supervisor,
|
||||
Mode::Abort,
|
||||
Mode::Undefined,
|
||||
Mode::System,
|
||||
];
|
||||
|
||||
fn program(instruction: u32) -> Memory {
|
||||
Memory::new(instruction.to_le_bytes().to_vec()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_start_and_reset_have_explicit_status() {
|
||||
let cpu = Cpu::new(ROM_START);
|
||||
assert_eq!(cpu.mode(), Mode::System);
|
||||
assert_eq!(cpu.cpsr(), 0x1f);
|
||||
assert_eq!(cpu.spsr(), None);
|
||||
let cpu = Cpu::at_reset();
|
||||
assert_eq!(cpu.pc(), 0);
|
||||
assert_eq!(cpu.cpsr(), 0xd3);
|
||||
assert_eq!(cpu.spsr(), Some(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_mode_has_the_correct_shared_and_private_registers() {
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
for register in 0..15 {
|
||||
cpu.registers[register] = register as u32 + 100;
|
||||
}
|
||||
// Initialize all five exception banks independently.
|
||||
for (index, mode) in MODES[1..6].iter().copied().enumerate() {
|
||||
cpu.switch_mode(mode);
|
||||
assert_eq!(
|
||||
cpu.registers[0..8],
|
||||
[100, 101, 102, 103, 104, 105, 106, 107]
|
||||
);
|
||||
if mode == Mode::Fiq {
|
||||
assert_eq!(cpu.registers[8..13], [0; 5]);
|
||||
cpu.registers[8..13].copy_from_slice(&[208, 209, 210, 211, 212]);
|
||||
} else {
|
||||
assert_eq!(cpu.registers[8..13], [108, 109, 110, 111, 112]);
|
||||
}
|
||||
assert_eq!(cpu.registers[13..15], [0, 0]);
|
||||
cpu.registers[13] = 300 + index as u32;
|
||||
cpu.registers[14] = 400 + index as u32;
|
||||
cpu.set_spsr(500 + index as u32);
|
||||
}
|
||||
for (index, mode) in MODES[1..6].iter().copied().enumerate() {
|
||||
cpu.switch_mode(mode);
|
||||
assert_eq!(
|
||||
cpu.registers[13..15],
|
||||
[300 + index as u32, 400 + index as u32]
|
||||
);
|
||||
assert_eq!(cpu.spsr(), Some(500 + index as u32));
|
||||
}
|
||||
cpu.switch_mode(Mode::User);
|
||||
assert_eq!(cpu.registers[8..15], [108, 109, 110, 111, 112, 113, 114]);
|
||||
cpu.registers[13] = 999;
|
||||
cpu.switch_mode(Mode::System);
|
||||
assert_eq!(cpu.registers[13], 999);
|
||||
assert_eq!(cpu.spsr(), None);
|
||||
cpu.switch_mode(Mode::Fiq);
|
||||
assert_eq!(cpu.registers[8..13], [208, 209, 210, 211, 212]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cpsr_round_trips_all_modes_flags_masks_and_states() {
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
for mode in MODES {
|
||||
for flags in 0..16 {
|
||||
for control in 0..8 {
|
||||
let value = (flags << 28) | (control << 5) | mode as u32;
|
||||
cpu.apply_status(value, mode);
|
||||
assert_eq!(cpu.cpsr(), value);
|
||||
assert_eq!(cpu.mode(), mode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mrs_reads_current_and_saved_status() {
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.apply_status(0xa000_00d3, Mode::Supervisor);
|
||||
cpu.set_spsr(0x5000_0030);
|
||||
let previous = cpu.cpsr();
|
||||
cpu.step(&mut program(0xe10f_0000)).unwrap(); // MRS r0, CPSR
|
||||
assert_eq!(cpu.registers[0], previous);
|
||||
assert_eq!(cpu.cpsr(), previous);
|
||||
cpu.registers[15] = ROM_START;
|
||||
cpu.step(&mut program(0xe14f_1000)).unwrap(); // MRS r1, SPSR
|
||||
assert_eq!(cpu.registers[1], 0x5000_0030);
|
||||
assert_eq!(cpu.pc(), ROM_START + 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn msr_register_and_immediate_write_selected_fields_only() {
|
||||
for fields in 0..16 {
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = 0xffff_ffd3; // reserved bits are ignored
|
||||
cpu.step(&mut program(0xe120_f000 | (fields << 16)))
|
||||
.unwrap();
|
||||
let expected = if fields & 1 != 0 { 0xd3 } else { 0x1f }
|
||||
| if fields & 8 != 0 { 0xf000_0000 } else { 0 };
|
||||
assert_eq!(cpu.cpsr(), expected);
|
||||
}
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.step(&mut program(0xe328_f480)).unwrap(); // MSR CPSR_f, #0x80000000
|
||||
assert_eq!(cpu.cpsr(), 0x8000_001f);
|
||||
cpu.registers[15] = ROM_START;
|
||||
cpu.step(&mut program(0xe321_f0d3)).unwrap(); // MSR CPSR_c, #0xd3
|
||||
assert_eq!(cpu.cpsr(), 0x8000_00d3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn msr_reads_old_banked_operand_before_switching_mode() {
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[13] = 0xd3;
|
||||
cpu.step(&mut program(0xe121_f00d)).unwrap(); // MSR CPSR_c, sp
|
||||
assert_eq!(cpu.mode(), Mode::Supervisor);
|
||||
assert_eq!(cpu.registers[13], 0);
|
||||
cpu.switch_mode(Mode::System);
|
||||
assert_eq!(cpu.registers[13], 0xd3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_msr_changes_flags_but_cannot_change_control() {
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.switch_mode(Mode::User);
|
||||
cpu.registers[0] = 0xf000_00ff; // Includes T, invalid mode, and interrupt masks
|
||||
cpu.step(&mut program(0xe12f_f000)).unwrap();
|
||||
assert_eq!(cpu.cpsr(), 0xf000_0010);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn msr_spsr_accepts_thumb_and_preserves_unselected_fields() {
|
||||
for fields in 0..16 {
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.switch_mode(Mode::Supervisor);
|
||||
cpu.set_spsr(0x2000_001f);
|
||||
cpu.registers[0] = 0x9fff_fff0;
|
||||
cpu.step(&mut program(0xe160_f000 | (fields << 16)))
|
||||
.unwrap();
|
||||
assert_eq!(cpu.cpsr(), 0x13);
|
||||
let expected = if fields & 1 != 0 { 0xf0 } else { 0x1f }
|
||||
| if fields & 8 != 0 {
|
||||
0x9000_0000
|
||||
} else {
|
||||
0x2000_0000
|
||||
};
|
||||
assert_eq!(cpu.spsr(), Some(expected));
|
||||
}
|
||||
let mut cpu = Cpu::at_reset();
|
||||
cpu.registers[15] = ROM_START;
|
||||
cpu.step(&mut program(0xe361_f03f)).unwrap(); // MSR SPSR_c, #0x3f
|
||||
assert_eq!(cpu.spsr(), Some(0x3f));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_status_transfers_preserve_all_banks() {
|
||||
for instruction in [
|
||||
0xe10f_f000, // MRS pc, CPSR
|
||||
0xe14f_0000, // MRS r0, SPSR without an SPSR
|
||||
0xe161_f000, // MSR SPSR_c, r0 without an SPSR
|
||||
0xe121_f00f, // MSR CPSR_c, pc
|
||||
0xe321_f000, // Invalid mode
|
||||
0xe321_f03f, // Attempt to change T
|
||||
] {
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
let before = cpu.clone();
|
||||
assert!(
|
||||
cpu.step(&mut program(instruction)).is_err(),
|
||||
"{instruction:08x}"
|
||||
);
|
||||
assert_eq!(cpu, before);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conditional_status_transfers_do_not_execute_when_condition_fails() {
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
for instruction in [0x010f_0000, 0x0121_f000, 0x0161_f000] {
|
||||
cpu.registers[15] = ROM_START;
|
||||
let mut expected = cpu.clone();
|
||||
expected.registers[15] += 4;
|
||||
cpu.step(&mut program(instruction)).unwrap();
|
||||
assert_eq!(cpu, expected);
|
||||
}
|
||||
}
|
||||
210
crates/gba-core/src/cpu/tests.rs
Normal file
210
crates/gba-core/src/cpu/tests.rs
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
use super::*;
|
||||
use crate::memory::ROM_START;
|
||||
|
||||
fn program(words: &[u32]) -> Memory {
|
||||
Memory::new(words.iter().flat_map(|word| word.to_le_bytes()).collect()).unwrap()
|
||||
}
|
||||
|
||||
fn flags_from_bits(bits: u32) -> Flags {
|
||||
Flags {
|
||||
negative: bits & 8 != 0,
|
||||
zero: bits & 4 != 0,
|
||||
carry: bits & 2 != 0,
|
||||
overflow: bits & 1 != 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_conditions_execute_or_skip_for_every_flag_combination() {
|
||||
// Each bit marks a passing NZCV combination, indexed as N*8+Z*4+C*2+V.
|
||||
// Conditions in order: EQ NE CS CC MI PL VS VC HI LS GE LT GT LE AL.
|
||||
let truth_tables = [
|
||||
0xf0f0_u16, 0x0f0f, 0xcccc, 0x3333, 0xff00, 0x00ff, 0xaaaa, 0x5555, 0x0c0c, 0xf3f3, 0xaa55,
|
||||
0x55aa, 0x0a05, 0xf5fa, 0xffff,
|
||||
];
|
||||
for (condition, truth_table) in truth_tables.into_iter().enumerate() {
|
||||
for instruction in [0x03a0_002a, 0x0a00_0000] {
|
||||
// MOV r0, #42; B +8
|
||||
let mut memory = program(&[instruction | ((condition as u32) << 28)]);
|
||||
for bits in 0..16 {
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.flags = flags_from_bits(bits);
|
||||
let before = cpu.flags();
|
||||
let passes = truth_table & (1 << bits) != 0;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
let branch = instruction == 0x0a00_0000;
|
||||
assert_eq!(cpu.registers()[0], if passes && !branch { 42 } else { 0 });
|
||||
assert_eq!(cpu.pc(), ROM_START + if passes && branch { 8 } else { 4 });
|
||||
assert_eq!(cpu.flags(), before);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arithmetic_flags_match_wide_integer_reference() {
|
||||
// Exercise every rotated-immediate encoding against boundary operands.
|
||||
let operands = [
|
||||
0,
|
||||
1,
|
||||
127,
|
||||
255,
|
||||
0x7fff_fffe,
|
||||
0x7fff_ffff,
|
||||
0x8000_0000,
|
||||
0x8000_0001,
|
||||
u32::MAX,
|
||||
];
|
||||
let address = 0x0200_0000;
|
||||
let mut memory = Memory::new(vec![]).unwrap();
|
||||
for opcode in [0x2, 0x4, 0xa] {
|
||||
for encoding in 0..4096_u32 {
|
||||
let immediate = (encoding & 0xff).rotate_right(((encoding >> 8) & 15) * 2);
|
||||
// CMP has no destination; ADD/SUB write r1 from r0.
|
||||
let destination = if opcode == 0xa { 0 } else { 1 };
|
||||
let instruction = 0xe210_0000 | (opcode << 21) | (destination << 12) | encoding;
|
||||
for (offset, byte) in instruction.to_le_bytes().into_iter().enumerate() {
|
||||
memory.write8(address + offset as u32, byte).unwrap();
|
||||
}
|
||||
for operand in operands {
|
||||
let mut cpu = Cpu::new(address);
|
||||
cpu.registers[0] = operand;
|
||||
cpu.registers[1] = 0x1234_5678;
|
||||
cpu.flags = flags_from_bits(15);
|
||||
let mut expected_registers = *cpu.registers();
|
||||
let (unsigned_result, signed_result, carry) = if opcode == 0x4 {
|
||||
let wide = u64::from(operand) + u64::from(immediate);
|
||||
(
|
||||
wide as u32,
|
||||
i64::from(operand as i32) + i64::from(immediate as i32),
|
||||
wide > u64::from(u32::MAX),
|
||||
)
|
||||
} else {
|
||||
let wide = i64::from(operand) - i64::from(immediate);
|
||||
(
|
||||
wide as u32,
|
||||
i64::from(operand as i32) - i64::from(immediate as i32),
|
||||
operand >= immediate,
|
||||
)
|
||||
};
|
||||
if opcode != 0xa {
|
||||
expected_registers[1] = unsigned_result;
|
||||
}
|
||||
expected_registers[15] += 4;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.registers(), &expected_registers);
|
||||
assert_eq!(
|
||||
cpu.flags(),
|
||||
Flags {
|
||||
negative: unsigned_result >> 31 != 0,
|
||||
zero: unsigned_result == 0,
|
||||
carry,
|
||||
overflow: i32::try_from(signed_result).is_err(),
|
||||
},
|
||||
"opcode={opcode:x} operand={operand:08x} encoding={encoding:03x}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn movs_uses_shifter_carry_and_preserves_overflow() {
|
||||
for (instruction, result, shifted_carry) in [
|
||||
(0xe3b0_0000, 0, None), // MOVS r0, #0, no rotation
|
||||
(0xe3b0_00ff, 255, None), // MOVS r0, #255, no rotation
|
||||
(0xe3b0_0480, 0x8000_0000, Some(true)), // rotated immediate
|
||||
(0xe3b0_0401, 0x0100_0000, Some(false)),
|
||||
(0xe3b0_0400, 0, Some(false)), // rotated zero still clears carry
|
||||
] {
|
||||
let mut memory = program(&[instruction]);
|
||||
for bits in 0..16 {
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.flags = flags_from_bits(bits);
|
||||
let before = cpu.flags();
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.registers()[0], result);
|
||||
assert_eq!(
|
||||
cpu.flags(),
|
||||
Flags {
|
||||
negative: result >> 31 != 0,
|
||||
zero: result == 0,
|
||||
carry: shifted_carry.unwrap_or(before.carry),
|
||||
overflow: before.overflow,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn instructions_without_s_preserve_all_flags() {
|
||||
for instruction in [0xe3a0_0480, 0xe280_0001, 0xe240_0001] {
|
||||
let mut memory = program(&[instruction]);
|
||||
for bits in 0..16 {
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.flags = flags_from_bits(bits);
|
||||
let before = cpu.flags();
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.flags(), before);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_condition_skips_flag_updates_and_unsupported_opcodes() {
|
||||
for instruction in [0x03b0_0000, 0x0f00_0000, 0x0350_0000] {
|
||||
// EQ is false, so MOVS, SWI, and CMP must all be skipped.
|
||||
let mut memory = program(&[instruction]);
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.flags = flags_from_bits(11); // N=1 Z=0 C=1 V=1
|
||||
let before = cpu.flags();
|
||||
let mut registers = *cpu.registers();
|
||||
registers[15] += 4;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.registers(), ®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
|
||||
}
|
||||
);
|
||||
}
|
||||
314
crates/gba-core/src/cpu/thumb.rs
Normal file
314
crates/gba-core/src/cpu/thumb.rs
Normal file
|
|
@ -0,0 +1,314 @@
|
|||
//! ARMv4T Thumb instructions, including software interrupts.
|
||||
|
||||
use super::{
|
||||
alu::{self, Shift},
|
||||
Cpu, CpuError,
|
||||
};
|
||||
use crate::memory::Memory;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum Transfer {
|
||||
StoreWord,
|
||||
StoreHalf,
|
||||
StoreByte,
|
||||
LoadSignedByte,
|
||||
LoadWord,
|
||||
LoadHalf,
|
||||
LoadByte,
|
||||
LoadSignedHalf,
|
||||
}
|
||||
|
||||
impl Cpu {
|
||||
pub(super) fn execute_thumb(
|
||||
&mut self,
|
||||
instruction: u16,
|
||||
memory: &mut Memory,
|
||||
) -> Result<(), CpuError> {
|
||||
let instruction = u32::from(instruction);
|
||||
let next_pc = self.pc().wrapping_add(2);
|
||||
match instruction {
|
||||
0x0000..=0x17ff => {
|
||||
let source = ((instruction >> 3) & 7) as usize;
|
||||
let destination = (instruction & 7) as usize;
|
||||
let (value, carry) = alu::shift(
|
||||
self.registers[source],
|
||||
Shift::decode(instruction >> 11),
|
||||
(instruction >> 6) & 31,
|
||||
self.flags.carry,
|
||||
false,
|
||||
);
|
||||
self.thumb_result(destination, 0xd, 0, value, carry);
|
||||
}
|
||||
0x1800..=0x1fff => {
|
||||
let source = ((instruction >> 3) & 7) as usize;
|
||||
let destination = (instruction & 7) as usize;
|
||||
let operand = (instruction >> 6) & 7;
|
||||
let operand = if instruction & (1 << 10) != 0 {
|
||||
operand
|
||||
} else {
|
||||
self.registers[operand as usize]
|
||||
};
|
||||
let opcode = if instruction & (1 << 9) != 0 { 2 } else { 4 };
|
||||
self.thumb_result(
|
||||
destination,
|
||||
opcode,
|
||||
self.registers[source],
|
||||
operand,
|
||||
self.flags.carry,
|
||||
);
|
||||
}
|
||||
0x2000..=0x3fff => {
|
||||
let destination = ((instruction >> 8) & 7) as usize;
|
||||
let opcode = [0xd, 0xa, 4, 2][((instruction >> 11) & 3) as usize];
|
||||
self.thumb_result(
|
||||
destination,
|
||||
opcode,
|
||||
self.registers[destination],
|
||||
instruction & 255,
|
||||
self.flags.carry,
|
||||
);
|
||||
}
|
||||
0x4000..=0x43ff => self.thumb_register_alu(instruction),
|
||||
0x4400..=0x47ff => return self.thumb_high_register(instruction),
|
||||
0x4800..=0x4fff => {
|
||||
let address =
|
||||
(self.pc().wrapping_add(4) & !3).wrapping_add((instruction & 255) * 4);
|
||||
self.thumb_transfer(
|
||||
memory,
|
||||
Transfer::LoadWord,
|
||||
((instruction >> 8) & 7) as usize,
|
||||
address,
|
||||
)?;
|
||||
}
|
||||
0x5000..=0x5fff => {
|
||||
let base = self.registers[((instruction >> 3) & 7) as usize];
|
||||
let offset = self.registers[((instruction >> 6) & 7) as usize];
|
||||
let kind = [
|
||||
Transfer::StoreWord,
|
||||
Transfer::StoreHalf,
|
||||
Transfer::StoreByte,
|
||||
Transfer::LoadSignedByte,
|
||||
Transfer::LoadWord,
|
||||
Transfer::LoadHalf,
|
||||
Transfer::LoadByte,
|
||||
Transfer::LoadSignedHalf,
|
||||
][((instruction >> 9) & 7) as usize];
|
||||
self.thumb_transfer(
|
||||
memory,
|
||||
kind,
|
||||
(instruction & 7) as usize,
|
||||
base.wrapping_add(offset),
|
||||
)?;
|
||||
}
|
||||
0x6000..=0x7fff => {
|
||||
let byte = instruction & (1 << 12) != 0;
|
||||
let load = instruction & (1 << 11) != 0;
|
||||
let offset = ((instruction >> 6) & 31) * if byte { 1 } else { 4 };
|
||||
let base = self.registers[((instruction >> 3) & 7) as usize];
|
||||
let kind = match (load, byte) {
|
||||
(false, false) => Transfer::StoreWord,
|
||||
(false, true) => Transfer::StoreByte,
|
||||
(true, false) => Transfer::LoadWord,
|
||||
(true, true) => Transfer::LoadByte,
|
||||
};
|
||||
self.thumb_transfer(
|
||||
memory,
|
||||
kind,
|
||||
(instruction & 7) as usize,
|
||||
base.wrapping_add(offset),
|
||||
)?;
|
||||
}
|
||||
0x8000..=0x8fff => {
|
||||
let kind = if instruction & (1 << 11) != 0 {
|
||||
Transfer::LoadHalf
|
||||
} else {
|
||||
Transfer::StoreHalf
|
||||
};
|
||||
let base = self.registers[((instruction >> 3) & 7) as usize];
|
||||
let address = base.wrapping_add(((instruction >> 6) & 31) * 2);
|
||||
self.thumb_transfer(memory, kind, (instruction & 7) as usize, address)?;
|
||||
}
|
||||
0x9000..=0x9fff => {
|
||||
let kind = if instruction & (1 << 11) != 0 {
|
||||
Transfer::LoadWord
|
||||
} else {
|
||||
Transfer::StoreWord
|
||||
};
|
||||
let address = self.registers[13].wrapping_add((instruction & 255) * 4);
|
||||
self.thumb_transfer(memory, kind, ((instruction >> 8) & 7) as usize, address)?;
|
||||
}
|
||||
0xa000..=0xafff => {
|
||||
let base = if instruction & (1 << 11) != 0 {
|
||||
self.registers[13]
|
||||
} else {
|
||||
self.pc().wrapping_add(4) & !3
|
||||
};
|
||||
self.registers[((instruction >> 8) & 7) as usize] =
|
||||
base.wrapping_add((instruction & 255) * 4);
|
||||
}
|
||||
0xb000..=0xb0ff => {
|
||||
let offset = (instruction & 127) * 4;
|
||||
self.registers[13] = if instruction & 128 != 0 {
|
||||
self.registers[13].wrapping_sub(offset)
|
||||
} else {
|
||||
self.registers[13].wrapping_add(offset)
|
||||
};
|
||||
}
|
||||
0xb400..=0xb5ff | 0xbc00..=0xbdff => {
|
||||
let pop = instruction & (1 << 11) != 0;
|
||||
let extra = if instruction & (1 << 8) != 0 {
|
||||
1 << if pop { 15 } else { 14 }
|
||||
} else {
|
||||
0
|
||||
};
|
||||
// Shared block logic uses Thumb PC alignment and pipeline offsets.
|
||||
let arm = if pop { 0xe8bd_0000 } else { 0xe92d_0000 };
|
||||
return self.block_transfer(arm | extra | (instruction & 255), memory);
|
||||
}
|
||||
0xc000..=0xcfff => {
|
||||
let arm = 0xe8a0_0000
|
||||
| ((instruction & 0x800) << 9)
|
||||
| (((instruction >> 8) & 7) << 16)
|
||||
| (instruction & 255);
|
||||
return self.block_transfer(arm, memory);
|
||||
}
|
||||
0xdf00..=0xdfff => {
|
||||
self.enter_exception(super::Exception::SoftwareInterrupt);
|
||||
return Ok(());
|
||||
}
|
||||
0xd000..=0xddff => {
|
||||
if self.flags.condition_passed((instruction >> 8) & 15) {
|
||||
let offset = (instruction as u8 as i8 as i32 * 2) as u32;
|
||||
self.registers[15] = self.pc().wrapping_add(4).wrapping_add(offset);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
0xe000..=0xe7ff => {
|
||||
let offset = (((instruction & 0x7ff) << 21) as i32 >> 20) as u32;
|
||||
self.registers[15] = self.pc().wrapping_add(4).wrapping_add(offset);
|
||||
return Ok(());
|
||||
}
|
||||
0xf000..=0xf7ff => {
|
||||
// BL prefix: sign-extend eleven bits, then shift left twelve.
|
||||
let offset = (((instruction & 0x7ff) << 21) as i32 >> 9) as u32;
|
||||
self.registers[14] = self.pc().wrapping_add(4).wrapping_add(offset);
|
||||
}
|
||||
0xf800..=0xffff => {
|
||||
// A standalone suffix is legal on ARM7; it uses the existing LR.
|
||||
let target = self.registers[14].wrapping_add((instruction & 0x7ff) * 2);
|
||||
self.registers[14] = next_pc | 1;
|
||||
self.registers[15] = target & !1;
|
||||
return Ok(());
|
||||
}
|
||||
// Includes undefined condition 0xE and ARMv5+ extensions.
|
||||
_ => return Err(self.unsupported(instruction)),
|
||||
}
|
||||
self.registers[15] = next_pc;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn thumb_result(&mut self, destination: usize, opcode: u32, a: u32, b: u32, carry: bool) {
|
||||
let (result, flags) = alu::execute(opcode, a, b, carry, self.flags);
|
||||
self.flags = flags;
|
||||
if !(8..=11).contains(&opcode) {
|
||||
self.registers[destination] = result;
|
||||
}
|
||||
}
|
||||
|
||||
fn thumb_register_alu(&mut self, instruction: u32) {
|
||||
let destination = (instruction & 7) as usize;
|
||||
let source = ((instruction >> 3) & 7) as usize;
|
||||
let opcode = (instruction >> 6) & 15;
|
||||
let a = self.registers[destination];
|
||||
let b = self.registers[source];
|
||||
match opcode {
|
||||
2 | 3 | 4 | 7 => {
|
||||
let kind = match opcode {
|
||||
2 => Shift::Lsl,
|
||||
3 => Shift::Lsr,
|
||||
4 => Shift::Asr,
|
||||
_ => Shift::Ror,
|
||||
};
|
||||
let (value, carry) = alu::shift(a, kind, b, self.flags.carry, true);
|
||||
self.thumb_result(destination, 0xd, 0, value, carry);
|
||||
}
|
||||
9 => self.thumb_result(destination, 2, 0, b, self.flags.carry), // NEG
|
||||
13 => {
|
||||
let result = a.wrapping_mul(b);
|
||||
self.registers[destination] = result;
|
||||
self.flags.negative = result >> 31 != 0;
|
||||
self.flags.zero = result == 0;
|
||||
// Preserve unspecified multiply carry, matching the ARM core policy.
|
||||
}
|
||||
_ => self.thumb_result(destination, opcode, a, b, self.flags.carry),
|
||||
}
|
||||
}
|
||||
|
||||
fn thumb_high_register(&mut self, instruction: u32) -> Result<(), CpuError> {
|
||||
let opcode = (instruction >> 8) & 3;
|
||||
let source = ((instruction >> 3) & 15) as usize;
|
||||
let destination = ((instruction & 7) | ((instruction >> 4) & 8)) as usize;
|
||||
if opcode == 3 {
|
||||
// Bit 7 selects BLX on ARMv5; low destination bits must be zero.
|
||||
if instruction & 0x87 != 0 {
|
||||
return Err(self.unsupported(instruction));
|
||||
}
|
||||
self.branch_exchange(self.operand_register(source, 4));
|
||||
return Ok(());
|
||||
}
|
||||
// ARMv4T requires at least one high register in this instruction format.
|
||||
if instruction & 0xc0 == 0 {
|
||||
return Err(self.unsupported(instruction));
|
||||
}
|
||||
let a = self.operand_register(destination, 4);
|
||||
let b = self.operand_register(source, 4);
|
||||
let value = match opcode {
|
||||
0 => a.wrapping_add(b),
|
||||
1 => {
|
||||
self.flags = alu::execute(0xa, a, b, self.flags.carry, self.flags).1;
|
||||
self.registers[15] = self.pc().wrapping_add(2);
|
||||
return Ok(());
|
||||
}
|
||||
_ => b,
|
||||
};
|
||||
if destination == 15 {
|
||||
self.registers[15] = value & !1; // ADD/MOV PC stay in Thumb state.
|
||||
} else {
|
||||
self.registers[destination] = value;
|
||||
self.registers[15] = self.pc().wrapping_add(2);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn thumb_transfer(
|
||||
&mut self,
|
||||
memory: &mut Memory,
|
||||
kind: Transfer,
|
||||
register: usize,
|
||||
address: u32,
|
||||
) -> Result<(), CpuError> {
|
||||
let value = match kind {
|
||||
Transfer::StoreWord => {
|
||||
return Ok(memory.write32(address & !3, self.registers[register])?)
|
||||
}
|
||||
Transfer::StoreHalf => {
|
||||
return Ok(memory.write16(address & !1, self.registers[register] as u16)?)
|
||||
}
|
||||
Transfer::StoreByte => {
|
||||
return Ok(memory.write8(address, self.registers[register] as u8)?)
|
||||
}
|
||||
Transfer::LoadWord => memory.read32(address & !3)?.rotate_right((address & 3) * 8),
|
||||
Transfer::LoadHalf => {
|
||||
u32::from(memory.read16(address & !1)?).rotate_right((address & 1) * 8)
|
||||
}
|
||||
Transfer::LoadByte => u32::from(memory.read8(address)?),
|
||||
Transfer::LoadSignedByte => memory.read8(address)? as i8 as i32 as u32,
|
||||
Transfer::LoadSignedHalf if address & 1 != 0 => {
|
||||
memory.read8(address)? as i8 as i32 as u32
|
||||
}
|
||||
Transfer::LoadSignedHalf => memory.read16(address)? as i16 as i32 as u32,
|
||||
};
|
||||
self.registers[register] = value;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
821
crates/gba-core/src/cpu/thumb_tests.rs
Normal file
821
crates/gba-core/src/cpu/thumb_tests.rs
Normal file
|
|
@ -0,0 +1,821 @@
|
|||
use super::*;
|
||||
use crate::memory::ROM_START;
|
||||
|
||||
const RAM: u32 = 0x0200_0000;
|
||||
|
||||
fn program(instructions: &[u16]) -> Memory {
|
||||
Memory::new(
|
||||
instructions
|
||||
.iter()
|
||||
.flat_map(|word| word.to_le_bytes())
|
||||
.collect(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn thumb_cpu() -> Cpu {
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.instruction_set = InstructionSet::Thumb;
|
||||
cpu
|
||||
}
|
||||
|
||||
fn flags() -> Flags {
|
||||
Flags {
|
||||
negative: true,
|
||||
zero: true,
|
||||
carry: true,
|
||||
overflow: true,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fetches_one_halfword_and_advances_two_bytes() {
|
||||
let mut memory = program(&[0x202a]); // MOV r0,#42, no trailing halfword
|
||||
let mut cpu = thumb_cpu();
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.registers[0], 42);
|
||||
assert_eq!(cpu.pc(), ROM_START + 2);
|
||||
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn immediate_shift_formats_use_arm7_zero_amount_rules() {
|
||||
for (instruction, expected, carry) in [
|
||||
(0x0008, 0x8000_0001, false), // LSL r0,r1,#0
|
||||
(0x0048, 2, true), // LSL #1
|
||||
(0x0808, 0, true), // LSR #32
|
||||
(0x0848, 0x4000_0000, true), // LSR #1
|
||||
(0x1008, u32::MAX, true), // ASR #32
|
||||
(0x1048, 0xc000_0000, true), // ASR #1
|
||||
] {
|
||||
let mut memory = program(&[instruction]);
|
||||
let mut cpu = thumb_cpu();
|
||||
cpu.registers[1] = 0x8000_0001;
|
||||
cpu.flags.overflow = true;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.registers[0], expected);
|
||||
assert_eq!(
|
||||
cpu.flags,
|
||||
Flags {
|
||||
negative: expected >> 31 != 0,
|
||||
zero: expected == 0,
|
||||
carry,
|
||||
overflow: true
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn three_register_and_small_immediate_add_subtract_formats() {
|
||||
for (instruction, expected) in [
|
||||
(0x1888, 12), // ADD r0,r1,r2
|
||||
(0x1a88, 8), // SUB r0,r1,r2
|
||||
(0x1cc8, 13), // ADD r0,r1,#3
|
||||
(0x1ec8, 7), // SUB r0,r1,#3
|
||||
] {
|
||||
let mut memory = program(&[instruction]);
|
||||
let mut cpu = thumb_cpu();
|
||||
cpu.registers[1] = 10;
|
||||
cpu.registers[2] = 2;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.registers[0], expected);
|
||||
assert!(!cpu.flags.overflow);
|
||||
assert_eq!(cpu.flags.carry, instruction & 0x200 != 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_subtract_and_compare_set_signed_overflow_and_borrow() {
|
||||
for (instruction, initial, expected, expected_flags) in [
|
||||
(
|
||||
0x3001,
|
||||
0x7fff_ffff,
|
||||
0x8000_0000,
|
||||
Flags {
|
||||
negative: true,
|
||||
zero: false,
|
||||
carry: false,
|
||||
overflow: true,
|
||||
},
|
||||
),
|
||||
(
|
||||
0x3801,
|
||||
0x8000_0000,
|
||||
0x7fff_ffff,
|
||||
Flags {
|
||||
negative: false,
|
||||
zero: false,
|
||||
carry: true,
|
||||
overflow: true,
|
||||
},
|
||||
),
|
||||
(
|
||||
0x3801,
|
||||
0,
|
||||
u32::MAX,
|
||||
Flags {
|
||||
negative: true,
|
||||
zero: false,
|
||||
carry: false,
|
||||
overflow: false,
|
||||
},
|
||||
),
|
||||
(
|
||||
0x2801,
|
||||
1,
|
||||
1,
|
||||
Flags {
|
||||
negative: false,
|
||||
zero: true,
|
||||
carry: true,
|
||||
overflow: false,
|
||||
},
|
||||
),
|
||||
] {
|
||||
let mut memory = program(&[instruction]);
|
||||
let mut cpu = thumb_cpu();
|
||||
cpu.registers[0] = initial;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.registers[0], expected);
|
||||
assert_eq!(cpu.flags, expected_flags);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn immediate_mov_preserves_carry_and_overflow() {
|
||||
let mut memory = program(&[0x2700]); // MOV r7,#0
|
||||
let mut cpu = thumb_cpu();
|
||||
cpu.flags = flags();
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(
|
||||
cpu.flags,
|
||||
Flags {
|
||||
negative: false,
|
||||
zero: true,
|
||||
carry: true,
|
||||
overflow: true
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_sixteen_register_alu_opcodes_decode_correctly() {
|
||||
let results = [
|
||||
0,
|
||||
0x36,
|
||||
0xd0,
|
||||
0xd,
|
||||
0xd,
|
||||
0x37,
|
||||
0x32,
|
||||
0xd,
|
||||
0,
|
||||
0xffff_fffe,
|
||||
0x32,
|
||||
0x36,
|
||||
0x36,
|
||||
0x68,
|
||||
0x34,
|
||||
0xffff_fffd,
|
||||
];
|
||||
for (opcode, result) in results.into_iter().enumerate() {
|
||||
let mut memory = program(&[0x4008 | ((opcode as u16) << 6)]); // op r0,r1
|
||||
let mut cpu = thumb_cpu();
|
||||
cpu.registers[0] = 0x34;
|
||||
cpu.registers[1] = 2;
|
||||
cpu.flags = flags();
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(
|
||||
cpu.registers[0],
|
||||
if [8, 10, 11].contains(&opcode) {
|
||||
0x34
|
||||
} else {
|
||||
result
|
||||
},
|
||||
"opcode {opcode}"
|
||||
);
|
||||
assert_eq!(cpu.flags.negative, result >> 31 != 0);
|
||||
assert_eq!(cpu.flags.zero, result == 0);
|
||||
assert_eq!(cpu.flags.overflow, ![5, 6, 9, 10, 11].contains(&opcode));
|
||||
assert_eq!(cpu.flags.carry, ![2, 3, 4, 5, 7, 9, 11].contains(&opcode));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_shift_amounts_include_zero_32_and_255_and_use_low_byte() {
|
||||
for (opcode, amount, expected, carry) in [
|
||||
(2, 0, 0x8000_0001, true),
|
||||
(2, 32, 0, true),
|
||||
(2, 33, 0, false),
|
||||
(3, 32, 0, true),
|
||||
(3, 255, 0, false),
|
||||
(4, 255, u32::MAX, true),
|
||||
(7, 32, 0x8000_0001, true),
|
||||
(7, 256, 0x8000_0001, true),
|
||||
(2, 257, 2, true),
|
||||
] {
|
||||
let mut memory = program(&[0x4008 | (opcode << 6)]);
|
||||
let mut cpu = thumb_cpu();
|
||||
cpu.registers[0] = 0x8000_0001;
|
||||
cpu.registers[1] = amount;
|
||||
cpu.flags.carry = true;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.registers[0], expected);
|
||||
assert_eq!(cpu.flags.carry, carry);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adc_sbc_and_neg_handle_boundary_values() {
|
||||
for (instruction, a, b, carry, expected, out_carry, overflow) in [
|
||||
(0x4148, u32::MAX, 0, true, 0, true, false),
|
||||
(0x4148, 0x7fff_ffff, 0, true, 0x8000_0000, false, true),
|
||||
(0x4188, 0, 0, false, u32::MAX, false, false),
|
||||
(0x4188, 0x8000_0000, 0, false, 0x7fff_ffff, true, true),
|
||||
(0x4248, 123, 0x8000_0000, false, 0x8000_0000, false, true),
|
||||
] {
|
||||
let mut memory = program(&[instruction]);
|
||||
let mut cpu = thumb_cpu();
|
||||
cpu.registers[0] = a;
|
||||
cpu.registers[1] = b;
|
||||
cpu.flags.carry = carry;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.registers[0], expected);
|
||||
assert_eq!(cpu.flags.carry, out_carry);
|
||||
assert_eq!(cpu.flags.overflow, overflow);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiply_wraps_and_preserves_the_unspecified_carry_policy() {
|
||||
let mut memory = program(&[0x4348]); // MUL r0,r1
|
||||
let mut cpu = thumb_cpu();
|
||||
cpu.registers[0] = 0x8000_0000;
|
||||
cpu.registers[1] = 2;
|
||||
cpu.flags = flags();
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.registers[0], 0);
|
||||
assert_eq!(
|
||||
cpu.flags,
|
||||
Flags {
|
||||
negative: false,
|
||||
zero: true,
|
||||
carry: true,
|
||||
overflow: true
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_register_add_and_mov_preserve_flags_while_cmp_updates_them() {
|
||||
let mut memory = program(&[
|
||||
0x4680, // MOV r8,r0
|
||||
0x4448, // ADD r0,r9
|
||||
0x4540, // CMP r0,r8
|
||||
]);
|
||||
let mut cpu = thumb_cpu();
|
||||
cpu.registers[0] = 10;
|
||||
cpu.registers[9] = 5;
|
||||
cpu.flags = flags();
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.registers[8], 10);
|
||||
assert_eq!(cpu.flags, flags());
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.registers[0], 15);
|
||||
assert_eq!(cpu.flags, flags());
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(
|
||||
cpu.flags,
|
||||
Flags {
|
||||
negative: false,
|
||||
zero: false,
|
||||
carry: true,
|
||||
overflow: false
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_register_pc_reads_use_pc_plus_four_without_word_alignment() {
|
||||
for instruction in [0x4678, 0x4478] {
|
||||
// MOV/ADD r0,pc
|
||||
let mut memory = program(&[0x46c0, instruction]);
|
||||
let mut cpu = thumb_cpu();
|
||||
cpu.registers[15] += 2;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.registers[0], ROM_START + 6);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_register_pc_writes_remain_thumb_and_keep_bit_one() {
|
||||
for instruction in [0x4687, 0x4487] {
|
||||
// MOV/ADD pc,r0
|
||||
let mut memory = program(&[instruction]);
|
||||
let mut cpu = thumb_cpu();
|
||||
cpu.registers[0] = if instruction == 0x4687 {
|
||||
ROM_START + 11
|
||||
} else {
|
||||
7
|
||||
};
|
||||
cpu.flags = flags();
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.pc(), ROM_START + 10);
|
||||
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
|
||||
assert_eq!(cpu.flags, flags());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thumb_bx_selects_instruction_set_from_bit_zero() {
|
||||
for (target, expected, state) in [
|
||||
(ROM_START + 11, ROM_START + 10, InstructionSet::Thumb),
|
||||
(ROM_START + 10, ROM_START + 8, InstructionSet::Arm),
|
||||
] {
|
||||
let mut memory = program(&[0x4700]); // BX r0
|
||||
let mut cpu = thumb_cpu();
|
||||
cpu.registers[0] = target;
|
||||
cpu.flags = flags();
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.pc(), expected);
|
||||
assert_eq!(cpu.instruction_set(), state);
|
||||
assert_eq!(cpu.flags, flags());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bx_pc_switches_to_arm_with_word_alignment() {
|
||||
let mut memory = program(&[0x46c0, 0x4778]); // BX pc at halfword boundary
|
||||
let mut cpu = thumb_cpu();
|
||||
cpu.registers[15] += 2;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.pc(), ROM_START + 4);
|
||||
assert_eq!(cpu.instruction_set(), InstructionSet::Arm);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn literal_load_and_adr_align_pc_plus_four() {
|
||||
for instruction in [0x4801, 0xa001] {
|
||||
let mut memory = program(&[0x46c0, instruction, 0, 0, 0x5678, 0x1234]);
|
||||
let mut cpu = thumb_cpu();
|
||||
cpu.registers[15] += 2;
|
||||
cpu.flags = flags();
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(
|
||||
cpu.registers[0],
|
||||
if instruction == 0x4801 {
|
||||
0x1234_5678
|
||||
} else {
|
||||
ROM_START + 8
|
||||
}
|
||||
);
|
||||
assert_eq!(cpu.flags, flags());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_register_offset_memory_formats_decode_and_handle_odd_addresses() {
|
||||
let loaded = [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0xffff_ff80,
|
||||
0xfe12_3480,
|
||||
0xfe00_0080,
|
||||
0x80,
|
||||
0xffff_ff80,
|
||||
];
|
||||
let stored = [0xaabb_ccdd, 0x1234_ccdd, 0x1234_ddfe];
|
||||
for opcode in 0..8 {
|
||||
let mut memory = program(&[0x5081 | (opcode << 9)]); // Rd=r1,Rb=r0,Ro=r2
|
||||
memory.write32(RAM, 0x1234_80fe).unwrap();
|
||||
let mut cpu = thumb_cpu();
|
||||
cpu.registers[0] = RAM;
|
||||
cpu.registers[1] = 0xaabb_ccdd;
|
||||
cpu.registers[2] = 1;
|
||||
cpu.flags = flags();
|
||||
cpu.step(&mut memory).unwrap();
|
||||
if opcode < 3 {
|
||||
assert_eq!(memory.read32(RAM).unwrap(), stored[opcode as usize]);
|
||||
assert_eq!(cpu.registers[1], 0xaabb_ccdd);
|
||||
} else {
|
||||
assert_eq!(cpu.registers[1], loaded[opcode as usize]);
|
||||
assert_eq!(memory.read32(RAM).unwrap(), 0x1234_80fe);
|
||||
}
|
||||
assert_eq!(cpu.flags, flags());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn immediate_memory_offsets_scale_by_transfer_size() {
|
||||
for (store, load, address, expected) in [
|
||||
(0x6041, 0x6842, RAM + 4, 0x1234_abcd),
|
||||
(0x7041, 0x7842, RAM + 1, 0xcd),
|
||||
(0x8041, 0x8842, RAM + 2, 0xabcd),
|
||||
] {
|
||||
let mut memory = program(&[store, load]);
|
||||
let mut cpu = thumb_cpu();
|
||||
cpu.registers[0] = RAM;
|
||||
cpu.registers[1] = 0x1234_abcd;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.registers[2], expected);
|
||||
assert_eq!(memory.read8(address).unwrap(), 0xcd);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sp_relative_memory_and_address_operations_preserve_flags() {
|
||||
let mut memory = program(&[
|
||||
0x9001, // STR r0,[sp,#4]
|
||||
0x9901, // LDR r1,[sp,#4]
|
||||
0xaa02, // ADD r2,sp,#8
|
||||
0xb001, // ADD sp,#4
|
||||
0xb081, // SUB sp,#4
|
||||
]);
|
||||
let mut cpu = thumb_cpu();
|
||||
cpu.registers[13] = RAM;
|
||||
cpu.registers[0] = 42;
|
||||
cpu.flags = flags();
|
||||
for _ in 0..5 {
|
||||
cpu.step(&mut memory).unwrap();
|
||||
}
|
||||
assert_eq!(cpu.registers[1], 42);
|
||||
assert_eq!(cpu.registers[2], RAM + 8);
|
||||
assert_eq!(cpu.registers[13], RAM);
|
||||
assert_eq!(cpu.flags, flags());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn memory_offsets_wrap_and_sp_addition_wraps_without_panicking() {
|
||||
let mut memory = program(&[0x5081, 0xb001]); // STR r1,[r0,r2]; ADD sp,#4
|
||||
let mut cpu = thumb_cpu();
|
||||
cpu.registers[0] = 0xffff_fffc;
|
||||
cpu.registers[1] = 42;
|
||||
cpu.registers[2] = RAM + 4;
|
||||
cpu.registers[13] = u32::MAX - 3;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(memory.read32(RAM).unwrap(), 42);
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.registers[13], 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signed_halfword_aligned_load_sign_extends_sixteen_bits() {
|
||||
let mut memory = program(&[0x5e81]);
|
||||
memory.write16(RAM, 0x8001).unwrap();
|
||||
let mut cpu = thumb_cpu();
|
||||
cpu.registers[0] = RAM;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.registers[1], 0xffff_8001);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_and_pop_restore_registers_and_pc_without_arm_exchange() {
|
||||
for return_address in [ROM_START + 10, ROM_START + 11] {
|
||||
let mut memory = program(&[0xb505, 0x2000, 0x2200, 0xbd05]); // PUSH/POP {r0,r2,lr/pc}
|
||||
let mut cpu = thumb_cpu();
|
||||
cpu.registers[0] = 11;
|
||||
cpu.registers[2] = 22;
|
||||
cpu.registers[13] = RAM + 0x100;
|
||||
cpu.registers[14] = return_address;
|
||||
for _ in 0..4 {
|
||||
cpu.step(&mut memory).unwrap();
|
||||
}
|
||||
assert_eq!(cpu.registers[0], 11);
|
||||
assert_eq!(cpu.registers[2], 22);
|
||||
assert_eq!(cpu.registers[13], RAM + 0x100);
|
||||
assert_eq!(cpu.pc(), ROM_START + 10);
|
||||
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
|
||||
assert_eq!(memory.read32(RAM + 0xf4).unwrap(), 11);
|
||||
assert_eq!(memory.read32(RAM + 0xf8).unwrap(), 22);
|
||||
assert_eq!(memory.read32(RAM + 0xfc).unwrap(), return_address);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_pop_without_lr_or_pc_advance_by_two() {
|
||||
let mut memory = program(&[0xb401, 0xbc02]); // PUSH {r0}; POP {r1}
|
||||
let mut cpu = thumb_cpu();
|
||||
cpu.registers[0] = 123;
|
||||
cpu.registers[13] = RAM + 16;
|
||||
cpu.flags = flags();
|
||||
cpu.step(&mut memory).unwrap();
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.registers[1], 123);
|
||||
assert_eq!(cpu.registers[13], RAM + 16);
|
||||
assert_eq!(cpu.pc(), ROM_START + 4);
|
||||
assert_eq!(cpu.flags, flags());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thumb_multiple_transfers_use_low_register_base_and_ascending_addresses() {
|
||||
let mut memory = program(&[0xc305, 0xcb30]); // STMIA r3!,{r0,r2}; LDMIA r3!,{r4,r5}
|
||||
let mut cpu = thumb_cpu();
|
||||
cpu.registers[0] = 11;
|
||||
cpu.registers[2] = 22;
|
||||
cpu.registers[3] = RAM + 1;
|
||||
cpu.flags = flags();
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.registers[3], RAM + 9);
|
||||
cpu.registers[3] = RAM + 1;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(&cpu.registers[4..6], &[11, 22]);
|
||||
assert_eq!(cpu.registers[3], RAM + 9);
|
||||
assert_eq!(cpu.pc(), ROM_START + 4);
|
||||
assert_eq!(cpu.flags, flags());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thumb_multiple_transfer_base_overlap_matches_arm7() {
|
||||
for (instruction, old_base_stored) in [(0xc103, false), (0xc106, true)] {
|
||||
let mut memory = program(&[instruction]);
|
||||
let mut cpu = thumb_cpu();
|
||||
cpu.registers[1] = RAM;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(
|
||||
memory
|
||||
.read32(if old_base_stored { RAM } else { RAM + 4 })
|
||||
.unwrap(),
|
||||
if old_base_stored { RAM } else { RAM + 8 }
|
||||
);
|
||||
}
|
||||
let mut memory = program(&[0xc903]); // LDMIA r1!,{r0,r1}
|
||||
memory.write32(RAM, 11).unwrap();
|
||||
memory.write32(RAM + 4, 22).unwrap();
|
||||
let mut cpu = thumb_cpu();
|
||||
cpu.registers[1] = RAM;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(&cpu.registers[..2], &[11, 22]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_thumb_multiple_lists_transfer_pc_with_sixty_four_byte_writeback() {
|
||||
for instruction in [0xc000, 0xc800] {
|
||||
let mut memory = program(&[instruction]);
|
||||
memory.write32(RAM, ROM_START + 11).unwrap();
|
||||
let mut cpu = thumb_cpu();
|
||||
cpu.registers[0] = RAM;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.registers[0], RAM + 64);
|
||||
if instruction == 0xc000 {
|
||||
assert_eq!(memory.read32(RAM).unwrap(), ROM_START + 4);
|
||||
assert_eq!(cpu.pc(), ROM_START + 2);
|
||||
} else {
|
||||
assert_eq!(cpu.pc(), ROM_START + 10);
|
||||
}
|
||||
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_push_pop_follow_arm7_block_transfer_rules() {
|
||||
let mut memory = program(&[0xb400, 0xbc00]);
|
||||
let mut cpu = thumb_cpu();
|
||||
cpu.registers[13] = RAM + 0x100;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.registers[13], RAM + 0xc0);
|
||||
assert_eq!(memory.read32(RAM + 0xc0).unwrap(), ROM_START + 4);
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.registers[13], RAM + 0x100);
|
||||
assert_eq!(cpu.pc(), ROM_START + 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conditional_branches_cover_every_condition_and_flag_combination() {
|
||||
let tables = [
|
||||
0xf0f0_u16, 0x0f0f, 0xcccc, 0x3333, 0xff00, 0x00ff, 0xaaaa, 0x5555, 0x0c0c, 0xf3f3, 0xaa55,
|
||||
0x55aa, 0x0a05, 0xf5fa,
|
||||
];
|
||||
for (condition, table) in tables.into_iter().enumerate() {
|
||||
let mut memory = program(&[0xd001 | ((condition as u16) << 8)]);
|
||||
for bits in 0..16 {
|
||||
let mut cpu = thumb_cpu();
|
||||
cpu.flags = Flags {
|
||||
negative: bits & 8 != 0,
|
||||
zero: bits & 4 != 0,
|
||||
carry: bits & 2 != 0,
|
||||
overflow: bits & 1 != 0,
|
||||
};
|
||||
let before = cpu.flags;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(
|
||||
cpu.pc(),
|
||||
ROM_START + if table & (1 << bits) != 0 { 6 } else { 2 }
|
||||
);
|
||||
assert_eq!(cpu.flags, before);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn branch_offsets_are_signed_and_relative_to_pc_plus_four() {
|
||||
for (instruction, target) in [
|
||||
(0xe3ff, ROM_START + 2050),
|
||||
(0xe400, ROM_START - 2044),
|
||||
(0xe7fe, ROM_START),
|
||||
(0xd080, ROM_START - 252),
|
||||
(0xd07f, ROM_START + 258),
|
||||
] {
|
||||
let mut memory = program(&[instruction]);
|
||||
let mut cpu = thumb_cpu();
|
||||
cpu.flags.zero = true;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.pc(), target);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bl_prefix_and_suffix_execute_as_separate_halfwords() {
|
||||
let mut memory = program(&[0xf000, 0xf802, 0xe7fe, 0x46c0, 0x202a, 0x4770]);
|
||||
let mut cpu = thumb_cpu();
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.registers[14], ROM_START + 4);
|
||||
assert_eq!(cpu.pc(), ROM_START + 2);
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.pc(), ROM_START + 8);
|
||||
assert_eq!(cpu.registers[14], ROM_START + 5);
|
||||
cpu.step(&mut memory).unwrap();
|
||||
cpu.step(&mut memory).unwrap(); // BX lr
|
||||
assert_eq!(cpu.registers[0], 42);
|
||||
assert_eq!(cpu.pc(), ROM_START + 4);
|
||||
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bl_prefix_sign_extends_and_suffix_can_execute_alone() {
|
||||
for (instruction, expected) in [
|
||||
(0xf3ff, ROM_START + 4 + 0x3ff000),
|
||||
(0xf400, ROM_START + 4 - 0x400000),
|
||||
(0xf7ff, ROM_START + 4 - 0x1000),
|
||||
] {
|
||||
let mut memory = program(&[instruction]);
|
||||
let mut cpu = thumb_cpu();
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.registers[14], expected);
|
||||
}
|
||||
let mut memory = program(&[0xf801]);
|
||||
let mut cpu = thumb_cpu();
|
||||
cpu.registers[14] = ROM_START + 9;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.pc(), ROM_START + 10);
|
||||
assert_eq!(cpu.registers[14], ROM_START + 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arm_thumb_arm_round_trip_fetches_the_correct_instruction_width() {
|
||||
let mut bytes: Vec<u8> = [0xe12f_ff10_u32, 0xe3a0_2063]
|
||||
.into_iter()
|
||||
.flat_map(u32::to_le_bytes)
|
||||
.collect();
|
||||
bytes.extend([0x212a_u16, 0x4718].into_iter().flat_map(u16::to_le_bytes)); // MOV r1,#42; BX r3
|
||||
let mut memory = Memory::new(bytes).unwrap();
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = ROM_START + 9;
|
||||
cpu.registers[3] = ROM_START + 4;
|
||||
for _ in 0..4 {
|
||||
cpu.step(&mut memory).unwrap();
|
||||
}
|
||||
assert_eq!(cpu.instruction_set(), InstructionSet::Arm);
|
||||
assert_eq!(cpu.registers[1], 42);
|
||||
assert_eq!(cpu.registers[2], 99);
|
||||
assert_eq!(cpu.pc(), ROM_START + 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_thumb_instructions_preserve_registers_flags_and_state() {
|
||||
for instruction in [
|
||||
0xde00, 0xdeff, 0xbe00, 0xe800, 0x4780, 0x4701, 0x4400, 0x4500, 0x4600, 0xb100, 0xb200,
|
||||
0xb600, 0xbf00,
|
||||
] {
|
||||
let mut memory = program(&[instruction]);
|
||||
let mut cpu = thumb_cpu();
|
||||
cpu.flags = flags();
|
||||
let before = cpu.registers;
|
||||
assert_eq!(
|
||||
cpu.step(&mut memory),
|
||||
Err(CpuError::UnsupportedThumbInstruction {
|
||||
address: ROM_START,
|
||||
instruction
|
||||
})
|
||||
);
|
||||
assert_eq!(cpu.registers, before);
|
||||
assert_eq!(cpu.flags, flags());
|
||||
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thumb_memory_errors_leave_state_unchanged() {
|
||||
for instruction in [
|
||||
0x6801, 0x6001, 0x8801, 0x8001, 0x7801, 0x7001, 0x5e81, 0x9801, 0x9001, 0x4801,
|
||||
] {
|
||||
let mut memory = program(&[instruction]);
|
||||
let mut cpu = thumb_cpu();
|
||||
cpu.flags = flags();
|
||||
cpu.registers[1] = 123;
|
||||
let before = cpu.registers;
|
||||
assert!(
|
||||
matches!(cpu.step(&mut memory), Err(CpuError::Memory(_))),
|
||||
"{instruction:04x}"
|
||||
);
|
||||
assert_eq!(cpu.registers, before);
|
||||
assert_eq!(cpu.flags, flags());
|
||||
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_thumb_store_to_rom_does_not_modify_rom_or_cpu() {
|
||||
let mut memory = program(&[0x6001]);
|
||||
let mut cpu = thumb_cpu();
|
||||
cpu.registers[0] = ROM_START;
|
||||
let before = cpu.registers;
|
||||
assert_eq!(
|
||||
cpu.step(&mut memory),
|
||||
Err(CpuError::Memory(MemoryError::ReadOnly(ROM_START)))
|
||||
);
|
||||
assert_eq!(cpu.registers, before);
|
||||
assert_eq!(memory.read16(ROM_START).unwrap(), 0x6001);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn late_thumb_stack_and_block_errors_do_not_partially_commit() {
|
||||
for instruction in [0xb403, 0xbc03, 0xc003, 0xc803] {
|
||||
let mut memory = Memory::new(vec![]).unwrap();
|
||||
memory.write16(0x0300_0000, instruction).unwrap();
|
||||
memory.write32(0x07ff_fffc, 0x1234_5678).unwrap();
|
||||
let mut cpu = thumb_cpu();
|
||||
cpu.registers[15] = 0x0300_0000;
|
||||
cpu.registers[0] = 0x07ff_fffc;
|
||||
cpu.registers[13] = if instruction == 0xb403 {
|
||||
0x0800_0004
|
||||
} else {
|
||||
0x07ff_fffc
|
||||
};
|
||||
let before = cpu.registers;
|
||||
assert!(matches!(cpu.step(&mut memory), Err(CpuError::Memory(_))));
|
||||
assert_eq!(cpu.registers, before);
|
||||
assert_eq!(memory.read32(0x07ff_fffc).unwrap(), 0x1234_5678);
|
||||
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skipped_arm_bx_does_not_switch_to_thumb() {
|
||||
let mut memory = Memory::new(0x012f_ff10_u32.to_le_bytes().to_vec()).unwrap(); // BXEQ r0
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = ROM_START + 9;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.instruction_set(), InstructionSet::Arm);
|
||||
assert_eq!(cpu.pc(), ROM_START + 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failing_fetch_after_bl_prefix_preserves_the_completed_prefix() {
|
||||
let mut memory = program(&[0xf000]);
|
||||
let mut cpu = thumb_cpu();
|
||||
cpu.step(&mut memory).unwrap();
|
||||
let before = cpu.registers;
|
||||
assert!(matches!(cpu.step(&mut memory), Err(CpuError::Memory(_))));
|
||||
assert_eq!(cpu.registers, before);
|
||||
assert_eq!(cpu.registers[14], ROM_START + 4);
|
||||
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_thumb_encoding_executes_or_returns_a_diagnostic_without_panicking() {
|
||||
let mut memory = Memory::new(vec![]).unwrap();
|
||||
for instruction in 0..=u16::MAX {
|
||||
memory.write16(RAM, instruction).unwrap();
|
||||
let mut cpu = thumb_cpu();
|
||||
for register in 0..15 {
|
||||
cpu.registers[register] = RAM + 0x100 + register as u32 * 4;
|
||||
}
|
||||
cpu.registers[15] = RAM;
|
||||
cpu.flags = flags();
|
||||
let before = cpu.clone();
|
||||
match cpu.step(&mut memory) {
|
||||
Ok(()) => assert_eq!(cpu.pc() & (cpu.instruction_set.width() - 1), 0),
|
||||
Err(_) => assert_eq!(cpu, before, "encoding {instruction:04x}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncated_or_misaligned_thumb_fetches_do_not_advance_pc() {
|
||||
let mut memory = Memory::new(vec![0x20]).unwrap();
|
||||
let mut cpu = thumb_cpu();
|
||||
assert_eq!(
|
||||
cpu.step(&mut memory),
|
||||
Err(CpuError::Memory(MemoryError::Unmapped(ROM_START + 1)))
|
||||
);
|
||||
assert_eq!(cpu.pc(), ROM_START);
|
||||
cpu.registers[15] += 1;
|
||||
assert_eq!(
|
||||
cpu.step(&mut memory),
|
||||
Err(CpuError::Memory(MemoryError::Unaligned(ROM_START + 1)))
|
||||
);
|
||||
assert_eq!(cpu.pc(), ROM_START + 1);
|
||||
}
|
||||
205
crates/gba-core/src/cpu/timing.rs
Normal file
205
crates/gba-core/src/cpu/timing.rs
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
//! Nominal ARM7 instruction-cycle summaries, separate from instruction semantics.
|
||||
//! Data costs come from actual bus accesses. No speculative pipeline reads occur.
|
||||
//! PC writes use target-region code costs, following GBATEK's cycle summary.
|
||||
|
||||
use super::{Cpu, CpuError, InstructionSet};
|
||||
use crate::{
|
||||
memory::Memory,
|
||||
timing::{bus_cycles, refill_cycles, AccessKind, AccessWidth, StepTiming},
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
struct Summary {
|
||||
code_kind: AccessKind,
|
||||
refill: bool,
|
||||
internal: u32,
|
||||
}
|
||||
|
||||
impl Summary {
|
||||
fn branch() -> Self {
|
||||
Self {
|
||||
refill: true,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn transfer(load: bool, pc: bool) -> Self {
|
||||
Self {
|
||||
code_kind: if load {
|
||||
AccessKind::Sequential
|
||||
} else {
|
||||
AccessKind::NonSequential
|
||||
},
|
||||
refill: load && pc,
|
||||
internal: u32::from(load),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl InstructionSet {
|
||||
fn access_width(self) -> AccessWidth {
|
||||
match self {
|
||||
Self::Arm => AccessWidth::Word,
|
||||
Self::Thumb => AccessWidth::Halfword,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Cpu {
|
||||
/// Execute one instruction and return its nominal cycle cost. Like Cpu::step,
|
||||
/// this does not advance device time or sample interrupts. Machine does that.
|
||||
/// Failed instructions discard their timing trace and retain existing diagnostics.
|
||||
pub fn step_timed(&mut self, memory: &mut Memory) -> Result<StepTiming, CpuError> {
|
||||
let instruction = self.fetch(memory)?;
|
||||
let summary = match self.instruction_set {
|
||||
InstructionSet::Arm => self.arm_summary(instruction),
|
||||
InstructionSet::Thumb => self.thumb_summary(instruction),
|
||||
};
|
||||
let pc = self.pc();
|
||||
let width = self.instruction_set.access_width();
|
||||
// A WAITCNT store affects subsequent instructions, not this code access.
|
||||
let waitcnt = memory.waitcnt();
|
||||
memory.begin_data_timing();
|
||||
let result = self.execute_fetched(instruction, memory);
|
||||
let data_cycles = memory.end_data_timing();
|
||||
result?;
|
||||
let code_cycles = if summary.refill {
|
||||
refill_cycles(waitcnt, self.pc(), self.instruction_set.access_width())
|
||||
} else {
|
||||
bus_cycles(waitcnt, pc, width, summary.code_kind)
|
||||
};
|
||||
Ok(StepTiming {
|
||||
code_cycles,
|
||||
data_cycles,
|
||||
internal_cycles: summary.internal,
|
||||
idle_cycles: 0,
|
||||
})
|
||||
}
|
||||
|
||||
/// Nominal refill cost after explicit exception entry. No vector bytes are read.
|
||||
pub(crate) fn exception_timing(&self, memory: &Memory) -> StepTiming {
|
||||
StepTiming {
|
||||
code_cycles: refill_cycles(
|
||||
memory.waitcnt(),
|
||||
self.pc(),
|
||||
self.instruction_set.access_width(),
|
||||
),
|
||||
..StepTiming::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn arm_summary(&self, instruction: u32) -> Summary {
|
||||
if !self.flags.condition_passed(instruction >> 28) {
|
||||
return Summary::default();
|
||||
}
|
||||
if instruction & 0x0f00_0000 == 0x0f00_0000
|
||||
|| instruction & 0x0fff_fff0 == 0x012f_ff10
|
||||
|| instruction & 0x0e00_0000 == 0x0a00_0000
|
||||
{
|
||||
return Summary::branch();
|
||||
}
|
||||
if instruction & 0x0fbf_0fff == 0x010f_0000
|
||||
|| instruction & 0x0fb0_fff0 == 0x0120_f000
|
||||
|| instruction & 0x0fb0_f000 == 0x0320_f000
|
||||
{
|
||||
return Summary::default(); // MRS/MSR overlap the data-processing space.
|
||||
}
|
||||
if instruction & 0x0e00_0000 == 0x0800_0000 {
|
||||
let list = instruction & 0xffff;
|
||||
return Summary::transfer(
|
||||
instruction & (1 << 20) != 0,
|
||||
list == 0 || list & 0x8000 != 0,
|
||||
);
|
||||
}
|
||||
if instruction & 0x0fb0_0ff0 == 0x0100_0090 {
|
||||
return Summary {
|
||||
internal: 1,
|
||||
..Summary::default()
|
||||
}; // SWP: two N data accesses
|
||||
}
|
||||
if instruction & 0x0f80_00f0 == 0x0080_0090 {
|
||||
let signed = instruction & (1 << 22) != 0;
|
||||
let multiplier = self.registers[((instruction >> 8) & 15) as usize];
|
||||
return Summary {
|
||||
internal: multiply_cycles(multiplier, signed)
|
||||
+ 1
|
||||
+ u32::from(instruction & (1 << 21) != 0),
|
||||
..Summary::default()
|
||||
};
|
||||
}
|
||||
if instruction & 0x0fc0_00f0 == 0x0000_0090 {
|
||||
let multiplier = self.registers[((instruction >> 8) & 15) as usize];
|
||||
return Summary {
|
||||
internal: multiply_cycles(multiplier, true)
|
||||
+ u32::from(instruction & (1 << 21) != 0),
|
||||
..Summary::default()
|
||||
};
|
||||
}
|
||||
if instruction & 0x0e00_0090 == 0x0000_0090 {
|
||||
return Summary::transfer(instruction & (1 << 20) != 0, false);
|
||||
}
|
||||
if instruction & 0x0c00_0000 == 0x0400_0000 {
|
||||
return Summary::transfer(instruction & (1 << 20) != 0, (instruction >> 12) & 15 == 15);
|
||||
}
|
||||
let test = (8..=11).contains(&((instruction >> 21) & 15));
|
||||
Summary {
|
||||
refill: !test && (instruction >> 12) & 15 == 15,
|
||||
internal: u32::from(instruction & (1 << 25) == 0 && instruction & (1 << 4) != 0),
|
||||
..Summary::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn thumb_summary(&self, instruction: u32) -> Summary {
|
||||
match instruction {
|
||||
0x4000..=0x43ff => {
|
||||
let opcode = (instruction >> 6) & 15;
|
||||
let internal = match opcode {
|
||||
2 | 3 | 4 | 7 => 1,
|
||||
// Thumb MUL maps the incoming destination to ARM's Rs.
|
||||
13 => multiply_cycles(self.registers[(instruction & 7) as usize], true),
|
||||
_ => 0,
|
||||
};
|
||||
Summary {
|
||||
internal,
|
||||
..Summary::default()
|
||||
}
|
||||
}
|
||||
0x4400..=0x47ff => {
|
||||
let opcode = (instruction >> 8) & 3;
|
||||
let destination = (instruction & 7) | ((instruction >> 4) & 8);
|
||||
if opcode == 3 || (opcode != 1 && destination == 15) {
|
||||
Summary::branch()
|
||||
} else {
|
||||
Summary::default()
|
||||
}
|
||||
}
|
||||
0x4800..=0x4fff => Summary::transfer(true, false),
|
||||
0x5000..=0x5fff => Summary::transfer((instruction >> 9) & 7 >= 3, false),
|
||||
0x6000..=0x9fff => Summary::transfer(instruction & (1 << 11) != 0, false),
|
||||
0xb400..=0xb5ff | 0xbc00..=0xbdff => Summary::transfer(
|
||||
instruction & (1 << 11) != 0,
|
||||
instruction & 0x100 != 0 || instruction & 0xff == 0,
|
||||
),
|
||||
0xc000..=0xcfff => {
|
||||
Summary::transfer(instruction & (1 << 11) != 0, instruction & 255 == 0)
|
||||
}
|
||||
0xd000..=0xddff if self.flags.condition_passed((instruction >> 8) & 15) => {
|
||||
Summary::branch()
|
||||
}
|
||||
0xdf00..=0xdfff | 0xe000..=0xe7ff | 0xf800..=0xffff => Summary::branch(),
|
||||
// Thumb BL prefix is 1S in the old region; its suffix is 2S+1N at the target.
|
||||
_ => Summary::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn multiply_cycles(value: u32, signed: bool) -> u32 {
|
||||
for bytes in 1..=3 {
|
||||
let mask = u32::MAX << (bytes * 8);
|
||||
let high = value & mask;
|
||||
if high == 0 || signed && high == mask {
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
4
|
||||
}
|
||||
383
crates/gba-core/src/cpu/timing_tests.rs
Normal file
383
crates/gba-core/src/cpu/timing_tests.rs
Normal file
|
|
@ -0,0 +1,383 @@
|
|||
use super::*;
|
||||
use crate::{io::WAITCNT, memory::ROM_START, timing::StepTiming};
|
||||
|
||||
const CODE: u32 = 0x0300_0000;
|
||||
const DATA: u32 = 0x0200_0100;
|
||||
|
||||
fn arm(instruction: u32) -> (Cpu, Memory) {
|
||||
let mut memory = Memory::new(vec![0; 0x40000]).unwrap();
|
||||
memory.write32(CODE, instruction).unwrap();
|
||||
(Cpu::new(CODE), memory)
|
||||
}
|
||||
|
||||
fn thumb(instruction: u16) -> (Cpu, Memory) {
|
||||
let (mut cpu, mut memory) = arm(0);
|
||||
cpu.instruction_set = InstructionSet::Thumb;
|
||||
memory.write16(CODE, instruction).unwrap();
|
||||
(cpu, memory)
|
||||
}
|
||||
|
||||
fn expected(code: u32, data: u32, internal: u32) -> StepTiming {
|
||||
StepTiming {
|
||||
code_cycles: code,
|
||||
data_cycles: data,
|
||||
internal_cycles: internal,
|
||||
idle_cycles: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arm_alu_immediate_shifts_register_shifts_and_status_transfers() {
|
||||
for opcode in 0..16 {
|
||||
for operand in [0x0200_0001, 0x0000_0082, 0x0000_0312] {
|
||||
// r0 destination, r1 first operand; S set makes TST/TEQ/CMP/CMN valid.
|
||||
let instruction = 0xe011_0000 | (opcode << 21) | operand;
|
||||
let (mut cpu, mut memory) = arm(instruction);
|
||||
cpu.registers[1] = 123;
|
||||
cpu.registers[2] = 12;
|
||||
cpu.registers[3] = 0; // Register shift still costs I when amount is zero.
|
||||
let cycles = cpu.step_timed(&mut memory).unwrap();
|
||||
assert_eq!(cycles, expected(1, 0, u32::from(operand == 0x312)));
|
||||
assert_eq!(memory.cycles(), 0); // Timed CPU API returns costs; it does not run devices.
|
||||
}
|
||||
}
|
||||
for instruction in [0xe10f_0000, 0xe328_f480, 0xe128_f000, 0xe321_f013] {
|
||||
let (mut cpu, mut memory) = arm(instruction);
|
||||
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(1, 0, 0));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conditional_skips_do_not_charge_memory_multiply_or_branch_work() {
|
||||
for instruction in [
|
||||
0x0590_1000,
|
||||
0x0580_1000,
|
||||
0x0a00_0000,
|
||||
0x0f00_0000,
|
||||
0x0000_0291,
|
||||
] {
|
||||
let (mut cpu, mut memory) = arm(instruction);
|
||||
cpu.registers[2] = 0x1234_5678;
|
||||
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(1, 0, 0));
|
||||
assert_eq!(cpu.pc(), CODE + 4);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn branch_and_pc_write_refill_even_when_target_equals_fallthrough() {
|
||||
for instruction in [0xeaff_ffff, 0xe1a0_f000, 0xe1a0_f110] {
|
||||
let (mut cpu, mut memory) = arm(instruction);
|
||||
cpu.registers[0] = CODE + 4;
|
||||
let timing = cpu.step_timed(&mut memory).unwrap();
|
||||
assert_eq!(
|
||||
timing,
|
||||
expected(3, 0, u32::from(instruction == 0xe1a0_f110))
|
||||
);
|
||||
assert_eq!(cpu.pc(), CODE + 4);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cross_region_bx_uses_destination_width_and_wait_states() {
|
||||
let (mut cpu, mut memory) = arm(0xe12f_ff10);
|
||||
cpu.registers[0] = 0x0a00_0101; // Thumb in WS1: 5N + 5S + 5S
|
||||
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(15, 0, 0));
|
||||
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
|
||||
let (mut cpu, mut memory) = thumb(0x4700);
|
||||
cpu.registers[0] = ROM_START + 0x100; // ARM in WS0: 8N + 6S + 6S
|
||||
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(20, 0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arm_single_load_store_widths_charge_one_actual_bus_access() {
|
||||
for (instruction, data_cycles, internal) in [
|
||||
(0xe590_1000, 6, 1), // LDR
|
||||
(0xe580_1000, 6, 0), // STR
|
||||
(0xe5d0_1000, 3, 1), // LDRB
|
||||
(0xe5c0_1000, 3, 0), // STRB
|
||||
(0xe1d0_10b0, 3, 1), // LDRH
|
||||
(0xe1c0_10b0, 3, 0), // STRH
|
||||
(0xe1d0_10d0, 3, 1), // LDRSB
|
||||
(0xe1d0_10f0, 3, 1), // LDRSH
|
||||
] {
|
||||
for offset in 0..4 {
|
||||
let (mut cpu, mut memory) = arm(instruction);
|
||||
cpu.registers[0] = DATA + offset;
|
||||
assert_eq!(
|
||||
cpu.step_timed(&mut memory).unwrap(),
|
||||
expected(1, data_cycles, internal)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn actual_pre_post_and_shifted_addresses_determine_load_costs() {
|
||||
for (instruction, base, offset) in [
|
||||
(0xe790_1102, 0x0100_0100, 0x0080_0000), // LDR r1,[r0,r2,LSL #2] -> IWRAM
|
||||
(0xe490_1004, 0x0300_0100, 0), // post-index uses old base
|
||||
(0xe510_1100, 0x0300_0200, 0), // negative immediate -> IWRAM
|
||||
] {
|
||||
let (mut cpu, mut memory) = arm(instruction);
|
||||
cpu.registers[0] = base;
|
||||
cpu.registers[2] = offset;
|
||||
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(1, 1, 1));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_pc_adds_refill_and_does_not_count_fetch_as_data() {
|
||||
let (mut cpu, mut memory) = arm(0xe590_f000);
|
||||
cpu.registers[0] = DATA;
|
||||
memory.write32(DATA, ROM_START + 0x100).unwrap();
|
||||
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(20, 6, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_transfers_count_words_and_apply_refill_only_for_load_pc() {
|
||||
for list in [0, 1, 0x000f, 0x1fff, 0xffff] {
|
||||
for load in [false, true] {
|
||||
let (mut cpu, mut memory) = arm(0xe880_0000 | (u32::from(load) << 20) | list);
|
||||
cpu.registers[0] = CODE + 0x100;
|
||||
let count = if list == 0 { 1 } else { list.count_ones() };
|
||||
let pc = load && (list == 0 || list & 0x8000 != 0);
|
||||
assert_eq!(
|
||||
cpu.step_timed(&mut memory).unwrap(),
|
||||
expected(if pc { 3 } else { 1 }, count, u32::from(load))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rom_block_reads_use_n_then_s_and_restart_at_128_kib_boundary() {
|
||||
let (mut cpu, mut memory) = arm(0xe890_001e); // LDMIA r0, {r1-r4}
|
||||
cpu.registers[0] = ROM_START + 0x1fff8;
|
||||
// Word accesses: N+S=8, S+S=6, forced N+S=8, S+S=6.
|
||||
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(1, 28, 1));
|
||||
let (mut cpu, mut memory) = arm(0xe590_1000);
|
||||
cpu.registers[0] = ROM_START + 0x100;
|
||||
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(1, 8, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swap_uses_two_nonsequential_accesses_and_one_internal_cycle() {
|
||||
for (instruction, data_cycles) in [(0xe100_1092, 12), (0xe140_1092, 6)] {
|
||||
let (mut cpu, mut memory) = arm(instruction);
|
||||
cpu.registers[0] = DATA;
|
||||
cpu.registers[2] = 123;
|
||||
assert_eq!(
|
||||
cpu.step_timed(&mut memory).unwrap(),
|
||||
expected(1, data_cycles, 1)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiply_early_termination_uses_rs_and_signedness_before_execution() {
|
||||
for (value, signed_m, unsigned_m) in [
|
||||
(0, 1, 1),
|
||||
(255, 1, 1),
|
||||
(256, 2, 2),
|
||||
(65535, 2, 2),
|
||||
(65536, 3, 3),
|
||||
(0x00ff_ffff, 3, 3),
|
||||
(0x0100_0000, 4, 4),
|
||||
(0xffff_ffff, 1, 4),
|
||||
(0xffff_ff00, 1, 4),
|
||||
(0xffff_0000, 2, 4),
|
||||
(0xff00_0000, 3, 4),
|
||||
(0x8000_0000, 4, 4),
|
||||
] {
|
||||
for (instruction, internal) in [
|
||||
(0xe000_0291, signed_m), // MUL r0,r1,r2
|
||||
(0xe020_3291, signed_m + 1), // MLA r0,r1,r2,r3
|
||||
(0xe081_0392, unsigned_m + 1), // UMULL r0,r1,r2,r3
|
||||
(0xe0a1_0392, unsigned_m + 2), // UMLAL
|
||||
(0xe0c1_0392, signed_m + 1), // SMULL
|
||||
(0xe0e1_0392, signed_m + 2), // SMLAL
|
||||
] {
|
||||
let (mut cpu, mut memory) = arm(instruction);
|
||||
cpu.registers[1] = 0x1234_5678;
|
||||
cpu.registers[2] = value;
|
||||
cpu.registers[3] = value;
|
||||
assert_eq!(
|
||||
cpu.step_timed(&mut memory).unwrap(),
|
||||
expected(1, 0, internal),
|
||||
"{instruction:08x}, {value:08x}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thumb_alu_shifts_and_multiply_use_correct_internal_cycles() {
|
||||
for opcode in 0..16 {
|
||||
for value in [7, 0x1234_5678, 0xffff_ffff] {
|
||||
let (mut cpu, mut memory) = thumb(0x4008 | opcode << 6);
|
||||
cpu.registers[0] = value;
|
||||
cpu.registers[1] = 0; // Multiplication timing must not use this source.
|
||||
let internal = match opcode {
|
||||
2 | 3 | 4 | 7 => 1,
|
||||
13 if value == 0x1234_5678 => 4,
|
||||
13 => 1,
|
||||
_ => 0,
|
||||
};
|
||||
assert_eq!(
|
||||
cpu.step_timed(&mut memory).unwrap(),
|
||||
expected(1, 0, internal)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thumb_transfers_literals_sp_and_register_offsets_use_actual_bus_widths() {
|
||||
for (instruction, data_cycles, internal) in [
|
||||
(0x4800, 1, 1), // literal LDR from IWRAM
|
||||
(0x5081, 6, 0),
|
||||
(0x5281, 3, 0),
|
||||
(0x5481, 3, 0),
|
||||
(0x5681, 3, 1),
|
||||
(0x5881, 6, 1),
|
||||
(0x5a81, 3, 1),
|
||||
(0x5c81, 3, 1),
|
||||
(0x5e81, 3, 1),
|
||||
(0x6001, 6, 0),
|
||||
(0x6801, 6, 1),
|
||||
(0x7001, 3, 0),
|
||||
(0x7801, 3, 1),
|
||||
(0x8001, 3, 0),
|
||||
(0x8801, 3, 1),
|
||||
(0x9000, 6, 0),
|
||||
(0x9800, 6, 1),
|
||||
] {
|
||||
let (mut cpu, mut memory) = thumb(instruction);
|
||||
cpu.registers[0] = DATA;
|
||||
cpu.registers[13] = DATA;
|
||||
assert_eq!(
|
||||
cpu.step_timed(&mut memory).unwrap(),
|
||||
expected(1, data_cycles, internal),
|
||||
"{instruction:04x}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thumb_branches_bl_halves_and_high_pc_writes_refill_only_when_taken() {
|
||||
for (instruction, code) in [
|
||||
(0xd000, 1),
|
||||
(0xd100, 3),
|
||||
(0xe000, 3),
|
||||
(0x4687, 3),
|
||||
(0x4487, 3),
|
||||
(0x4587, 1),
|
||||
] {
|
||||
let (mut cpu, mut memory) = thumb(instruction);
|
||||
cpu.registers[0] = CODE + 4;
|
||||
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(code, 0, 0));
|
||||
}
|
||||
let (mut cpu, mut memory) = thumb(0xf000);
|
||||
memory.write16(CODE + 2, 0xf800).unwrap();
|
||||
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(1, 0, 0));
|
||||
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(3, 0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thumb_push_pop_and_multiple_transfers_charge_each_word() {
|
||||
for (instruction, data, internal, code) in [
|
||||
(0xb403, 12, 0, 1),
|
||||
(0xb503, 18, 0, 1),
|
||||
(0xbc03, 12, 1, 1),
|
||||
(0xbd03, 18, 1, 3),
|
||||
(0xc003, 12, 0, 1),
|
||||
(0xc803, 12, 1, 1),
|
||||
(0xc800, 6, 1, 3),
|
||||
(0xbc00, 6, 1, 3),
|
||||
] {
|
||||
let (mut cpu, mut memory) = thumb(instruction);
|
||||
cpu.registers[0] = DATA;
|
||||
cpu.registers[13] = DATA;
|
||||
assert_eq!(
|
||||
cpu.step_timed(&mut memory).unwrap(),
|
||||
expected(code, data, internal)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn software_interrupts_and_exception_returns_use_new_code_region_and_state() {
|
||||
for thumb_state in [false, true] {
|
||||
let (mut cpu, mut memory) = if thumb_state {
|
||||
thumb(0xdf00)
|
||||
} else {
|
||||
arm(0xef00_0000)
|
||||
};
|
||||
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(3, 0, 0));
|
||||
assert_eq!(cpu.mode(), Mode::Supervisor);
|
||||
// Return via MOVS pc,lr, executing from IWRAM without a BIOS image.
|
||||
memory.write32(CODE + 0x100, 0xe1b0_f00e).unwrap();
|
||||
cpu.registers[15] = CODE + 0x100;
|
||||
cpu.registers[14] = ROM_START + 0x100;
|
||||
assert_eq!(
|
||||
cpu.step_timed(&mut memory).unwrap(),
|
||||
expected(if thumb_state { 11 } else { 20 }, 0, 0)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn waitcnt_store_charges_old_code_settings_and_new_settings_apply_next_step() {
|
||||
let mut memory = Memory::new(
|
||||
[0xe580_1000_u32, 0xe1a0_0000]
|
||||
.into_iter()
|
||||
.flat_map(u32::to_le_bytes)
|
||||
.collect(),
|
||||
)
|
||||
.unwrap();
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = WAITCNT;
|
||||
cpu.registers[1] = 0x18; // N=3 and S=2 per halfword
|
||||
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(8, 1, 0));
|
||||
assert_eq!(memory.waitcnt(), 0x18);
|
||||
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(4, 0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_data_access_discards_trace_before_retry() {
|
||||
let (mut cpu, mut memory) = arm(0xe8b0_0006); // LDMIA r0!,{r1,r2}
|
||||
cpu.registers[0] = 0x0400_0054;
|
||||
let before = cpu.clone();
|
||||
assert!(cpu.step_timed(&mut memory).is_err());
|
||||
assert_eq!(cpu, before);
|
||||
assert_eq!(memory.cycles(), 0);
|
||||
cpu.registers[0] = DATA;
|
||||
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(1, 12, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timing_preserves_semantics_for_every_thumb_encoding() {
|
||||
let mut timed_memory = Memory::new(vec![]).unwrap();
|
||||
let mut plain_memory = Memory::new(vec![]).unwrap();
|
||||
for instruction in 0..=u16::MAX {
|
||||
timed_memory.write16(CODE, instruction).unwrap();
|
||||
plain_memory.write16(CODE, instruction).unwrap();
|
||||
let mut plain = Cpu::new(CODE);
|
||||
plain.instruction_set = InstructionSet::Thumb;
|
||||
for register in 0..15 {
|
||||
plain.registers[register] = DATA + register as u32 * 4;
|
||||
}
|
||||
let mut timed = plain.clone();
|
||||
let cost = timed.step_timed(&mut timed_memory);
|
||||
if let Ok(cost) = cost {
|
||||
assert!(cost.total() > 0);
|
||||
}
|
||||
assert_eq!(
|
||||
cost.map(|_| ()),
|
||||
plain.step(&mut plain_memory),
|
||||
"{instruction:04x}"
|
||||
);
|
||||
assert_eq!(timed, plain, "{instruction:04x}");
|
||||
assert_eq!(timed_memory.cycles(), 0);
|
||||
}
|
||||
}
|
||||
145
crates/gba-core/src/cpu/transfer.rs
Normal file
145
crates/gba-core/src/cpu/transfer.rs
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
//! Block transfers shared by ARM and Thumb, plus ARM swaps.
|
||||
//! Data aborts and per-access device event timing are not modeled.
|
||||
|
||||
use super::{Cpu, CpuError, InstructionSet};
|
||||
use crate::memory::Memory;
|
||||
|
||||
impl Cpu {
|
||||
pub(super) fn block_transfer(
|
||||
&mut self,
|
||||
instruction: u32,
|
||||
memory: &mut Memory,
|
||||
) -> Result<(), CpuError> {
|
||||
let base_register = ((instruction >> 16) & 15) as usize;
|
||||
if base_register == 15 {
|
||||
return Err(self.unsupported(instruction));
|
||||
}
|
||||
|
||||
let encoded_list = instruction & 0xffff;
|
||||
// ARM7's empty list transfers PC, but calculates addresses/writeback
|
||||
// as if sixteen registers were selected.
|
||||
let (list, span) = if encoded_list == 0 {
|
||||
(1 << 15, 64)
|
||||
} else {
|
||||
(encoded_list, encoded_list.count_ones() * 4)
|
||||
};
|
||||
let pre = instruction & (1 << 24) != 0;
|
||||
let up = instruction & (1 << 23) != 0;
|
||||
let write_back = instruction & (1 << 21) != 0;
|
||||
let load = instruction & (1 << 20) != 0;
|
||||
let special = instruction & (1 << 22) != 0;
|
||||
let user_bank = special && !(load && list & (1 << 15) != 0);
|
||||
// Diagnose unpredictable user-bank forms and the unverified S+empty case.
|
||||
if special && (self.mode == super::Mode::User || encoded_list == 0)
|
||||
|| user_bank && write_back
|
||||
{
|
||||
return Err(self.unsupported(instruction));
|
||||
}
|
||||
let restore = if special && !user_bank {
|
||||
Some(self.return_status(instruction)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let base = self.registers[base_register];
|
||||
let adjusted = if up {
|
||||
base.wrapping_add(span)
|
||||
} else {
|
||||
base.wrapping_sub(span)
|
||||
};
|
||||
let start = match (up, pre) {
|
||||
(true, false) => base, // IA
|
||||
(true, true) => base.wrapping_add(4), // IB
|
||||
(false, false) => adjusted.wrapping_add(4), // DA
|
||||
(false, true) => adjusted, // DB
|
||||
};
|
||||
// Unlike LDR, block loads align down without rotating the loaded word.
|
||||
// The low base bits are retained in writeback.
|
||||
let mut address = start & !3;
|
||||
let mut next = self.registers;
|
||||
next[15] = self.pc().wrapping_add(self.instruction_set.width());
|
||||
let base_in_list = list & (1 << base_register) != 0;
|
||||
if write_back && !(load && base_in_list) {
|
||||
next[base_register] = adjusted;
|
||||
}
|
||||
|
||||
let mut writes = [(0, 0); 16];
|
||||
let mut write_count = 0;
|
||||
for (register, destination) in next.iter_mut().enumerate() {
|
||||
if list & (1 << register) == 0 {
|
||||
continue;
|
||||
}
|
||||
if load {
|
||||
let value = memory.read32(address)?;
|
||||
*destination = if register == 15 && restore.is_none() {
|
||||
self.instruction_set.align(value)
|
||||
} else {
|
||||
value
|
||||
};
|
||||
} else {
|
||||
let value = if write_back
|
||||
&& register == base_register
|
||||
&& register as u32 != list.trailing_zeros()
|
||||
{
|
||||
// ARM7 stores the updated base when it is not first in Rlist.
|
||||
adjusted
|
||||
} else if user_bank {
|
||||
self.user_register(register)
|
||||
} else {
|
||||
let pc_offset = if self.instruction_set == InstructionSet::Arm {
|
||||
12
|
||||
} else {
|
||||
4
|
||||
};
|
||||
self.operand_register(register, pc_offset)
|
||||
};
|
||||
writes[write_count] = (address, value);
|
||||
write_count += 1;
|
||||
}
|
||||
address = address.wrapping_add(4);
|
||||
}
|
||||
if !load {
|
||||
memory.write_words(&writes[..write_count])?;
|
||||
}
|
||||
// Commit only after every access succeeds. This keeps diagnostics retryable.
|
||||
if user_bank && load {
|
||||
for (register, value) in next.into_iter().enumerate() {
|
||||
if list & (1 << register) != 0 {
|
||||
self.set_user_register(register, value);
|
||||
}
|
||||
}
|
||||
self.registers[15] = next[15];
|
||||
} else {
|
||||
self.registers = next;
|
||||
}
|
||||
if let Some((status, mode)) = restore {
|
||||
// Save loaded banked registers and writeback in the outgoing bank.
|
||||
self.apply_status(status, mode);
|
||||
self.registers[15] = self.instruction_set.align(self.pc());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn swap(&mut self, instruction: u32, memory: &mut Memory) -> Result<(), CpuError> {
|
||||
let base_register = ((instruction >> 16) & 15) as usize;
|
||||
let destination = ((instruction >> 12) & 15) as usize;
|
||||
let source = (instruction & 15) as usize;
|
||||
if [base_register, destination, source].contains(&15) {
|
||||
return Err(self.unsupported(instruction));
|
||||
}
|
||||
// Snapshot operands so source/destination/base aliases use old values.
|
||||
let address = self.registers[base_register];
|
||||
let value = self.registers[source];
|
||||
let previous = if instruction & (1 << 22) != 0 {
|
||||
let previous = u32::from(memory.read8(address)?);
|
||||
memory.write8(address, value as u8)?;
|
||||
previous
|
||||
} else {
|
||||
let previous = memory.read32(address & !3)?.rotate_right((address & 3) * 8);
|
||||
memory.write32(address & !3, value)?;
|
||||
previous
|
||||
};
|
||||
self.registers[destination] = previous;
|
||||
self.registers[15] = self.pc().wrapping_add(4);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
493
crates/gba-core/src/cpu/transfer_tests.rs
Normal file
493
crates/gba-core/src/cpu/transfer_tests.rs
Normal file
|
|
@ -0,0 +1,493 @@
|
|||
use super::*;
|
||||
use crate::memory::ROM_START;
|
||||
|
||||
const RAM: u32 = 0x0200_0000;
|
||||
const BASE: u32 = RAM + 0x100;
|
||||
|
||||
fn program(words: &[u32]) -> Memory {
|
||||
Memory::new(words.iter().flat_map(|word| word.to_le_bytes()).collect()).unwrap()
|
||||
}
|
||||
|
||||
fn flags() -> Flags {
|
||||
Flags {
|
||||
negative: true,
|
||||
zero: true,
|
||||
carry: true,
|
||||
overflow: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn block(mode: u32, load: bool, write_back: bool, base: u32, list: u32) -> u32 {
|
||||
0xe800_0000
|
||||
| mode
|
||||
| (u32::from(load) << 20)
|
||||
| (u32::from(write_back) << 21)
|
||||
| (base << 16)
|
||||
| list
|
||||
}
|
||||
|
||||
// Explicit address fixtures for five-register transfers with BASE as Rn.
|
||||
const MODES: [(u32, u32, u32); 4] = [
|
||||
(0x0080_0000, BASE, BASE + 20), // IA
|
||||
(0x0180_0000, BASE + 4, BASE + 20), // IB
|
||||
(0x0000_0000, BASE - 16, BASE - 20), // DA
|
||||
(0x0100_0000, BASE - 20, BASE - 20), // DB
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn stm_all_addressing_modes_store_sparse_registers_in_ascending_order() {
|
||||
let list = (1 << 0) | (1 << 3) | (1 << 7) | (1 << 14) | (1 << 15);
|
||||
for (mode, first, updated) in MODES {
|
||||
for write_back in [false, true] {
|
||||
let mut memory = program(&[block(mode, false, write_back, 10, list)]);
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = 0x11;
|
||||
cpu.registers[3] = 0x22;
|
||||
cpu.registers[7] = 0x33;
|
||||
cpu.registers[14] = 0x44;
|
||||
cpu.registers[10] = BASE;
|
||||
cpu.flags = flags();
|
||||
memory.write32(first - 4, 0xdead_beef).unwrap();
|
||||
memory.write32(first + 20, 0xcafe_babe).unwrap();
|
||||
cpu.step(&mut memory).unwrap();
|
||||
for (offset, value) in [0x11, 0x22, 0x33, 0x44, ROM_START + 12]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
assert_eq!(memory.read32(first + offset as u32 * 4).unwrap(), value);
|
||||
}
|
||||
assert_eq!(memory.read32(first - 4).unwrap(), 0xdead_beef);
|
||||
assert_eq!(memory.read32(first + 20).unwrap(), 0xcafe_babe);
|
||||
assert_eq!(cpu.registers[10], if write_back { updated } else { BASE });
|
||||
assert_eq!(cpu.flags, flags());
|
||||
assert_eq!(cpu.pc(), ROM_START + 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ldm_all_addressing_modes_load_sparse_registers_and_align_pc() {
|
||||
let registers = [0, 3, 7, 14, 15];
|
||||
let list = registers
|
||||
.iter()
|
||||
.fold(0, |list, register| list | (1 << register));
|
||||
for (mode, first, updated) in MODES {
|
||||
for write_back in [false, true] {
|
||||
let mut memory = program(&[block(mode, true, write_back, 10, list)]);
|
||||
for (offset, value) in [11, 22, 33, 44, ROM_START + 0x47].into_iter().enumerate() {
|
||||
memory.write32(first + offset as u32 * 4, value).unwrap();
|
||||
}
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[10] = BASE;
|
||||
cpu.flags = flags();
|
||||
cpu.step(&mut memory).unwrap();
|
||||
for (register, value) in registers
|
||||
.into_iter()
|
||||
.zip([11, 22, 33, 44, ROM_START + 0x44])
|
||||
{
|
||||
assert_eq!(cpu.registers[register], value);
|
||||
}
|
||||
assert_eq!(cpu.registers[10], if write_back { updated } else { BASE });
|
||||
assert_eq!(cpu.flags, flags());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stm_base_in_list_stores_old_base_only_when_first_with_writeback() {
|
||||
// The five-entry list is r1,r3,r5,r7,r9. Exercise each possible base position.
|
||||
let registers = [1, 3, 5, 7, 9];
|
||||
let list = registers
|
||||
.iter()
|
||||
.fold(0, |list, register| list | (1 << register));
|
||||
for (mode, first, updated) in MODES {
|
||||
for write_back in [false, true] {
|
||||
for (position, base_register) in registers.into_iter().enumerate() {
|
||||
let mut memory = program(&[block(mode, false, write_back, base_register, list)]);
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[base_register as usize] = BASE;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
let stored = memory.read32(first + position as u32 * 4).unwrap();
|
||||
assert_eq!(
|
||||
stored,
|
||||
if write_back && position != 0 {
|
||||
updated
|
||||
} else {
|
||||
BASE
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
cpu.registers[base_register as usize],
|
||||
if write_back { updated } else { BASE }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ldm_base_in_list_keeps_loaded_value_instead_of_writeback() {
|
||||
let registers = [1, 3, 5, 7, 9];
|
||||
let list = registers
|
||||
.iter()
|
||||
.fold(0, |list, register| list | (1 << register));
|
||||
for (mode, first, _) in MODES {
|
||||
for write_back in [false, true] {
|
||||
for base_register in registers {
|
||||
let mut memory = program(&[block(mode, true, write_back, base_register, list)]);
|
||||
for (offset, register) in registers.into_iter().enumerate() {
|
||||
memory
|
||||
.write32(first + offset as u32 * 4, 100 + register)
|
||||
.unwrap();
|
||||
}
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[base_register as usize] = BASE;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
for register in registers {
|
||||
assert_eq!(cpu.registers[register as usize], 100 + register);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_single_base_register_in_the_list_uses_arm7_writeback_rules() {
|
||||
for load in [false, true] {
|
||||
let mut memory = program(&[block(0x0080_0000, load, true, 2, 1 << 2)]);
|
||||
memory.write32(BASE, 0x1234_5678).unwrap();
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[2] = BASE;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
if load {
|
||||
assert_eq!(cpu.registers[2], 0x1234_5678);
|
||||
} else {
|
||||
assert_eq!(cpu.registers[2], BASE + 4);
|
||||
assert_eq!(memory.read32(BASE).unwrap(), BASE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_register_list_loads_all_registers_including_the_base_and_pc() {
|
||||
let mut memory = program(&[block(0x0080_0000, true, true, 10, 0xffff)]);
|
||||
for register in 0..16 {
|
||||
memory
|
||||
.write32(BASE + register * 4, 0x1000 + register * 4)
|
||||
.unwrap();
|
||||
}
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[10] = BASE;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
for register in 0..16 {
|
||||
assert_eq!(cpu.registers[register], 0x1000 + register as u32 * 4);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_register_list_stores_pc_and_updated_base_without_array_overflow() {
|
||||
let mut memory = program(&[block(0x0080_0000, false, true, 10, 0xffff)]);
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
for register in 0..15 {
|
||||
cpu.registers[register] = register as u32;
|
||||
}
|
||||
cpu.registers[10] = BASE;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
for register in 0..16 {
|
||||
let expected = match register {
|
||||
10 => BASE + 64,
|
||||
15 => ROM_START + 12,
|
||||
_ => register,
|
||||
};
|
||||
assert_eq!(memory.read32(BASE + register * 4).unwrap(), expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_list_transfers_only_pc_with_a_sixty_four_byte_span() {
|
||||
let modes = [
|
||||
(0x0080_0000, BASE, BASE + 64),
|
||||
(0x0180_0000, BASE + 4, BASE + 64),
|
||||
(0x0000_0000, BASE - 60, BASE - 64),
|
||||
(0x0100_0000, BASE - 64, BASE - 64),
|
||||
];
|
||||
for (mode, address, updated) in modes {
|
||||
for write_back in [false, true] {
|
||||
for load in [false, true] {
|
||||
let mut memory = program(&[block(mode, load, write_back, 1, 0)]);
|
||||
memory.write32(address - 4, 0xfeed_face).unwrap();
|
||||
memory.write32(address, ROM_START + 0x47).unwrap();
|
||||
memory.write32(address + 4, 0xdead_beef).unwrap();
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[1] = BASE;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.registers[1], if write_back { updated } else { BASE });
|
||||
assert_eq!(cpu.pc(), ROM_START + if load { 0x44 } else { 4 });
|
||||
assert_eq!(
|
||||
memory.read32(address).unwrap(),
|
||||
if load {
|
||||
ROM_START + 0x47
|
||||
} else {
|
||||
ROM_START + 12
|
||||
}
|
||||
);
|
||||
assert_eq!(memory.read32(address - 4).unwrap(), 0xfeed_face);
|
||||
assert_eq!(memory.read32(address + 4).unwrap(), 0xdead_beef);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unaligned_block_transfers_align_words_without_rotation_and_preserve_base_low_bits() {
|
||||
for offset in 1..4 {
|
||||
let mut memory = program(&[
|
||||
block(0x0080_0000, false, true, 0, 0x6),
|
||||
block(0x0100_0000, true, true, 0, 0x18),
|
||||
]);
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = BASE + offset;
|
||||
cpu.registers[1] = 0x1234_5678;
|
||||
cpu.registers[2] = 0x90ab_cdef;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.registers[0], BASE + offset + 8);
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.registers[0], BASE + offset);
|
||||
assert_eq!(cpu.registers[3], 0x1234_5678);
|
||||
assert_eq!(cpu.registers[4], 0x90ab_cdef);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stack_push_and_pop_restore_registers_and_return_through_pc() {
|
||||
let mut memory = program(&[
|
||||
0xe92d_4010, // STMDB sp!,{r4,lr}
|
||||
0xe3a0_4000, // MOV r4,#0
|
||||
0xe8bd_8010, // LDMIA sp!,{r4,pc}
|
||||
]);
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[4] = 42;
|
||||
cpu.registers[13] = BASE;
|
||||
cpu.registers[14] = ROM_START + 20;
|
||||
for _ in 0..3 {
|
||||
cpu.step(&mut memory).unwrap();
|
||||
}
|
||||
assert_eq!(cpu.registers[4], 42);
|
||||
assert_eq!(cpu.registers[13], BASE);
|
||||
assert_eq!(cpu.pc(), ROM_START + 20);
|
||||
assert_eq!(memory.read32(BASE - 8).unwrap(), 42);
|
||||
assert_eq!(memory.read32(BASE - 4).unwrap(), ROM_START + 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_transfer_can_cross_ram_regions() {
|
||||
let mut memory = program(&[block(0x0080_0000, false, true, 0, 0x6)]);
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = 0x02ff_fffc;
|
||||
cpu.registers[1] = 11;
|
||||
cpu.registers[2] = 22;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(memory.read32(0x0203_fffc).unwrap(), 11); // External RAM mirror
|
||||
assert_eq!(memory.read32(0x0300_0000).unwrap(), 22); // Internal RAM
|
||||
assert_eq!(cpu.registers[0], 0x0300_0004);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_diagnostics_do_not_commit_partial_register_or_ram_changes() {
|
||||
for load in [false, true] {
|
||||
let mut memory = Memory::new(vec![]).unwrap();
|
||||
memory
|
||||
.write32(0x0300_0000, block(0x0080_0000, load, true, 2, 0x8001))
|
||||
.unwrap();
|
||||
memory.write32(0x07ff_fffc, 0xdead_beef).unwrap();
|
||||
let mut cpu = Cpu::new(0x0300_0000);
|
||||
cpu.registers[0] = 42;
|
||||
cpu.registers[2] = 0x07ff_fffc; // Second transfer reaches absent/read-only ROM.
|
||||
cpu.flags = flags();
|
||||
let before = cpu.registers;
|
||||
assert_eq!(
|
||||
cpu.step(&mut memory),
|
||||
Err(CpuError::Memory(if load {
|
||||
MemoryError::Unmapped(ROM_START)
|
||||
} else {
|
||||
MemoryError::ReadOnly(ROM_START)
|
||||
}))
|
||||
);
|
||||
assert_eq!(cpu.registers, before);
|
||||
assert_eq!(cpu.flags, flags());
|
||||
assert_eq!(memory.read32(0x07ff_fffc).unwrap(), 0xdead_beef);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_load_can_read_rom_and_store_reports_read_only_memory() {
|
||||
for load in [false, true] {
|
||||
let mut memory = program(&[block(0x0080_0000, load, true, 0, 0x6), 11, 22]);
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = ROM_START + 4;
|
||||
if load {
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(&cpu.registers[1..3], &[11, 22]);
|
||||
assert_eq!(cpu.registers[0], ROM_START + 12);
|
||||
} else {
|
||||
let before = cpu.registers;
|
||||
assert_eq!(
|
||||
cpu.step(&mut memory),
|
||||
Err(CpuError::Memory(MemoryError::ReadOnly(ROM_START + 4)))
|
||||
);
|
||||
assert_eq!(cpu.registers, before);
|
||||
assert_eq!(memory.read32(ROM_START + 4).unwrap(), 11);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncated_rom_does_not_partially_load_a_register_list() {
|
||||
let mut memory = program(&[block(0x0080_0000, true, true, 0, 0x6), 11]);
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = ROM_START + 4;
|
||||
let before = cpu.registers;
|
||||
assert!(matches!(cpu.step(&mut memory), Err(CpuError::Memory(_))));
|
||||
assert_eq!(cpu.registers, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swap_word_rotates_loads_and_aligns_stores_at_all_offsets() {
|
||||
for offset in 0..4 {
|
||||
let mut memory = program(&[0xe100_1092]); // SWP r1,r2,[r0]
|
||||
memory.write32(BASE, 0x1234_5678).unwrap();
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = BASE + offset;
|
||||
cpu.registers[2] = 0xaabb_ccdd;
|
||||
cpu.flags = flags();
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.registers[1], 0x1234_5678_u32.rotate_right(offset * 8));
|
||||
assert_eq!(memory.read32(BASE).unwrap(), 0xaabb_ccdd);
|
||||
assert_eq!(cpu.registers[0], BASE + offset);
|
||||
assert_eq!(cpu.registers[2], 0xaabb_ccdd);
|
||||
assert_eq!(cpu.flags, flags());
|
||||
assert_eq!(cpu.pc(), ROM_START + 4);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swap_byte_zero_extends_and_preserves_neighboring_bytes() {
|
||||
let mut memory = program(&[0xe140_1092]); // SWPB r1,r2,[r0]
|
||||
memory.write32(BASE, 0x1234_8078).unwrap();
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = BASE + 1;
|
||||
cpu.registers[2] = 0xaabb_ccdd;
|
||||
cpu.flags = flags();
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.registers[1], 0x80);
|
||||
assert_eq!(memory.read32(BASE).unwrap(), 0x1234_dd78);
|
||||
assert_eq!(cpu.flags, flags());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swap_snapshots_aliased_source_destination_and_base_registers() {
|
||||
for byte in [false, true] {
|
||||
for (destination, source) in [(1, 2), (1, 1), (1, 0), (0, 1), (0, 0)] {
|
||||
let instruction = 0xe100_0090 | (u32::from(byte) << 22) | (destination << 12) | source;
|
||||
let mut memory = program(&[instruction]);
|
||||
memory.write32(BASE, 0x1234_5678).unwrap();
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = BASE;
|
||||
cpu.registers[1] = 0x1122_3344;
|
||||
cpu.registers[2] = 0x5566_7788;
|
||||
let old_source = cpu.registers[source as usize];
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(
|
||||
cpu.registers[destination as usize],
|
||||
if byte { 0x78 } else { 0x1234_5678 }
|
||||
);
|
||||
assert_eq!(
|
||||
memory.read32(BASE).unwrap(),
|
||||
if byte {
|
||||
0x1234_5600 | (old_source & 255)
|
||||
} else {
|
||||
old_source
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swap_write_failure_preserves_destination_and_memory() {
|
||||
for byte in [false, true] {
|
||||
let mut memory = program(&[0xe100_1092 | (u32::from(byte) << 22), 0x1234_5678]);
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = ROM_START + 4;
|
||||
cpu.registers[1] = 123;
|
||||
cpu.registers[2] = 456;
|
||||
let before = cpu.registers;
|
||||
assert_eq!(
|
||||
cpu.step(&mut memory),
|
||||
Err(CpuError::Memory(MemoryError::ReadOnly(ROM_START + 4)))
|
||||
);
|
||||
assert_eq!(cpu.registers, before);
|
||||
assert_eq!(memory.read32(ROM_START + 4).unwrap(), 0x1234_5678);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swap_read_failure_preserves_cpu_state() {
|
||||
let mut memory = program(&[0xe100_1092]);
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[1] = 42;
|
||||
let before = cpu.registers;
|
||||
assert_eq!(
|
||||
cpu.step(&mut memory),
|
||||
Err(CpuError::Memory(MemoryError::Unmapped(0)))
|
||||
);
|
||||
assert_eq!(cpu.registers, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skipped_block_transfers_and_swaps_do_not_touch_unmapped_memory() {
|
||||
for instruction in [0x08a0_0006, 0x08b0_0006, 0x0100_1092, 0x0140_1092] {
|
||||
let mut memory = program(&[instruction]); // EQ while Z is clear
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[1] = 42;
|
||||
let mut expected = cpu.registers;
|
||||
expected[15] += 4;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.registers, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_transfer_variants_preserve_registers_and_memory() {
|
||||
for instruction in [
|
||||
0xe8ef_0001, // PC base plus S bit
|
||||
0xe89f_0001, // PC base without S bit
|
||||
0xe8e0_0001, // STM user-bank transfer with writeback
|
||||
0xe8f0_0001, // LDM user-bank transfer with writeback
|
||||
0xe8fd_8000, // LDM with status restore
|
||||
0xe8c0_0000, // Empty-list STM with S bit
|
||||
0xe10f_1092, // SWP PC base
|
||||
0xe100_f092, // SWP PC destination
|
||||
0xe100_109f, // SWP PC source
|
||||
0xe110_1092, // Reserved SWP bit 20
|
||||
0xe100_1192, // Reserved SWP bits 11..8
|
||||
] {
|
||||
let mut memory = program(&[instruction]);
|
||||
memory.write32(BASE, 0xdead_beef).unwrap();
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = BASE;
|
||||
cpu.registers[13] = BASE;
|
||||
cpu.flags = flags();
|
||||
let before = cpu.registers;
|
||||
assert_eq!(
|
||||
cpu.step(&mut memory),
|
||||
Err(CpuError::UnsupportedInstruction {
|
||||
address: ROM_START,
|
||||
instruction
|
||||
})
|
||||
);
|
||||
assert_eq!(cpu.registers, before);
|
||||
assert_eq!(cpu.flags, flags());
|
||||
assert_eq!(memory.read32(BASE).unwrap(), 0xdead_beef);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue