use alloc::format;
use alloc::string::String;
use alloc::sync::Arc;
use alloc::vec::Vec;
use crate::core::props::{Props, Value};
use crate::core::space::{AddressSpace, RamStore, Region, UnassignedPolicy};
use crate::core::state::{MachineShape, Migrations, StateReader, StateWriter};
use crate::core::value::Width;
use super::isa::{
Cond, DpOp, Insn, MemOffset, Operand, ShiftType, Size, decode, decode_imm_shift, is_32bit,
thumb_expand_imm,
};
use super::sys::{Access, Exception, Sys, ccr, exc_return, fsr, shcsr};
use super::*;
const VECTORS: u32 = 0;
const ENTRY: u32 = 0x200;
const STACK: u32 = 0x1000;
const RAM: u64 = 0x4000;
struct Harness {
cpu: Arc<ArmV7m>,
ram: Arc<RamStore>,
}
impl Harness {
fn new(cfg: Config, code: &[u16]) -> Harness {
let ram = Arc::new(RamStore::new(RAM));
ram.write_at(u64::from(VECTORS), &STACK.to_le_bytes())
.unwrap();
ram.write_at(u64::from(VECTORS) + 4, &(ENTRY | 1).to_le_bytes())
.unwrap();
ram.write_at(0x100, &0xe7feu16.to_le_bytes()).unwrap();
for n in 2..48u64 {
ram.write_at(n * 4, &0x101u32.to_le_bytes()).unwrap();
}
for (i, half) in code.iter().enumerate() {
ram.write_at(u64::from(ENTRY) + (i as u64) * 2, &half.to_le_bytes())
.unwrap();
}
let space = AddressSpace::new("mem", 32).with_unassigned(UnassignedPolicy::FAULT);
space
.topology()
.map(Region::ram("ram", Arc::clone(&ram)), 0)
.unwrap();
let cpu = Arc::new(ArmV7m::new(cfg));
cpu.attach_space(Arc::new(space));
cpu.step();
Harness { cpu, ram }
}
fn m4(code: &[u16]) -> Harness {
Harness::new(Config::CORTEX_M4, code)
}
fn word(&self, addr: u32) -> u32 {
let mut v = 0u32;
for k in 0..4 {
v |= u32::from(self.ram.read_u8(u64::from(addr) + k).unwrap()) << (8 * k);
}
v
}
fn set_word(&self, addr: u32, value: u32) {
self.ram
.write_at(u64::from(addr), &value.to_le_bytes())
.unwrap();
}
}
const fn wide(encoding: u32) -> [u16; 2] {
[(encoding >> 16) as u16, encoding as u16]
}
#[test]
fn the_three_escape_prefixes_start_a_wide_instruction() {
assert!(!is_32bit(0xe000));
assert!(!is_32bit(0xe7ff));
assert!(is_32bit(0xe800));
assert!(is_32bit(0xf000));
assert!(is_32bit(0xffff));
}
#[test]
fn thumb_expand_imm_covers_both_halves_of_its_encoding() {
assert_eq!(thumb_expand_imm(0x0ab), (0x0000_00ab, None));
assert_eq!(thumb_expand_imm(0x1ab), (0x00ab_00ab, None));
assert_eq!(thumb_expand_imm(0x2ab), (0xab00_ab00, None));
assert_eq!(thumb_expand_imm(0x3ab), (0xabab_abab, None));
let (value, carry) = thumb_expand_imm(0x87f);
assert_eq!(value, 0x00ff_0000);
assert_eq!(carry, Some(false));
let (value, carry) = thumb_expand_imm(0x400);
assert_eq!(value, 0x8000_0000);
assert_eq!(carry, Some(true));
}
#[test]
fn decode_imm_shift_rewrites_the_three_zero_amounts() {
assert_eq!(decode_imm_shift(0, 0).ty, ShiftType::Lsl);
assert_eq!(decode_imm_shift(0, 0).amount, 0);
assert_eq!(decode_imm_shift(1, 0).amount, 32, "LSR #0 means LSR #32");
assert_eq!(decode_imm_shift(2, 0).amount, 32, "ASR #0 means ASR #32");
assert_eq!(decode_imm_shift(3, 0).ty, ShiftType::Rrx, "ROR #0 is RRX");
assert_eq!(decode_imm_shift(3, 5).ty, ShiftType::Ror);
}
#[test]
fn the_sixteen_bit_encodings_decode_to_what_they_say() {
assert_eq!(
decode(0x2042, 0),
Insn::DataProc {
op: DpOp::Mov,
s: true,
rd: 0,
rn: 0,
operand: Operand::Imm {
value: 0x42,
carry: None
},
}
);
assert_eq!(decode(0x4770, 0), Insn::Bx { rm: 14 });
assert_eq!(
decode(0xb10a, 0),
Insn::Cbz {
nonzero: false,
rn: 2,
offset: 2
}
);
assert_eq!(
decode(0xbf18, 0),
Insn::It {
cond: Cond(1),
mask: 8
}
);
assert_eq!(
decode(0xbf00, 0),
Insn::Hint {
op: isa::HintOp::Nop
}
);
assert_eq!(decode(0xdf07, 0), Insn::Svc { imm: 7 });
assert_eq!(decode(0xde00, 0), Insn::Udf { imm: 0 });
}
#[test]
fn the_wide_encodings_decode_to_what_they_say() {
let [a, b] = wide(0xf04f_01ab);
assert_eq!(
decode(a, b),
Insn::DataProc {
op: DpOp::Mov,
s: false,
rd: 1,
rn: 0,
operand: Operand::Imm {
value: 0xab,
carry: None
},
}
);
let [a, b] = wide(0xf1bc_0f00);
assert!(matches!(
decode(a, b),
Insn::DataProc {
op: DpOp::Cmp,
rn: 12,
..
}
));
let [a, b] = wide(0xfab1_f381);
assert_eq!(
decode(a, b),
Insn::Misc {
op: isa::MiscOp::Clz,
rd: 3,
rm: 1
}
);
let [a, b] = wide(0xeac1_4322);
assert!(matches!(
decode(a, b),
Insn::Pkh {
tb: true,
rd: 3,
rn: 1,
rm: 2,
..
}
));
let [a, b] = wide(0xfa91_f302);
assert_eq!(
decode(a, b),
Insn::Simd {
mode: isa::SimdMode::Signed,
shape: isa::SimdShape::Add16,
rd: 3,
rn: 1,
rm: 2
}
);
let [a, b] = wide(0xf8d0_1004);
assert_eq!(
decode(a, b),
Insn::LoadStore {
load: true,
size: Size::Word,
signed: false,
rt: 1,
rn: 0,
offset: MemOffset::Imm(4),
index: true,
add: true,
wback: false,
unpriv: false,
}
);
let [a, b] = wide(0xe8df_f002);
assert_eq!(
decode(a, b),
Insn::TableBranch {
rn: 15,
rm: 2,
half: false
}
);
let [a, b] = wide(0xeeb0_0a40);
assert!(matches!(decode(a, b), Insn::Coproc { cp: 10 }));
}
#[test]
fn the_disassembler_and_the_decoder_are_the_same_description() {
let show = |encoding: u32| {
let [a, b] = wide(encoding);
format!("{}", decode(a, b))
};
let show16 = |half: u16| format!("{}", decode(half, 0));
assert_eq!(show16(0x2042), "MOVS r0, #66");
assert_eq!(show16(0x4770), "BX lr");
assert_eq!(show16(0xb510), "PUSH {r4, lr}");
assert_eq!(show16(0xbd10), "POP {r4, pc}");
assert_eq!(show16(0xbf18), "IT NE");
assert_eq!(show(0xfab1_f381), "CLZ r3, r1");
assert_eq!(show(0xfa91_f302), "SADD16 r3, r1, r2");
assert_eq!(show(0xf8d0_1004), "LDR r1, [r0, #4]");
assert_eq!(show(0xfb02_f303), "MUL r3, r2, r3");
assert_eq!(show(0xf364_0207), "BFI r2, r4, #0, #8");
assert_eq!(show(0xfa81_f382), "QADD r3, r2, r1");
}
#[test]
fn an_it_block_disassembles_with_its_then_and_else_letters() {
assert_eq!(format!("{}", decode(0xbf07, 0)), "ITTEE EQ");
assert_eq!(format!("{}", decode(0xbf14, 0)), "ITE NE");
assert_eq!(format!("{}", decode(0xbf1c, 0)), "ITT NE");
}
#[test]
fn reset_takes_sp_and_pc_from_the_vector_table() {
let h = Harness::m4(&[0xbf00]);
assert_eq!(h.cpu.pc(), ENTRY);
assert_eq!(h.cpu.msp(), STACK);
assert!(!h.cpu.regs().in_handler());
}
#[test]
fn a_reset_vector_with_bit_zero_clear_is_an_invalid_state_fault() {
let ram = Arc::new(RamStore::new(RAM));
ram.write_at(0, &STACK.to_le_bytes()).unwrap();
ram.write_at(4, &ENTRY.to_le_bytes()).unwrap();
ram.write_at(0x100, &0xe7feu16.to_le_bytes()).unwrap();
for n in 2..48u64 {
ram.write_at(n * 4, &0x101u32.to_le_bytes()).unwrap();
}
let space = AddressSpace::new("mem", 32).with_unassigned(UnassignedPolicy::FAULT);
space
.topology()
.map(Region::ram("ram", Arc::clone(&ram)), 0)
.unwrap();
let cpu = ArmV7m::new(Config::CORTEX_M4);
cpu.attach_space(Arc::new(space));
cpu.step();
assert_eq!(cpu.xpsr() & xpsr::T, 0);
cpu.step();
assert!(cpu.with_sys(|s| s.cfsr & fsr::UF_INVSTATE != 0));
}
#[test]
fn the_pc_reads_as_the_instruction_plus_four_in_both_widths() {
let h = Harness::m4(&[0xa000, 0xf20f_0100u32 as u16, 0]);
h.step_and(|_| {});
assert_eq!(h.cpu.reg(0), (ENTRY + 4) & !3);
}
impl Harness {
fn step_and(&self, f: impl FnOnce(&ArmV7m)) {
self.cpu.step();
f(&self.cpu);
}
}
#[test]
fn an_it_block_suppresses_the_flags_of_its_sixteen_bit_slots() {
let h = Harness::m4(&[0x2100, 0x2900, 0xbf04, 0x2202, 0x2303]);
for _ in 0..5 {
h.cpu.step();
}
assert_eq!(h.cpu.reg(2), 2);
assert_eq!(h.cpu.reg(3), 3);
}
#[test]
fn an_it_block_runs_its_else_slots_when_the_condition_fails() {
let h = Harness::m4(&[0x2101, 0x2900, 0xbf0c, 0x2202, 0x2303]);
for _ in 0..5 {
h.cpu.step();
}
assert_eq!(h.cpu.reg(2), 0);
assert_eq!(h.cpu.reg(3), 3);
}
#[test]
fn unaligned_access_works_unless_ccr_says_otherwise() {
let h = Harness::m4(&wide(0xf8d0_1000));
h.set_word(0x800, 0x1122_3344);
h.set_word(0x804, 0x5566_7788);
h.cpu.set_reg(0, 0x801);
h.cpu.step();
assert_eq!(h.cpu.reg(1), 0x8811_2233);
let h = Harness::m4(&wide(0xf8d0_1000));
h.set_word(0x800, 0x1122_3344);
h.cpu.set_reg(0, 0x801);
h.cpu.with_sys(|s| s.ccr |= ccr::UNALIGN_TRP);
h.cpu.with_sys(|s| s.shcsr |= shcsr::USGFAULTENA);
h.cpu.step();
assert_eq!(h.cpu.current_exception(), Exception::USAGE_FAULT);
assert!(h.cpu.with_sys(|s| s.cfsr & fsr::UF_UNALIGNED != 0));
}
#[test]
fn a_block_transfer_is_always_word_aligned() {
let h = Harness::m4(&wide(0xe890_0006));
h.cpu.set_reg(0, 0x802);
h.cpu.with_sys(|s| s.shcsr |= shcsr::USGFAULTENA);
h.cpu.step();
assert_eq!(h.cpu.current_exception(), Exception::USAGE_FAULT);
assert!(h.cpu.with_sys(|s| s.cfsr & fsr::UF_UNALIGNED != 0));
}
#[test]
fn a_branch_to_an_even_address_is_an_invalid_state_fault() {
let h = Harness::m4(&[0x4700]);
h.cpu.set_reg(0, 0x300);
h.cpu.with_sys(|s| s.shcsr |= shcsr::USGFAULTENA);
h.cpu.step();
assert_eq!(h.cpu.current_exception(), Exception::USAGE_FAULT);
assert!(h.cpu.with_sys(|s| s.cfsr & fsr::UF_INVSTATE != 0));
}
#[test]
fn the_exclusive_monitor_lets_exactly_one_store_through() {
let mut code = Vec::new();
code.extend_from_slice(&wide(0xe850_1f00));
code.extend_from_slice(&wide(0xe840_3200));
code.extend_from_slice(&wide(0xe840_3400));
let h = Harness::m4(&code);
h.cpu.set_reg(0, 0x900);
h.cpu.set_reg(3, 0xabcd);
h.cpu.step();
h.cpu.step();
assert_eq!(h.cpu.reg(2), 0, "the tagged store succeeds");
assert_eq!(h.word(0x900), 0xabcd);
h.cpu.step();
assert_eq!(h.cpu.reg(4), 1, "the tag is consumed");
}
#[test]
fn wfi_sleeps_and_an_interrupt_wakes_it() {
let h = Harness::m4(&[0xbf30, 0xbf00]);
h.cpu.step();
assert!(h.cpu.is_asleep());
let before = h.cpu.cycles();
h.cpu.step();
assert!(h.cpu.is_asleep(), "still asleep with nothing pending");
assert!(h.cpu.cycles() > before, "a sleeping core still spends time");
h.cpu.with_sys(|s| s.set_enable(Exception::IRQ0, true));
h.cpu.pend_irq(0);
h.cpu.step();
assert!(!h.cpu.is_asleep());
}
#[test]
fn a_hardfault_inside_a_hardfault_locks_the_core_up() {
let h = Harness::m4(&[0xde00]);
h.set_word(3 * 4, ENTRY | 1);
h.cpu.step();
assert_eq!(h.cpu.current_exception(), Exception::HARD_FAULT);
h.cpu.step();
assert!(h.cpu.is_locked_up());
assert_eq!(h.cpu.pc(), 0xffff_fffe);
let before = h.cpu.cycles();
h.cpu.step();
assert!(h.cpu.cycles() > before);
}
#[test]
fn a_cortex_m3_has_no_dsp_extension() {
let code = wide(0xfa91_f302);
let m4 = Harness::new(Config::CORTEX_M4, &code);
m4.cpu.set_reg(1, 0x0001_0002);
m4.cpu.set_reg(2, 0x0003_0004);
m4.cpu.step();
assert_eq!(m4.cpu.reg(3), 0x0004_0006);
let m3 = Harness::new(Config::CORTEX_M3, &code);
m3.cpu.with_sys(|s| s.shcsr |= shcsr::USGFAULTENA);
m3.cpu.step();
assert_eq!(m3.cpu.current_exception(), Exception::USAGE_FAULT);
assert!(m3.cpu.with_sys(|s| s.cfsr & fsr::UF_UNDEFINSTR != 0));
}
#[test]
fn a_coprocessor_access_is_nocp_rather_than_undefined() {
let h = Harness::m4(&wide(0xeeb0_0a40));
h.cpu.with_sys(|s| s.shcsr |= shcsr::USGFAULTENA);
h.cpu.step();
assert_eq!(h.cpu.current_exception(), Exception::USAGE_FAULT);
assert!(h.cpu.with_sys(|s| s.cfsr & fsr::UF_NOCP != 0));
}
#[test]
fn big_endian_is_byte_invariant() {
let ram = Arc::new(RamStore::new(RAM));
ram.write_at(0, &STACK.to_be_bytes()).unwrap();
ram.write_at(4, &(ENTRY | 1).to_be_bytes()).unwrap();
for (i, half) in wide(0xf8d0_1000).iter().enumerate() {
ram.write_at(u64::from(ENTRY) + (i as u64) * 2, &half.to_le_bytes())
.unwrap();
}
ram.write_at(0x900, &[0x11, 0x22, 0x33, 0x44]).unwrap();
let space = AddressSpace::new("mem", 32).with_unassigned(UnassignedPolicy::FAULT);
space
.topology()
.map(Region::ram("ram", Arc::clone(&ram)), 0)
.unwrap();
let cpu = ArmV7m::new(Config::CORTEX_M4.with_endian(crate::core::value::Endian::Big));
cpu.attach_space(Arc::new(space));
cpu.step();
assert_eq!(cpu.pc(), ENTRY);
cpu.set_reg(0, 0x900);
cpu.step();
assert_eq!(cpu.reg(1), 0x1122_3344);
}
#[test]
fn an_svc_stacks_a_frame_and_returns_through_exc_return() {
let h = Harness::m4(&[0xdf07, 0x2009]);
h.set_word(11 * 4, 0x301);
h.set_word(0x300, 0x4770); h.cpu.set_reg(0, 0xa0);
h.cpu.set_reg(1, 0xa1);
h.cpu.set_reg(2, 0xa2);
h.cpu.set_reg(3, 0xa3);
h.cpu.set_reg(12, 0xac);
h.cpu.step();
assert_eq!(h.cpu.current_exception(), Exception::SVCALL);
assert_eq!(h.cpu.reg(14), exc_return::THREAD_MSP);
assert_eq!(h.cpu.last_svc(), 7);
let sp = h.cpu.msp();
assert_eq!(h.word(sp), 0xa0);
assert_eq!(h.word(sp + 4), 0xa1);
assert_eq!(h.word(sp + 8), 0xa2);
assert_eq!(h.word(sp + 12), 0xa3);
assert_eq!(h.word(sp + 16), 0xac);
assert_eq!(
h.word(sp + 24),
ENTRY + 2,
"SVC stacks the *next* instruction"
);
assert_eq!(h.word(sp + 28) & xpsr::T, xpsr::T);
h.cpu.step();
assert_eq!(h.cpu.current_exception(), Exception::THREAD);
assert_eq!(h.cpu.msp(), STACK);
assert_eq!(h.cpu.pc(), ENTRY + 2);
h.cpu.step();
assert_eq!(h.cpu.reg(0), 9);
}
#[test]
fn a_synchronous_fault_stacks_the_faulting_instruction() {
let h = Harness::m4(&[0xde00]);
h.set_word(6 * 4, 0x301);
h.set_word(0x300, 0x4770);
h.cpu.with_sys(|s| s.shcsr |= shcsr::USGFAULTENA);
h.cpu.step();
let sp = h.cpu.msp();
assert_eq!(h.word(sp + 24), ENTRY);
}
#[test]
fn thread_mode_can_run_on_the_process_stack() {
let mut code = Vec::new();
code.extend_from_slice(&wide(0xf380_8809)); code.extend_from_slice(&wide(0xf381_8814)); code.extend_from_slice(&wide(0xf3bf_8f6f)); code.push(0x466a); let h = Harness::m4(&code);
h.cpu.set_reg(0, 0x0c00);
h.cpu.set_reg(1, 2);
for _ in 0..4 {
h.cpu.step();
}
assert_eq!(h.cpu.reg(2), 0x0c00);
assert_eq!(h.cpu.psp(), 0x0c00);
assert_eq!(h.cpu.msp(), STACK, "the main stack is untouched");
}
#[test]
fn an_exception_from_the_process_stack_returns_with_fffffffd() {
let mut code = Vec::new();
code.extend_from_slice(&wide(0xf380_8809)); code.extend_from_slice(&wide(0xf381_8814)); code.extend_from_slice(&wide(0xf3bf_8f6f)); code.push(0xdf00); let h = Harness::m4(&code);
h.set_word(11 * 4, 0x301);
h.set_word(0x300, 0x4770);
h.cpu.set_reg(0, 0x0c00);
h.cpu.set_reg(1, 2);
for _ in 0..4 {
h.cpu.step();
}
assert_eq!(h.cpu.reg(14), exc_return::THREAD_PSP);
assert_eq!(h.cpu.msp(), STACK);
assert!(h.cpu.psp() < 0x0c00);
}
#[test]
fn stack_alignment_pads_an_odd_frame_and_records_it() {
let h = Harness::m4(&[0xdf00]);
h.set_word(11 * 4, 0x301);
h.set_word(0x300, 0x4770);
h.cpu.set_reg(13, STACK - 4);
h.cpu.set_regs(Regs {
msp: STACK - 4,
..h.cpu.regs()
});
h.cpu.step();
let sp = h.cpu.msp();
assert_eq!(sp % 8, 0, "the frame is eight-byte aligned");
assert_ne!(h.word(sp + 28) & (1 << 9), 0, "and it says it padded");
h.cpu.step();
assert_eq!(h.cpu.msp(), STACK - 4, "the padding is undone on return");
}
#[test]
fn a_higher_priority_interrupt_preempts_a_lower_one() {
let h = Harness::m4(&[0xbf00, 0xbf00, 0xbf00, 0xbf00]);
h.set_word(16 * 4, 0x301);
h.set_word(0x300, 0xe7fe); h.set_word(17 * 4, 0x401);
h.set_word(0x400, 0xe7fe);
h.cpu.with_sys(|s| {
s.set_enable(Exception::IRQ0, true);
s.set_enable(Exception(17), true);
s.priority[16] = 0x80;
s.priority[17] = 0x00;
});
h.cpu.pend_irq(0);
h.cpu.step();
assert_eq!(h.cpu.current_exception(), Exception::IRQ0);
h.cpu.pend_irq(1);
h.cpu.step();
assert_eq!(h.cpu.current_exception(), Exception(17));
assert_eq!(h.cpu.reg(14), exc_return::HANDLER_MSP);
assert!(h.cpu.with_sys(|s| s.is_active(Exception::IRQ0)));
}
#[test]
fn an_equal_priority_interrupt_waits_for_the_handler_to_finish() {
let h = Harness::m4(&[0xbf00]);
h.set_word(16 * 4, 0x301);
h.set_word(0x300, 0xe7fe);
h.set_word(17 * 4, 0x401);
h.set_word(0x400, 0xe7fe);
h.cpu.with_sys(|s| {
s.set_enable(Exception::IRQ0, true);
s.set_enable(Exception(17), true);
});
h.cpu.pend_irq(0);
h.cpu.step();
assert_eq!(h.cpu.current_exception(), Exception::IRQ0);
h.cpu.pend_irq(1);
h.cpu.step();
assert_eq!(
h.cpu.current_exception(),
Exception::IRQ0,
"same priority does not preempt"
);
assert!(h.cpu.with_sys(|s| s.is_pending(Exception(17))));
}
#[test]
fn an_exception_return_tail_chains_rather_than_unstacking_twice() {
let h = Harness::m4(&[0xbf00]);
h.set_word(16 * 4, 0x301);
h.set_word(0x300, 0x4770); h.set_word(17 * 4, 0x401);
h.set_word(0x400, 0xe7fe);
h.cpu.with_sys(|s| {
s.set_enable(Exception::IRQ0, true);
s.set_enable(Exception(17), true);
});
h.cpu.pend_irq(0);
h.cpu.step();
let stacked_sp = h.cpu.msp();
assert_eq!(h.cpu.current_exception(), Exception::IRQ0);
h.cpu.pend_irq(1);
h.cpu.step(); assert_eq!(h.cpu.current_exception(), Exception(17));
assert_eq!(
h.cpu.msp(),
stacked_sp,
"the frame stayed where it was: no pop, no second push"
);
assert_eq!(h.cpu.reg(14), exc_return::THREAD_MSP);
assert!(!h.cpu.with_sys(|s| s.is_active(Exception::IRQ0)));
}
#[test]
fn primask_and_basepri_hold_an_interrupt_off() {
let h = Harness::m4(&[0xb672, 0xbf00, 0xb662, 0xbf00]); h.set_word(16 * 4, 0x301);
h.set_word(0x300, 0xe7fe);
h.cpu.with_sys(|s| s.set_enable(Exception::IRQ0, true));
h.cpu.step(); h.cpu.pend_irq(0);
h.cpu.step(); assert_eq!(h.cpu.current_exception(), Exception::THREAD);
h.cpu.step(); h.cpu.step(); assert_eq!(h.cpu.current_exception(), Exception::IRQ0);
}
#[test]
fn faultmask_masks_everything_but_nmi() {
let h = Harness::m4(&[0xb673, 0xbf00, 0xbf00]); h.set_word(16 * 4, 0x301);
h.set_word(0x300, 0xe7fe);
h.set_word(2 * 4, 0x401);
h.set_word(0x400, 0xe7fe);
h.cpu.with_sys(|s| s.set_enable(Exception::IRQ0, true));
h.cpu.step();
assert_eq!(h.cpu.execution_priority(), -1);
h.cpu.pend_irq(0);
h.cpu.step();
assert_eq!(h.cpu.current_exception(), Exception::THREAD);
h.cpu.with_sys(|s| s.set_pending(Exception::NMI, true));
h.cpu.step();
assert_eq!(h.cpu.current_exception(), Exception::NMI);
}
#[test]
fn only_the_implemented_priority_bits_stick() {
let mut sys = Sys::new(CPUID_CORTEX_M4, 3, 8);
sys.write_word(0xe000_e400, 0xffff_ffff);
assert_eq!(sys.priority[16], 0xe0);
let mut sys = Sys::new(CPUID_CORTEX_M4, 8, 8);
sys.write_word(0xe000_e400, 0xffff_ffff);
assert_eq!(sys.priority[16], 0xff);
}
#[test]
fn prigroup_masks_the_sub_priority_out_of_a_comparison() {
let mut sys = Sys::new(CPUID_CORTEX_M4, 8, 8);
sys.priority[16] = 0x11;
sys.priority[17] = 0x10;
assert_eq!(
sys.priority_of(Exception(16)),
0x10,
"prigroup 0 drops bit 0"
);
assert_eq!(sys.priority_of(Exception(17)), 0x10);
sys.prigroup = 0;
sys.set_enable(Exception(16), true);
sys.set_enable(Exception(17), true);
sys.set_pending(Exception(16), true);
sys.set_pending(Exception(17), true);
assert_eq!(sys.highest_pending().unwrap().0, Exception(16));
}
#[test]
fn the_architectural_priorities_are_negative_and_fixed() {
let sys = Sys::new(CPUID_CORTEX_M4, 8, 8);
assert_eq!(sys.priority_of(Exception::RESET), -3);
assert_eq!(sys.priority_of(Exception::NMI), -2);
assert_eq!(sys.priority_of(Exception::HARD_FAULT), -1);
}
#[test]
fn systick_counts_down_and_reloads() {
let mut sys = Sys::new(CPUID_CORTEX_M4, 8, 8);
sys.syst_rvr = 4;
sys.syst_cvr = 4;
sys.syst_csr = 1;
assert!(!sys.tick_systick(3));
assert_eq!(sys.syst_cvr, 1);
assert!(sys.tick_systick(1), "reaching zero is the wrap");
assert_ne!(sys.syst_csr & (1 << 16), 0, "COUNTFLAG");
assert_ne!(sys.read_word(0xe000_e010, true).unwrap() & (1 << 16), 0);
assert_ne!(sys.syst_csr & (1 << 16), 0, "a debug read is not a read");
assert_ne!(sys.read_word(0xe000_e010, false).unwrap() & (1 << 16), 0);
assert_eq!(sys.syst_csr & (1 << 16), 0);
}
#[test]
fn aircr_ignores_a_write_without_its_key() {
let mut sys = Sys::new(CPUID_CORTEX_M4, 8, 8);
sys.write_word(0xe000_ed0c, 0x0000_0700);
assert_eq!(sys.prigroup, 0);
sys.write_word(0xe000_ed0c, 0x05fa_0500);
assert_eq!(sys.prigroup, 5);
sys.write_word(0xe000_ed0c, 0x05fa_0004);
assert!(sys.reset_requested);
}
#[test]
fn the_nvic_set_and_clear_registers_address_one_bitmap() {
let mut sys = Sys::new(CPUID_CORTEX_M4, 8, 8);
sys.write_word(0xe000_e100, 0b0101);
assert_eq!(sys.read_word(0xe000_e100, false).unwrap(), 0b0101);
sys.write_word(0xe000_e180, 0b0001);
assert_eq!(sys.read_word(0xe000_e100, false).unwrap(), 0b0100);
assert!(sys.is_enabled(Exception(18)));
assert!(!sys.is_enabled(Exception(16)));
}
#[test]
fn the_configurable_faults_are_enabled_by_shcsr_and_nothing_else() {
let mut sys = Sys::new(CPUID_CORTEX_M4, 8, 8);
assert!(!sys.is_enabled(Exception::USAGE_FAULT));
sys.write_word(0xe000_ed24, shcsr::USGFAULTENA);
assert!(sys.is_enabled(Exception::USAGE_FAULT));
assert!(!sys.is_enabled(Exception::BUS_FAULT));
assert!(sys.is_enabled(Exception::NMI));
assert!(sys.is_enabled(Exception::HARD_FAULT));
}
#[test]
fn an_unimplemented_ppb_register_reads_as_zero_rather_than_faulting() {
let mut sys = Sys::new(CPUID_CORTEX_M4, 8, 8);
assert_eq!(sys.read_word(0xe000_1004, false), Some(0));
assert_eq!(sys.read_word(0x2000_0000, false), None);
}
#[test]
fn the_mpu_permission_matrix_matches_the_manual() {
let mut sys = Sys::new(CPUID_CORTEX_M4, 8, 8);
sys.mpu_rbar[0] = 0x1000;
sys.mpu_rasr[0] = (0b001 << 24) | (7 << 1) | 1;
sys.mpu_ctrl = 0b101; assert!(sys.mpu_permits(0x1000, Access::Write, true, 0));
assert!(!sys.mpu_permits(0x1000, Access::Write, false, 0));
assert!(!sys.mpu_permits(0x1000, Access::Read, false, 0));
assert!(sys.mpu_permits(0x2000, Access::Write, true, 0));
assert!(!sys.mpu_permits(0x2000, Access::Write, false, 0));
sys.mpu_rasr[0] = (0b110 << 24) | (7 << 1) | 1;
assert!(sys.mpu_permits(0x1000, Access::Read, false, 0));
assert!(!sys.mpu_permits(0x1000, Access::Write, true, 0));
sys.mpu_rasr[0] = (0b011 << 24) | (1 << 28) | (7 << 1) | 1;
assert!(sys.mpu_permits(0x1000, Access::Read, false, 0));
assert!(!sys.mpu_permits(0x1000, Access::Fetch, false, 0));
sys.mpu_rasr[0] = (7 << 1) | 1; assert!(!sys.mpu_permits(0x1000, Access::Read, true, 0));
assert!(sys.mpu_permits(0x1000, Access::Read, true, -1));
}
#[test]
fn a_disabled_mpu_sub_region_falls_through_to_the_next_match() {
let mut sys = Sys::new(CPUID_CORTEX_M4, 8, 8);
sys.mpu_rbar[0] = 0x1000;
sys.mpu_rasr[0] = (1 << 9) | (10 << 1) | 1; sys.mpu_ctrl = 0b101;
assert!(!sys.mpu_permits(0x1000, Access::Read, true, 0));
assert!(
sys.mpu_permits(0x1100, Access::Read, true, 0),
"the disabled eighth is background, and privileged code has the \
default map"
);
}
#[test]
fn the_private_peripheral_bus_is_reachable_from_guest_memory() {
let h = Harness::m4(&wide(0xf8d0_1000));
h.cpu.set_reg(0, 0xe000_ed00);
h.cpu.step();
assert_eq!(h.cpu.reg(1), CPUID_CORTEX_M4);
}
#[test]
fn the_state_round_trips_through_a_snapshot() {
let h = Harness::m4(&[0x2042]);
h.cpu.step();
h.cpu.set_irq(3, true);
h.cpu.pend_irq(5);
h.cpu.with_sys(|s| {
s.vtor = 0x2000_0000;
s.priority[20] = 0x40;
s.cfsr = fsr::UF_UNDEFINSTR;
s.mpu_rbar[2] = 0x1234_5600;
});
h.cpu.set_reg(9, 0x9999);
let mut shape = MachineShape::new();
shape.add_device("cpu", CLASS.name).unwrap();
let mut writer = StateWriter::new(shape);
{
let mut chunk = writer.chunk("cpu", CLASS.name, CLASS.version).unwrap();
Device::save(h.cpu.as_ref(), &mut chunk).unwrap();
}
let bytes = writer.to_vec().unwrap();
let restored = ArmV7m::new(Config::CORTEX_M4);
let reader = StateReader::new(&bytes).unwrap();
let migrations = Migrations::new();
let chunk = reader
.load("cpu", CLASS.name, CLASS.version, &migrations)
.unwrap();
Device::load(&restored, &mut chunk.reader()).unwrap();
assert_eq!(restored.regs(), h.cpu.regs());
assert_eq!(restored.cycles(), h.cpu.cycles());
assert!(restored.irq_asserted(3));
assert!(restored.with_sys(|s| s.is_pending(Exception(21))));
assert_eq!(restored.vtor(), 0x2000_0000);
assert_eq!(restored.with_sys(|s| s.priority[20]), 0x40);
assert_eq!(restored.with_sys(|s| s.cfsr), fsr::UF_UNDEFINSTR);
assert_eq!(restored.with_sys(|s| s.mpu_rbar[2]), 0x1234_5600);
}
#[test]
fn the_device_surface_is_wired_up() {
let h = Harness::m4(&[0x2042, 0x2043]);
assert!(Device::is_runnable(h.cpu.as_ref()));
assert_eq!(h.cpu.class().name, "cpu.arm.v7m");
let used = Device::run(
h.cpu.as_ref(),
crate::core::sched::Budget {
until: crate::core::clock::GlobalTime::ZERO,
ticks: 1,
},
);
assert!(used.ticks > 0);
Device::reset(h.cpu.as_ref(), crate::core::device::ResetKind::Cold);
assert!(h.cpu.reset_pending());
h.cpu.step();
assert_eq!(h.cpu.pc(), ENTRY);
}
#[test]
fn realize_does_nothing_outward_and_bind_is_what_needs_a_space() {
use crate::core::device::{Deferred, RealizeCtx};
use crate::core::space::RequesterId;
let cpu = ArmV7m::new(Config::CORTEX_M4);
let mut deferred = Deferred::new();
let hosts = crate::core::HostObjects::new();
let mut ctx = RealizeCtx::new("cpu", RequesterId::ANONYMOUS, &mut deferred, &hosts);
assert!(Device::realize(&cpu, &mut ctx).is_ok());
assert!(cpu.space().is_none(), "realize attaches nothing");
}
#[test]
fn the_interrupt_pins_are_named_by_number() {
use crate::core::wire::{Level, WireId};
let h = Harness::m4(&[0xbf00]);
let src = WireId::new(1);
let pin = Device::sink(h.cpu.as_ref(), "irq38", &[src]).expect("irq38");
assert_eq!(pin.line, 38);
pin.sink.set_level(src, 0, Level::High);
assert!(h.cpu.irq_asserted(38));
assert!(Device::sink(h.cpu.as_ref(), "irq0", &[src]).is_some());
assert!(Device::sink(h.cpu.as_ref(), "irq239", &[src]).is_some());
assert!(Device::sink(h.cpu.as_ref(), "irq240", &[src]).is_none());
assert!(Device::sink(h.cpu.as_ref(), "irq07", &[src]).is_none());
assert!(Device::sink(h.cpu.as_ref(), "irq", &[src]).is_none());
assert!(Device::sink(h.cpu.as_ref(), "irqx", &[src]).is_none());
}
#[test]
fn a_sink_survives_the_handover_to_the_wire() {
use crate::core::wire::{Level, WireId};
let h = Harness::m4(&[0xbf00]);
let src = WireId::new(1);
let pin = Device::sink(h.cpu.as_ref(), "irq5", &[src]).expect("irq5");
let weak = Arc::downgrade(&pin.sink);
drop(pin);
let alive = weak.upgrade().expect("the core still owns the pin");
alive.set_level(src, 0, Level::High);
assert!(h.cpu.irq_asserted(5));
}
#[test]
fn the_nmi_pin_is_not_one_of_the_numbered_ones() {
use crate::core::wire::{Level, WireId};
let h = Harness::m4(&[0xbf00, 0xbf00]);
let src = WireId::new(1);
h.set_word(2 * 4, 0x301); h.set_word(0x300, 0xe7fe); let nmi = Device::sink(h.cpu.as_ref(), "nmi", &[src]).expect("nmi");
assert_eq!(nmi.line, u32::from(Exception::NMI.0));
nmi.sink.set_level(src, 0, Level::High);
assert!(h.cpu.nmi_asserted());
h.cpu.step();
assert_eq!(
h.cpu.current_exception(),
Exception::NMI,
"NMI has no enable bit to be disabled by"
);
}
#[test]
fn the_reset_pin_is_a_signal_and_not_a_method_call() {
use crate::core::wire::{Level, WireId};
let h = Harness::m4(&[0x2042]);
let src = WireId::new(1);
h.cpu.step();
assert_eq!(h.cpu.reg(0), 0x42);
let reset = Device::sink(h.cpu.as_ref(), "reset", &[src]).expect("reset");
reset.sink.set_level(src, 0, Level::High);
h.cpu.step(); assert_eq!(h.cpu.pc(), ENTRY, "the reset sequence re-read the vectors");
}
#[test]
fn a_warm_reset_does_not_invent_a_level_for_an_input_pin() {
let h = Harness::m4(&[0xbf00]);
h.cpu.set_irq(7, true);
h.cpu.set_nmi(true);
Device::reset(h.cpu.as_ref(), crate::core::device::ResetKind::Warm);
assert!(
h.cpu.irq_asserted(7) && h.cpu.nmi_asserted(),
"a warm reset has drivers; clearing what they assert would lie"
);
Device::reset(h.cpu.as_ref(), crate::core::device::ResetKind::Cold);
assert!(!h.cpu.irq_asserted(7) && !h.cpu.nmi_asserted());
}
#[test]
fn a_scheduler_budget_is_never_overrun() {
use crate::core::clock::GlobalTime;
use crate::core::sched::Budget;
let h = Harness::m4(&[0xfb00, 0xf000, 0xfb00, 0xf000, 0xfb00, 0xf000]);
let before = h.cpu.cycles();
let mut consumed = 0u64;
for _ in 0..6 {
let used = Device::run(
h.cpu.as_ref(),
Budget {
until: GlobalTime::ZERO,
ticks: 1,
},
);
assert!(
used.ticks <= 1,
"consumed {} of a 1-tick budget",
used.ticks
);
consumed += used.ticks;
}
assert_eq!(consumed, 6, "a paid-down debt still spends the budget");
assert_eq!(h.cpu.cycles() - before, consumed + h.cpu.cycle_debt());
}
#[test]
fn the_schema_and_the_sinks_agree_about_every_pin() {
use crate::core::wire::WireId;
use crate::machine::validate::PortDir;
let h = Harness::m4(&[0xbf00]);
let schema = super::schema();
let src = WireId::new(1);
for name in ["irq0", "irq38", "irq239", "nmi", "reset"] {
let port = schema
.port_named(name)
.unwrap_or_else(|| panic!("the schema should know `{name}`"));
assert_eq!(port.dir, PortDir::In, "{name}");
assert!(
Device::sink(h.cpu.as_ref(), name, &[src]).is_some(),
"the device should build `{name}`"
);
}
for name in ["irq240", "irq07", "irq", "wfi"] {
assert!(schema.port_named(name).is_none(), "{name}");
assert!(
Device::sink(h.cpu.as_ref(), name, &[src]).is_none(),
"{name}"
);
}
assert!(schema.ports.iter().all(|p| p.dir == PortDir::In));
}
#[test]
fn from_props_names_a_part_and_rejects_a_typo() {
let props = Props::new().with("part", Value::from("cortex-m7"));
let cpu = ArmV7m::from_props(&props).unwrap();
assert_eq!(cpu.config().cpuid, CPUID_CORTEX_M7);
assert_eq!(cpu.config().priority_bits, 4);
let props = Props::new().with("part", Value::from("cortex-m0"));
assert!(ArmV7m::from_props(&props).is_err());
let props = Props::new().with("prioritybits", Value::from(4u64));
assert!(ArmV7m::from_props(&props).is_err());
let props = Props::new()
.with("part", Value::from("cortex-m4"))
.with("dsp", Value::from(false));
let cpu = ArmV7m::from_props(&props).unwrap();
assert!(!cpu.config().ext.dsp);
}
#[test]
fn the_disassembler_walks_both_widths() {
let mut code = Vec::new();
code.push(0x2042);
code.extend_from_slice(&wide(0xf8d0_1004));
code.push(0x4770);
let h = Harness::m4(&code);
let listed = h.cpu.disassemble(ENTRY, 3);
assert_eq!(listed.len(), 3);
assert_eq!(listed[0].width, 2);
assert_eq!(listed[1].width, 4);
assert_eq!(listed[2].addr, ENTRY + 6);
let text: Vec<String> = listed.iter().map(|l| format!("{}", l.insn)).collect();
assert_eq!(text[0], "MOVS r0, #66");
assert_eq!(text[1], "LDR r1, [r0, #4]");
assert_eq!(text[2], "BX lr");
}
#[test]
fn a_debug_read_does_not_disturb_the_system_block() {
let h = Harness::m4(&[0xbf00]);
h.cpu.with_sys(|s| {
s.syst_csr = 1 | (1 << 16);
});
let _ = h.cpu.disassemble(ENTRY, 1);
assert_ne!(h.cpu.with_sys(|s| s.syst_csr) & (1 << 16), 0);
}
#[test]
fn an_interrupt_pin_drives_the_nvic() {
use crate::core::wire::{Level, WireId, WireSink};
let h = Harness::m4(&[0xbf00]);
let src = WireId::new(1);
let pin = InterruptPin::new(h.cpu.lines(), 3, &[src]);
assert_eq!(pin.irq(), 3);
pin.set_level(src, 0, Level::High);
assert!(h.cpu.irq_asserted(3));
pin.set_level(src, 0, Level::Low);
assert!(!h.cpu.irq_asserted(3));
}
#[test]
fn a_level_input_re_pends_until_it_is_released() {
let h = Harness::m4(&[0xbf00, 0xbf00, 0xbf00, 0xbf00]);
h.set_word(16 * 4, 0x301);
h.set_word(0x300, 0x4770); h.cpu.with_sys(|s| s.set_enable(Exception::IRQ0, true));
h.cpu.set_irq(0, true);
h.cpu.step();
assert_eq!(h.cpu.current_exception(), Exception::IRQ0);
h.cpu.step(); h.cpu.step();
assert_eq!(
h.cpu.current_exception(),
Exception::IRQ0,
"a level that nobody cleared comes straight back"
);
h.cpu.set_irq(0, false);
h.cpu.step();
h.cpu.step();
assert_eq!(h.cpu.current_exception(), Exception::THREAD);
}
#[test]
fn a_pended_interrupt_does_not_re_pend_on_its_own() {
let h = Harness::m4(&[0xbf00, 0xbf00, 0xbf00]);
h.set_word(16 * 4, 0x301);
h.set_word(0x300, 0x4770);
h.cpu.with_sys(|s| s.set_enable(Exception::IRQ0, true));
h.cpu.pend_irq(0);
h.cpu.step();
assert_eq!(h.cpu.current_exception(), Exception::IRQ0);
h.cpu.step();
h.cpu.step();
assert_eq!(h.cpu.current_exception(), Exception::THREAD);
}
#[test]
fn a_bus_fault_names_the_address_it_could_not_reach() {
let h = Harness::m4(&wide(0xf8d0_1000));
h.cpu.set_reg(0, 0x4000_0000);
h.cpu.with_sys(|s| s.shcsr |= shcsr::BUSFAULTENA);
h.cpu.step();
assert_eq!(h.cpu.current_exception(), Exception::BUS_FAULT);
assert!(h.cpu.with_sys(|s| s.cfsr & fsr::BF_PRECISERR != 0));
assert!(h.cpu.with_sys(|s| s.cfsr & fsr::BF_BFARVALID != 0));
assert_eq!(h.cpu.with_sys(|s| s.bfar), 0x4000_0000);
let (count, last) = h.cpu.bus_faults();
assert_eq!(count, 1);
assert_eq!(last, 0x4000_0000);
}
#[test]
fn a_fault_with_its_handler_disabled_escalates() {
let h = Harness::m4(&[0xde00]);
h.set_word(3 * 4, 0x301);
h.set_word(0x300, 0xe7fe);
h.cpu.step();
assert_eq!(h.cpu.current_exception(), Exception::HARD_FAULT);
assert!(h.cpu.with_sys(|s| s.hfsr & fsr::HF_FORCED != 0));
assert!(h.cpu.with_sys(|s| s.cfsr & fsr::UF_UNDEFINSTR != 0));
}
#[test]
fn access_width_reads_the_right_slice_of_a_ppb_word() {
let h = Harness::m4(&[0xbf00]);
h.cpu.with_sys(|s| s.priority[11] = 0xc0);
let space = h.cpu.space().unwrap();
let _ = space;
let mut sys = Sys::new(CPUID_CORTEX_M4, 8, 8);
sys.priority[11] = 0xc0;
assert_eq!(sys.read_word(0xe000_ed1c, false).unwrap() >> 24, 0xc0);
}
#[test]
fn a_part_without_an_mpu_says_so_and_permits_everything() {
let cfg = Config {
ext: Extensions {
mpu: false,
..Config::CORTEX_M4.ext
},
..Config::CORTEX_M4
};
let h = Harness::new(cfg, &wide(0xf8d0_1000));
h.cpu.set_reg(0, 0xe000_ed90);
h.cpu.step();
assert_eq!(h.cpu.reg(1) & 0xff00, 0);
h.cpu.with_sys(|s| {
s.write_word(0xe000_ed94, 0b001);
assert_eq!(s.mpu_ctrl, 0);
assert!(s.mpu_permits(0x1000, Access::Write, false, 0));
});
}
#[test]
fn the_configuration_never_claims_a_floating_point_unit() {
let cfg = Config {
ext: Extensions {
fp: true,
..Config::CORTEX_M4.ext
},
..Config::CORTEX_M4
};
assert!(!ArmV7m::new(cfg).config().ext.fp);
}
#[test]
fn regs_display_names_the_mode_it_is_in() {
let regs = Regs {
xpsr: xpsr::T | xpsr::Z | 11,
..Regs::new()
};
let text = format!("{regs}");
assert!(text.contains("nZcvq"), "{text}");
assert!(text.contains("svcall"), "{text}");
}
#[test]
fn width_matches_the_fetch_the_decoder_would_ask_for() {
assert_eq!(Insn::width_of(0x2042), 2);
assert_eq!(Insn::width_of(0xf8d0), 4);
assert_eq!(Width::U16.bytes(), 2);
}