use hg80::{BusCycle, BusRequest, Cpu, Host};
use std::path::PathBuf;
struct Machine {
memory: Vec<u8>,
reports: Vec<(u16, u32)>,
opened: Vec<BusRequest>,
closed: Vec<BusRequest>,
held: u32,
}
fn address_of(request: BusRequest) -> u16 {
match request {
BusRequest::OpcodeFetch { address }
| BusRequest::MemoryRead { address }
| BusRequest::MemoryWrite { address, .. }
| BusRequest::Refresh { address }
| BusRequest::Internal { address }
| BusRequest::InterruptAcknowledge { address } => address,
BusRequest::PortRead { port } | BusRequest::PortWrite { port, .. } => port,
_ => 0,
}
}
impl Host for Machine {
fn read(&mut self, address: u16, _at: u32) -> u8 {
self.memory[address as usize]
}
fn write(&mut self, address: u16, value: u8, _at: u32) {
self.memory[address as usize] = value;
}
fn input(&mut self, port: u16, _at: u32) -> u8 {
self.memory[port as usize]
}
fn output(&mut self, _port: u16, _value: u8, _at: u32) {}
fn wait_states(&mut self, request: &BusRequest) -> u32 {
self.opened.push(*request);
self.held
}
fn bus_cycle(&mut self, cycle: &BusCycle) {
self.closed.push(cycle.request);
self.reports
.push((address_of(cycle.request), cycle.t_states));
}
}
fn parse_image(text: &str) -> Vec<u8> {
let mut memory = vec![0u8; 0x10000];
for line in text.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let mut words = line.split_whitespace();
let Some(Ok(start)) = words.next().map(|word| u16::from_str_radix(word, 16)) else {
continue;
};
let mut at = start;
for word in words {
if let Ok(byte) = u8::from_str_radix(word, 16) {
memory[at as usize] = byte;
at = at.wrapping_add(1);
}
}
}
memory
}
fn reference_cycles(text: &str) -> Vec<(u16, u32)> {
let mut cycles: Vec<(u16, u32)> = Vec::new();
let mut previous: Option<(u32, u32)> = None;
for line in text.lines() {
let fields: Vec<u32> = line
.split_whitespace()
.filter_map(|word| word.parse().ok())
.collect();
if fields.len() < 14 {
continue;
}
let (machine_cycle, t_state, address) = (fields[0], fields[1], fields[2]);
let opening = previous != Some((machine_cycle, t_state))
&& (previous.is_none_or(|(cycle, _)| cycle != machine_cycle) || t_state == 1);
if opening && t_state == 1 {
cycles.push((u16::try_from(address).unwrap_or_default(), 1));
} else if let Some(last) = cycles.last_mut() {
last.1 += 1;
}
previous = Some((machine_cycle, t_state));
}
cycles
}
fn signals(text: &str) -> (Option<usize>, Option<usize>) {
let header = text.lines().next().unwrap_or_default();
let field = |key: &str| {
header
.split_whitespace()
.find_map(|word| word.strip_prefix(key))
.and_then(|value| value.parse().ok())
};
(field("int="), field("nmi="))
}
fn simulation() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("sim/t80n")
}
fn run(
image: &[u8],
t_states: usize,
held: u32,
(interrupt_at, nonmaskable_at): (Option<usize>, Option<usize>),
) -> Machine {
let mut cpu = Cpu::new();
cpu.reset();
cpu.set_z80n_enabled(true);
let mut machine = Machine {
memory: image.to_vec(),
reports: Vec::new(),
opened: Vec::new(),
closed: Vec::new(),
held,
};
for index in 0..t_states {
if let Some(at) = interrupt_at {
cpu.set_interrupt_requested(index > at);
}
if nonmaskable_at.is_some_and(|at| index == at + 1) {
cpu.request_nmi();
}
cpu.tick(&mut machine);
cpu.tick(&mut machine);
}
machine
}
#[test]
fn each_machine_cycle_is_reported_once_with_the_length_the_reference_spends_in_it() {
let root = simulation();
let programs = [
"arithmetic",
"loads",
"blocks",
"prefixed",
"extended",
"branches",
"z80n_arith",
"z80n_block",
"z80n_wide",
"halted",
];
let mut failures = Vec::new();
let mut compared = 0;
for name in programs {
for held in [0, 2] {
let trace = if held == 0 {
format!("traces/{name}.trace")
} else {
format!("traces/{name}.wait{held}.trace")
};
let Ok(text) = std::fs::read_to_string(root.join(&trace)) else {
failures.push(format!(" {name}: no reference trace"));
continue;
};
let image = std::fs::read_to_string(root.join(format!("programs/{name}.hex")))
.unwrap_or_else(|_| panic!("{name}: no program image"));
let want = reference_cycles(&text);
let got = run(
&parse_image(&image),
text.lines().count(),
held,
signals(&image),
)
.reports;
assert_eq!(
want.len(),
got.len() + 1,
"{name} stalled {held}: {} reports against the reference's {}",
got.len(),
want.len()
);
let pairs = got.len();
compared += pairs;
for (index, (ours, theirs)) in got.iter().zip(want.iter()).take(pairs).enumerate() {
if ours != theirs {
failures.push(format!(
" {name} stalled {held}: cycle {index}: reported {:04x} for {} \
T-states, reference opens {:04x} and runs {}",
ours.0, ours.1, theirs.0, theirs.1
));
break;
}
}
}
}
println!("{compared} machine cycles compared against the reference");
for failure in &failures {
println!("{failure}");
}
assert!(failures.is_empty(), "{} reports diverged", failures.len());
assert!(compared > 200, "only {compared} cycles were compared");
}
#[test]
fn the_opening_hook_and_the_closing_hook_describe_the_same_cycles_in_the_same_order() {
let root = simulation();
let mut compared = 0;
for name in [
"arithmetic",
"prefixed",
"z80n_block",
"halted",
"interrupt_mode2",
] {
let Ok(text) = std::fs::read_to_string(root.join(format!("traces/{name}.trace"))) else {
continue;
};
let image = std::fs::read_to_string(root.join(format!("programs/{name}.hex")))
.unwrap_or_else(|_| panic!("{name}: no program image"));
let seen = run(
&parse_image(&image),
text.lines().count(),
0,
signals(&image),
);
assert!(
seen.opened.len() == seen.closed.len() || seen.opened.len() == seen.closed.len() + 1,
"{name}: {} cycles opened against {} closed, which should differ by at most the one \
still running",
seen.opened.len(),
seen.closed.len()
);
for (index, (opened, closed)) in seen.opened.iter().zip(seen.closed.iter()).enumerate() {
assert_eq!(
opened, closed,
"{name}: cycle {index} opened as {opened:?} and closed as {closed:?}"
);
}
compared += seen.closed.len();
}
println!("{compared} cycles matched between the opening and closing hooks");
assert!(compared > 200, "only {compared} cycles were compared");
}