use super::*;
#[test]
fn the_refresh_counter_advances_once_per_fetch_and_keeps_its_top_bit() {
let (cpu, _, _) = run(&[0x00, 0x00, 0x00], 3);
assert_eq!(cpu.registers().r, 3);
let (cpu, _, _) = run_from(&[0x00], |cpu| cpu.registers_mut().r = 0xFF, 1);
assert_eq!(cpu.registers().r, 0x80);
}
#[test]
fn a_port_read_drives_its_address_as_a_port_cycle() {
let mut cpu = Cpu::new();
cpu.reset();
cpu.registers_mut().af = 0x0000;
let mut ram = Ram::with(&[0xDB, 0x34]);
ram.ports[0x0034] = 0x99;
cpu.step_by_edges(&mut ram);
assert!(
ram.seen
.iter()
.any(|request| matches!(request, crate::BusRequest::PortRead { port: 0x0034 }))
);
assert!(
!ram.seen
.iter()
.any(|request| matches!(request, crate::BusRequest::MemoryRead { address: 0x0034 }))
);
}
#[test]
fn a_port_write_is_reported_with_the_byte_it_carries() {
let mut cpu = Cpu::new();
cpu.reset();
let mut ram = Ram::with(&[0x3E, 0x5A, 0xD3, 0x34]);
cpu.step_by_edges(&mut ram);
cpu.step_by_edges(&mut ram);
assert!(ram.seen.iter().any(|request| matches!(
request,
crate::BusRequest::PortWrite {
port: 0x5A34,
value: 0x5A
}
)));
}
#[test]
fn an_opcode_fetch_is_followed_by_a_refresh_cycle() {
let mut cpu = Cpu::new();
cpu.reset();
let mut ram = Ram::with(&[0x00]);
cpu.step_by_edges(&mut ram);
assert!(matches!(
ram.seen[0],
crate::BusRequest::OpcodeFetch { address: 0 }
));
assert!(
ram.seen
.iter()
.any(|request| matches!(request, crate::BusRequest::Refresh { .. }))
);
}
struct Openings {
bytes: Box<[u8; 0x10000]>,
opened: Vec<crate::BusRequest>,
}
impl crate::Host for Openings {
fn read(&mut self, address: u16, _at: u32) -> u8 {
self.bytes[address as usize]
}
fn write(&mut self, _: u16, _: u8, _at: u32) {}
fn input(&mut self, _: u16, _at: u32) -> u8 {
0xFF
}
fn output(&mut self, _: u16, _: u8, _at: u32) {}
fn wait_states(&mut self, request: &crate::BusRequest) -> u32 {
self.opened.push(*request);
0
}
}
#[test]
fn only_a_real_machine_cycle_opens_one() {
fn opened(program: &[u8]) -> (u32, Vec<&'static str>) {
let mut bytes = vec![0u8; 0x10000].into_boxed_slice();
bytes[..program.len()].copy_from_slice(program);
let mut host = Openings {
bytes: bytes.try_into().unwrap(),
opened: Vec::new(),
};
let mut cpu = Cpu::new();
cpu.reset();
let t_states = cpu.step_by_edges(&mut host);
let names = host
.opened
.iter()
.map(|request| match request {
crate::BusRequest::OpcodeFetch { .. } => "fetch",
crate::BusRequest::MemoryRead { .. } => "read",
crate::BusRequest::MemoryWrite { .. } => "write",
crate::BusRequest::Internal { .. } => "internal",
crate::BusRequest::Refresh { .. } => "refresh",
_ => "other",
})
.collect::<Vec<_>>();
let (last, rest) = names.split_last().expect("a step opens at least one cycle");
assert_eq!(*last, "fetch", "a step leaves the next fetch open");
(t_states, rest.to_vec())
}
assert_eq!(opened(&[0x00]), (4, vec!["fetch"]));
assert_eq!(opened(&[0x23]), (6, vec!["fetch"]));
assert_eq!(opened(&[0x09]), (11, vec!["fetch", "internal", "internal"]));
assert_eq!(opened(&[0x36, 0xAA]), (10, vec!["fetch", "read", "write"]));
}
#[test]
fn a_refresh_reaches_the_edges_but_never_opens_a_cycle() {
let program = [
0x21, 0x00, 0x80, 0x36, 0xAA, 0x23, 0x09, 0xDD, 0x23, ];
let mut bytes = vec![0u8; 0x10000].into_boxed_slice();
bytes[..program.len()].copy_from_slice(&program);
let mut host = Openings {
bytes: bytes.try_into().unwrap(),
opened: Vec::new(),
};
let mut cpu = Cpu::new();
cpu.reset();
for _ in 0..5 {
cpu.step_by_edges(&mut host);
}
assert!(
!host
.opened
.iter()
.any(|request| matches!(request, crate::BusRequest::Refresh { .. }))
);
assert!(
host.opened
.iter()
.any(|request| matches!(request, crate::BusRequest::Internal { .. })),
"the internal cycles of ADD HL,BC do open"
);
let mut ram = Ram::with(&program);
let mut cpu = Cpu::new();
cpu.reset();
for _ in 0..5 {
cpu.step_by_edges(&mut ram);
}
assert!(
ram.seen
.iter()
.any(|request| matches!(request, crate::BusRequest::Refresh { .. })),
"but every fetch still shows its refresh on the edges"
);
}
struct Acknowledge {
bytes: Box<[u8; 0x10000]>,
edges: Vec<crate::BusRequest>,
opened: Vec<crate::BusRequest>,
waits: u32,
}
impl crate::Host for Acknowledge {
fn read(&mut self, address: u16, _at: u32) -> u8 {
self.bytes[address as usize]
}
fn write(&mut self, _: u16, _: u8, _at: u32) {}
fn input(&mut self, _: u16, _at: u32) -> u8 {
0xFF
}
fn output(&mut self, _: u16, _: u8, _at: u32) {}
fn wait_states(&mut self, request: &crate::BusRequest) -> u32 {
self.opened.push(*request);
self.waits
}
fn bus_edge(&mut self, request: &crate::BusRequest) {
self.edges.push(*request);
}
fn interrupt_vector(&mut self) -> u8 {
0xC7
}
}
#[test]
fn an_accepted_interrupt_keeps_its_shape_in_every_mode_at_every_wait_depth() {
fn runs(mode: u8, waits: u32) -> (Vec<(&'static str, usize)>, usize) {
let interrupt_mode = match mode {
0 => 0x46,
1 => 0x56,
_ => 0x5E,
};
let mut bytes = vec![0u8; 0x10000].into_boxed_slice();
bytes[..5].copy_from_slice(&[0xFB, 0xED, interrupt_mode, 0x00, 0x00]);
let mut host = Acknowledge {
bytes: bytes.try_into().unwrap(),
edges: Vec::new(),
opened: Vec::new(),
waits,
};
let mut cpu = Cpu::new();
cpu.reset();
cpu.registers_mut().sp = 0xFF00;
cpu.step_by_edges(&mut host);
cpu.step_by_edges(&mut host);
cpu.set_interrupt_requested(true);
host.edges.clear();
host.opened.clear();
for _ in 0..(20 + waits * 4) {
cpu.tick(&mut host);
cpu.tick(&mut host);
}
let name = |request: &crate::BusRequest| match request {
crate::BusRequest::InterruptAcknowledge { .. } => "ack",
crate::BusRequest::Refresh { .. } => "refresh",
crate::BusRequest::Internal { .. } => "internal",
crate::BusRequest::OpcodeFetch { .. } => "fetch",
crate::BusRequest::MemoryWrite { .. } => "write",
crate::BusRequest::MemoryRead { .. } => "read",
_ => "other",
};
let mut compressed: Vec<(&'static str, usize)> = Vec::new();
for edge in &host.edges {
match compressed.last_mut() {
Some((last, count)) if *last == name(edge) => *count += 1,
_ => compressed.push((name(edge), 1)),
}
}
let from = compressed
.iter()
.position(|(kind, _)| *kind == "ack")
.expect("the interrupt is accepted");
let acknowledges = host
.opened
.iter()
.filter(|request| matches!(request, crate::BusRequest::InterruptAcknowledge { .. }))
.count();
(compressed[from..=from + 3].to_vec(), acknowledges)
}
for mode in [0u8, 1, 2] {
for waits in [0u32, 1, 2, 5] {
let (shape, acknowledges) = runs(mode, waits);
assert_eq!(
shape,
vec![
("ack", 4 + waits as usize),
("refresh", 2),
("internal", 1),
("write", 2 * (3 + waits as usize)),
],
"mode {mode} at {waits} wait states"
);
assert_eq!(acknowledges, 1, "mode {mode} at {waits} wait states");
}
}
}
struct Split {
bytes: Box<[u8; 0x10000]>,
fetched: Vec<u16>,
read: Vec<u16>,
}
impl Split {
fn with(program: &[u8]) -> Self {
let mut bytes = vec![0u8; 0x10000].into_boxed_slice();
bytes[..program.len()].copy_from_slice(program);
Self {
bytes: bytes.try_into().unwrap(),
fetched: Vec::new(),
read: Vec::new(),
}
}
}
impl crate::Host for Split {
fn read(&mut self, address: u16, _at: u32) -> u8 {
self.read.push(address);
self.bytes[address as usize]
}
fn fetch(&mut self, address: u16, _at: u32) -> u8 {
self.fetched.push(address);
self.bytes[address as usize]
}
fn write(&mut self, address: u16, value: u8, _at: u32) {
self.bytes[address as usize] = value;
}
fn input(&mut self, _: u16, _at: u32) -> u8 {
0xFF
}
fn output(&mut self, _: u16, _: u8, _at: u32) {}
fn interrupt_vector(&mut self) -> u8 {
0x40
}
}
#[test]
fn every_prefix_byte_is_a_fetch_and_every_operand_is_a_read() {
let mut cpu = Cpu::new();
cpu.reset();
cpu.registers_mut().ix = 0x4000;
let mut host = Split::with(&[0xDD, 0xCB, 0x05, 0x46, 0x21, 0x34, 0x12]);
cpu.step_by_edges(&mut host);
cpu.step_by_edges(&mut host);
assert_eq!(host.fetched, vec![0x0000, 0x0001, 0x0004, 0x0007]);
assert_eq!(host.read, vec![0x0002, 0x0003, 0x4005, 0x0005, 0x0006]);
}
#[test]
fn the_byte_an_interrupting_device_supplies_is_not_a_fetch() {
let mut cpu = Cpu::new();
cpu.reset();
let mut host = Split::with(&[0xFB, 0xED, 0x56, 0x00, 0x00]);
cpu.step_by_edges(&mut host);
cpu.step_by_edges(&mut host);
cpu.set_interrupt_requested(true);
cpu.step_by_edges(&mut host);
let before = host.fetched.len();
cpu.step_by_edges(&mut host);
assert_eq!(cpu.registers().pc, 0x0038);
assert_eq!(host.fetched[before..], [0x0038]);
}
#[test]
fn abandoning_the_instruction_makes_a_moved_program_counter_take_effect() {
let mut program = vec![0x00; 0x40];
program[0x0020] = 0x3C;
let redirected = |abandon: bool| {
let mut cpu = Cpu::new();
cpu.reset();
cpu.registers_mut().af = 0x0000;
let mut host = Split::with(&program);
cpu.step_by_edges(&mut host);
cpu.registers_mut().pc = 0x0020;
if abandon {
cpu.abandon_instruction();
}
let before = host.fetched.len();
cpu.step_by_edges(&mut host);
(
high_byte(cpu.registers().af),
host.fetched[before..].to_vec(),
)
};
assert_eq!(
redirected(false),
(0x00, vec![0x0021]),
"the opcode already fetched at 0x0001 runs, and 0x0020 is skipped entirely"
);
assert_eq!(
redirected(true),
(0x01, vec![0x0020, 0x0021]),
"the fetch is re-opened at the moved program counter"
);
}
#[test]
fn abandoning_the_instruction_keeps_the_lines_and_the_flip_flops() {
let mut cpu = Cpu::new();
cpu.reset();
cpu.set_z80n_enabled(true);
cpu.set_undocumented_flags(crate::UndocumentedFlags::Accumulator);
cpu.registers_mut().interrupt_mode = crate::InterruptMode::Mode2;
cpu.registers_mut().iff1 = true;
cpu.registers_mut().iff2 = true;
cpu.registers_mut().i = 0x7E;
cpu.registers_mut().wz = 0xABCD;
cpu.registers_mut().bc = 0x1234;
cpu.set_interrupt_requested(true);
cpu.request_nmi();
cpu.set_halted(true);
cpu.abandon_instruction();
assert!(cpu.is_interrupt_requested(), "the maskable line survives");
assert!(cpu.is_nmi_requested(), "the non-maskable line survives");
assert!(cpu.is_halted(), "halting is separately controllable");
assert!(cpu.is_z80n_enabled());
assert_eq!(
cpu.undocumented_flags(),
crate::UndocumentedFlags::Accumulator
);
assert_eq!(cpu.registers().interrupt_mode, crate::InterruptMode::Mode2);
assert!(cpu.registers().iff1 && cpu.registers().iff2);
assert_eq!(cpu.registers().i, 0x7E);
assert_eq!(cpu.registers().wz, 0xABCD);
assert_eq!(cpu.registers().bc, 0x1234);
let mut fresh = Cpu::new();
fresh.reset();
fresh.set_interrupt_requested(true);
fresh.request_nmi();
fresh.reset();
assert!(
!fresh.is_interrupt_requested() && !fresh.is_nmi_requested(),
"reset drops both, which is why it is the wrong tool for this"
);
}
#[test]
fn a_halted_core_refetches_the_instruction_after_the_halt() {
let mut cpu = Cpu::new();
cpu.reset();
let mut host = Split::with(&[0x76]);
cpu.step_by_edges(&mut host);
cpu.step_by_edges(&mut host);
cpu.step_by_edges(&mut host);
assert!(cpu.is_halted());
assert_eq!(host.fetched, vec![0x0000, 0x0001, 0x0001, 0x0001]);
assert!(host.read.is_empty());
}
#[test]
fn a_host_that_stalls_the_core_lengthens_the_cycle_it_stalls() {
struct Slow {
bytes: Box<[u8; 0x10000]>,
waits: u32,
}
impl crate::Host for Slow {
fn read(&mut self, address: u16, _at: u32) -> u8 {
self.bytes[address as usize]
}
fn write(&mut self, _: u16, _: u8, _at: u32) {}
fn input(&mut self, _: u16, _at: u32) -> u8 {
0xFF
}
fn output(&mut self, _: u16, _: u8, _at: u32) {}
fn wait_states(&mut self, _: &crate::BusRequest) -> u32 {
self.waits
}
}
fn second_step(waits: u32) -> u32 {
let mut bytes = vec![0u8; 0x10000].into_boxed_slice();
bytes[0] = 0x00;
bytes[1] = 0x00;
let mut host = Slow {
bytes: bytes.try_into().unwrap(),
waits,
};
let mut cpu = Cpu::new();
cpu.reset();
cpu.step_by_edges(&mut host);
cpu.step_by_edges(&mut host)
}
assert_eq!(second_step(0), 4);
assert_eq!(second_step(1), 5);
assert_eq!(second_step(3), 7);
}
#[test]
fn stepping_from_any_clock_phase_reaches_the_same_place() {
let program = [0x3E, 0x05, 0x3C, 0x3C, 0x3C, 0x21, 0x34, 0x12, 0x23, 0x76];
let settled = |leading: usize| {
let mut cpu = Cpu::new();
cpu.reset();
let mut host = Split::with(&program);
for _ in 0..leading {
cpu.tick(&mut host);
}
for _ in 0..16 {
cpu.step_by_edges(&mut host);
}
assert!(
matches!(cpu.clock_edge(), crate::ClockEdge::Falling),
"a completed step leaves the clock on its falling edge"
);
assert!(
cpu.is_halted(),
"the program runs to its halt from every phase"
);
(cpu.registers().pc, cpu.registers().af, cpu.registers().hl)
};
let reference = settled(0);
assert_eq!(reference, (0x000A, 0x0809, 0x1235));
for leading in [1usize, 2, 3, 4, 5, 6, 7, 8] {
assert_eq!(
settled(leading),
reference,
"{leading} ticks before stepping"
);
}
}
struct Timed {
bytes: Box<[u8; 0x10000]>,
waits: u32,
seen: Vec<(&'static str, u32)>,
inputs: u32,
}
impl crate::Host for Timed {
fn read(&mut self, address: u16, at: u32) -> u8 {
self.seen.push(("read", at));
self.bytes[address as usize]
}
fn fetch(&mut self, address: u16, at: u32) -> u8 {
self.seen.push(("fetch", at));
self.bytes[address as usize]
}
fn write(&mut self, _: u16, _: u8, at: u32) {
self.seen.push(("write", at));
}
fn input(&mut self, _: u16, at: u32) -> u8 {
self.seen.push(("in", at));
self.inputs += 1;
0xFF
}
fn output(&mut self, _: u16, _: u8, at: u32) {
self.seen.push(("out", at));
}
fn wait_states(&mut self, _: &crate::BusRequest) -> u32 {
self.waits
}
}
fn timed(program: &[u8], waits: u32, steps: usize) -> Timed {
let mut bytes = vec![0u8; 0x10000].into_boxed_slice();
bytes[..program.len()].copy_from_slice(program);
let mut host = Timed {
bytes: bytes.try_into().unwrap(),
waits,
seen: Vec::new(),
inputs: 0,
};
let mut cpu = Cpu::new();
cpu.reset();
for _ in 0..steps {
cpu.step_by_edges(&mut host);
}
host
}
#[test]
fn a_transfer_reports_where_it_completes_on_the_bus() {
let program = [0x3A, 0x00, 0x02, 0x32, 0x00, 0x02, 0xDB, 0xFE, 0xD3, 0xFE];
for waits in [0u32, 1, 2, 5] {
let host = timed(&program, waits, 4);
let mut first = std::collections::BTreeMap::new();
for (kind, at) in &host.seen {
first.entry(*kind).or_insert(*at);
}
assert_eq!(
first.get("fetch"),
Some(&(2 + waits)),
"fetch at {waits} waits"
);
assert_eq!(
first.get("read"),
Some(&(3 + waits)),
"read at {waits} waits"
);
assert_eq!(
first.get("write"),
Some(&(3 + waits)),
"write at {waits} waits"
);
assert_eq!(
first.get("in"),
Some(&(4 + waits)),
"port read at {waits} waits"
);
assert_eq!(
first.get("out"),
Some(&(4 + waits)),
"port write at {waits} waits"
);
}
}
#[test]
fn a_stalled_port_read_asks_the_machine_once() {
let program = [0xDB, 0xFE, 0x76];
for waits in [0u32, 1, 2, 5] {
assert_eq!(
timed(&program, waits, 1).inputs,
1,
"at {waits} wait states"
);
}
}