use alloc::boxed::Box;
use alloc::collections::VecDeque;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::sync::Arc;
use alloc::vec::Vec;
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::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::{DmaPeripheral, Level, WireSource};
use crate::machine::realize::Instance;
use crate::machine::validate::{ClassSchema, PortDir, PropSchema};
pub const CLASS_NAME: &str = "pc.fdc";
const STATE_VERSION: u32 = 1;
pub const REGISTER_WINDOW_LEN: u64 = 8;
pub const SECTOR_LEN: u64 = 512;
const N_512: u8 = 2;
const DRIVES: usize = 4;
const REG_DOR: u64 = 2;
const REG_MSR: u64 = 4;
const REG_DATA: u64 = 5;
const REG_DIR_CCR: u64 = 7;
const DOR_DRIVE: u8 = 0x03;
const DOR_RESET: u8 = 0x04;
const DOR_DMA_INT: u8 = 0x08;
const MSR_RQM: u8 = 0x80;
const MSR_DIO: u8 = 0x40;
const MSR_NDMA: u8 = 0x20;
const MSR_CB: u8 = 0x10;
const ST0_HD: u8 = 0x04;
const ST0_NR: u8 = 0x08;
const ST0_EC: u8 = 0x10;
const ST0_SE: u8 = 0x20;
const ST0_ABNORMAL: u8 = 0x40;
const ST0_INVALID: u8 = 0x80;
const ST0_READY_CHANGED: u8 = 0xc0;
const ST1_NW: u8 = 0x02;
const ST1_ND: u8 = 0x04;
const ST1_EN: u8 = 0x80;
const ST3_HD: u8 = 0x04;
const ST3_TS: u8 = 0x08;
const ST3_T0: u8 = 0x10;
const ST3_RY: u8 = 0x20;
const ST3_WP: u8 = 0x40;
const CMD_MASK: u8 = 0x1f;
const CMD_MT: u8 = 0x80;
const CMD_SPECIFY: u8 = 0x03;
const CMD_SENSE_DRIVE: u8 = 0x04;
const CMD_WRITE_DATA: u8 = 0x05;
const CMD_READ_DATA: u8 = 0x06;
const CMD_RECALIBRATE: u8 = 0x07;
const CMD_SENSE_INTERRUPT: u8 = 0x08;
const CMD_READ_ID: u8 = 0x0a;
const CMD_FORMAT_TRACK: u8 = 0x0d;
const CMD_SEEK: u8 = 0x0f;
const SPECIFY_ND: u8 = 0x01;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Geometry {
cylinders: u8,
heads: u8,
sectors: u8,
}
impl Geometry {
fn len(&self) -> u64 {
u64::from(self.cylinders) * u64::from(self.heads) * u64::from(self.sectors) * SECTOR_LEN
}
}
const STANDARD: [(u64, Geometry, &str); 5] = [
(
368_640,
Geometry {
cylinders: 40,
heads: 2,
sectors: 9,
},
"360K",
),
(
737_280,
Geometry {
cylinders: 80,
heads: 2,
sectors: 9,
},
"720K",
),
(
1_228_800,
Geometry {
cylinders: 80,
heads: 2,
sectors: 15,
},
"1.2M",
),
(
1_474_560,
Geometry {
cylinders: 80,
heads: 2,
sectors: 18,
},
"1.44M",
),
(
2_949_120,
Geometry {
cylinders: 80,
heads: 2,
sectors: 36,
},
"2.88M",
),
];
fn infer_geometry(name: &str, len: u64) -> Result<Geometry> {
for (bytes, geom, _) in STANDARD {
if bytes == len {
return Ok(geom);
}
}
let mut known = String::new();
for (bytes, geom, label) in STANDARD {
if !known.is_empty() {
known.push_str(", ");
}
known.push_str(&format!(
"{bytes} ({label}, {}/{}/{})",
geom.cylinders, geom.heads, geom.sectors
));
}
Err(Error::Property(format!(
"floppy image `{name}` is {len} bytes, which is no standard geometry; \
`geometry = \"auto\"` recognises {known}, and any other image needs an \
explicit `geometry = \"cylinders/heads/sectors\"`"
)))
}
fn parse_geometry(spec: &str, name: &str, len: u64) -> Result<Geometry> {
if spec == "auto" {
return infer_geometry(name, len);
}
let mut parts = spec.split('/');
let mut next = |what: &str| -> Result<u8> {
let field = parts.next().unwrap_or("");
field.parse::<u8>().map_err(|_| {
Error::Property(format!(
"geometry `{spec}`: `{field}` is not a {what} count in 0..=255 \
(write `cylinders/heads/sectors`, as in \"80/2/18\", or \"auto\")"
))
})
};
let geom = Geometry {
cylinders: next("cylinder")?,
heads: next("head")?,
sectors: next("sector")?,
};
if parts.next().is_some() {
return Err(Error::Property(format!(
"geometry `{spec}` has more than the three fields cylinders/heads/sectors"
)));
}
if geom.cylinders == 0 || geom.heads == 0 || geom.sectors == 0 {
return Err(Error::Property(format!(
"geometry `{spec}`: a medium needs at least one cylinder, head and sector"
)));
}
if geom.len() != len {
return Err(Error::Property(format!(
"geometry `{spec}` describes {} bytes but image `{name}` is {len}",
geom.len()
)));
}
Ok(geom)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Phase {
Idle,
Command,
Execution,
Result,
}
impl Phase {
fn as_u8(self) -> u8 {
match self {
Phase::Idle => 0,
Phase::Command => 1,
Phase::Execution => 2,
Phase::Result => 3,
}
}
fn from_u8(v: u8) -> Result<Phase> {
match v {
0 => Ok(Phase::Idle),
1 => Ok(Phase::Command),
2 => Ok(Phase::Execution),
3 => Ok(Phase::Result),
other => Err(Error::State(format!("unknown fdc phase {other}"))),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Dir {
ToCpu,
ToDevice,
Format,
}
impl Dir {
fn as_u8(self) -> u8 {
match self {
Dir::ToCpu => 0,
Dir::ToDevice => 1,
Dir::Format => 2,
}
}
fn from_u8(v: u8) -> Result<Dir> {
match v {
0 => Ok(Dir::ToCpu),
1 => Ok(Dir::ToDevice),
2 => Ok(Dir::Format),
other => Err(Error::State(format!(
"unknown fdc transfer direction {other}"
))),
}
}
}
#[derive(Debug, Clone, Copy)]
struct Xfer {
dir: Dir,
drive: u8,
c: u8,
h: u8,
r: u8,
n: u8,
eot: u8,
mt: bool,
filler: u8,
}
struct State {
phase: Phase,
command: u8,
params: Vec<u8>,
params_needed: u8,
results: VecDeque<u8>,
dor: u8,
ccr: u8,
specify: [u8; 2],
pcn: [u8; DRIVES],
seeking: u8,
seek_st0: [u8; DRIVES],
reset_senses: u8,
st0: u8,
st1: u8,
st2: u8,
st3: u8,
irq: bool,
changed: [bool; DRIVES],
dirty: bool,
buf: Vec<u8>,
pos: u64,
xfer: Option<Xfer>,
image: Vec<u8>,
geom: Geometry,
readonly: bool,
}
impl fmt::Debug for State {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("State")
.field("phase", &self.phase)
.field("command", &self.command)
.field("params", &self.params)
.field("results", &self.results)
.field("dor", &self.dor)
.field("pcn", &self.pcn)
.field("st0", &self.st0)
.field("xfer", &self.xfer)
.field("pos", &self.pos)
.field("image_len", &self.image.len())
.field("geometry", &self.geom)
.finish()
}
}
impl State {
fn new(image: Vec<u8>, geom: Geometry, readonly: bool) -> State {
State {
phase: Phase::Idle,
command: 0,
params: Vec::new(),
params_needed: 0,
results: VecDeque::new(),
dor: 0,
ccr: 0,
specify: [0; 2],
pcn: [0; DRIVES],
seeking: 0,
seek_st0: [0; DRIVES],
reset_senses: 0,
st0: 0,
st1: 0,
st2: 0,
st3: 0,
irq: false,
changed: [true; DRIVES],
dirty: false,
buf: Vec::new(),
pos: 0,
xfer: None,
image,
geom,
readonly,
}
}
fn reset_controller(&mut self) {
self.phase = Phase::Idle;
self.command = 0;
self.params.clear();
self.params_needed = 0;
self.results.clear();
self.specify = [0; 2];
self.seeking = 0;
self.seek_st0 = [0; DRIVES];
self.reset_senses = 0;
self.st0 = 0;
self.st1 = 0;
self.st2 = 0;
self.st3 = 0;
self.irq = false;
self.buf.clear();
self.pos = 0;
self.xfer = None;
}
fn non_dma(&self) -> bool {
self.dor & DOR_DMA_INT == 0 || self.specify[1] & SPECIFY_ND != 0
}
fn irq_level(&self) -> bool {
self.irq && self.dor & DOR_DMA_INT != 0
}
fn drq_level(&self) -> bool {
self.phase == Phase::Execution && !self.non_dma() && self.pos < self.buf.len() as u64
}
fn msr(&self) -> u8 {
let mut v = self.seeking & 0x0f;
match self.phase {
Phase::Idle => v |= MSR_RQM,
Phase::Command => v |= MSR_RQM | MSR_CB,
Phase::Result => v |= MSR_RQM | MSR_DIO | MSR_CB,
Phase::Execution => {
v |= MSR_CB;
if self.non_dma() {
v |= MSR_NDMA;
if self.pos < self.buf.len() as u64 {
v |= MSR_RQM;
}
if matches!(self.xfer.map(|x| x.dir), Some(Dir::ToCpu)) {
v |= MSR_DIO;
}
}
}
}
v
}
fn medium(&self, unit: u8) -> bool {
unit == 0 && !self.image.is_empty()
}
fn lba(&self, c: u8, h: u8, r: u8) -> Option<u64> {
if c >= self.geom.cylinders || h >= self.geom.heads || r == 0 || r > self.geom.sectors {
return None;
}
Some(
(u64::from(c) * u64::from(self.geom.heads) + u64::from(h))
* u64::from(self.geom.sectors)
+ u64::from(r - 1),
)
}
fn sector_range(&self, c: u8, h: u8, r: u8) -> Option<(usize, usize)> {
let lba = self.lba(c, h, r)?;
let start = lba * SECTOR_LEN;
let end = start + SECTOR_LEN;
if end > self.image.len() as u64 {
return None;
}
Some((start as usize, end as usize))
}
fn param(&self, i: usize) -> u8 {
self.params.get(i).copied().unwrap_or(0)
}
fn start_command(&mut self, byte: u8) {
self.command = byte;
self.params.clear();
let needed = match byte & CMD_MASK {
CMD_SPECIFY => 2,
CMD_SENSE_DRIVE => 1,
CMD_WRITE_DATA | CMD_READ_DATA => 8,
CMD_RECALIBRATE => 1,
CMD_SENSE_INTERRUPT => 0,
CMD_READ_ID => 1,
CMD_FORMAT_TRACK => 5,
CMD_SEEK => 2,
_ => {
self.invalid();
return;
}
};
self.params_needed = needed;
if needed == 0 {
self.execute();
} else {
self.phase = Phase::Command;
}
}
fn invalid(&mut self) {
self.st0 = ST0_INVALID;
self.results.clear();
self.results.push_back(ST0_INVALID);
self.phase = Phase::Result;
}
fn enter_result(&mut self, raise: bool) {
self.phase = Phase::Result;
if raise {
self.irq = true;
}
}
fn execute(&mut self) {
match self.command & CMD_MASK {
CMD_SPECIFY => {
self.specify = [self.param(0), self.param(1)];
self.phase = Phase::Idle;
}
CMD_SENSE_DRIVE => self.sense_drive(),
CMD_RECALIBRATE => self.seek_to(0, true),
CMD_SENSE_INTERRUPT => self.sense_interrupt(),
CMD_SEEK => self.seek_to(self.param(1), false),
CMD_READ_DATA => self.start_transfer(Dir::ToCpu),
CMD_WRITE_DATA => self.start_transfer(Dir::ToDevice),
CMD_READ_ID => self.read_id(),
CMD_FORMAT_TRACK => self.start_format(),
_ => self.invalid(),
}
}
fn sense_drive(&mut self) {
let unit = self.param(0) & 0x03;
let head = (self.param(0) >> 2) & 1;
let mut st3 = unit | ST3_TS;
if head != 0 {
st3 |= ST3_HD;
}
if self.pcn[unit as usize] == 0 {
st3 |= ST3_T0;
}
if self.medium(unit) {
st3 |= ST3_RY;
if self.readonly {
st3 |= ST3_WP;
}
}
self.st3 = st3;
self.results.clear();
self.results.push_back(st3);
self.enter_result(false);
}
fn seek_to(&mut self, cylinder: u8, recalibrate: bool) {
let unit = self.param(0) & 0x03;
let head = if recalibrate {
0
} else {
(self.param(0) >> 2) & 1
};
let mut st0 = ST0_SE | unit;
if head != 0 {
st0 |= ST0_HD;
}
if self.medium(unit) {
self.pcn[unit as usize] = cylinder;
self.changed[unit as usize] = false;
} else {
self.pcn[unit as usize] = if recalibrate { 0 } else { cylinder };
st0 |= ST0_ABNORMAL | ST0_NR;
if recalibrate {
st0 |= ST0_EC;
}
}
self.st0 = st0;
self.seek_st0[unit as usize] = st0;
self.seeking |= 1 << unit;
self.irq = true;
self.phase = Phase::Idle;
}
fn sense_interrupt(&mut self) {
self.irq = false;
self.results.clear();
if self.reset_senses > 0 {
let unit = DRIVES as u8 - self.reset_senses;
self.reset_senses -= 1;
let st0 = ST0_READY_CHANGED | unit;
self.st0 = st0;
self.results.push_back(st0);
self.results.push_back(self.pcn[unit as usize]);
self.enter_result(false);
} else if self.seeking != 0 {
let unit = self.seeking.trailing_zeros() as u8;
self.seeking &= !(1 << unit);
let st0 = self.seek_st0[unit as usize];
self.st0 = st0;
self.results.push_back(st0);
self.results.push_back(self.pcn[unit as usize]);
self.enter_result(false);
} else {
self.invalid();
}
}
fn read_id(&mut self) {
let unit = self.param(0) & 0x03;
let head = (self.param(0) >> 2) & 1;
let c = self.pcn[unit as usize];
if !self.medium(unit) || self.lba(c, head, 1).is_none() {
let st0 = ST0_ABNORMAL | ST0_NR | (head << 2) | unit;
self.finish_results(st0, ST1_ND, 0, c, head, 1, N_512);
return;
}
let st0 = (head << 2) | unit;
self.finish_results(st0, 0, 0, c, head, 1, N_512);
}
#[allow(clippy::too_many_arguments)]
fn finish_results(&mut self, st0: u8, st1: u8, st2: u8, c: u8, h: u8, r: u8, n: u8) {
self.st0 = st0;
self.st1 = st1;
self.st2 = st2;
self.results.clear();
for byte in [st0, st1, st2, c, h, r, n] {
self.results.push_back(byte);
}
self.enter_result(true);
}
fn start_transfer(&mut self, dir: Dir) {
let unit = self.param(0) & 0x03;
let head = (self.param(0) >> 2) & 1;
let (c, r, n, eot) = (self.param(1), self.param(3), self.param(4), self.param(5));
let base = (head << 2) | unit;
if !self.medium(unit) {
self.finish_results(ST0_ABNORMAL | ST0_NR | base, 0, 0, c, head, r, n);
return;
}
if dir == Dir::ToDevice && self.readonly {
self.finish_results(ST0_ABNORMAL | base, ST1_NW, 0, c, head, r, n);
return;
}
if n != N_512 {
self.finish_results(ST0_ABNORMAL | base, ST1_ND, 0, c, head, r, n);
return;
}
self.xfer = Some(Xfer {
dir,
drive: unit,
c,
h: head,
r,
n,
eot,
mt: self.command & CMD_MT != 0,
filler: 0,
});
if !self.load_sector() {
self.finish_transfer(false, ST1_ND);
return;
}
self.phase = Phase::Execution;
if self.non_dma() {
self.irq = true;
}
}
fn start_format(&mut self) {
let unit = self.param(0) & 0x03;
let head = (self.param(0) >> 2) & 1;
let (n, sectors, filler) = (self.param(1), self.param(2), self.param(4));
let base = (head << 2) | unit;
let c = self.pcn[unit as usize];
if !self.medium(unit) {
self.finish_results(ST0_ABNORMAL | ST0_NR | base, 0, 0, c, head, 1, n);
return;
}
if self.readonly {
self.finish_results(ST0_ABNORMAL | base, ST1_NW, 0, c, head, 1, n);
return;
}
if n != N_512 || sectors == 0 {
self.finish_results(ST0_ABNORMAL | base, ST1_ND, 0, c, head, 1, n);
return;
}
self.xfer = Some(Xfer {
dir: Dir::Format,
drive: unit,
c,
h: head,
r: 1,
n,
eot: sectors,
mt: false,
filler,
});
self.buf = alloc::vec![0u8; usize::from(sectors) * 4];
self.pos = 0;
self.phase = Phase::Execution;
if self.non_dma() {
self.irq = true;
}
}
fn load_sector(&mut self) -> bool {
let Some(x) = self.xfer else { return false };
let Some((start, end)) = self.sector_range(x.c, x.h, x.r) else {
return false;
};
self.pos = 0;
if x.dir == Dir::ToDevice {
self.buf = alloc::vec![0u8; SECTOR_LEN as usize];
} else {
self.buf = self.image[start..end].to_vec();
}
true
}
fn flush_sector(&mut self) {
let Some(x) = self.xfer else { return };
if x.dir != Dir::ToDevice || self.readonly {
return;
}
let Some((start, end)) = self.sector_range(x.c, x.h, x.r) else {
return;
};
let len = core::cmp::min(self.buf.len(), end - start);
self.image[start..start + len].copy_from_slice(&self.buf[..len]);
self.dirty = true;
}
fn next_address(x: &Xfer) -> (u8, u8, u8) {
if x.r < x.eot {
(x.c, x.h, x.r.wrapping_add(1))
} else if x.mt && x.h == 0 {
(x.c, 1, 1)
} else if x.mt {
(x.c.wrapping_add(1), 0, 1)
} else {
(x.c.wrapping_add(1), x.h, 1)
}
}
fn advance_sector(&mut self) {
let Some(x) = self.xfer.as_mut() else { return };
if x.r < x.eot {
x.r = x.r.wrapping_add(1);
} else if x.mt && x.h == 0 {
x.h = 1;
x.r = 1;
} else {
self.finish_transfer(false, ST1_EN);
return;
}
if !self.load_sector() {
self.finish_transfer(false, ST1_ND);
}
}
fn finish_transfer(&mut self, normal: bool, st1: u8) {
let Some(x) = self.xfer.take() else { return };
let (c, h, r) = Self::next_address(&x);
let mut st0 = (x.h << 2) | x.drive;
if !normal {
st0 |= ST0_ABNORMAL;
}
self.buf.clear();
self.pos = 0;
self.finish_results(st0, st1, 0, c, h, r, x.n);
}
fn apply_format(&mut self) {
let Some(x) = self.xfer else { return };
let ids = core::mem::take(&mut self.buf);
if !self.readonly {
for id in ids.as_chunks::<4>().0 {
if let Some((start, end)) = self.sector_range(id[0], id[1], id[2]) {
self.image[start..end].fill(x.filler);
self.dirty = true;
}
}
}
self.buf = ids;
self.finish_transfer(true, 0);
}
fn step(&mut self, terminal: bool) {
let Some(x) = self.xfer else { return };
if x.dir == Dir::Format {
if terminal || self.pos >= self.buf.len() as u64 {
self.apply_format();
}
return;
}
if terminal {
self.finish_transfer(true, 0);
return;
}
if self.pos >= self.buf.len() as u64 {
self.advance_sector();
}
}
fn take_byte(&mut self, terminal: bool) -> u8 {
let byte = self.buf.get(self.pos as usize).copied().unwrap_or(0xff);
self.pos += 1;
self.step(terminal);
byte
}
fn put_byte(&mut self, byte: u8, terminal: bool) {
if let Some(slot) = self.buf.get_mut(self.pos as usize) {
*slot = byte;
self.pos += 1;
}
let full = self.pos >= self.buf.len() as u64;
if (full || terminal) && matches!(self.xfer.map(|x| x.dir), Some(Dir::ToDevice)) {
self.flush_sector();
}
self.step(terminal);
}
fn non_dma_execution(&self, dir: Dir) -> bool {
self.phase == Phase::Execution
&& self.non_dma()
&& matches!(self.xfer.map(|x| x.dir), Some(d) if d == dir)
}
fn read_data(&mut self, debug: bool) -> u8 {
match self.phase {
Phase::Result => {
if debug {
return self.results.front().copied().unwrap_or(0xff);
}
let byte = self.results.pop_front().unwrap_or(0xff);
self.irq = false;
if self.results.is_empty() {
self.phase = Phase::Idle;
}
byte
}
Phase::Execution if self.non_dma_execution(Dir::ToCpu) => {
if debug {
return self.buf.get(self.pos as usize).copied().unwrap_or(0xff);
}
let byte = self.take_byte(false);
self.irq = self.phase == Phase::Execution;
byte
}
_ => 0xff,
}
}
fn write_data(&mut self, byte: u8) {
match self.phase {
Phase::Idle => self.start_command(byte),
Phase::Command => {
self.params.push(byte);
if self.params.len() >= usize::from(self.params_needed) {
self.execute();
}
}
Phase::Execution
if self.non_dma_execution(Dir::ToDevice) || self.non_dma_execution(Dir::Format) =>
{
self.put_byte(byte, false);
self.irq = self.phase == Phase::Execution;
}
_ => {}
}
}
fn write_dor(&mut self, value: u8) {
let was = self.dor;
self.dor = value;
if was & DOR_RESET != 0 && value & DOR_RESET == 0 {
self.reset_controller();
} else if was & DOR_RESET == 0 && value & DOR_RESET != 0 {
self.reset_controller();
self.reset_senses = DRIVES as u8;
self.irq = true;
}
}
fn dir_register(&self) -> u8 {
let unit = (self.dor & DOR_DRIVE) as usize;
let changed = self.changed[unit] || !self.medium(unit as u8);
if changed { 0xff } else { 0x7f }
}
}
struct Registers {
state: Mutex<State>,
irq_out: Mutex<Option<WireSource>>,
drq_out: Mutex<Option<WireSource>>,
}
impl fmt::Debug for Registers {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut s = f.debug_struct("Registers");
match self.state.try_lock() {
Some(state) => s.field("state", &*state).finish(),
None => s.field("state", &"<in use>").finish(),
}
}
}
impl Registers {
fn drive(out: &Mutex<Option<WireSource>>, asserted: bool) {
let pin = out.lock().clone();
if let Some(pin) = pin {
pin.set(Level::from_bool(asserted));
}
}
fn refresh(&self) {
let (irq, drq) = {
let state = self.state.lock();
(state.irq_level(), state.drq_level())
};
Registers::drive(&self.irq_out, irq);
Registers::drive(&self.drq_out, drq);
}
fn read_register(&self, offset: u64, debug: bool) -> u8 {
let mut state = self.state.lock();
match offset {
REG_MSR => state.msr(),
REG_DATA => state.read_data(debug),
REG_DIR_CCR => state.dir_register(),
_ => 0xff,
}
}
fn write_register(&self, offset: u64, value: u8) {
{
let mut state = self.state.lock();
match offset {
REG_DOR => state.write_dor(value),
REG_DATA => state.write_data(value),
REG_DIR_CCR => state.ccr = value,
_ => {}
}
}
self.refresh();
}
}
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 & 7, attrs.debug);
if !attrs.debug {
self.refresh();
}
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);
}
self.write_register(offset & 7, *value);
Ok(())
}
fn constraints(&self) -> AccessConstraints {
AccessConstraints::word(Width::U8, Endian::Little)
}
}
impl DmaPeripheral for Registers {
fn dma_read(&self, terminal: bool) -> u8 {
let byte = {
let mut state = self.state.lock();
if !matches!(state.xfer.map(|x| x.dir), Some(Dir::ToCpu))
|| state.phase != Phase::Execution
|| state.non_dma()
{
return 0xff;
}
state.take_byte(terminal)
};
self.refresh();
byte
}
fn dma_write(&self, byte: u8, terminal: bool) {
{
let mut state = self.state.lock();
if !matches!(
state.xfer.map(|x| x.dir),
Some(Dir::ToDevice) | Some(Dir::Format)
) || state.phase != Phase::Execution
|| state.non_dma()
{
return;
}
state.put_byte(byte, terminal);
}
self.refresh();
}
fn dma_ready(&self) -> bool {
self.state.lock().drq_level()
}
}
#[derive(Debug)]
pub struct Fdc765 {
regs: Arc<Registers>,
region: RegionRef,
}
impl Fdc765 {
pub fn new(props: &Props) -> Result<Fdc765> {
let mut r = props.reader();
let media = r.optional_media("image")?;
let geometry: String = r.or("geometry", String::from("auto"))?;
let readonly: bool = r.or("readonly", false)?;
r.finish()?;
let (name, image) = match media {
Some(m) => (m.name().to_string(), m.bytes().to_vec()),
None => (String::from("<none>"), Vec::new()),
};
Fdc765::with_image(name, image, &geometry, readonly)
}
pub fn with_image(
name: String,
image: Vec<u8>,
geometry: &str,
readonly: bool,
) -> Result<Fdc765> {
let geom = if image.is_empty() {
Geometry {
cylinders: 0,
heads: 0,
sectors: 0,
}
} else {
parse_geometry(geometry, &name, image.len() as u64)?
};
let regs = Arc::new(Registers {
state: Mutex::with_rank(LockRank::DEVICE, State::new(image, geom, readonly)),
irq_out: Mutex::with_rank(LockRank::LEAF, None),
drq_out: Mutex::with_rank(LockRank::LEAF, None),
});
let region: RegionRef = Arc::new(Region::io(
CLASS_NAME,
REGISTER_WINDOW_LEN,
Arc::clone(®s) as Arc<dyn MemOps>,
));
Ok(Fdc765 { regs, region })
}
#[must_use]
pub fn contents(&self) -> Vec<u8> {
self.regs.state.lock().image.clone()
}
#[must_use]
pub fn is_dirty(&self) -> bool {
self.regs.state.lock().dirty
}
#[must_use]
pub fn irq_asserted(&self) -> bool {
self.regs.state.lock().irq_level()
}
#[must_use]
pub fn geometry(&self) -> (u8, u8, u8) {
let g = self.regs.state.lock().geom;
(g.cylinders, g.heads, g.sectors)
}
}
pub static CLASS: DeviceClass = DeviceClass {
name: CLASS_NAME,
version: STATE_VERSION,
summary: "NEC uPD765A floppy disk controller, with DMA and non-DMA transfer",
properties: &[
PropertySpec {
name: "image",
kind: ValueKind::Media,
required: false,
summary: "the media slot a raw sector image is bound to; absent means an empty drive",
},
PropertySpec {
name: "geometry",
kind: ValueKind::Str,
required: false,
summary: "\"auto\" (default, from the image length) or \"cylinders/heads/sectors\"",
},
PropertySpec {
name: "readonly",
kind: ValueKind::Bool,
required: false,
summary: "write protect the medium: a write sets ST1's not-writable bit \
(default false)",
},
],
construct: |props| Ok(Box::new(Fdc765::new(props)?)),
};
impl Device for Fdc765 {
fn class(&self) -> &'static DeviceClass {
&CLASS
}
fn realize(&self, _ctx: &mut RealizeCtx<'_>) -> Result<()> {
Ok(())
}
fn reset(&self, _kind: ResetKind) {
{
let mut state = self.regs.state.lock();
state.reset_controller();
state.dor = 0;
state.ccr = 0;
state.pcn = [0; DRIVES];
state.changed = [true; DRIVES];
}
self.regs.refresh();
}
fn region(&self, name: &str) -> Option<RegionRef> {
matches!(name, "" | "regs").then(|| Arc::clone(&self.region))
}
fn connect(&self, port: &str, source: WireSource) -> Result<()> {
match port {
"irq" => *self.regs.irq_out.lock() = Some(source),
"drq" => *self.regs.drq_out.lock() = Some(source),
_ => {
return Err(Error::Config {
at: port.to_string(),
message: String::from("a uPD765 drives two pins, `irq` and `drq`"),
});
}
}
Ok(())
}
fn dma_peripheral(&self, port: &str) -> Option<Arc<dyn DmaPeripheral>> {
(port == "drq").then(|| Arc::clone(&self.regs) as Arc<dyn DmaPeripheral>)
}
fn announce(&self, port: &str) {
if matches!(port, "irq" | "drq") {
self.regs.refresh();
}
}
fn save(&self, w: &mut ChunkWriter<'_>) -> Result<()> {
let state = self.regs.state.lock();
w.write_u8(state.phase.as_u8())?;
w.write_u8(state.command)?;
w.write_seq_len(state.params.len() as u64)?;
for byte in &state.params {
w.write_u8(*byte)?;
}
w.write_u8(state.params_needed)?;
w.write_seq_len(state.results.len() as u64)?;
for byte in &state.results {
w.write_u8(*byte)?;
}
w.write_u8(state.dor)?;
w.write_u8(state.ccr)?;
w.write_u8(state.specify[0])?;
w.write_u8(state.specify[1])?;
for unit in 0..DRIVES {
w.write_u8(state.pcn[unit])?;
w.write_u8(state.seek_st0[unit])?;
w.write_bool(state.changed[unit])?;
}
w.write_u8(state.seeking)?;
w.write_u8(state.reset_senses)?;
w.write_u8(state.st0)?;
w.write_u8(state.st1)?;
w.write_u8(state.st2)?;
w.write_u8(state.st3)?;
w.write_bool(state.irq)?;
w.write_bool(state.dirty)?;
w.write_seq_len(state.buf.len() as u64)?;
w.write_all(&state.buf)?;
w.write_u64(state.pos)?;
match state.xfer {
None => w.write_bool(false)?,
Some(x) => {
w.write_bool(true)?;
w.write_u8(x.dir.as_u8())?;
for byte in [x.drive, x.c, x.h, x.r, x.n, x.eot, x.filler] {
w.write_u8(byte)?;
}
w.write_bool(x.mt)?;
}
}
Ok(())
}
fn load(&self, r: &mut ChunkReader<'_>) -> Result<()> {
let phase = Phase::from_u8(r.read_u8()?)?;
let command = r.read_u8()?;
let param_count = r.read_seq_len(1)?;
if param_count > 8 {
return Err(Error::State(format!(
"snapshot has {param_count} command parameter(s); no uPD765 command takes over 8"
)));
}
let mut params = Vec::with_capacity(param_count as usize);
for _ in 0..param_count {
params.push(r.read_u8()?);
}
let params_needed = r.read_u8()?;
let result_count = r.read_seq_len(1)?;
if result_count > 7 {
return Err(Error::State(format!(
"snapshot has {result_count} result byte(s); no uPD765 command returns over 7"
)));
}
let mut results = VecDeque::with_capacity(result_count as usize);
for _ in 0..result_count {
results.push_back(r.read_u8()?);
}
let dor = r.read_u8()?;
let ccr = r.read_u8()?;
let specify = [r.read_u8()?, r.read_u8()?];
let mut pcn = [0u8; DRIVES];
let mut seek_st0 = [0u8; DRIVES];
let mut changed = [false; DRIVES];
for unit in 0..DRIVES {
pcn[unit] = r.read_u8()?;
seek_st0[unit] = r.read_u8()?;
changed[unit] = r.read_bool()?;
}
let seeking = r.read_u8()?;
let reset_senses = r.read_u8()?;
if reset_senses > DRIVES as u8 {
return Err(Error::State(format!(
"snapshot owes {reset_senses} reset senses for {DRIVES} drives"
)));
}
let st0 = r.read_u8()?;
let st1 = r.read_u8()?;
let st2 = r.read_u8()?;
let st3 = r.read_u8()?;
let irq = r.read_bool()?;
let dirty = r.read_bool()?;
let buf_len = r.read_seq_len(1)?;
let buf = r.take(buf_len as usize)?.to_vec();
let pos = r.read_u64()?;
let xfer = if r.read_bool()? {
let dir = Dir::from_u8(r.read_u8()?)?;
let drive = r.read_u8()?;
let c = r.read_u8()?;
let h = r.read_u8()?;
let sector = r.read_u8()?;
let n = r.read_u8()?;
let eot = r.read_u8()?;
let filler = r.read_u8()?;
let mt = r.read_bool()?;
Some(Xfer {
dir,
drive,
c,
h,
r: sector,
n,
eot,
mt,
filler,
})
} else {
None
};
{
let mut state = self.regs.state.lock();
state.phase = phase;
state.command = command;
state.params = params;
state.params_needed = params_needed;
state.results = results;
state.dor = dor;
state.ccr = ccr;
state.specify = specify;
state.pcn = pcn;
state.seek_st0 = seek_st0;
state.changed = changed;
state.seeking = seeking;
state.reset_senses = reset_senses;
state.st0 = st0;
state.st1 = st1;
state.st2 = st2;
state.st3 = st3;
state.irq = irq;
state.dirty = dirty;
state.buf = buf;
state.pos = pos;
state.xfer = xfer;
}
self.regs.refresh();
Ok(())
}
}
impl Instance for Fdc765 {}
pub fn register(registry: &mut crate::core::Registry) -> Result<()> {
registry.add(&CLASS)
}
pub fn bind(bindings: &mut crate::machine::Bindings) -> Result<()> {
bindings.bind(CLASS_NAME, |props| Ok(Arc::new(Fdc765::new(props)?)))
}
#[must_use]
pub fn schema() -> ClassSchema {
ClassSchema::new(CLASS_NAME)
.prop(PropSchema::new("image", ValueKind::Media))
.prop(PropSchema::new("geometry", ValueKind::Str))
.prop(PropSchema::new("readonly", ValueKind::Bool))
.region("")
.region("regs")
.port("irq", PortDir::Out)
.port("drq", PortDir::Out)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::state::{MachineShape, Migrations, StateReader, StateWriter};
use crate::core::sync::{AtomicU32, Ordering};
use crate::core::wire::{Wire, WireId, WireIdAllocator, WireSink};
fn image_1440k() -> Vec<u8> {
let mut image = alloc::vec![0u8; 1_474_560];
for (lba, sector) in image.as_chunks_mut::<512>().0.iter_mut().enumerate() {
for (i, byte) in sector.iter_mut().enumerate() {
*byte = (lba as u8) ^ (i as u8);
}
}
image
}
fn sector_of(image: &[u8], lba: usize) -> Vec<u8> {
image[lba * 512..(lba + 1) * 512].to_vec()
}
#[derive(Debug, Default)]
struct Probe {
level: AtomicU32,
}
impl Probe {
fn high(&self) -> bool {
self.level.load(Ordering::Relaxed) != 0
}
}
impl WireSink for Probe {
fn set_level(&self, _src: WireId, _line: u32, level: Level) {
self.level
.store(u32::from(level.is_high()), Ordering::Relaxed);
}
}
struct Rig {
fdc: Fdc765,
irq: Arc<Probe>,
drq: Arc<Probe>,
}
fn rig_with(image: Vec<u8>, readonly: bool) -> Rig {
let fdc = Fdc765::with_image("test".to_string(), image, "auto", readonly)
.expect("a standard geometry");
let ids = WireIdAllocator::new();
let irq = Arc::new(Probe::default());
let drq = Arc::new(Probe::default());
for (port, probe) in [("irq", Arc::clone(&irq)), ("drq", Arc::clone(&drq))] {
let id = ids.alloc();
let wire = Wire::builder()
.source(id)
.sink(probe as Arc<dyn WireSink>, 0)
.build_shared();
fdc.connect(port, WireSource::new(wire, id))
.expect("both pins exist");
}
Rig { fdc, irq, drq }
}
fn rig() -> Rig {
rig_with(image_1440k(), false)
}
impl Rig {
fn peek(&self, offset: u64) -> u8 {
let mut byte = [0u8; 1];
self.fdc
.regs
.read(offset, &mut byte, MemAttrs::DEFAULT)
.expect("a byte read is legal");
byte[0]
}
fn poke(&self, offset: u64, value: u8) {
self.fdc
.regs
.write(offset, &[value], MemAttrs::DEFAULT)
.expect("a byte write is legal");
}
fn msr(&self) -> u8 {
self.peek(REG_MSR)
}
fn power_on(&self) {
self.poke(REG_DOR, 0x00);
self.poke(REG_DOR, 0x1c);
for _ in 0..DRIVES {
self.command(&[CMD_SENSE_INTERRUPT]);
let _ = self.results();
}
}
fn command(&self, bytes: &[u8]) {
for byte in bytes {
assert_eq!(
self.msr() & (MSR_RQM | MSR_DIO),
MSR_RQM,
"the controller must be asking for a command byte"
);
self.poke(REG_DATA, *byte);
}
}
fn results(&self) -> Vec<u8> {
let mut out = Vec::new();
while self.msr() & (MSR_RQM | MSR_DIO | MSR_CB) == (MSR_RQM | MSR_DIO | MSR_CB) {
out.push(self.peek(REG_DATA));
}
out
}
fn dma_in(&self, count: usize) -> Vec<u8> {
let peer = self
.fdc
.dma_peripheral("drq")
.expect("the controller offers one on drq");
let mut out = Vec::with_capacity(count);
for i in 0..count {
assert!(peer.dma_ready(), "DRQ must still be asserted at byte {i}");
out.push(peer.dma_read(i + 1 == count));
}
out
}
fn dma_out(&self, bytes: &[u8]) {
let peer = self
.fdc
.dma_peripheral("drq")
.expect("the controller offers one on drq");
for (i, byte) in bytes.iter().enumerate() {
assert!(peer.dma_ready(), "DRQ must still be asserted at byte {i}");
peer.dma_write(*byte, i + 1 == bytes.len());
}
}
}
#[test]
fn a_reset_reports_each_drive_and_then_says_invalid() {
let rig = rig();
rig.poke(REG_DOR, 0x00);
rig.poke(REG_DOR, 0x0c);
assert!(rig.irq.high(), "the reset interrupts");
assert_eq!(rig.msr(), MSR_RQM, "and the chip is ready for a command");
for unit in 0..4u8 {
rig.command(&[CMD_SENSE_INTERRUPT]);
let out = rig.results();
assert_eq!(out.len(), 2, "ST0 and the present cylinder");
assert_eq!(out[0], ST0_READY_CHANGED | unit);
}
assert!(!rig.irq.high(), "sensing the interrupt drops it");
rig.command(&[CMD_SENSE_INTERRUPT]);
assert_eq!(
rig.results(),
alloc::vec![ST0_INVALID],
"and then there is nothing left to say"
);
}
#[test]
fn holding_the_chip_in_reset_forgets_a_command_in_progress() {
let rig = rig();
rig.power_on();
rig.poke(REG_DATA, 0x46); assert_eq!(rig.msr() & MSR_CB, MSR_CB);
rig.poke(REG_DOR, 0x10); assert_eq!(rig.msr(), MSR_RQM, "back to idle with nothing pending");
}
#[test]
fn recalibrate_seeks_to_zero_and_interrupts() {
let rig = rig();
rig.power_on();
rig.command(&[CMD_SEEK, 0x00, 40]);
rig.command(&[CMD_SENSE_INTERRUPT]);
assert_eq!(rig.results()[1], 40, "the head moved out to cylinder 40");
rig.command(&[CMD_RECALIBRATE, 0x00]);
assert!(rig.irq.high(), "a recalibrate ends with an interrupt");
assert_eq!(rig.msr() & 0x0f, 0x01, "drive 0 is in the seek mode");
assert_eq!(rig.msr() & MSR_DIO, 0);
rig.command(&[CMD_SENSE_INTERRUPT]);
assert_eq!(
rig.results(),
alloc::vec![ST0_SE, 0],
"seek end on drive 0, present cylinder 0"
);
assert!(!rig.irq.high(), "and the interrupt is dropped");
assert_eq!(rig.msr() & 0x0f, 0, "and the seek bit with it");
}
#[test]
fn seek_reports_the_cylinder_it_was_given() {
let rig = rig();
rig.power_on();
rig.command(&[CMD_SEEK, 0x04, 17]); assert!(rig.irq.high());
rig.command(&[CMD_SENSE_INTERRUPT]);
let out = rig.results();
assert_eq!(out[0], ST0_SE | ST0_HD, "seek end, head 1");
assert_eq!(out[1], 17);
}
#[test]
fn read_data_delivers_the_first_sector_through_dma() {
let image = image_1440k();
let rig = rig_with(image.clone(), false);
rig.power_on();
rig.command(&[CMD_SPECIFY, 0xdf, 0x02]);
rig.command(&[0x46, 0x00, 0, 0, 1, 2, 18, 0x1b, 0xff]);
assert!(rig.drq.high(), "the execution phase asks for service");
assert_eq!(rig.msr() & (MSR_CB | MSR_RQM | MSR_NDMA), MSR_CB);
let got = rig.dma_in(512);
assert_eq!(got, sector_of(&image, 0));
assert!(!rig.drq.high(), "and DRQ drops when the count expires");
assert!(rig.irq.high(), "the result phase interrupts");
assert_eq!(rig.results(), alloc::vec![0x00, 0x00, 0x00, 0, 0, 2, 2]);
assert!(!rig.irq.high(), "reading the first result byte drops it");
}
#[test]
fn a_multitrack_read_crosses_to_the_second_head() {
let image = image_1440k();
let rig = rig_with(image.clone(), false);
rig.power_on();
rig.command(&[CMD_MT | 0x46, 0x00, 0, 0, 18, 2, 18, 0x1b, 0xff]);
let got = rig.dma_in(1024);
assert_eq!(got[..512], sector_of(&image, 17)[..]);
assert_eq!(got[512..], sector_of(&image, 18)[..]);
let out = rig.results();
assert_eq!(out[0], ST0_HD, "the command ended on head 1");
assert_eq!(&out[3..], &[0, 1, 2, 2], "cylinder 0, head 1, sector 2");
}
#[test]
fn a_read_that_runs_off_the_track_without_a_terminal_count_ends_abnormally() {
let rig = rig();
rig.power_on();
rig.command(&[0x46, 0x00, 0, 0, 1, 2, 1, 0x1b, 0xff]);
let peer = rig.fdc.dma_peripheral("drq").expect("a peripheral on drq");
for _ in 0..512 {
peer.dma_read(false);
}
let out = rig.results();
assert_eq!(out[0], ST0_ABNORMAL);
assert_eq!(out[1], ST1_EN);
}
#[test]
fn write_data_changes_the_image_and_reads_back() {
let image = image_1440k();
let rig = rig_with(image.clone(), false);
rig.power_on();
let payload: Vec<u8> = (0..512u32).map(|i| (i as u8).wrapping_mul(3)).collect();
rig.command(&[0x45, 0x00, 0, 0, 3, 2, 18, 0x1b, 0xff]);
rig.dma_out(&payload);
assert_eq!(rig.results()[0], 0x00, "a normal termination");
assert!(rig.fdc.is_dirty());
assert_eq!(sector_of(&rig.fdc.contents(), 2), payload);
assert_eq!(
sector_of(&rig.fdc.contents(), 1),
sector_of(&image, 1),
"and no neighbour was touched"
);
rig.command(&[0x46, 0x00, 0, 0, 3, 2, 18, 0x1b, 0xff]);
assert_eq!(rig.dma_in(512), payload);
assert_eq!(rig.results()[0], 0x00);
}
#[test]
fn a_write_to_a_readonly_medium_sets_write_protect() {
let image = image_1440k();
let rig = rig_with(image.clone(), true);
rig.power_on();
rig.command(&[0x45, 0x00, 0, 0, 1, 2, 18, 0x1b, 0xff]);
let out = rig.results();
assert_eq!(out[0], ST0_ABNORMAL);
assert_eq!(out[1] & ST1_NW, ST1_NW, "ST1's not-writable bit");
assert!(!rig.fdc.is_dirty());
assert_eq!(rig.fdc.contents(), image, "and the medium is untouched");
rig.command(&[CMD_SENSE_DRIVE, 0x00]);
let st3 = rig.results();
assert_eq!(st3.len(), 1, "SENSE DRIVE STATUS returns one byte");
assert_eq!(
st3[0] & (ST3_WP | ST3_RY | ST3_T0),
ST3_WP | ST3_RY | ST3_T0
);
}
#[test]
fn an_unrecognised_command_answers_with_one_invalid_byte() {
let rig = rig();
rig.power_on();
for opcode in [0x00u8, 0x01, 0x1a, 0x9c] {
rig.command(&[opcode]);
assert_eq!(
rig.results(),
alloc::vec![ST0_INVALID],
"opcode {opcode:#04x} is not a uPD765 command"
);
assert!(!rig.irq.high(), "and an invalid command does not interrupt");
}
}
#[test]
fn read_id_answers_the_header_under_the_head() {
let rig = rig();
rig.power_on();
rig.command(&[CMD_SEEK, 0x00, 5]);
rig.command(&[CMD_SENSE_INTERRUPT]);
let _ = rig.results();
rig.command(&[0x4a, 0x00]);
let out = rig.results();
assert_eq!(out.len(), 7);
assert_eq!(out[0], 0x00, "a normal termination");
assert_eq!(&out[3..], &[5, 0, 1, N_512], "cylinder 5, head 0, sector 1");
}
#[test]
fn a_drive_with_no_disk_reports_not_ready() {
let rig = rig_with(Vec::new(), false);
rig.power_on();
rig.command(&[0x46, 0x00, 0, 0, 1, 2, 18, 0x1b, 0xff]);
let out = rig.results();
assert_eq!(out[0] & ST0_NR, ST0_NR, "not ready, rather than a fault");
assert_eq!(out[0] & 0xc0, ST0_ABNORMAL);
rig.command(&[CMD_SENSE_DRIVE, 0x00]);
assert_eq!(rig.results()[0] & ST3_RY, 0, "and the drive says so");
assert_eq!(
rig.peek(REG_DIR_CCR) & 0x80,
0x80,
"an empty drive reads as changed"
);
}
#[test]
fn a_debug_read_does_not_advance_the_result_phase() {
let rig = rig();
rig.power_on();
rig.command(&[0x4a, 0x00]);
assert!(rig.irq.high());
let mut byte = [0u8; 1];
rig.fdc
.regs
.read(REG_DATA, &mut byte, MemAttrs::DEBUG)
.expect("a debug read is legal");
assert_eq!(byte[0], 0x00, "ST0, peeked");
assert!(rig.irq.high(), "and the interrupt is still asserted");
assert_eq!(
rig.msr() & (MSR_RQM | MSR_DIO | MSR_CB),
MSR_RQM | MSR_DIO | MSR_CB,
"still in the result phase"
);
assert_eq!(rig.results().len(), 7, "with every byte still there");
assert!(
rig.fdc
.regs
.write(REG_DOR, &[0x00], MemAttrs::DEBUG)
.is_err()
);
}
#[test]
fn non_dma_mode_moves_a_sector_through_the_data_register() {
let image = image_1440k();
let rig = rig_with(image.clone(), false);
rig.power_on();
rig.poke(REG_DOR, 0x14);
rig.command(&[0x46, 0x00, 0, 0, 1, 2, 1, 0x1b, 0xff]);
assert!(!rig.drq.high(), "no DRQ with the gate closed");
let mut got = Vec::new();
while rig.msr() & (MSR_NDMA | MSR_RQM | MSR_DIO) == (MSR_NDMA | MSR_RQM | MSR_DIO) {
got.push(rig.peek(REG_DATA));
}
assert_eq!(got, sector_of(&image, 0));
let out = rig.results();
assert_eq!(out[0], ST0_ABNORMAL);
assert_eq!(out[1], ST1_EN);
rig.poke(REG_DOR, 0x1c);
rig.command(&[CMD_SPECIFY, 0xdf, 0x03]);
rig.command(&[0x46, 0x00, 0, 0, 1, 2, 1, 0x1b, 0xff]);
assert!(rig.irq.high(), "a byte is waiting");
assert_eq!(rig.peek(REG_DATA), image[0]);
assert!(rig.irq.high(), "and so is the next one");
while rig.msr() & MSR_NDMA != 0 {
let _ = rig.peek(REG_DATA);
}
let _ = rig.results();
rig.command(&[CMD_SPECIFY, 0xdf, 0x03]);
rig.command(&[0x45, 0x00, 0, 0, 2, 2, 2, 0x1b, 0xff]);
assert_eq!(
rig.msr() & (MSR_NDMA | MSR_DIO),
MSR_NDMA,
"the controller wants bytes rather than offering them"
);
for i in 0..512u32 {
rig.poke(REG_DATA, (i as u8) ^ 0x5a);
}
let _ = rig.results();
let written = sector_of(&rig.fdc.contents(), 1);
assert_eq!(written[0], 0x5a);
assert_eq!(written[511], 0xffu8 ^ 0x5a);
}
#[test]
fn format_track_fills_the_track_and_does_not_hang() {
let rig = rig();
rig.power_on();
rig.command(&[CMD_FORMAT_TRACK, 0x00, 2, 18, 0x54, 0xf6]);
let mut ids = Vec::new();
for sector in 1..=18u8 {
ids.extend_from_slice(&[0, 0, sector, 2]);
}
rig.dma_out(&ids);
let out = rig.results();
assert_eq!(out.len(), 7);
assert_eq!(out[0], 0x00);
let contents = rig.fdc.contents();
assert!(contents[..18 * 512].iter().all(|b| *b == 0xf6));
assert_eq!(
sector_of(&contents, 18),
sector_of(&image_1440k(), 18),
"and the second head's track is untouched"
);
}
#[test]
fn an_image_of_no_known_geometry_is_refused_by_name() {
let e = Fdc765::with_image("odd".to_string(), alloc::vec![0u8; 1000], "auto", false)
.expect_err("1000 bytes is no floppy");
let text = e.to_string();
assert!(text.contains("1000"), "{text}");
assert!(
text.contains("1474560"),
"and it lists what would have worked: {text}"
);
assert!(Fdc765::with_image("odd".to_string(), image_1440k(), "80/2/9", false).is_err());
let fdc = Fdc765::with_image("ok".to_string(), image_1440k(), "80/2/18", false)
.expect("an explicit geometry");
assert_eq!(fdc.geometry(), (80, 2, 18));
}
#[test]
fn the_undecoded_offsets_read_as_ones() {
let rig = rig();
for offset in [0, 1, 2, 3, 6] {
assert_eq!(rig.peek(offset), 0xff, "offset {offset} is not decoded");
}
}
#[test]
fn an_access_that_is_not_a_single_byte_is_refused() {
let rig = rig();
assert!(
rig.fdc
.regs
.read(REG_MSR, &mut [0u8; 2], MemAttrs::DEFAULT)
.is_err()
);
assert!(
rig.fdc
.regs
.write(REG_DOR, &[0u8; 4], MemAttrs::DEFAULT)
.is_err()
);
}
#[test]
fn properties_are_checked_rather_than_ignored() {
let fdc = Fdc765::new(&Props::new()).expect("an empty drive is a legal machine");
assert_eq!(fdc.geometry(), (0, 0, 0));
assert!(!fdc.irq_asserted());
assert!(Fdc765::new(&Props::new().with("geometery", "auto")).is_err());
let props = Props::new()
.with(
"image",
crate::core::props::Media::new("floppy", image_1440k()),
)
.with("readonly", true);
let fdc = Fdc765::new(&props).expect("a 1.44M image in a bound slot");
assert_eq!(fdc.geometry(), (80, 2, 18));
}
#[test]
fn a_snapshot_round_trips_the_controller_state() {
let saved = rig();
saved.power_on();
saved.command(&[CMD_SPECIFY, 0xdf, 0x02]);
saved.command(&[CMD_SEEK, 0x01, 12]); saved.command(&[0x46, 0x00, 0, 0, 4, 2, 18, 0x1b, 0xff]);
let peer = saved
.fdc
.dma_peripheral("drq")
.expect("a peripheral on drq");
for _ in 0..100 {
peer.dma_read(false);
}
let mut shape = MachineShape::new();
shape.add_device("fdc", CLASS.name).unwrap();
let mut w = StateWriter::new(shape);
{
let mut chunk = w.chunk("fdc", CLASS.name, CLASS.version).unwrap();
saved.fdc.save(&mut chunk).unwrap();
}
let first = w.to_vec().unwrap();
let restored = rig();
let reader = StateReader::new(&first).unwrap();
let chunk = reader
.load("fdc", CLASS.name, CLASS.version, &Migrations::new())
.unwrap();
restored.fdc.load(&mut chunk.reader()).unwrap();
let mut shape = MachineShape::new();
shape.add_device("fdc", CLASS.name).unwrap();
let mut w = StateWriter::new(shape);
{
let mut chunk = w.chunk("fdc", CLASS.name, CLASS.version).unwrap();
restored.fdc.save(&mut chunk).unwrap();
}
assert_eq!(
first,
w.to_vec().unwrap(),
"the same state saves the same bytes"
);
assert_eq!(restored.msr() & MSR_CB, MSR_CB);
assert!(restored.drq.high(), "the transfer is still live");
assert_eq!(
restored.dma_in(412),
sector_of(&image_1440k(), 3)[100..].to_vec()
);
assert_eq!(restored.results()[3..].to_vec(), alloc::vec![0, 0, 5, 2]);
restored.command(&[CMD_SENSE_INTERRUPT]);
assert_eq!(restored.results()[1], 12);
}
}