use anyhow::{Result, ensure};
use core::fmt::Write as _;
use crate::{
asm::disassemble,
clock::Clock,
cpu::{Cpu, Phase},
memory::Memory,
regs::Reg::*,
};
#[non_exhaustive]
#[derive(Clone, Debug, Default)]
pub struct Bus {
pub addr: u16,
pub data: u8,
pub debug: bool,
pub mem: bool,
pub pending_write: Option<Vec<State>>,
pub write: bool,
}
impl Bus {
#[inline]
pub fn assert(&self, states: &[State], msg: impl AsRef<str>) -> Result<()> {
let msg = msg.as_ref();
for state in states {
match *state {
State::Addr(addr) => ensure!(
self.addr == addr,
"want bus addr {:04X}, got {:04X} {msg}",
addr,
self.addr
),
State::Data(data) => ensure!(
self.data == data,
"want bus data {:02X}, got {:02X} {msg}",
data,
self.data
),
State::Mem(mem) => ensure!(
self.mem == mem,
"/MEM line {} {msg}",
if self.mem { "active" } else { "inactive" }
),
State::Write(wr) => ensure!(
self.write == wr,
"/WR line {} {msg}",
if self.write { "active" } else { "inactive" }
),
}
}
Ok(())
}
#[inline]
pub fn reconcile(&mut self) {
if let Some(states) = self.pending_write.take() {
for state in states {
match state {
State::Addr(addr) => self.addr = addr,
State::Data(data) => self.data = data,
State::Mem(mem) => self.mem = mem,
State::Write(wr) => self.write = wr,
}
}
}
}
}
pub trait Device {
fn tick(&mut self, bus: &mut Bus);
}
#[non_exhaustive]
pub struct Snapshot {
pub bus: Bus,
pub phase: Phase,
pub tick: u16,
}
#[non_exhaustive]
#[derive(Clone, Debug)]
pub enum State {
Addr(u16),
Data(u8),
Mem(bool),
Write(bool),
}
#[non_exhaustive]
pub struct System {
pub bus: Bus,
pub clock: Box<dyn Device>,
pub cpu: Cpu,
pub cycles: u16,
pub debug: bool,
pub devices: Vec<Box<dyn Device>>,
pub history: Vec<Snapshot>,
pub mem: Memory,
}
impl Default for System {
#[inline]
fn default() -> Self {
Self {
bus: Bus::default(),
clock: Box::new(Clock::default()),
cpu: Cpu::default(),
debug: false,
devices: Vec::new(),
history: Vec::new(),
mem: Memory::default(),
cycles: 0,
}
}
}
impl System {
#[expect(clippy::as_conversions, reason = "truncation is correct")]
#[expect(clippy::cast_possible_truncation, reason = "truncation is correct")]
#[inline]
pub fn debug_print(&self) {
println!(" PC A B C D E F G H Z | NEXT");
println!(
"{:04X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:1b} | {}",
self.cpu.pc,
self.cpu.regs.get(A) as u8,
self.cpu.regs.get(B) as u8,
self.cpu.regs.get(C) as u8,
self.cpu.regs.get(D) as u8,
self.cpu.regs.get(E) as u8,
self.cpu.regs.get(F) as u8,
self.cpu.regs.get(G) as u8,
self.cpu.regs.get(H) as u8,
u8::from(self.cpu.flags.zero),
disassemble(
self.mem
.0
.get(usize::from(self.cpu.pc)..)
.unwrap_or_default()
)
.unwrap_or_default(),
);
}
#[inline]
pub fn run(&mut self) {
self.cpu.halt = false;
while !self.cpu.halt {
self.tick();
}
}
#[inline]
pub fn run_program(&mut self, program: &[u8]) -> Result<()> {
self.mem.load(0x0000, program)?;
self.cpu.pc = 0x0000;
self.run();
Ok(())
}
#[inline]
pub fn tick(&mut self) {
let phase = self.cpu.phase; self.cpu.tick(&mut self.bus);
self.mem.tick(&mut self.bus);
for device in &mut self.devices {
device.tick(&mut self.bus);
}
self.bus.reconcile();
if self.debug {
self.history.push(Snapshot {
tick: self.cycles,
phase, bus: self.bus.clone(),
});
}
self.clock.tick(&mut self.bus);
self.cycles = self.cycles.wrapping_add(1);
}
#[expect(clippy::non_ascii_literal, reason = "looks nice")]
#[expect(clippy::unwrap_used, reason = "panic is okay here")]
#[inline]
pub fn trace(&self) {
if self.history.is_empty() {
return;
}
for chunk in self.history.chunks(16) {
let mut tick = String::from("TICK ");
let mut header = String::from("─────");
let mut phase = String::from("CPU ");
let mut addr = String::from("ADDR ");
let mut data = String::from("DATA ");
let mut mem = String::from("/MEM ");
for snapshot in chunk {
write!(tick, " {:04X}", snapshot.tick).unwrap();
write!(header, "─────").unwrap();
write!(phase, " {}", snapshot.phase).unwrap();
write!(addr, " {:04X}", snapshot.bus.addr).unwrap();
write!(data, " ──{:02X}", snapshot.bus.data).unwrap();
write!(
mem,
"{}",
if snapshot.bus.mem {
" ████"
} else {
" ────"
}
)
.unwrap();
}
println!("{tick}");
println!("{header}");
println!("{phase}");
println!("{addr}");
println!("{data}");
println!("{mem}");
println!();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::instructions::InstructionKind::*;
#[expect(clippy::unwrap_used, reason = "test")]
#[test]
fn trace_formatting_copes_with_long_lines() {
let mut sys = System {
debug: true,
..System::default()
};
let mut nops = vec![u8::from(Nop); 7];
nops.push(u8::from(Halt));
sys.run_program(&nops).unwrap();
sys.trace();
}
}