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, 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::host::chardev::{CharDevice, ports};
use crate::machine::realize::Instance;
const CLASS_NAME: &str = "apple1.pia";
const STATE_VERSION: u32 = 1;
const DEFAULT_PORT: &str = "console";
pub const REGISTER_COUNT: u64 = 4;
const CR_DDR_ACCESS: u8 = 0x04;
const CR_FLAGS: u8 = 0xc0;
const CR_IRQ1: u8 = 0x80;
const PB_BUSY: u8 = 0x80;
const PA_STRAP: u8 = 0x80;
const APPLE1_RUBOUT: u8 = 0x5f;
#[derive(Debug)]
pub struct Pia {
regs: Arc<Registers>,
region: RegionRef,
}
struct Registers {
state: Mutex<State>,
port: Arc<dyn CharDevice>,
port_name: String,
paced: bool,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
struct State {
cra: u8,
crb: u8,
ddra: u8,
ddrb: u8,
ora: u8,
orb: u8,
key: u8,
key_ready: bool,
busy: 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);
match self.state.try_lock() {
Some(state) => s.field("state", &*state).finish(),
None => s.field("state", &"<in use>").finish(),
}
}
}
impl Pia {
pub fn new(props: &Props) -> Result<Pia> {
let mut r = props.reader();
let port_name = r.or("port", String::from(DEFAULT_PORT))?;
let paced = r.or("paced", true)?;
r.finish()?;
Ok(Pia::with_port(ports::open(&port_name), port_name, paced))
}
#[must_use]
pub fn with_port(port: Arc<dyn CharDevice>, port_name: String, paced: bool) -> Pia {
let regs = Arc::new(Registers {
state: Mutex::with_rank(LockRank::DEVICE, State::default()),
port,
port_name,
paced,
});
let region = Arc::new(Region::io(
"pia",
REGISTER_COUNT,
Arc::clone(®s) as Arc<dyn MemOps>,
));
Pia { 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 display_busy(&self) -> bool {
self.regs.state.lock().busy
}
#[must_use]
pub fn key_waiting(&self) -> bool {
self.regs.poll_keyboard();
self.regs.state.lock().key_ready
}
}
impl Registers {
fn poll_keyboard(&self) {
let mut state = self.state.lock();
if state.key_ready {
return;
}
let Some(byte) = self.port.read_byte() else {
return;
};
state.key = keyboard_code(byte);
state.key_ready = true;
state.cra |= CR_IRQ1;
}
fn release_character(&self) -> bool {
let byte = {
let state = self.state.lock();
if !state.busy {
return false;
}
state.orb & !PB_BUSY
};
if !self.port.write_byte(byte) {
return false;
}
self.state.lock().busy = false;
true
}
fn read_register(&self, index: u8, debug: bool) -> u8 {
if !debug {
self.poll_keyboard();
}
let mut state = self.state.lock();
match index {
0 => {
if state.cra & CR_DDR_ACCESS == 0 {
return state.ddra;
}
if !debug {
state.cra &= !CR_FLAGS;
state.key_ready = false;
}
state.key
}
1 => state.cra,
2 => {
if state.crb & CR_DDR_ACCESS == 0 {
return state.ddrb;
}
let inputs = if state.busy { PB_BUSY } else { 0 };
(state.orb & state.ddrb) | (inputs & !state.ddrb)
}
_ => state.crb,
}
}
fn write_register(&self, index: u8, value: u8) -> bool {
let mut state = self.state.lock();
match index {
0 => {
if state.cra & CR_DDR_ACCESS == 0 {
state.ddra = value;
} else {
state.ora = value;
}
}
1 => state.cra = (state.cra & CR_FLAGS) | (value & !CR_FLAGS),
2 => {
if state.crb & CR_DDR_ACCESS == 0 {
state.ddrb = value;
} else {
state.orb = value;
state.busy = true;
return true;
}
}
_ => state.crb = (state.crb & CR_FLAGS) | (value & !CR_FLAGS),
}
false
}
}
impl MemOps for Registers {
fn read(&self, offset: u64, dst: &mut [u8], attrs: MemAttrs) -> MemResult {
let [byte] = dst else {
return Err(BusError::BadAccess);
};
*byte = self.read_register((offset & 3) as u8, attrs.debug);
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);
}
if self.write_register((offset & 3) as u8, *value) && !self.paced {
self.release_character();
}
Ok(())
}
fn constraints(&self) -> AccessConstraints {
AccessConstraints::word(Width::U8, Endian::Little)
}
}
pub static PIA_CLASS: DeviceClass = DeviceClass {
name: CLASS_NAME,
version: STATE_VERSION,
summary: "Apple 1 MC6821: keyboard at $D010/$D011, display at $D012/$D013",
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 display takes one character per clock tick (default true)",
},
],
construct: |props| Ok(Box::new(Pia::new(props)?)),
};
impl Device for Pia {
fn class(&self) -> &'static DeviceClass {
&PIA_CLASS
}
fn realize(&self, _ctx: &mut RealizeCtx<'_>) -> Result<()> {
Ok(())
}
fn reset(&self, _kind: ResetKind) {
*self.regs.state.lock() = State::default();
}
fn save(&self, w: &mut ChunkWriter<'_>) -> Result<()> {
let state = *self.regs.state.lock();
w.write_u8(state.cra)?;
w.write_u8(state.crb)?;
w.write_u8(state.ddra)?;
w.write_u8(state.ddrb)?;
w.write_u8(state.ora)?;
w.write_u8(state.orb)?;
w.write_u8(state.key)?;
w.write_bool(state.key_ready)?;
w.write_bool(state.busy)
}
fn load(&self, r: &mut ChunkReader<'_>) -> Result<()> {
let state = State {
cra: r.read_u8()?,
crb: r.read_u8()?,
ddra: r.read_u8()?,
ddrb: r.read_u8()?,
ora: r.read_u8()?,
orb: r.read_u8()?,
key: r.read_u8()?,
key_ready: r.read_bool()?,
busy: r.read_bool()?,
};
*self.regs.state.lock() = state;
Ok(())
}
fn region(&self, name: &str) -> Option<RegionRef> {
matches!(name, "" | "regs").then(|| Arc::clone(&self.region))
}
fn is_runnable(&self) -> bool {
self.regs.paced
}
fn run(&self, budget: Budget) -> Consumed {
self.regs.release_character();
self.regs.poll_keyboard();
Consumed::new(budget.ticks)
}
}
impl Instance for Pia {}
fn keyboard_code(byte: u8) -> u8 {
let ascii = match byte {
b'\n' | b'\r' => 0x0d,
0x08 | 0x7f => APPLE1_RUBOUT,
b'a'..=b'z' => byte.to_ascii_uppercase(),
other => other & 0x7f,
};
ascii | PA_STRAP
}
pub fn register(registry: &mut crate::core::Registry) -> Result<()> {
registry.add(&PIA_CLASS)
}
pub fn bind(bindings: &mut crate::machine::Bindings) -> Result<()> {
bindings.bind(CLASS_NAME, |props| Ok(Arc::new(Pia::new(props)?)))
}
#[must_use]
pub fn schema() -> crate::machine::validate::ClassSchema {
use crate::machine::validate::{ClassSchema, PropSchema};
ClassSchema::new(CLASS_NAME)
.prop(PropSchema::new("port", ValueKind::Str))
.prop(PropSchema::new("paced", ValueKind::Bool))
.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() -> (Pia, Arc<CharPort>) {
let port = Arc::new(CharPort::new());
let pia = Pia::with_port(
Arc::clone(&port) as Arc<dyn CharDevice>,
"test".to_string(),
false,
);
(pia, port)
}
fn peek(pia: &Pia, index: u64) -> u8 {
let mut byte = [0u8; 1];
pia.regs
.read(index, &mut byte, MemAttrs::DEFAULT)
.expect("a byte read is legal");
byte[0]
}
fn peek_debug(pia: &Pia, index: u64) -> u8 {
let mut byte = [0u8; 1];
pia.regs
.read(index, &mut byte, MemAttrs::DEBUG)
.expect("a byte read is legal");
byte[0]
}
fn poke(pia: &Pia, index: u64, value: u8) {
pia.regs
.write(index, &[value], MemAttrs::DEFAULT)
.expect("a byte write is legal");
}
fn initialise(pia: &Pia) {
poke(pia, 2, 0x7f);
poke(pia, 1, 0xa7);
poke(pia, 3, 0xa7);
}
#[test]
fn before_the_control_registers_are_set_the_data_addresses_are_the_ddrs() {
let (pia, port) = wired();
poke(&pia, 2, 0x7f);
assert!(port.drain().is_empty(), "$7F was taken for a character");
assert_eq!(peek(&pia, 2), 0x7f, "reads back as DDRB");
poke(&pia, 3, 0xa7);
assert_eq!(peek(&pia, 3), 0x27, "CRB, less the read-only flag bits");
assert_eq!(peek(&pia, 2) & PB_BUSY, 0);
}
#[test]
fn a_character_written_to_the_display_reaches_the_port() {
let (pia, port) = wired();
initialise(&pia);
poke(&pia, 2, 0xc1); assert_eq!(port.drain(), b"A".to_vec());
assert!(
!pia.display_busy(),
"unpaced: gone before the store returns"
);
assert_eq!(peek(&pia, 2), 0x41);
}
#[test]
fn the_display_stays_busy_until_a_clock_tick_releases_the_character() {
let port = Arc::new(CharPort::new());
let pia = Pia::with_port(
Arc::clone(&port) as Arc<dyn CharDevice>,
"test".to_string(),
true,
);
initialise(&pia);
poke(&pia, 2, 0xc8); assert!(pia.display_busy());
assert_eq!(peek(&pia, 2) & PB_BUSY, PB_BUSY, "software sees DA set");
assert!(
port.drain().is_empty(),
"the video section has not taken it"
);
let consumed = pia.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!(!pia.display_busy());
assert_eq!(peek(&pia, 2) & PB_BUSY, 0);
}
#[test]
fn a_key_sets_the_control_flag_and_reading_the_data_clears_it() {
let (pia, port) = wired();
initialise(&pia);
assert_eq!(peek(&pia, 1) & CR_IRQ1, 0, "no key yet");
port.feed(b"a");
assert_eq!(peek(&pia, 1) & CR_IRQ1, CR_IRQ1, "bit 7 of $D011");
assert_eq!(peek(&pia, 0), 0xc1);
assert_eq!(peek(&pia, 1) & CR_IRQ1, 0, "reading $D010 cleared it");
}
#[test]
fn the_keyboard_is_the_upper_case_one_the_apple_1_had() {
assert_eq!(keyboard_code(b'a'), 0xc1);
assert_eq!(keyboard_code(b'A'), 0xc1);
assert_eq!(keyboard_code(b'\r'), 0x8d);
assert_eq!(keyboard_code(b'\n'), 0x8d, "either line ending is Return");
assert_eq!(keyboard_code(0x7f), 0xdf, "delete is rub out");
assert_eq!(keyboard_code(0x08), 0xdf, "and so is backspace");
assert_eq!(keyboard_code(b'.'), 0xae);
assert_eq!(keyboard_code(0xff) & PA_STRAP, PA_STRAP);
}
#[test]
fn a_debug_access_changes_nothing() {
let (pia, port) = wired();
initialise(&pia);
port.feed(b"Z");
assert_eq!(peek_debug(&pia, 1) & CR_IRQ1, 0);
assert_eq!(peek(&pia, 1) & CR_IRQ1, CR_IRQ1);
assert_eq!(peek_debug(&pia, 0), 0xda);
assert_eq!(peek(&pia, 1) & CR_IRQ1, CR_IRQ1, "still waiting");
assert_eq!(peek(&pia, 0), 0xda);
assert_eq!(peek(&pia, 1) & CR_IRQ1, 0);
assert_eq!(
pia.regs.write(2, &[0xc1], MemAttrs::DEBUG),
Err(BusError::BadAccess)
);
assert!(port.drain().is_empty());
}
#[test]
fn only_byte_accesses_are_accepted() {
let (pia, _port) = wired();
assert_eq!(
pia.regs.read(0, &mut [0u8; 2], MemAttrs::DEFAULT),
Err(BusError::BadAccess)
);
assert_eq!(
pia.regs.write(0, &[0, 0], MemAttrs::DEFAULT),
Err(BusError::BadAccess)
);
assert_eq!(pia.regs.constraints().min, Width::U8);
assert_eq!(pia.regs.constraints().max, Width::U8);
}
#[test]
fn a_port_that_will_not_take_the_byte_keeps_the_guest_waiting() {
let port = Arc::new(CharPort::new());
let pia = Pia::with_port(
Arc::clone(&port) as Arc<dyn CharDevice>,
"test".to_string(),
true,
);
initialise(&pia);
port.write(&alloc::vec![b'x'; crate::host::chardev::PORT_CAPACITY]);
poke(&pia, 2, 0xc1);
pia.run(Budget {
until: crate::core::clock::GlobalTime::from_nanos(0),
ticks: 1,
});
assert!(pia.display_busy(), "still holding it");
let _ = port.drain();
pia.run(Budget {
until: crate::core::clock::GlobalTime::from_nanos(0),
ticks: 1,
});
assert!(!pia.display_busy());
assert_eq!(port.drain(), b"A".to_vec(), "and nothing was lost");
}
#[test]
fn a_reset_clears_every_register() {
let (pia, _port) = wired();
initialise(&pia);
pia.reset(ResetKind::Cold);
assert_eq!(peek(&pia, 1), 0);
assert_eq!(peek(&pia, 3), 0);
assert_eq!(peek(&pia, 2), 0);
}
#[test]
fn properties_are_checked() {
assert!(Pia::new(&Props::new()).is_ok(), "everything has a default");
let pia = Pia::new(&Props::new().with("port", "test.pia.props")).expect("a name");
assert_eq!(pia.port_name(), "test.pia.props");
assert!(pia.is_paced());
ports::close("test.pia.props");
let pia = Pia::new(&Props::new().with("paced", Value::Bool(false))).expect("unpaced");
assert!(!pia.is_paced());
assert!(!pia.is_runnable(), "nothing to schedule");
let err = Pia::new(&Props::new().with("prot", "console"))
.expect_err("a typo")
.to_string();
assert!(err.contains("prot") && err.contains("port"), "{err}");
}
#[test]
fn the_whole_register_block_is_the_region() {
let (pia, _port) = wired();
let region = pia.region("").expect("the default region");
assert_eq!(region.len(), REGISTER_COUNT);
assert!(pia.region("regs").is_some());
assert!(pia.region("keyboard").is_none());
}
#[test]
fn a_snapshot_round_trips_to_identical_state() {
let (saved, port) = wired();
initialise(&saved);
port.feed(b"Q");
assert!(saved.key_waiting());
let mut shape = MachineShape::new();
shape.add_device("pia", CLASS_NAME).unwrap();
let mut w = StateWriter::new(shape);
{
let mut chunk = w.chunk("pia", 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("pia", 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!(restored.regs.state.lock().key, 0xd1);
}
#[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(), 2);
let device = (class.construct)(&Props::new().with("port", "test.pia.registry"))
.expect("defaults are enough");
assert_eq!(device.class().name, CLASS_NAME);
ports::close("test.pia.registry");
}
}