use core::fmt::{Display, Formatter};
use crate::{
bus::Bus,
instructions::{InstructionKind, Operands},
regs::{Reg, Regs, source_and_target_from, source_from},
system::Device,
};
use State::*;
pub const TRAP_ILLEGAL: u8 = 0x00;
pub const VEC_RESET: u16 = 0xFFFE;
#[non_exhaustive]
#[derive(Debug)]
pub struct Cpu {
pub flags: Flags,
pub halt: bool,
pub ins: InstructionKind,
pub op_hi: u8,
pub op_lo: u8,
pub pc: u16,
pub regs: Regs,
pub state: State,
}
impl Default for Cpu {
#[inline]
fn default() -> Self {
Self {
flags: Flags::default(),
halt: Default::default(),
ins: InstructionKind::Nop,
op_hi: Default::default(),
op_lo: Default::default(),
pc: Default::default(),
state: State::default(),
regs: Regs::default(),
}
}
}
impl Device for Cpu {
#[expect(clippy::too_many_lines, reason = "it's just long")]
#[inline]
fn tick(&mut self, bus: &mut Bus) {
self.state = match self.state {
Decode => {
let opcode = bus.data;
let Ok(ins) = InstructionKind::try_from(opcode) else {
self.trap(TRAP_ILLEGAL, bus);
return;
};
self.ins = ins;
match self.ins.operands() {
Operands::Zero => {
bus.disable_mem();
Execute
}
Operands::One => {
self.fetch_and_advance(bus);
WaitOp
}
Operands::Two => {
self.fetch_and_advance(bus);
WaitOpLo
}
}
}
Execute => {
let ins = self.ins;
self.state = FetchOpcode;
ins.execute(self, bus);
self.state
}
FetchOpcode => {
if self.halt {
FetchOpcode
} else {
self.fetch_and_advance(bus);
WaitOpcode
}
}
ReadDec(addr) => {
let mut val = bus.data;
val = val.wrapping_sub(1);
bus.write_mem(addr, val);
self.flags.zero = val == 0;
FetchOpcode
}
ReadInc(addr) => {
let mut val = bus.data;
val = val.wrapping_add(1);
bus.write_mem(addr, val);
self.flags.zero = val == 0;
FetchOpcode
}
ReadLoad(reg) => {
self.op_lo = bus.data;
self.regs.set(reg, self.op());
FetchOpcode
}
ReadOp => {
self.op_hi = 0;
self.op_lo = bus.data;
Execute
}
ReadOpLo => {
self.op_lo = bus.data;
self.fetch_and_advance(bus);
WaitOpHi
}
ReadOpHi => {
self.op_hi = bus.data;
Execute
}
ReadResetLo => {
self.op_lo = bus.data;
bus.read_mem(VEC_RESET.wrapping_add(1));
WaitAddrHi
}
ReadAddrHi => {
self.op_hi = bus.data;
self.pc = self.op();
FetchOpcode
}
ReadRetHi => {
self.op_hi = bus.data;
self.stack_pop(bus);
WaitRetLo
}
ReadRetLo => {
self.op_lo = bus.data;
self.pc = self.op();
FetchOpcode
}
ReadStackHi(reg) => {
self.op_hi = bus.data;
self.stack_pop(bus);
WaitLoad(reg)
}
ReadTrapVecLo(addr) => {
self.op_lo = bus.data;
bus.read_mem(addr);
WaitAddrHi
}
WaitCall(hi, subr_addr) => {
self.stack_push(hi, bus);
self.pc = subr_addr;
FetchOpcode
}
WaitDec(addr) => ReadDec(addr),
WaitInc(addr) => ReadInc(addr),
WaitLoad(reg) => ReadLoad(reg),
WaitOp => ReadOp,
WaitOpLo => ReadOpLo,
WaitOpHi => ReadOpHi,
WaitOpcode => Decode,
WaitResetLo => ReadResetLo,
WaitAddrHi => ReadAddrHi,
WaitStackHi(reg) => ReadStackHi(reg),
WaitPush(val) => {
self.stack_push(val, bus);
FetchOpcode
}
WaitRetHi => ReadRetHi,
WaitRetLo => ReadRetLo,
WaitTrapCode(trap_code) => {
let mut vec_addr = u16::from(trap_code.strict_mul(2));
bus.read_mem(vec_addr);
vec_addr = vec_addr.wrapping_add(1);
WaitTrapVecLo(vec_addr)
}
WaitTrapLo(hi, trap_code) => {
self.stack_push(hi, bus);
WaitTrapHi(trap_code)
}
WaitTrapHi(trap_code) => {
self.stack_push(trap_code, bus);
WaitTrapCode(trap_code)
}
WaitTrapVecLo(addr) => ReadTrapVecLo(addr),
};
}
}
impl Cpu {
#[expect(clippy::cast_possible_wrap, reason = "i8 to u16 is sound")]
#[expect(clippy::cast_sign_loss, reason = "okay with wrapping_add")]
#[inline]
pub fn branch(&mut self, dis: u8) {
self.pc = self.pc.wrapping_add(dis as i8 as u16); }
#[inline]
pub fn call(&mut self, addr: u16, bus: &mut Bus) {
let ret_addr = self.pc;
let [hi, lo] = ret_addr.to_be_bytes();
self.stack_push(lo, bus);
self.state = WaitCall(hi, addr);
}
#[inline]
pub fn cmp(&mut self, reg: Reg, rhs: u16) {
let lhs = self.regs.get(reg);
self.flags.zero = lhs == rhs;
self.flags.carry = lhs >= rhs;
}
#[inline]
pub fn dec_indirect(&mut self, bus: &mut Bus) {
if let Some(source) = source_from(self.op_lo)
&& source.is16()
{
let addr = self.regs.get(source);
self.dec_mem(addr, bus);
} else {
self.trap(TRAP_ILLEGAL, bus);
}
}
#[inline]
pub fn dec_mem(&mut self, addr: u16, bus: &mut Bus) {
bus.read_mem(addr);
self.state = WaitDec(addr);
}
#[inline]
pub fn decrement(&mut self, reg: Reg) {
let value = self.regs.get(reg).wrapping_sub(1);
self.flags.zero = self.regs.set(reg, value) == 0;
}
#[inline]
pub fn fetch_and_advance(&mut self, bus: &mut Bus) {
bus.read_mem(self.pc);
self.pc = self.pc.wrapping_add(1);
}
#[inline]
pub fn halt(&mut self) {
self.halt = true;
}
#[inline]
pub fn inc_indirect(&mut self, bus: &mut Bus) {
if let Some(source) = source_from(self.op_lo)
&& source.is16()
{
let addr = self.regs.get(source);
self.inc_mem(addr, bus);
} else {
self.trap(TRAP_ILLEGAL, bus);
}
}
#[inline]
pub fn inc_mem(&mut self, addr: u16, bus: &mut Bus) {
bus.read_mem(addr);
self.state = WaitInc(addr);
}
#[inline]
pub fn increment(&mut self, reg: Reg) {
let value = self.regs.get(reg).wrapping_add(1);
self.flags.zero = self.regs.set(reg, value) == 0;
}
#[inline]
pub fn ld_reg_indirect(&mut self, bus: &mut Bus) {
if let Some((source, target)) = source_and_target_from(self.op_lo) {
bus.read_mem(self.regs.get(source));
self.state = WaitLoad(target);
} else {
self.trap(TRAP_ILLEGAL, bus);
}
}
#[inline]
pub fn ld_reg_reg(&mut self, bus: &mut Bus) {
if let Some((source, target)) = source_and_target_from(self.op_lo)
&& source.is16() == target.is16()
{
self.regs.set(target, self.regs.get(source));
} else {
self.trap(TRAP_ILLEGAL, bus);
}
}
#[inline]
#[must_use]
pub fn op(&self) -> u16 {
u16::from_be_bytes([self.op_hi, self.op_lo])
}
#[inline]
pub fn pop(&mut self, reg: Reg, bus: &mut Bus) {
self.stack_pop(bus);
if reg.is16() {
self.state = WaitStackHi(reg);
} else {
self.op_hi = 0;
self.state = WaitLoad(reg);
}
}
#[expect(clippy::cast_possible_truncation, reason = "truncation is correct")]
#[inline]
pub fn push(&mut self, reg: Reg, bus: &mut Bus) {
let val = self.regs.get(reg);
if reg.is16() {
let [hi, lo] = val.to_be_bytes();
self.stack_push(lo, bus);
self.state = WaitPush(hi);
} else {
self.stack_push(val as u8, bus);
}
}
#[inline]
pub fn reset(&mut self, bus: &mut Bus) {
*self = Self::default();
bus.read_mem(VEC_RESET);
self.state = WaitResetLo;
}
#[inline]
pub fn ret(&mut self, bus: &mut Bus) {
self.stack_pop(bus);
self.state = WaitRetHi;
}
#[inline]
pub fn rti(&mut self, bus: &mut Bus) {
let mut addr = self.regs.get(Reg::SP);
addr = addr.wrapping_add(2); bus.read_mem(addr);
self.regs.set(Reg::SP, addr);
self.state = WaitRetHi;
}
#[inline]
pub fn stack_pop(&mut self, bus: &mut Bus) {
let mut addr = self.regs.get(Reg::SP);
addr = addr.wrapping_add(1);
bus.read_mem(addr);
self.regs.set(Reg::SP, addr);
}
#[inline]
pub fn stack_push(&mut self, val: u8, bus: &mut Bus) {
let mut addr = self.regs.get(Reg::SP);
bus.write_mem(addr, val);
addr = addr.wrapping_sub(1);
self.regs.set(Reg::SP, addr);
}
#[expect(clippy::cast_possible_truncation, reason = "truncation is correct")]
#[inline]
pub fn store_reg_direct(&mut self, reg: Reg, bus: &mut Bus) {
bus.write_mem(self.op(), self.regs.get(reg) as u8);
}
#[expect(clippy::cast_possible_truncation, reason = "truncation is correct")]
#[inline]
pub fn store_reg_indirect(&mut self, bus: &mut Bus) {
if let Some((source, target)) = source_and_target_from(self.op_lo) {
bus.write_mem(self.regs.get(target), self.regs.get(source) as u8);
} else {
self.trap(TRAP_ILLEGAL, bus);
}
}
#[expect(clippy::cast_possible_truncation, reason = "truncation is correct")]
#[inline]
pub fn trap(&mut self, mut trap_code: u8, bus: &mut Bus) {
if trap_code == 0x20 {
print!("{}", self.regs.get(Reg::A) as u8 as char);
}
if trap_code >= 0x40 {
trap_code = TRAP_ILLEGAL;
}
let ret_addr = self.pc;
let [hi, lo] = ret_addr.to_be_bytes();
self.stack_push(lo, bus);
self.state = WaitTrapLo(hi, trap_code);
}
}
#[non_exhaustive]
#[derive(Debug, Default)]
pub struct Flags {
pub carry: bool,
pub zero: bool,
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum State {
Decode,
Execute,
#[default]
FetchOpcode,
ReadAddrHi,
ReadDec(u16),
ReadInc(u16),
ReadLoad(Reg),
ReadOp,
ReadOpHi,
ReadOpLo,
ReadResetLo,
ReadRetHi,
ReadRetLo,
ReadStackHi(Reg),
ReadTrapVecLo(u16),
WaitAddrHi,
WaitCall(u8, u16),
WaitDec(u16),
WaitInc(u16),
WaitLoad(Reg),
WaitOp,
WaitOpHi,
WaitOpLo,
WaitOpcode,
WaitPush(u8),
WaitResetLo,
WaitRetHi,
WaitRetLo,
WaitStackHi(Reg),
WaitTrapCode(u8),
WaitTrapHi(u8),
WaitTrapLo(u8, u8),
WaitTrapVecLo(u16),
}
impl Display for State {
#[inline]
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
write!(
f,
"{}",
match *self {
Decode => "DCOD",
Execute => "EXEC",
FetchOpcode => "FOPC",
ReadAddrHi => "RDAH",
ReadDec(_) => "RDEC",
ReadInc(_) => "RINC",
ReadLoad(_) => "RDLD",
ReadOp => "RDOP",
ReadOpHi => "ROPH",
ReadOpLo => "ROPL",
ReadResetLo => "RRSL",
ReadRetHi => "RRTH",
ReadRetLo => "RRTL",
ReadStackHi(_) => "RSTH",
ReadTrapVecLo(_) => "RTVL",
WaitCall(_, _) => "WCAL",
WaitDec(_) => "WDEC",
WaitInc(_) => "WINC",
WaitLoad(_) => "WTLD",
WaitOp => "WTOP",
WaitOpHi => "WOPH",
WaitOpLo => "WOPL",
WaitOpcode => "WOPC",
WaitPush(_) => "WPSH",
WaitAddrHi => "WTAH",
WaitResetLo => "WRSL",
WaitRetHi => "WRTH",
WaitRetLo => "WRTL",
WaitStackHi(_) => "WSTH",
WaitTrapCode(_) => "WTTC",
WaitTrapHi(_) => "WTTH",
WaitTrapLo(_, _) => "WTTL",
WaitTrapVecLo(_) => "WTVL",
}
)
}
}
#[cfg(test)]
#[expect(clippy::unwrap_used, reason = "test")]
mod tests {
use crate::{
asm::{as_hex, assemble},
instructions::InstructionKind::Halt,
regs::Reg::*,
system::System,
};
use super::*;
#[test]
fn cpu_states_are_correct_for_1_byte_instruction() {
let mut sys = System::default();
let source = "
nop
halt";
sys.mem.load(0x0100, &assemble(source)).unwrap();
sys.cpu.pc = 0x0100;
assert_eq!(sys.cpu.state, FetchOpcode);
assert_eq!(sys.cpu.pc, 0x0100);
sys.tick();
assert_eq!(sys.cpu.state, WaitOpcode);
assert_eq!(sys.cpu.pc, 0x0101);
sys.tick();
assert_eq!(sys.cpu.state, Decode);
assert_eq!(sys.cpu.pc, 0x0101);
sys.tick();
assert_eq!(sys.cpu.state, Execute);
assert_eq!(sys.cpu.pc, 0x0101);
sys.tick();
assert_eq!(sys.cpu.state, FetchOpcode);
assert_eq!(sys.cpu.pc, 0x0101);
}
#[test]
fn cpu_states_are_correct_for_2_byte_instruction() {
let mut sys = System::default();
let source = "
ld a, 0xFF
halt";
sys.mem.load(0x0100, &assemble(source)).unwrap();
sys.cpu.pc = 0x0100;
assert_eq!(sys.cpu.state, FetchOpcode);
sys.tick();
assert_eq!(sys.cpu.pc, 0x0101);
assert_eq!(sys.cpu.state, WaitOpcode);
sys.tick();
assert_eq!(sys.cpu.state, Decode);
sys.tick();
assert_eq!(sys.cpu.state, WaitOp);
assert_eq!(sys.cpu.pc, 0x0102);
sys.tick();
assert_eq!(sys.cpu.state, ReadOp);
sys.tick();
assert_eq!(sys.cpu.state, Execute);
sys.tick();
assert_eq!(sys.cpu.regs.get(A), 0x00FF);
assert_eq!(sys.cpu.pc, 0x0102);
}
#[test]
fn cpu_states_are_correct_for_3_byte_instruction() {
let mut sys = System::default();
let source = "
ld ab, 0xBEEF
halt";
sys.mem.load(0x0100, &assemble(source)).unwrap();
sys.cpu.pc = 0x0100;
assert_eq!(sys.cpu.state, FetchOpcode);
sys.tick();
assert_eq!(sys.cpu.state, WaitOpcode);
sys.tick();
assert_eq!(sys.cpu.state, Decode);
assert_eq!(sys.cpu.pc, 0x0101);
sys.tick();
assert_eq!(sys.cpu.state, WaitOpLo);
sys.tick();
assert_eq!(sys.cpu.state, ReadOpLo);
assert_eq!(sys.cpu.pc, 0x0102);
sys.tick();
assert_eq!(sys.cpu.state, WaitOpHi);
sys.tick();
assert_eq!(sys.cpu.state, ReadOpHi);
assert_eq!(sys.cpu.pc, 0x0103);
sys.tick();
assert_eq!(sys.cpu.state, Execute);
sys.tick();
assert_eq!(sys.cpu.regs.get(AB), 0xBEEF);
assert_eq!(sys.cpu.pc, 0x0103);
}
#[test]
fn cpu_states_are_correct_for_mem_read_instruction() {
let mut sys = System::default();
let source = "
ld b, (cd)
halt";
sys.mem.set(0x0110, 0xFF);
sys.cpu.regs.set(Reg::CD, 0x0110);
sys.mem.load(0x0100, &assemble(source)).unwrap();
sys.cpu.pc = 0x0100;
assert_eq!(sys.cpu.state, FetchOpcode);
sys.tick();
assert_eq!(sys.cpu.state, WaitOpcode);
assert_eq!(sys.cpu.pc, 0x0101);
sys.tick();
assert_eq!(sys.cpu.state, Decode);
sys.tick();
assert_eq!(sys.cpu.state, WaitOp);
sys.tick();
assert_eq!(sys.cpu.state, ReadOp);
sys.tick();
assert_eq!(sys.cpu.state, Execute);
sys.tick();
assert_eq!(sys.cpu.pc, 0x0102);
assert_eq!(sys.cpu.state, WaitLoad(B));
sys.tick();
assert_eq!(sys.cpu.state, ReadLoad(B));
sys.tick();
assert_eq!(sys.cpu.regs.get(B), 0x00FF);
assert_eq!(sys.cpu.pc, 0x0102);
assert_eq!(sys.cpu.state, FetchOpcode);
}
#[test]
fn cpu_states_are_correct_for_mem_write_instruction() {
let mut sys = System::default();
let source = "
ld 0xBEEF, a
halt";
sys.mem.load(0x0100, &assemble(source)).unwrap();
sys.cpu.pc = 0x0100;
sys.cpu.regs.set(A, 0xFF);
assert_eq!(sys.cpu.state, FetchOpcode);
sys.tick();
assert_eq!(sys.cpu.state, WaitOpcode);
sys.tick();
assert_eq!(sys.cpu.state, Decode);
sys.tick();
assert_eq!(sys.cpu.state, WaitOpLo);
assert_eq!(sys.cpu.pc, 0x0102);
sys.tick();
assert_eq!(sys.cpu.state, ReadOpLo);
sys.tick();
assert_eq!(sys.cpu.state, WaitOpHi);
sys.tick();
assert_eq!(sys.cpu.state, ReadOpHi);
assert_eq!(sys.cpu.pc, 0x0103);
sys.tick();
assert_eq!(sys.cpu.state, Execute);
sys.tick();
assert_eq!(sys.cpu.state, FetchOpcode);
sys.tick();
assert_eq!(sys.cpu.state, WaitOpcode);
assert_eq!(sys.cpu.pc, 0x0104);
}
#[test]
fn cpu_states_are_correct_for_16_bit_pop_instruction() {
let mut sys = System::default();
let source = "
pop cd
halt";
sys.mem.load(0xBFFE, &[0xBA, 0xBE]).unwrap();
sys.cpu.regs.set(SP, 0xBFFD);
sys.mem.load(0x0100, &assemble(source)).unwrap();
sys.cpu.pc = 0x0100;
assert_eq!(sys.cpu.state, FetchOpcode);
sys.tick();
assert_eq!(sys.cpu.state, WaitOpcode);
sys.tick();
assert_eq!(sys.cpu.state, Decode);
sys.tick();
assert_eq!(sys.cpu.state, Execute);
sys.tick();
assert_eq!(sys.cpu.state, WaitStackHi(CD));
sys.tick();
assert_eq!(sys.cpu.state, ReadStackHi(CD));
sys.tick();
assert_eq!(sys.cpu.state, WaitLoad(CD));
sys.tick();
assert_eq!(sys.cpu.state, ReadLoad(CD));
sys.tick();
assert_eq!(sys.cpu.state, FetchOpcode);
}
#[test]
fn cpu_states_are_correct_for_16_bit_push_instruction() {
let mut sys = System::default();
let source = "
push ab
halt";
sys.cpu.regs.set(SP, 0xBFFF);
sys.cpu.regs.set(AB, 0xCAFE);
sys.mem.load(0x0100, &assemble(source)).unwrap();
sys.cpu.pc = 0x0100;
assert_eq!(sys.cpu.state, FetchOpcode);
sys.tick();
assert_eq!(sys.cpu.state, WaitOpcode);
sys.tick();
assert_eq!(sys.cpu.state, Decode);
sys.tick();
assert_eq!(sys.cpu.state, Execute);
sys.tick();
assert_eq!(sys.cpu.state, WaitPush(0xCA));
sys.tick();
assert_eq!(sys.cpu.state, FetchOpcode);
}
#[test]
fn cpu_traps_for_various_illegal_programs() {
let mut sys = System::default();
sys.mem.load(0x0000, &[0x02, 0x00, u8::from(Halt)]).unwrap();
let cases: &[&[u8]] = &[
&[0x01, 0xFF], &[0x2D, 0xFF], &[0x2E, 0xFF], &[0x2F, 0x08], &[0x3D, 0xFF], &[0x4D, 0xFF], &[0xF9, 0x40], ];
for prog in cases {
sys.cpu.regs.set(SP, 0xBFFF);
sys.mem.load(0xBFFD, &[0xFF, 0xFF, 0xFF]).unwrap();
sys.trace_program(prog).unwrap();
assert_eq!(
sys.mem.get(0xBFFD),
TRAP_ILLEGAL,
"{}: wrong trap code",
as_hex(prog)
);
assert_eq!(
sys.mem.get(0xBFFE),
0x01,
"{}: wrong return address high byte",
as_hex(prog)
);
assert_eq!(
sys.mem.get(0xBFFF),
u8::try_from(prog.len()).unwrap(),
"{}: wrong return address low byte",
as_hex(prog)
);
}
}
#[expect(clippy::bool_assert_comparison, reason = "clarity")]
#[test]
fn reset_resets_cpu() {
let mut sys = System::default();
sys.cpu.regs.set(AB, 0xBEEF);
sys.cpu.regs.set(SP, 0xFFFD);
sys.cpu.pc = 0x0000;
sys.cpu.flags.carry = true;
sys.cpu.flags.zero = true;
sys.cpu.reset(&mut sys.bus);
assert_eq!(sys.cpu.state, WaitResetLo);
sys.tick();
assert_eq!(sys.cpu.state, ReadResetLo);
sys.tick();
assert_eq!(sys.cpu.state, WaitAddrHi);
sys.tick();
assert_eq!(sys.cpu.state, ReadAddrHi);
sys.tick();
assert_eq!(sys.cpu.state, FetchOpcode);
assert_eq!(sys.cpu.regs.get(AB), 0x0000, "AB not reset");
assert_eq!(sys.cpu.regs.get(SP), 0x0000, "SP not reset");
assert_eq!(sys.cpu.pc, 0xC000, "PC not initialized from reset vector");
assert_eq!(sys.cpu.flags.carry, false, "carry not reset");
assert_eq!(sys.cpu.flags.zero, false, "zero not reset");
}
}