use alloc::collections::VecDeque;
use alloc::string::String;
use alloc::sync::Arc;
use alloc::vec::Vec;
use std::path::PathBuf;
use crate::core::space::{
AccessConstraints, AddressSpace, MemAttrs, MemOps, MemResult, Region, UnassignedPolicy,
};
use crate::core::sync::{self, LockRank};
use super::{Config, Mos6502, Variant};
const ROM_BASE: u16 = 0x8000;
const RAM_TOP: u16 = 0x3fff;
const ACIA_BASE: u16 = 0x5000;
const ACIA_TOP: u16 = 0x5003;
const RX_FULL: u8 = 0x08;
const TX_EMPTY: u8 = 0x10;
#[derive(Debug)]
struct Board(sync::Mutex<Inner>);
#[derive(Debug)]
struct Inner {
ram: Vec<u8>,
rom: Vec<u8>,
rx: VecDeque<u8>,
tx: Vec<u8>,
}
impl Board {
fn new(rom: Vec<u8>) -> Board {
assert_eq!(rom.len(), 0x8000, "the image is a 32 KiB ROM");
Board(sync::Mutex::with_rank(
LockRank::DEVICE,
Inner {
ram: alloc::vec![0; usize::from(RAM_TOP) + 1],
rom,
rx: VecDeque::new(),
tx: Vec::new(),
},
))
}
fn send(&self, text: &str) {
let mut inner = self.0.lock();
inner.rx.extend(text.as_bytes());
}
fn output(&self) -> String {
String::from_utf8_lossy(&self.0.lock().tx).into_owned()
}
fn printed(&self) -> usize {
self.0.lock().tx.len()
}
}
impl MemOps for Board {
fn read(&self, offset: u64, dst: &mut [u8], attrs: MemAttrs) -> MemResult {
let mut inner = self.0.lock();
for (i, slot) in dst.iter_mut().enumerate() {
let addr = (offset as u16).wrapping_add(i as u16);
*slot = match addr {
0..=RAM_TOP => inner.ram[usize::from(addr)],
ACIA_BASE => {
if attrs.debug {
inner.rx.front().copied().unwrap_or(0)
} else {
inner.rx.pop_front().unwrap_or(0)
}
}
0x5001 => {
let ready = if inner.rx.is_empty() { 0 } else { RX_FULL };
TX_EMPTY | ready
}
0x5002 | 0x5003 => 0,
ROM_BASE.. => inner.rom[usize::from(addr - ROM_BASE)],
_ => return Err(crate::core::error::BusError::Unassigned),
};
}
Ok(())
}
fn write(&self, offset: u64, src: &[u8], attrs: MemAttrs) -> MemResult {
let mut inner = self.0.lock();
for (i, byte) in src.iter().enumerate() {
let addr = (offset as u16).wrapping_add(i as u16);
match addr {
0..=RAM_TOP => inner.ram[usize::from(addr)] = *byte,
ACIA_BASE => {
if !attrs.debug {
inner.tx.push(*byte);
}
}
0x5001..=ACIA_TOP => {}
ROM_BASE.. => {}
_ => return Err(crate::core::error::BusError::Unassigned),
}
}
Ok(())
}
fn constraints(&self) -> AccessConstraints {
AccessConstraints::ANY
}
}
fn board(variant: Variant, rom: Vec<u8>) -> (Arc<Mos6502>, Arc<Board>) {
let board = Arc::new(Board::new(rom));
let space = AddressSpace::new("cpu", 16).with_unassigned(UnassignedPolicy::FAULT);
space
.topology()
.map(Region::io("board", 0x1_0000, board.clone()), 0)
.expect("64 KiB fits in a 16-bit space");
let cpu = Arc::new(Mos6502::new(Config::NMOS_6502.with_variant(variant)));
cpu.attach_space(Arc::new(space));
(cpu, board)
}
fn run_until(cpu: &Mos6502, budget: u64, mut done: impl FnMut() -> bool) -> bool {
let mut spent = 0;
while spent < budget {
if done() {
return true;
}
let n = cpu.step();
if n == 0 {
break;
}
spent += n;
}
done()
}
fn rom() -> Option<Vec<u8>> {
let root = match std::env::var("RSEMU_TESTDATA") {
Ok(dir) => PathBuf::from(dir),
Err(_) => PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("testdata"),
};
let path = root.join("wozmon/wozmon.bin");
match std::fs::read(&path) {
Ok(bytes) if bytes.len() == 0x8000 => Some(bytes),
Ok(bytes) => panic!(
"{}: expected a 32 KiB image, got {} bytes",
path.display(),
bytes.len()
),
Err(_) => {
println!(
"wozmon: {} is absent; fetch it with \
`scripts/fetch-testdata.sh wozmon --wozmon-url ...` \
(Ben Eater, CC-BY, https://eater.net/6502) to run this test",
path.display()
);
None
}
}
}
#[test]
fn wozmon_reaches_its_prompt_and_dumps_memory_on_a_65c02() {
let Some(image) = rom() else { return };
let (cpu, board) = board(Variant::Wdc65C02, image);
assert!(
run_until(&cpu, 5_000_000, || board.printed() >= 2),
"the monitor never printed its banner; output so far {:?}",
board.output()
);
assert_eq!(
cpu.reg(super::Reg::Pc) & 0xff00,
0xff00,
"running in the ROM"
);
assert_eq!(board.output(), "\\\r", "the Woz Monitor banner");
board.send("FF00\r");
assert!(
run_until(&cpu, 5_000_000, || board.output().contains("FF00: A9")),
"no dump came back; output was {:?}",
board.output()
);
let out = board.output();
assert!(
out.starts_with("\\\rFF00\r"),
"the input was echoed: {out:?}"
);
assert!(out.ends_with("FF00: A9"), "and answered: {out:?}");
assert_eq!(
cpu.bus_faults().0,
0,
"the ROM stayed inside the memory map"
);
board.send("FFFA.FFFF\r");
let want = "FFFA: 00 0F 00 FF 00 00";
assert!(
run_until(&cpu, 5_000_000, || board.output().contains(want)),
"the vector table came back wrong; output was {:?}",
board.output()
);
}
#[test]
fn the_same_rom_hangs_on_an_nmos_part_after_one_character() {
let Some(image) = rom() else { return };
let (cpu, board) = board(Variant::Nmos6502, image);
let reached = run_until(&cpu, 5_000_000, || board.printed() >= 2);
assert!(!reached, "an NMOS 6502 cannot get past the delay loop");
assert_eq!(
board.output(),
"\\",
"the character written before the loop"
);
assert!(!cpu.is_halted());
assert_eq!(cpu.reg(super::Reg::Pc) & 0xfff0, 0xfff0, "stuck in ECHO");
}