feat(cpu): implement ARM7TDMI ARM and Thumb execution
Add ALU, transfers, banked status, exceptions, instruction timing, and CPU regression tests.
This commit is contained in:
parent
85bc2de6e7
commit
535c69e09f
17 changed files with 4838 additions and 0 deletions
178
tests/core.rs
Normal file
178
tests/core.rs
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
use gba_rust::{
|
||||
cpu::{Cpu, CpuError, InstructionSet},
|
||||
demo_program,
|
||||
memory::{Memory, MemoryError, ROM_START},
|
||||
DEMO_STEPS,
|
||||
};
|
||||
|
||||
fn program(words: &[u32]) -> Memory {
|
||||
Memory::new(words.iter().flat_map(|word| word.to_le_bytes()).collect()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn demo_computes_values_and_loops() {
|
||||
let mut memory = Memory::new(demo_program()).unwrap();
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
let mut saw_thumb = false;
|
||||
for _ in 0..DEMO_STEPS + 5 {
|
||||
saw_thumb |= cpu.instruction_set() == InstructionSet::Thumb;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
}
|
||||
assert!(saw_thumb);
|
||||
assert_eq!(cpu.instruction_set(), InstructionSet::Arm);
|
||||
assert_eq!(&cpu.registers()[..3], &[42, 7, 42]);
|
||||
assert_eq!(cpu.pc(), ROM_START + 96);
|
||||
assert_eq!(cpu.registers()[3], 0x0200_0000);
|
||||
assert_eq!(&cpu.registers()[4..8], &[42, 84, 3528, 42]);
|
||||
assert_eq!(memory.read32(0x0200_0000).unwrap(), 3528);
|
||||
assert_eq!(memory.read32(0x0200_0004).unwrap(), 42);
|
||||
assert_eq!(cpu.registers()[13], 0x0300_0100);
|
||||
assert_eq!(cpu.registers()[14], ROM_START + 109);
|
||||
assert_eq!(memory.read32(0x0300_00f8).unwrap(), 42);
|
||||
assert_eq!(memory.read32(0x0300_00fc).unwrap(), ROM_START + 109);
|
||||
assert!(cpu.flags().zero);
|
||||
assert!(cpu.flags().carry);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn memory_is_little_endian() {
|
||||
let memory = program(&[0x1234_5678]);
|
||||
assert_eq!(memory.read8(ROM_START).unwrap(), 0x78);
|
||||
assert_eq!(memory.read32(ROM_START).unwrap(), 0x1234_5678);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ram_is_zeroed_writable_and_mirrored() {
|
||||
let mut memory = Memory::new(vec![]).unwrap();
|
||||
for (base, size) in [(0x0200_0000, 0x40000), (0x0300_0000, 0x8000)] {
|
||||
assert_eq!(memory.read32(base).unwrap(), 0);
|
||||
memory.write8(base, 0x12).unwrap();
|
||||
memory.write8(base + 1, 0x34).unwrap();
|
||||
assert_eq!(memory.read32(base + size).unwrap(), 0x3412);
|
||||
memory.write8(base + size - 1, 0xab).unwrap();
|
||||
assert_eq!(memory.read8(base + size * 2 - 1).unwrap(), 0xab);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cartridge_has_three_windows() {
|
||||
let memory = program(&[0x1234_5678]);
|
||||
for address in [0x0800_0000, 0x0a00_0000, 0x0c00_0000] {
|
||||
assert_eq!(memory.read32(address).unwrap(), 0x1234_5678);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_memory_accesses_return_errors() {
|
||||
let mut memory = program(&[0]);
|
||||
assert_eq!(memory.read8(0), Err(MemoryError::Unmapped(0)));
|
||||
assert_eq!(
|
||||
memory.read32(ROM_START + 1),
|
||||
Err(MemoryError::Unaligned(ROM_START + 1))
|
||||
);
|
||||
assert_eq!(
|
||||
memory.read8(ROM_START + 4),
|
||||
Err(MemoryError::Unmapped(ROM_START + 4))
|
||||
);
|
||||
assert_eq!(
|
||||
memory.write8(ROM_START, 1),
|
||||
Err(MemoryError::ReadOnly(ROM_START))
|
||||
);
|
||||
assert_eq!(memory.write8(0, 1), Err(MemoryError::Unmapped(0)));
|
||||
assert_eq!(
|
||||
memory.read32(0xffff_fffc),
|
||||
Err(MemoryError::Unmapped(0xffff_fffc))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oversized_rom_is_rejected() {
|
||||
assert!(matches!(
|
||||
Memory::new(vec![0; 32 * 1024 * 1024 + 1]),
|
||||
Err(MemoryError::RomTooLarge(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rotated_immediate_is_decoded() {
|
||||
let mut memory = program(&[0xe3a0_0480]); // MOV r0, #0x80000000
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.registers()[0], 0x8000_0000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arithmetic_wraps_at_32_bits() {
|
||||
let mut memory = program(&[
|
||||
0xe240_0001, // SUB r0, r0, #1
|
||||
0xe280_1001, // ADD r1, r0, #1
|
||||
]);
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.registers()[0], u32::MAX);
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.registers()[1], 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reading_pc_as_operand_includes_pipeline_offset() {
|
||||
let mut memory = program(&[0xe28f_0004]); // ADD r0, pc, #4
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.registers()[0], ROM_START + 12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn branches_use_signed_displacement_and_pipeline_offset() {
|
||||
let mut memory = program(&[
|
||||
0xea00_0000, // B +8
|
||||
0xe3a0_0063, // skipped
|
||||
0xeaff_fffc, // B -8, back to start
|
||||
]);
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.pc(), ROM_START + 8);
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.pc(), ROM_START);
|
||||
assert_eq!(cpu.registers()[0], 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_instructions_do_not_change_cpu() {
|
||||
for instruction in [
|
||||
0xf3a0_0001, // reserved condition
|
||||
0xe340_0001, // CMP encoding with S=0
|
||||
0xe3b0_f001, // MOVS pc, #1 (status restore unsupported)
|
||||
0xe10f_f000, // MRS pc, CPSR
|
||||
0xe8f0_0001, // LDM user-bank transfer with writeback
|
||||
0xe100_f090, // SWP with PC destination
|
||||
0xe14f_0000, // MRS SPSR in System mode
|
||||
0xe16f_0f10, // CLZ (ARMv5)
|
||||
0xe12f_ff30, // BLX (ARMv5)
|
||||
] {
|
||||
let mut memory = program(&[instruction]);
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
let before = *cpu.registers();
|
||||
let flags_before = cpu.flags();
|
||||
assert_eq!(
|
||||
cpu.step(&mut memory),
|
||||
Err(CpuError::UnsupportedInstruction {
|
||||
address: ROM_START,
|
||||
instruction
|
||||
}),
|
||||
);
|
||||
assert_eq!(cpu.registers(), &before);
|
||||
assert_eq!(cpu.flags(), flags_before);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fetch_errors_do_not_advance_pc() {
|
||||
let mut memory = Memory::new(vec![]).unwrap();
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
assert_eq!(
|
||||
cpu.step(&mut memory),
|
||||
Err(CpuError::Memory(MemoryError::Unmapped(ROM_START)))
|
||||
);
|
||||
assert_eq!(cpu.pc(), ROM_START);
|
||||
}
|
||||
90
tests/exceptions.rs
Normal file
90
tests/exceptions.rs
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
use gba_rust::{
|
||||
cpu::{Cpu, InstructionSet, Mode},
|
||||
demo_bios, exception_demo_program,
|
||||
memory::{Memory, MemoryError, BIOS_SIZE, ROM_START},
|
||||
EXCEPTION_DEMO_STEPS,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn original_demo_enters_supervisor_and_returns_to_arm_and_thumb() {
|
||||
let mut memory = Memory::with_bios(exception_demo_program(), demo_bios()).unwrap();
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
let expected = [
|
||||
(8, Mode::Supervisor, InstructionSet::Arm),
|
||||
(0x40, Mode::Supervisor, InstructionSet::Arm),
|
||||
(0x44, Mode::Supervisor, InstructionSet::Arm),
|
||||
(ROM_START + 4, Mode::System, InstructionSet::Arm),
|
||||
(ROM_START + 8, Mode::System, InstructionSet::Arm),
|
||||
(ROM_START + 12, Mode::System, InstructionSet::Thumb),
|
||||
(8, Mode::Supervisor, InstructionSet::Arm),
|
||||
(0x40, Mode::Supervisor, InstructionSet::Arm),
|
||||
(0x44, Mode::Supervisor, InstructionSet::Arm),
|
||||
(ROM_START + 14, Mode::System, InstructionSet::Thumb),
|
||||
(ROM_START + 14, Mode::System, InstructionSet::Thumb),
|
||||
];
|
||||
assert_eq!(expected.len(), EXCEPTION_DEMO_STEPS);
|
||||
for (pc, mode, state) in expected {
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(
|
||||
(cpu.pc(), cpu.mode(), cpu.instruction_set()),
|
||||
(pc, mode, state)
|
||||
);
|
||||
}
|
||||
assert_eq!(cpu.registers()[10], 2);
|
||||
assert_eq!(cpu.registers()[14], 0); // Supervisor LR did not replace System LR
|
||||
assert_eq!(cpu.cpsr(), 0x3f);
|
||||
for _ in 0..5 {
|
||||
cpu.step(&mut memory).unwrap();
|
||||
}
|
||||
assert_eq!(cpu.pc(), ROM_START + 14);
|
||||
assert_eq!(cpu.registers()[10], 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optional_bios_image_is_exactly_16_kib_read_only_and_not_mirrored() {
|
||||
for size in [0, BIOS_SIZE - 1, BIOS_SIZE + 1] {
|
||||
assert!(matches!(
|
||||
Memory::with_bios(vec![], vec![0; size]),
|
||||
Err(MemoryError::InvalidBiosSize(actual)) if actual == size
|
||||
));
|
||||
}
|
||||
let bios: Vec<u8> = (0..BIOS_SIZE).map(|index| index as u8).collect();
|
||||
let mut memory = Memory::with_bios(vec![], bios).unwrap();
|
||||
assert_eq!(memory.read32(0).unwrap(), 0x0302_0100);
|
||||
assert_eq!(memory.read16(0x3ffe).unwrap(), 0xfffe);
|
||||
assert_eq!(memory.read8(0x3fff).unwrap(), 255);
|
||||
for address in [0x4000, 0x0100_0000] {
|
||||
assert_eq!(memory.read8(address), Err(MemoryError::Unmapped(address)));
|
||||
assert_eq!(
|
||||
memory.write8(address, 1),
|
||||
Err(MemoryError::Unmapped(address))
|
||||
);
|
||||
}
|
||||
assert_eq!(memory.write8(0x3fff, 0), Err(MemoryError::ReadOnly(0x3fff)));
|
||||
assert_eq!(
|
||||
memory.write16(0x3ffe, 0),
|
||||
Err(MemoryError::ReadOnly(0x3ffe))
|
||||
);
|
||||
assert_eq!(memory.write32(0, 0), Err(MemoryError::ReadOnly(0)));
|
||||
assert_eq!(memory.read32(0).unwrap(), 0x0302_0100);
|
||||
assert_eq!(memory.read16(0x3ffe).unwrap(), 0xfffe);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_bios_is_mapped_by_default() {
|
||||
let mut memory = Memory::new(vec![]).unwrap();
|
||||
assert_eq!(memory.read32(0), Err(MemoryError::Unmapped(0)));
|
||||
assert_eq!(memory.write32(0, 0), Err(MemoryError::Unmapped(0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_can_fetch_caller_supplied_vector_code() {
|
||||
let mut bios = vec![0; BIOS_SIZE];
|
||||
bios[0..4].copy_from_slice(&0xe3a0_002a_u32.to_le_bytes()); // MOV r0, #42
|
||||
let mut memory = Memory::with_bios(vec![], bios).unwrap();
|
||||
let mut cpu = Cpu::at_reset();
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.registers()[0], 42);
|
||||
assert_eq!(cpu.pc(), 4);
|
||||
assert_eq!(cpu.cpsr(), 0xd3);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue