feat(cpu): implement ARM7TDMI ARM and Thumb execution

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

View file

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