use alloc::boxed::Box;
use alloc::string::String;
use alloc::sync::Arc;
use core::fmt;
use crate::core::device::{Device, DeviceClass, PropertySpec, RealizeCtx, ResetKind};
use crate::core::error::{BusError, Error, Result};
use crate::core::props::{Props, ValueKind};
use crate::core::sched::{Budget, Consumed};
use crate::core::space::{AccessConstraints, MemAttrs, MemOps, MemResult, Region, RegionRef};
use crate::core::state::{ChunkReader, ChunkWriter, Sink, Source};
use crate::core::sync::{LockRank, Mutex};
use crate::core::value::{Endian, Width};
use crate::core::wire::{Level, WireSource};
use crate::host::chardev::{CharDevice, ports};
use crate::machine::realize::Instance;
const CLASS_NAME: &str = "wdc.w65c51n";
const STATE_VERSION: u32 = 1;
const DEFAULT_PORT: &str = "console";
pub const REGISTER_COUNT: u64 = 4;
pub const IRQ_PIN: &str = "irq";
const ST_IRQ: u8 = 0x80;
const ST_TDRE: u8 = 0x10;
const ST_RDRF: u8 = 0x08;
#[cfg(test)]
const ST_OVRN: u8 = 0x04;
const CMD_DTR: u8 = 0x01;
const CMD_IRD: u8 = 0x02;
const CMD_PROGRAMMED_RESET: u8 = 0x1f;
const CTRL_SBR: u8 = 0x0f;
const CTRL_WL: u8 = 0x60;
const BAUD_RATES: [u32; 16] = [
115_200, 50, 75, 109, 134, 150, 300, 600, 1200, 1800, 2400, 3600, 4800, 7200, 9600, 19_200,
];
#[derive(Debug)]
pub struct Acia {
regs: Arc<Registers>,
region: RegionRef,
}
struct Registers {
state: Mutex<State>,
port: Arc<dyn CharDevice>,
port_name: String,
paced: bool,
erratum: bool,
irq: Mutex<Option<WireSource>>,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
struct State {
command: u8,
control: u8,
rdr: u8,
rdrf: bool,
tdr: u8,
tx_full: bool,
irq: bool,
}
impl fmt::Debug for Registers {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut s = f.debug_struct("Registers");
s.field("port", &self.port_name)
.field("paced", &self.paced)
.field("erratum", &self.erratum);
match self.state.try_lock() {
Some(state) => s.field("state", &*state).finish(),
None => s.field("state", &"<in use>").finish(),
}
}
}
impl Acia {
pub fn new(props: &Props) -> Result<Acia> {
let mut r = props.reader();
let port_name = r.or("port", String::from(DEFAULT_PORT))?;
let paced = r.or("paced", true)?;
let erratum = r.or("erratum", false)?;
r.finish()?;
Ok(Acia::with_port(
ports::open(&port_name),
port_name,
paced,
erratum,
))
}
#[must_use]
pub fn with_port(
port: Arc<dyn CharDevice>,
port_name: String,
paced: bool,
erratum: bool,
) -> Acia {
let regs = Arc::new(Registers {
state: Mutex::with_rank(LockRank::DEVICE, State::default()),
port,
port_name,
paced,
erratum,
irq: Mutex::with_rank(LockRank::WIRE, None),
});
let region = Arc::new(Region::io(
"acia",
REGISTER_COUNT,
Arc::clone(®s) as Arc<dyn MemOps>,
));
Acia { regs, region }
}
#[must_use]
pub fn port_name(&self) -> &str {
&self.regs.port_name
}
#[must_use]
pub fn is_paced(&self) -> bool {
self.regs.paced
}
#[must_use]
pub fn has_erratum(&self) -> bool {
self.regs.erratum
}
#[must_use]
pub fn status(&self) -> u8 {
self.regs.status(&self.regs.state.lock())
}
#[must_use]
pub fn baud(&self) -> u32 {
BAUD_RATES[(self.regs.state.lock().control & CTRL_SBR) as usize]
}
#[must_use]
pub fn word_length(&self) -> u8 {
8 - ((self.regs.state.lock().control & CTRL_WL) >> 5)
}
pub fn connect_irq(&self, source: WireSource) {
*self.regs.irq.lock() = Some(source);
self.regs.refresh_irq();
}
#[must_use]
pub fn irq_level(&self) -> Level {
self.regs.irq_level(&self.regs.state.lock())
}
}
impl Registers {
fn status(&self, state: &State) -> u8 {
let mut status = 0u8;
if state.irq {
status |= ST_IRQ;
}
if self.erratum || !state.tx_full {
status |= ST_TDRE;
}
if state.rdrf {
status |= ST_RDRF;
}
status
}
fn irq_pending(state: &State) -> bool {
state.rdrf && state.command & CMD_DTR != 0 && state.command & CMD_IRD == 0
}
fn irq_level(&self, state: &State) -> Level {
if state.irq { Level::High } else { Level::Low }
}
fn refresh_irq(&self) {
let level = self.irq_level(&self.state.lock());
let port = self.irq.lock().clone();
if let Some(port) = port {
port.set(level);
}
}
fn poll_receiver(&self) -> bool {
let mut state = self.state.lock();
if state.rdrf {
return false;
}
let Some(byte) = self.port.read_byte() else {
return false;
};
state.rdr = byte;
state.rdrf = true;
if Registers::irq_pending(&state) && !state.irq {
state.irq = true;
return true;
}
false
}
fn transmit(&self) -> bool {
let byte = {
let state = self.state.lock();
if !state.tx_full {
return false;
}
state.tdr
};
if !self.port.write_byte(byte) {
return false;
}
self.state.lock().tx_full = false;
true
}
fn read_register(&self, index: u8, debug: bool) -> (u8, bool) {
let mut moved = if debug { false } else { self.poll_receiver() };
let mut state = self.state.lock();
let byte = match index {
0 => {
if !debug {
state.rdrf = false;
}
state.rdr
}
1 => {
let status = self.status(&state);
if !debug && state.irq {
state.irq = false;
moved = true;
}
status
}
2 => state.command,
_ => state.control,
};
(byte, moved)
}
fn write_register(&self, index: u8, value: u8) -> (bool, bool) {
let mut state = self.state.lock();
match index {
0 => {
state.tdr = value;
state.tx_full = true;
return (true, false);
}
1 => {
state.command &= !CMD_PROGRAMMED_RESET;
}
2 => state.command = value,
_ => state.control = value,
}
let want = Registers::irq_pending(&state);
if want != state.irq {
state.irq = want;
return (false, true);
}
(false, false)
}
}
impl MemOps for Registers {
fn read(&self, offset: u64, dst: &mut [u8], attrs: MemAttrs) -> MemResult {
let [byte] = dst else {
return Err(BusError::BadAccess);
};
let (value, moved) = self.read_register((offset & 3) as u8, attrs.debug);
*byte = value;
if moved {
self.refresh_irq();
}
Ok(())
}
fn write(&self, offset: u64, src: &[u8], attrs: MemAttrs) -> MemResult {
let [value] = src else {
return Err(BusError::BadAccess);
};
if attrs.debug {
return Err(BusError::BadAccess);
}
let (transmit, moved) = self.write_register((offset & 3) as u8, *value);
if transmit && !self.paced {
self.transmit();
}
if moved {
self.refresh_irq();
}
Ok(())
}
fn constraints(&self) -> AccessConstraints {
AccessConstraints::word(Width::U8, Endian::Little)
}
}
impl Device for Acia {
fn class(&self) -> &'static DeviceClass {
&ACIA_CLASS
}
fn realize(&self, _ctx: &mut RealizeCtx<'_>) -> Result<()> {
Ok(())
}
fn reset(&self, _kind: ResetKind) {
*self.regs.state.lock() = State::default();
self.regs.refresh_irq();
}
fn save(&self, w: &mut ChunkWriter<'_>) -> Result<()> {
let state = *self.regs.state.lock();
w.write_u8(state.command)?;
w.write_u8(state.control)?;
w.write_u8(state.rdr)?;
w.write_u8(state.tdr)?;
w.write_bool(state.rdrf)?;
w.write_bool(state.tx_full)?;
w.write_bool(state.irq)
}
fn load(&self, r: &mut ChunkReader<'_>) -> Result<()> {
let state = State {
command: r.read_u8()?,
control: r.read_u8()?,
rdr: r.read_u8()?,
tdr: r.read_u8()?,
rdrf: r.read_bool()?,
tx_full: r.read_bool()?,
irq: r.read_bool()?,
};
*self.regs.state.lock() = state;
self.regs.refresh_irq();
Ok(())
}
fn region(&self, name: &str) -> Option<RegionRef> {
matches!(name, "" | "regs").then(|| Arc::clone(&self.region))
}
fn connect(&self, port: &str, source: WireSource) -> Result<()> {
if port != IRQ_PIN {
return Err(Error::Config {
at: String::from(port),
message: alloc::format!("the ACIA drives only `{IRQ_PIN}`"),
});
}
self.connect_irq(source);
Ok(())
}
fn announce(&self, port: &str) {
if port == IRQ_PIN {
self.regs.refresh_irq();
}
}
fn is_runnable(&self) -> bool {
self.regs.paced
}
fn run(&self, budget: Budget) -> Consumed {
self.regs.transmit();
if self.regs.poll_receiver() {
self.regs.refresh_irq();
}
Consumed::new(budget.ticks)
}
}
impl Instance for Acia {}
pub static ACIA_CLASS: DeviceClass = DeviceClass {
name: CLASS_NAME,
version: STATE_VERSION,
summary: "WDC W65C51N ACIA: data, status, command and control over a character port",
properties: &[
PropertySpec {
name: "port",
kind: ValueKind::Str,
required: false,
summary: "the character port to attach to, by name (default \"console\")",
},
PropertySpec {
name: "paced",
kind: ValueKind::Bool,
required: false,
summary: "whether the transmitter sends one byte per clock tick (default true)",
},
PropertySpec {
name: "erratum",
kind: ValueKind::Bool,
required: false,
summary: "model the W65C51N's permanently-set TDRE bit (default false)",
},
],
construct: |props| Ok(Box::new(Acia::new(props)?)),
};
pub fn register(registry: &mut crate::core::Registry) -> Result<()> {
registry.add(&ACIA_CLASS)
}
pub fn bind(bindings: &mut crate::machine::Bindings) -> Result<()> {
bindings.bind(CLASS_NAME, |props| Ok(Arc::new(Acia::new(props)?)))
}
#[must_use]
pub fn schema() -> crate::machine::validate::ClassSchema {
use crate::machine::validate::{ClassSchema, PortDir, PropSchema};
ClassSchema::new(CLASS_NAME)
.prop(PropSchema::new("port", ValueKind::Str))
.prop(PropSchema::new("paced", ValueKind::Bool))
.prop(PropSchema::new("erratum", ValueKind::Bool))
.port(IRQ_PIN, PortDir::Out)
.region("")
.region("regs")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::props::Value;
use crate::core::state::{MachineShape, Migrations, StateReader, StateWriter};
use crate::host::chardev::CharPort;
use alloc::string::ToString;
use alloc::vec::Vec;
fn wired() -> (Acia, Arc<CharPort>) {
let port = Arc::new(CharPort::new());
let acia = Acia::with_port(
Arc::clone(&port) as Arc<dyn CharDevice>,
"test".to_string(),
false,
false,
);
(acia, port)
}
fn peek(acia: &Acia, index: u64) -> u8 {
let mut byte = [0u8; 1];
acia.regs
.read(index, &mut byte, MemAttrs::DEFAULT)
.expect("a byte read is legal");
byte[0]
}
fn peek_debug(acia: &Acia, index: u64) -> u8 {
let mut byte = [0u8; 1];
acia.regs
.read(index, &mut byte, MemAttrs::DEBUG)
.expect("a byte read is legal");
byte[0]
}
fn poke(acia: &Acia, index: u64, value: u8) {
acia.regs
.write(index, &[value], MemAttrs::DEFAULT)
.expect("a byte write is legal");
}
fn initialise(acia: &Acia) {
poke(acia, 3, 0x1f);
poke(acia, 2, 0x0b);
}
#[test]
fn the_reset_state_is_the_data_sheets() {
let (acia, _port) = wired();
initialise(&acia);
acia.reset(ResetKind::Cold);
assert_eq!(peek(&acia, 2), 0x00, "command");
assert_eq!(peek(&acia, 3), 0x00, "control");
assert_eq!(peek(&acia, 1), ST_TDRE, "status is TDRE and nothing else");
}
#[test]
fn a_byte_written_to_the_data_register_reaches_the_port() {
let (acia, port) = wired();
initialise(&acia);
poke(&acia, 0, b'A');
assert_eq!(port.drain(), b"A".to_vec());
assert_eq!(peek(&acia, 1) & ST_TDRE, ST_TDRE, "unpaced: already gone");
}
#[test]
fn a_byte_from_the_port_sets_rdrf_and_reading_the_data_clears_it() {
let (acia, port) = wired();
initialise(&acia);
assert_eq!(peek(&acia, 1) & ST_RDRF, 0, "nothing waiting");
port.feed(b"Q");
assert_eq!(peek(&acia, 1) & ST_RDRF, ST_RDRF);
assert_eq!(peek(&acia, 0), b'Q');
assert_eq!(peek(&acia, 1) & ST_RDRF, 0, "reading the data cleared it");
}
#[test]
fn a_byte_the_receiver_has_no_room_for_waits_rather_than_being_lost() {
let (acia, port) = wired();
initialise(&acia);
port.feed(b"AB");
assert_eq!(peek(&acia, 1) & (ST_RDRF | ST_OVRN), ST_RDRF);
assert_eq!(
peek(&acia, 1) & (ST_RDRF | ST_OVRN),
ST_RDRF,
"polling again must not report an overrun it invented"
);
assert_eq!(peek(&acia, 0), b'A', "the older byte, in order");
assert_eq!(peek(&acia, 1) & ST_RDRF, ST_RDRF, "and then the newer");
assert_eq!(peek(&acia, 0), b'B');
assert_eq!(peek(&acia, 1) & (ST_RDRF | ST_OVRN), 0);
}
#[test]
fn the_erratum_is_a_property_and_the_default_is_the_correct_behaviour() {
let port = Arc::new(CharPort::new());
let correct = Acia::with_port(
Arc::clone(&port) as Arc<dyn CharDevice>,
"test".to_string(),
true,
false,
);
initialise(&correct);
poke(&correct, 0, b'X');
assert_eq!(
peek(&correct, 1) & ST_TDRE,
0,
"the default models the 6551 contract: TDRE is clear while busy"
);
assert!(port.drain().is_empty(), "and the byte has not gone yet");
let broken = Acia::with_port(
Arc::new(CharPort::new()) as Arc<dyn CharDevice>,
"test".to_string(),
true,
true,
);
initialise(&broken);
poke(&broken, 0, b'X');
assert_eq!(
peek(&broken, 1) & ST_TDRE,
ST_TDRE,
"erratum = true: TDRE is set even though the transmitter is full"
);
assert!(broken.has_erratum());
}
#[test]
fn the_transmitter_holds_the_byte_until_a_clock_tick_releases_it() {
let port = Arc::new(CharPort::new());
let acia = Acia::with_port(
Arc::clone(&port) as Arc<dyn CharDevice>,
"test".to_string(),
true,
false,
);
initialise(&acia);
poke(&acia, 0, b'H');
assert!(port.drain().is_empty(), "one character time has not passed");
let consumed = acia.run(Budget {
until: crate::core::clock::GlobalTime::from_nanos(0),
ticks: 1,
});
assert_eq!(consumed.ticks, 1, "the domain must advance");
assert_eq!(port.drain(), b"H".to_vec());
assert_eq!(acia.status() & ST_TDRE, ST_TDRE);
assert!(acia.is_paced());
}
#[test]
fn a_port_that_will_not_take_the_byte_keeps_the_guest_waiting() {
let (acia, port) = wired();
initialise(&acia);
port.write(&alloc::vec![b'x'; crate::host::chardev::PORT_CAPACITY]);
poke(&acia, 0, b'A');
assert_eq!(acia.status() & ST_TDRE, 0, "still holding it");
let _ = port.drain();
acia.regs.transmit();
assert_eq!(acia.status() & ST_TDRE, ST_TDRE);
assert_eq!(port.drain(), b"A".to_vec(), "and nothing was lost");
}
#[test]
fn a_programmed_reset_clears_five_command_bits_and_not_the_control() {
let (acia, _port) = wired();
poke(&acia, 3, 0x1f);
poke(&acia, 2, 0xff);
poke(&acia, 1, 0x00); assert_eq!(peek(&acia, 2), 0xe0, "bits 7-5 survive, 4-0 are cleared");
assert_eq!(peek(&acia, 3), 0x1f, "the control register is unchanged");
assert_eq!(peek(&acia, 1) & ST_OVRN, 0);
}
#[test]
fn the_control_register_is_read_back_and_decoded() {
let (acia, _port) = wired();
poke(&acia, 3, 0x1f);
assert_eq!(peek(&acia, 3), 0x1f);
assert_eq!(acia.baud(), 19_200);
assert_eq!(acia.word_length(), 8);
poke(&acia, 3, 0x10);
assert_eq!(acia.baud(), 115_200);
poke(&acia, 3, 0x70);
assert_eq!(acia.word_length(), 5);
}
#[test]
fn the_interrupt_follows_the_command_register() {
let (acia, port) = wired();
acia.connect_irq(dummy_source());
poke(&acia, 2, 0x00);
port.feed(b"a");
let _ = peek(&acia, 1);
assert_eq!(acia.irq_level(), Level::Low, "DTR = 0 disables them all");
let _ = peek(&acia, 0);
poke(&acia, 2, CMD_DTR);
port.feed(b"b");
assert_eq!(peek(&acia, 1) & ST_IRQ, ST_IRQ, "bit 7 with the flag");
assert_eq!(
acia.irq_level(),
Level::Low,
"and reading the status cleared it"
);
let _ = peek(&acia, 0);
poke(&acia, 2, 0x0b);
port.feed(b"c");
assert_eq!(peek(&acia, 1) & ST_IRQ, 0);
assert_eq!(acia.irq_level(), Level::Low);
}
#[test]
fn a_debug_access_changes_nothing() {
let (acia, port) = wired();
initialise(&acia);
port.feed(b"Z");
assert_eq!(peek_debug(&acia, 1) & ST_RDRF, 0);
assert_eq!(peek(&acia, 1) & ST_RDRF, ST_RDRF);
assert_eq!(peek_debug(&acia, 0), b'Z');
assert_eq!(peek(&acia, 1) & ST_RDRF, ST_RDRF, "still waiting");
assert_eq!(peek(&acia, 0), b'Z');
assert_eq!(peek(&acia, 1) & ST_RDRF, 0);
assert_eq!(
acia.regs.write(0, b"!", MemAttrs::DEBUG),
Err(BusError::BadAccess)
);
assert!(port.drain().is_empty());
}
#[test]
fn only_byte_accesses_are_accepted() {
let (acia, _port) = wired();
assert_eq!(
acia.regs.read(0, &mut [0u8; 2], MemAttrs::DEFAULT),
Err(BusError::BadAccess)
);
assert_eq!(
acia.regs.write(0, &[0, 0], MemAttrs::DEFAULT),
Err(BusError::BadAccess)
);
assert_eq!(acia.regs.constraints().min, Width::U8);
assert_eq!(acia.regs.constraints().max, Width::U8);
}
#[test]
fn the_whole_register_block_is_the_region() {
let (acia, _port) = wired();
let region = acia.region("").expect("the default region");
assert_eq!(region.len(), REGISTER_COUNT);
assert!(acia.region("regs").is_some());
assert!(acia.region("data").is_none());
}
#[test]
fn a_snapshot_round_trips_to_identical_state() {
let (saved, port) = wired();
initialise(&saved);
port.feed(b"S");
assert_eq!(peek(&saved, 1) & ST_RDRF, ST_RDRF);
let mut shape = MachineShape::new();
shape.add_device("acia", CLASS_NAME).unwrap();
let mut w = StateWriter::new(shape);
{
let mut chunk = w.chunk("acia", CLASS_NAME, STATE_VERSION).unwrap();
saved.save(&mut chunk).unwrap();
}
let bytes = w.to_vec().unwrap();
let (restored, _other) = wired();
let reader = StateReader::new(&bytes).unwrap();
let chunk = reader
.load("acia", CLASS_NAME, STATE_VERSION, &Migrations::new())
.unwrap();
restored.load(&mut chunk.reader()).unwrap();
let before: Vec<u8> = (0..4).map(|i| peek_debug(&saved, i)).collect();
let after: Vec<u8> = (0..4).map(|i| peek_debug(&restored, i)).collect();
assert_eq!(before, after);
assert_eq!(peek(&restored, 0), b'S', "the latched byte came across");
}
#[test]
fn properties_are_checked() {
assert!(Acia::new(&Props::new()).is_ok(), "everything has a default");
let acia = Acia::new(&Props::new().with("port", "test.acia.props")).expect("a name");
assert_eq!(acia.port_name(), "test.acia.props");
assert!(acia.is_paced());
assert!(!acia.has_erratum());
ports::close("test.acia.props");
let acia = Acia::new(&Props::new().with("paced", Value::Bool(false))).expect("unpaced");
assert!(!acia.is_paced());
assert!(!acia.is_runnable(), "nothing to schedule");
let err = Acia::new(&Props::new().with("prot", "console"))
.expect_err("a typo")
.to_string();
assert!(err.contains("prot") && err.contains("port"), "{err}");
}
#[test]
fn the_class_is_registrable_and_describes_itself() {
let mut registry = crate::core::Registry::new();
register(&mut registry).expect("a fresh registry");
let class = registry.get(CLASS_NAME).expect("registered");
assert_eq!(class.version, STATE_VERSION);
assert_eq!(class.properties.len(), 3);
let device = (class.construct)(&Props::new().with("port", "test.acia.registry"))
.expect("defaults are enough");
assert_eq!(device.class().name, CLASS_NAME);
assert!(device.connect("cts", dummy_source()).is_err());
ports::close("test.acia.registry");
}
fn dummy_source() -> WireSource {
use crate::core::wire::{Wire, WireId};
let src = WireId::new(1);
WireSource::new(Wire::builder().source(src).build_shared(), src)
}
}