pub mod disasm;
mod exec;
pub mod isa;
#[cfg(test)]
mod tests;
#[cfg(all(test, feature = "std"))]
mod conformance;
#[cfg(all(test, feature = "std"))]
mod wozmon;
use alloc::boxed::Box;
use alloc::string::String;
use alloc::sync::Arc;
use alloc::vec::Vec;
use core::fmt;
use crate::core::device::{
CycleGate, Device, DeviceClass, ExportId, Initiator, PropertySpec, RealizeCtx, ResetKind,
SinkPin,
};
use crate::core::error::{Error, Result};
use crate::core::props::{Props, ValueKind};
use crate::core::registry::Registry;
use crate::core::sched::{Budget, Consumed, TickCursor};
use crate::core::space::{AddressSpace, MemAttrs, RequesterId};
use crate::core::state::{ChunkReader, ChunkWriter, Sink, Source};
use crate::core::sync::{self, AtomicBool, AtomicU32, LockRank, Ordering};
use crate::core::value::Width;
use crate::core::wire::{FanIn, Level, Resolve, WireId, WireSink};
use exec::{Exec, State};
pub use isa::Variant;
pub mod flags {
pub const C: u8 = 0x01;
pub const Z: u8 = 0x02;
pub const I: u8 = 0x04;
pub const D: u8 = 0x08;
pub const B: u8 = 0x10;
pub const U: u8 = 0x20;
pub const V: u8 = 0x40;
pub const N: u8 = 0x80;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct Regs {
pub a: u8,
pub x: u8,
pub y: u8,
pub s: u8,
pub p: u8,
pub pc: u16,
}
impl Regs {
#[must_use]
pub const fn new() -> Regs {
Regs {
a: 0,
x: 0,
y: 0,
s: 0,
p: flags::U,
pc: 0,
}
}
#[inline]
#[must_use]
pub const fn flag(&self, mask: u8) -> bool {
self.p & mask != 0
}
}
impl fmt::Display for Regs {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"A:{:02x} X:{:02x} Y:{:02x} P:{:02x} SP:{:02x} PC:{:04x}",
self.a, self.x, self.y, self.p, self.s, self.pc
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Reg {
A,
X,
Y,
S,
P,
Pc,
}
impl Reg {
pub const ALL: &'static [Reg] = &[Reg::A, Reg::X, Reg::Y, Reg::S, Reg::P, Reg::Pc];
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Reg::A => "a",
Reg::X => "x",
Reg::Y => "y",
Reg::S => "s",
Reg::P => "p",
Reg::Pc => "pc",
}
}
#[must_use]
pub const fn width(self) -> Width {
match self {
Reg::Pc => Width::U16,
_ => Width::U8,
}
}
#[must_use]
pub const fn get(self, regs: &Regs) -> u16 {
match self {
Reg::A => regs.a as u16,
Reg::X => regs.x as u16,
Reg::Y => regs.y as u16,
Reg::S => regs.s as u16,
Reg::P => regs.p as u16,
Reg::Pc => regs.pc,
}
}
pub const fn set(self, regs: &mut Regs, value: u16) {
match self {
Reg::A => regs.a = value as u8,
Reg::X => regs.x = value as u8,
Reg::Y => regs.y = value as u8,
Reg::S => regs.s = value as u8,
Reg::P => regs.p = value as u8,
Reg::Pc => regs.pc = value,
}
}
#[must_use]
pub fn from_name(name: &str) -> Option<Reg> {
Reg::ALL.iter().copied().find(|r| r.name() == name)
}
}
impl fmt::Display for Reg {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Interrupt {
Irq,
Nmi,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Config {
pub variant: Variant,
pub decimal: bool,
pub magic: u8,
pub requester: RequesterId,
}
impl Config {
pub const NMOS_6502: Config = Config {
variant: Variant::Nmos6502,
decimal: true,
magic: 0xee,
requester: RequesterId::ANONYMOUS,
};
pub const RP2A03: Config = Config {
variant: Variant::Ricoh2A03,
decimal: false,
..Config::NMOS_6502
};
pub const W65C02S: Config = Config {
variant: Variant::Wdc65C02,
..Config::NMOS_6502
};
#[must_use]
pub const fn with_variant(mut self, variant: Variant) -> Self {
self.variant = variant;
self
}
#[must_use]
pub const fn with_requester(mut self, id: RequesterId) -> Self {
self.requester = id;
self
}
#[must_use]
pub const fn with_magic(mut self, magic: u8) -> Self {
self.magic = magic;
self
}
}
impl Default for Config {
fn default() -> Self {
Config::NMOS_6502
}
}
#[derive(Debug, Default)]
pub(crate) struct Lines {
irq: AtomicBool,
nmi_level: AtomicBool,
nmi_sampled: AtomicBool,
nmi_latch: AtomicBool,
reset_req: AtomicBool,
}
impl Lines {
fn set_irq(&self, asserted: bool) {
self.irq.store(asserted, Ordering::Release);
}
fn irq_asserted(&self) -> bool {
self.irq.load(Ordering::Acquire)
}
fn set_nmi(&self, asserted: bool) {
self.nmi_level.store(asserted, Ordering::Release);
}
fn sample_nmi(&self) {
let now = self.nmi_level.load(Ordering::Acquire);
let previous = self.nmi_sampled.swap(now, Ordering::AcqRel);
if now && !previous {
self.nmi_latch.store(true, Ordering::Release);
}
}
fn nmi_pending(&self) -> bool {
self.nmi_latch.load(Ordering::Acquire)
}
fn take_nmi_pending(&self) -> bool {
self.nmi_latch.swap(false, Ordering::AcqRel)
}
fn clear_nmi_latch(&self) {
self.nmi_latch.store(false, Ordering::Release);
}
fn request_reset(&self) {
self.reset_req.store(true, Ordering::Release);
}
fn take_reset_request(&self) -> bool {
self.reset_req.swap(false, Ordering::AcqRel)
}
fn snapshot(&self) -> (bool, bool, bool, bool) {
(
self.irq_asserted(),
self.nmi_level.load(Ordering::Acquire),
self.nmi_pending(),
self.reset_req.load(Ordering::Acquire),
)
}
fn restore(&self, (irq, level, latch, reset): (bool, bool, bool, bool)) {
self.irq.store(irq, Ordering::Release);
self.nmi_level.store(level, Ordering::Release);
self.nmi_sampled.store(level, Ordering::Release);
self.nmi_latch.store(latch, Ordering::Release);
self.reset_req.store(reset, Ordering::Release);
}
}
#[derive(Debug)]
struct Session {
state: State,
space: Option<Arc<AddressSpace>>,
}
#[derive(Debug)]
pub struct Mos6502 {
cfg: Config,
lines: Arc<Lines>,
session: sync::Mutex<Session>,
requester: AtomicU32,
pins: sync::Mutex<Pins>,
links: sync::Mutex<CoreLinks>,
}
#[derive(Debug, Default, Clone)]
struct CoreLinks {
cursor: Option<TickCursor>,
rdy: Option<Arc<dyn CycleGate>>,
rdy_link: Option<String>,
}
#[derive(Debug, Default)]
struct Pins {
irq: Option<Arc<InterruptPin>>,
nmi: Option<Arc<InterruptPin>>,
reset: Option<Arc<ResetPin>>,
}
impl Mos6502 {
#[must_use]
pub fn new(cfg: Config) -> Mos6502 {
Mos6502 {
cfg,
lines: Arc::new(Lines::default()),
session: sync::Mutex::with_rank(
LockRank::BUS,
Session {
state: State::new(),
space: None,
},
),
requester: AtomicU32::new(cfg.requester.0),
pins: sync::Mutex::new(Pins::default()),
links: sync::Mutex::new(CoreLinks::default()),
}
}
fn effective_config(&self) -> Config {
Config {
requester: RequesterId(self.requester.load(Ordering::Relaxed)),
..self.cfg
}
}
pub fn set_requester(&self, id: RequesterId) {
self.requester.store(id.0, Ordering::Relaxed);
}
pub fn from_props(props: &Props) -> Result<Mos6502> {
let mut r = props.reader();
let variant = r.or_enum("variant", "6502", &["6502", "2a03", "65c02"])?;
let variant = Variant::from_name(variant).expect("the enum listed above");
let decimal = r.or("decimal", variant != Variant::Ricoh2A03)?;
let magic = r.or_range("magic", 0xeeu64, 0..=0xff)?;
let _ = r.or_enum("engine", "interp", &["interp"])?;
let rdy = r.optional_link("rdy")?.map(|l| String::from(l.as_str()));
r.finish()?;
let cpu = Mos6502::new(Config {
variant,
decimal,
magic: magic as u8,
requester: RequesterId::ANONYMOUS,
});
cpu.links.lock().rdy_link = rdy;
Ok(cpu)
}
#[must_use]
pub fn config(&self) -> Config {
self.cfg
}
pub fn attach_space(&self, space: Arc<AddressSpace>) {
self.session.lock().space = Some(space);
}
#[must_use]
pub fn space(&self) -> Option<Arc<AddressSpace>> {
self.session.lock().space.clone()
}
#[must_use]
pub fn regs(&self) -> Regs {
self.session.lock().state.regs
}
pub fn set_regs(&self, regs: Regs) {
self.session.lock().state.regs = regs;
}
#[must_use]
pub fn reg(&self, reg: Reg) -> u16 {
reg.get(&self.session.lock().state.regs)
}
pub fn set_reg(&self, reg: Reg, value: u16) {
reg.set(&mut self.session.lock().state.regs, value);
}
#[must_use]
pub fn cycles(&self) -> u64 {
self.session.lock().state.cycles
}
#[must_use]
pub fn is_halted(&self) -> bool {
self.session.lock().state.halted
}
#[must_use]
pub fn is_waiting(&self) -> bool {
self.session.lock().state.waiting
}
#[must_use]
pub fn reset_pending(&self) -> bool {
self.session.lock().state.reset_pending
}
#[must_use]
pub fn bus_faults(&self) -> (u64, u16) {
let s = self.session.lock();
(s.state.faults, s.state.last_fault)
}
pub fn set_irq(&self, asserted: bool) {
self.lines.set_irq(asserted);
}
#[must_use]
pub fn irq_asserted(&self) -> bool {
self.lines.irq_asserted()
}
pub fn set_nmi(&self, asserted: bool) {
self.lines.set_nmi(asserted);
}
pub fn pulse_nmi(&self) {
self.lines.set_nmi(true);
self.lines.sample_nmi();
self.lines.set_nmi(false);
}
#[must_use]
pub fn nmi_pending(&self) -> bool {
self.lines.nmi_pending()
}
#[must_use]
pub fn pending_interrupt(&self) -> Option<Interrupt> {
self.session.lock().state.pending
}
pub fn request_reset(&self) {
self.session.lock().state.reset_pending = true;
}
pub fn step(&self) -> u64 {
let cfg = self.effective_config();
let links = self.links.lock().clone();
let mut session = self.session.lock();
let Session { state, space } = &mut *session;
if self.lines.take_reset_request() {
state.reset_pending = true;
state.halted = false;
state.waiting = false;
state.pending = None;
}
let Some(space) = space.clone() else {
return 0;
};
Exec::new(state, &space, &cfg, &self.lines)
.with_cursor(links.cursor.as_ref())
.with_rdy(links.rdy.as_deref())
.step()
}
pub fn run(&self, budget: u64) -> u64 {
let mut used = 0;
while used < budget {
let n = self.step();
if n == 0 {
break;
}
used += n;
}
used
}
pub fn run_budget(&self, ticks: u64) -> u64 {
let owed = self.session.lock().state.debt;
if owed >= ticks {
self.session.lock().state.debt = owed - ticks;
return ticks;
}
let allowance = ticks - owed;
let mut used = 0u64;
while used < allowance {
let n = self.step();
if n == 0 {
break;
}
used += n;
}
if used >= allowance {
self.session.lock().state.debt = used - allowance;
ticks
} else {
self.session.lock().state.debt = 0;
owed + used
}
}
pub fn attach_cursor(&self, cursor: TickCursor) {
self.links.lock().cursor = Some(cursor);
}
pub fn attach_rdy(&self, gate: Arc<dyn CycleGate>) {
self.links.lock().rdy = Some(gate);
}
#[must_use]
pub fn cycle_debt(&self) -> u64 {
self.session.lock().state.debt
}
#[must_use]
pub fn disassemble(&self, pc: u16, count: usize) -> Vec<disasm::Disassembled> {
let Some(space) = self.space() else {
return Vec::new();
};
disasm::disassemble_run_as(self.cfg.variant, pc, count, |addr| {
space
.read(u64::from(addr), Width::U8, MemAttrs::DEBUG)
.ok()
.map(|v| v as u8)
})
}
}
pub static CLASS: DeviceClass = DeviceClass {
name: "cpu.mos6502",
version: 4,
summary: "MOS 6502 / RP2A03 / W65C02S 8-bit CPU core, cycle-accurate interpreter",
properties: &[
PropertySpec {
name: "variant",
kind: ValueKind::Str,
required: false,
summary: "which part: `6502` (default), `2a03` for the NES, or `65c02`",
},
PropertySpec {
name: "decimal",
kind: ValueKind::Bool,
required: false,
summary: "whether the part has decimal mode; false for the NES's RP2A03",
},
PropertySpec {
name: "magic",
kind: ValueKind::Uint,
required: false,
summary: "the analog constant ANE and LXA OR into the accumulator (default 0xee)",
},
PropertySpec {
name: "engine",
kind: ValueKind::Str,
required: false,
summary: "which execution engine; only `interp` exists until phase 5",
},
PropertySpec {
name: "rdy",
kind: ValueKind::Link,
required: false,
summary: "the object that drives /RDY: a DMA unit that halts this core",
},
],
construct: |props| Ok(Box::new(Mos6502::from_props(props)?)),
};
pub fn register(reg: &mut Registry) -> Result<()> {
reg.add(&CLASS)
}
impl Device for Mos6502 {
fn class(&self) -> &'static DeviceClass {
&CLASS
}
fn realize(&self, _ctx: &mut RealizeCtx<'_>) -> Result<()> {
Ok(())
}
fn sink(&self, port: &str, sources: &[WireId]) -> Option<SinkPin> {
let mut pins = self.pins.lock();
let sink: Arc<dyn WireSink> = match port {
"irq" => {
let pin = Arc::new(InterruptPin::from_lines(
Arc::clone(&self.lines),
Interrupt::Irq,
sources,
));
pins.irq = Some(Arc::clone(&pin));
pin
}
"nmi" => {
let pin = Arc::new(InterruptPin::from_lines(
Arc::clone(&self.lines),
Interrupt::Nmi,
sources,
));
pins.nmi = Some(Arc::clone(&pin));
pin
}
"reset" => {
let pin = Arc::new(ResetPin::new(Arc::clone(&self.lines), sources));
pins.reset = Some(Arc::clone(&pin));
pin
}
_ => return None,
};
Some(SinkPin { sink, line: 0 })
}
fn is_runnable(&self) -> bool {
true
}
fn attach_cursor(&self, cursor: TickCursor) {
Mos6502::attach_cursor(self, cursor);
}
fn run(&self, budget: Budget) -> Consumed {
Consumed::new(self.run_budget(budget.ticks))
}
fn reset(&self, kind: ResetKind) {
let mut session = self.session.lock();
if kind == ResetKind::Cold {
session.state = State::new();
} else {
session.state.reset_pending = true;
session.state.halted = false;
session.state.waiting = false;
session.state.pending = None;
}
drop(session);
if kind == ResetKind::Cold {
self.lines.restore((false, false, false, false));
} else {
self.lines.clear_nmi_latch();
}
}
fn save(&self, w: &mut ChunkWriter<'_>) -> Result<()> {
let state = self.session.lock().state;
w.write_u8(state.regs.a)?;
w.write_u8(state.regs.x)?;
w.write_u8(state.regs.y)?;
w.write_u8(state.regs.s)?;
w.write_u8(state.regs.p)?;
w.write_u16(state.regs.pc)?;
w.write_u64(state.cycles)?;
w.write_bool(state.halted)?;
w.write_bool(state.reset_pending)?;
w.write_u8(match state.pending {
None => 0,
Some(Interrupt::Irq) => 1,
Some(Interrupt::Nmi) => 2,
})?;
w.write_u8(state.open_bus)?;
w.write_u64(state.faults)?;
w.write_u16(state.last_fault)?;
w.write_u64(state.debt)?;
let (irq, nmi_level, nmi_latch, reset_req) = self.lines.snapshot();
w.write_bool(irq)?;
w.write_bool(nmi_level)?;
w.write_bool(nmi_latch)?;
w.write_bool(reset_req)?;
w.write_bool(state.waiting)?;
w.write_u8(state.core_bus)?;
Ok(())
}
fn load(&self, r: &mut ChunkReader<'_>) -> Result<()> {
let mut state = State::new();
state.regs.a = r.read_u8()?;
state.regs.x = r.read_u8()?;
state.regs.y = r.read_u8()?;
state.regs.s = r.read_u8()?;
state.regs.p = r.read_u8()?;
state.regs.pc = r.read_u16()?;
state.cycles = r.read_u64()?;
state.halted = r.read_bool()?;
state.reset_pending = r.read_bool()?;
state.pending = match r.read_u8()? {
0 => None,
1 => Some(Interrupt::Irq),
2 => Some(Interrupt::Nmi),
other => {
return Err(Error::State(alloc::format!(
"unknown pending interrupt tag {other}"
)));
}
};
state.open_bus = r.read_u8()?;
state.faults = r.read_u64()?;
state.last_fault = r.read_u16()?;
state.debt = r.read_u64()?;
let irq = r.read_bool()?;
let nmi_level = r.read_bool()?;
let nmi_latch = r.read_bool()?;
let reset_req = r.read_bool()?;
state.waiting = r.read_bool()?;
state.core_bus = r.read_u8()?;
self.session.lock().state = state;
self.lines.restore((irq, nmi_level, nmi_latch, reset_req));
Ok(())
}
}
impl Initiator for Mos6502 {
fn requester(&self) -> RequesterId {
RequesterId(self.requester.load(Ordering::Relaxed))
}
}
impl crate::machine::Instance for Mos6502 {
fn bind(&self, ctx: &crate::machine::BindCtx<'_>) -> Result<()> {
let space = ctx.space().ok_or_else(|| Error::Config {
at: alloc::string::String::from(ctx.path()),
message: alloc::string::String::from(
"a 6502 needs an address space to fetch from (`space = cpubus`)",
),
})?;
self.attach_space(Arc::clone(space));
self.set_requester(ctx.requester());
let wanted = self.links.lock().rdy_link.clone();
if let Some(name) = wanted {
self.attach_rdy(ctx.export_gate(&name, ExportId::CYCLE_GATE)?);
}
Ok(())
}
}
pub fn bind(bindings: &mut crate::machine::Bindings) -> Result<()> {
bindings.bind(CLASS.name, |props| {
Ok(Arc::new(Mos6502::from_props(props)?))
})
}
#[must_use]
pub fn schema() -> crate::machine::validate::ClassSchema {
use crate::machine::validate::{ClassSchema, PortDir, PropSchema};
ClassSchema::new(CLASS.name)
.prop(PropSchema::new("variant", ValueKind::Str).values(&["6502", "2a03", "65c02"]))
.prop(PropSchema::new("decimal", ValueKind::Bool))
.prop(PropSchema::new("magic", ValueKind::Uint).range(0, 0xff))
.prop(PropSchema::new("engine", ValueKind::Str).values(&["interp"]))
.prop(PropSchema::new("rdy", ValueKind::Link))
.port("irq", PortDir::In)
.port("nmi", PortDir::In)
.port("reset", PortDir::In)
}
#[derive(Debug)]
pub struct InterruptPin {
lines: Arc<Lines>,
which: Interrupt,
inputs: FanIn,
resolve: Resolve,
}
impl InterruptPin {
#[must_use]
pub fn new(cpu: Arc<Mos6502>, which: Interrupt, sources: &[WireId]) -> InterruptPin {
InterruptPin::from_lines(Arc::clone(&cpu.lines), which, sources)
}
fn from_lines(lines: Arc<Lines>, which: Interrupt, sources: &[WireId]) -> InterruptPin {
InterruptPin {
lines,
which,
inputs: FanIn::new(sources),
resolve: Resolve::Or,
}
}
#[must_use]
pub fn with_resolve(mut self, resolve: Resolve) -> Self {
self.resolve = resolve;
self
}
#[must_use]
pub fn which(&self) -> Interrupt {
self.which
}
#[must_use]
pub fn inputs(&self) -> &FanIn {
&self.inputs
}
}
impl WireSink for InterruptPin {
fn set_level(&self, src: WireId, _line: u32, level: Level) {
self.inputs.set(src, level);
let asserted = self.inputs.resolve(self.resolve).is_high();
match self.which {
Interrupt::Irq => self.lines.set_irq(asserted),
Interrupt::Nmi => self.lines.set_nmi(asserted),
}
}
}
#[derive(Debug)]
pub struct ResetPin {
lines: Arc<Lines>,
inputs: FanIn,
resolve: Resolve,
}
impl ResetPin {
#[must_use]
pub fn new_for(cpu: Arc<Mos6502>, sources: &[WireId]) -> ResetPin {
ResetPin::new(Arc::clone(&cpu.lines), sources)
}
fn new(lines: Arc<Lines>, sources: &[WireId]) -> ResetPin {
ResetPin {
lines,
inputs: FanIn::new(sources),
resolve: Resolve::Or,
}
}
#[must_use]
pub fn inputs(&self) -> &FanIn {
&self.inputs
}
}
impl WireSink for ResetPin {
fn set_level(&self, src: WireId, _line: u32, level: Level) {
self.inputs.set(src, level);
if self.inputs.resolve(self.resolve).is_high() {
self.lines.request_reset();
}
}
}
#[must_use]
pub fn describe_isa() -> String {
describe_isa_for(Variant::Nmos6502)
}
#[must_use]
pub fn describe_isa_for(variant: Variant) -> String {
use core::fmt::Write as _;
let mut out = String::new();
for opcode in 0..=255u8 {
let insn = isa::decode_as(variant, opcode);
let mark = match insn.class {
isa::Class::Documented => ' ',
isa::Class::Undocumented => '*',
isa::Class::Unstable => '!',
};
let _ = writeln!(
out,
"{opcode:02x} {mark}{:<4} {:<6} {}",
insn.op.mnemonic(),
insn.mode.name(),
insn.op.summary()
);
}
out
}