use alloc::boxed::Box;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::sync::{Arc, Weak};
use alloc::vec::Vec;
use core::fmt;
use crate::core::device::{Device, DeviceClass, PropertySpec, RealizeCtx, ResetKind, SinkPin};
use crate::core::error::{BusError, Error, Result};
use crate::core::props::{Props, ValueKind};
use crate::core::space::{
AccessConstraints, AddressSpace, MemAttrs, MemOps, MemResult, Region, RegionRef, RequesterId,
};
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, FanIn, Level, Resolve, WireId, WireSink, WireSource};
use crate::machine::realize::{BindCtx, Instance};
use crate::machine::validate::ClassSchema;
pub const CLASS_NAME: &str = "pc.dma";
const STATE_VERSION: u32 = 1;
pub const CHANNELS: usize = 4;
pub const REGISTER_WINDOW_LEN: u64 = 16;
pub const WORD_REGISTER_WINDOW_LEN: u64 = 32;
pub const PAGE_WINDOW_LEN: u64 = 16;
pub const MAX_BURST_UNITS: u32 = 65_536;
const CMD_DISABLE: u8 = 0x04;
const CMD_ROTATING: u8 = 0x10;
const CMD_DREQ_ACTIVE_LOW: u8 = 0x40;
const CMD_DACK_ACTIVE_HIGH: u8 = 0x80;
const MODE_CHANNEL: u8 = 0x03;
const MODE_TRANSFER: u8 = 0x0c;
const MODE_AUTOINIT: u8 = 0x10;
const MODE_DECREMENT: u8 = 0x20;
const MODE_SELECT: u8 = 0xc0;
const XFER_VERIFY: u8 = 0;
const XFER_WRITE: u8 = 1;
const XFER_READ: u8 = 2;
const XFER_ILLEGAL: u8 = 3;
const SELECT_DEMAND: u8 = 0;
const SELECT_SINGLE: u8 = 1;
const SELECT_BLOCK: u8 = 2;
const SELECT_CASCADE: u8 = 3;
const PAGE_OFFSET: [u8; 8] = [0x7, 0x3, 0x1, 0x2, 0xf, 0xb, 0x9, 0xa];
#[derive(Debug, Clone, Copy)]
struct Config {
word: bool,
base: u8,
}
impl Config {
fn channel(&self, index: usize) -> u8 {
self.base.wrapping_add(index as u8)
}
fn unit(&self) -> u64 {
if self.word { 2 } else { 1 }
}
fn window(&self) -> u64 {
if self.word {
WORD_REGISTER_WINDOW_LEN
} else {
REGISTER_WINDOW_LEN
}
}
}
#[derive(Debug, Clone, Copy, Default)]
struct Channel {
addr: u16,
base_addr: u16,
count: u16,
base_count: u16,
mode: u8,
masked: bool,
}
#[derive(Debug, Clone)]
struct State {
ch: [Channel; CHANNELS],
command: u8,
tc: u8,
request: u8,
drq: u8,
flipflop: bool,
rotate: u8,
temp: u8,
pages: [u8; 16],
bus: Option<Weak<AddressSpace>>,
requester: RequesterId,
}
impl Default for State {
fn default() -> State {
State {
ch: [Channel {
masked: true,
..Channel::default()
}; CHANNELS],
command: 0,
tc: 0,
request: 0,
drq: 0,
flipflop: false,
rotate: 0,
temp: 0,
pages: [0; 16],
bus: None,
requester: RequesterId::ANONYMOUS,
}
}
}
impl State {
fn master_clear(&mut self) {
self.command = 0;
self.tc = 0;
self.request = 0;
self.flipflop = false;
self.rotate = 0;
self.temp = 0;
for ch in &mut self.ch {
ch.masked = true;
}
}
}
#[derive(Debug, Clone, Copy)]
struct Plan {
phys: u64,
xfer: u8,
terminal: bool,
single: bool,
}
struct Shared {
cfg: Config,
state: Mutex<State>,
peers: [Mutex<Option<Weak<dyn DmaPeripheral>>>; CHANNELS],
dack: [Mutex<Option<WireSource>>; CHANNELS],
eop: Mutex<Option<WireSource>>,
}
impl fmt::Debug for Shared {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut s = f.debug_struct("Shared");
s.field("cfg", &self.cfg);
match self.state.try_lock() {
Some(state) => s.field("state", &*state).finish_non_exhaustive(),
None => s.field("state", &"<in use>").finish_non_exhaustive(),
}
}
}
impl Shared {
fn decode(&self, offset: u64) -> Option<u8> {
if self.cfg.word {
if offset & 1 != 0 {
return None;
}
Some(((offset >> 1) & 0xf) as u8)
} else {
Some((offset & 0xf) as u8)
}
}
fn read_register(&self, index: u8, debug: bool) -> u8 {
let mut state = self.state.lock();
match index {
0x0..=0x7 => {
let ch = state.ch[usize::from(index >> 1)];
let value = if index & 1 == 0 { ch.addr } else { ch.count };
let half = if state.flipflop {
(value >> 8) as u8
} else {
value as u8
};
if !debug {
state.flipflop = !state.flipflop;
}
half
}
0x8 => {
let requesting = (state.drq | state.request) & 0x0f;
let status = (state.tc & 0x0f) | (requesting << 4);
if !debug {
state.tc = 0;
}
status
}
0xd => state.temp,
_ => 0xff,
}
}
fn write_register(&self, index: u8, value: u8) {
{
let mut state = self.state.lock();
let channel = usize::from(value & MODE_CHANNEL);
let bit = 1u8 << channel;
match index {
0x0..=0x7 => {
let ff = state.flipflop;
let is_count = index & 1 == 1;
let ch = &mut state.ch[usize::from(index >> 1)];
let reg = if is_count {
&mut ch.base_count
} else {
&mut ch.base_addr
};
*reg = if ff {
(*reg & 0x00ff) | (u16::from(value) << 8)
} else {
(*reg & 0xff00) | u16::from(value)
};
if is_count {
ch.count = ch.base_count;
} else {
ch.addr = ch.base_addr;
}
state.flipflop = !ff;
}
0x8 => state.command = value,
0x9 if value & 0x04 != 0 => state.request |= bit,
0x9 => state.request &= !bit,
0xa => state.ch[channel].masked = value & 0x04 != 0,
0xb => state.ch[channel].mode = value,
0xc => state.flipflop = false,
0xd => state.master_clear(),
0xe => {
for ch in &mut state.ch {
ch.masked = false;
}
}
_ => {
for (i, ch) in state.ch.iter_mut().enumerate() {
ch.masked = value & (1u8 << i) != 0;
}
}
}
}
self.service_pending();
}
fn set_request(&self, index: usize, level: Level) {
let asserted = {
let mut state = self.state.lock();
let asserted = if state.command & CMD_DREQ_ACTIVE_LOW != 0 {
level.is_low()
} else {
level.is_high()
};
Shared::record_request(&mut state, index, asserted);
asserted
};
if asserted {
self.service(index);
}
}
fn record_request(state: &mut State, index: usize, asserted: bool) {
let bit = 1u8 << index;
if asserted {
state.drq |= bit;
} else {
state.drq &= !bit;
}
}
fn priority_order(state: &State) -> [usize; CHANNELS] {
let start = if state.command & CMD_ROTATING != 0 {
usize::from(state.rotate)
} else {
0
};
let mut order = [0usize; CHANNELS];
for (i, slot) in order.iter_mut().enumerate() {
*slot = (start + i) % CHANNELS;
}
order
}
fn service_pending(&self) {
let (order, pending) = {
let state = self.state.lock();
(
Shared::priority_order(&state),
(state.drq | state.request) & 0x0f,
)
};
for index in order {
if pending & (1u8 << index) != 0 {
self.service(index);
}
}
}
fn plan(&self, index: usize) -> Option<Plan> {
let state = self.state.lock();
if state.command & CMD_DISABLE != 0 {
return None;
}
if self.cfg.channel(index) == 4 {
return None;
}
let ch = state.ch[index];
if ch.masked {
return None;
}
let xfer = (ch.mode & MODE_TRANSFER) >> 2;
if xfer == XFER_ILLEGAL {
return None;
}
let select = (ch.mode & MODE_SELECT) >> 6;
let single = match select {
SELECT_DEMAND | SELECT_BLOCK => false,
SELECT_SINGLE => true,
_ => {
debug_assert_eq!(select, SELECT_CASCADE);
return None;
}
};
let latch = usize::from(PAGE_OFFSET[usize::from(self.cfg.channel(index))]);
let page = u64::from(state.pages[latch]);
let addr = u64::from(ch.addr);
let phys = if self.cfg.word {
(page << 16) | (addr << 1)
} else {
(page << 16) | addr
};
Some(Plan {
phys,
xfer,
terminal: ch.count == 0,
single,
})
}
fn complete(&self, index: usize, terminal: bool) {
let mut state = self.state.lock();
let bit = 1u8 << index;
let ch = &mut state.ch[index];
let step = if ch.mode & MODE_DECREMENT != 0 {
u16::MAX } else {
1
};
ch.addr = ch.addr.wrapping_add(step);
ch.count = ch.count.wrapping_sub(1);
if terminal {
if ch.mode & MODE_AUTOINIT != 0 {
ch.addr = ch.base_addr;
ch.count = ch.base_count;
} else {
ch.masked = true;
}
state.tc |= bit;
state.request &= !bit;
}
}
fn drive_dack(&self, index: usize, asserted: bool) {
let Some(out) = self.dack[index].lock().clone() else {
return;
};
let active = if self.state.lock().command & CMD_DACK_ACTIVE_HIGH != 0 {
Level::High
} else {
Level::Low
};
out.set(if asserted { active } else { active.inverted() });
}
fn pulse_eop(&self) {
let Some(out) = self.eop.lock().clone() else {
return;
};
out.set(Level::Low);
out.set(Level::High);
}
fn service(&self, index: usize) {
let peer = self.peers[index]
.lock()
.clone()
.as_ref()
.and_then(Weak::upgrade);
let (bus, attrs) = {
let state = self.state.lock();
(
state.bus.as_ref().and_then(Weak::upgrade),
MemAttrs::DEFAULT.with_requester(state.requester),
)
};
let Some(bus) = bus else {
return;
};
let unit = self.cfg.unit();
let mut acknowledged = false;
let mut moved = 0u32;
while moved < MAX_BURST_UNITS {
let Some(plan) = self.plan(index) else { break };
if plan.xfer != XFER_VERIFY && peer.is_none() {
break;
}
if !acknowledged {
self.drive_dack(index, true);
acknowledged = true;
}
let mut faulted = false;
match (plan.xfer, peer.as_ref()) {
(XFER_WRITE, Some(peer)) => {
for i in 0..unit {
let byte = peer.dma_read(plan.terminal && i + 1 == unit);
if bus
.write(plan.phys.wrapping_add(i), Width::U8, u64::from(byte), attrs)
.is_err()
{
faulted = true;
break;
}
}
}
(XFER_READ, Some(peer)) => {
for i in 0..unit {
match bus.read(plan.phys.wrapping_add(i), Width::U8, attrs) {
Ok(value) => {
peer.dma_write(value as u8, plan.terminal && i + 1 == unit)
}
Err(_) => {
faulted = true;
break;
}
}
}
}
_ => {}
}
if faulted {
break;
}
self.complete(index, plan.terminal);
moved = moved.wrapping_add(1);
if plan.terminal {
self.pulse_eop();
}
let _ = plan.single;
match peer.as_ref() {
Some(peer) if peer.dma_ready() => {}
_ => break,
}
}
if acknowledged {
self.drive_dack(index, false);
let mut state = self.state.lock();
if state.command & CMD_ROTATING != 0 {
state.rotate = ((index + 1) % CHANNELS) as u8;
}
}
}
}
#[derive(Debug)]
struct ControlBlock {
shared: Arc<Shared>,
}
impl MemOps for ControlBlock {
fn read(&self, offset: u64, dst: &mut [u8], attrs: MemAttrs) -> MemResult {
let [byte] = dst else {
return Err(BusError::BadAccess);
};
*byte = match self.shared.decode(offset) {
Some(index) => self.shared.read_register(index, attrs.debug),
None => 0xff,
};
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 let Some(index) = self.shared.decode(offset) {
self.shared.write_register(index, *value);
}
Ok(())
}
fn constraints(&self) -> AccessConstraints {
AccessConstraints::word(Width::U8, Endian::Little)
}
}
#[derive(Debug)]
struct PageLatches {
shared: Arc<Shared>,
}
impl MemOps for PageLatches {
fn read(&self, offset: u64, dst: &mut [u8], _attrs: MemAttrs) -> MemResult {
let [byte] = dst else {
return Err(BusError::BadAccess);
};
*byte = self.shared.state.lock().pages[(offset & 0xf) as usize];
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.shared.state.lock().pages[(offset & 0xf) as usize] = *value;
Ok(())
}
fn constraints(&self) -> AccessConstraints {
AccessConstraints::word(Width::U8, Endian::Little)
}
}
#[derive(Debug)]
struct RequestPin {
shared: Arc<Shared>,
index: usize,
inputs: FanIn,
}
impl WireSink for RequestPin {
fn set_level(&self, src: WireId, _line: u32, level: Level) {
self.inputs.set(src, level);
self.shared
.set_request(self.index, self.inputs.resolve(Resolve::Or));
}
}
#[derive(Debug)]
pub struct Dma8237 {
shared: Arc<Shared>,
regs: RegionRef,
pages: RegionRef,
pins: Mutex<Vec<Arc<RequestPin>>>,
}
impl Dma8237 {
pub fn new(props: &Props) -> Result<Dma8237> {
let mut r = props.reader();
let mode = r.or_str("mode", "byte")?;
let base = r.or_range("base", 0u64, 0..=4)?;
r.finish()?;
let word = match mode {
"byte" => false,
"word" => true,
other => {
return Err(Error::Property(format!(
"property `mode` must be \"byte\" or \"word\", not \"{other}\""
)));
}
};
Dma8237::with_config(word, base as u8)
}
pub fn with_config(word: bool, base: u8) -> Result<Dma8237> {
if base != 0 && base != 4 {
return Err(Error::Property(format!(
"property `base` must be 0 or 4 (a controller owns four consecutive \
channels), not {base}"
)));
}
let cfg = Config { word, base };
let shared = Arc::new(Shared {
cfg,
state: Mutex::with_rank(LockRank::DEVICE, State::default()),
peers: core::array::from_fn(|_| Mutex::with_rank(LockRank::LEAF, None)),
dack: core::array::from_fn(|_| Mutex::with_rank(LockRank::LEAF, None)),
eop: Mutex::with_rank(LockRank::LEAF, None),
});
let regs: RegionRef = Arc::new(Region::io(
"pc.dma.regs",
cfg.window(),
Arc::new(ControlBlock {
shared: Arc::clone(&shared),
}) as Arc<dyn MemOps>,
));
let pages: RegionRef = Arc::new(Region::io(
"pc.dma.pages",
PAGE_WINDOW_LEN,
Arc::new(PageLatches {
shared: Arc::clone(&shared),
}) as Arc<dyn MemOps>,
));
Ok(Dma8237 {
shared,
regs,
pages,
pins: Mutex::with_rank(LockRank::LEAF, Vec::new()),
})
}
pub fn attach_bus(&self, space: &Arc<AddressSpace>, requester: RequesterId) {
let mut state = self.shared.state.lock();
state.bus = Some(Arc::downgrade(space));
state.requester = requester;
}
#[must_use]
pub fn is_word(&self) -> bool {
self.shared.cfg.word
}
#[must_use]
pub fn channel_base(&self) -> u8 {
self.shared.cfg.base
}
fn index_of(&self, channel: u8) -> Option<usize> {
let index = channel.checked_sub(self.shared.cfg.base)?;
(usize::from(index) < CHANNELS).then_some(usize::from(index))
}
fn pin_index(&self, port: &str, prefix: &str) -> Option<usize> {
let number: u8 = port.strip_prefix(prefix)?.parse().ok()?;
self.index_of(number)
}
pub fn request(&self, channel: u8, asserted: bool) {
let Some(index) = self.index_of(channel) else {
return;
};
{
let mut state = self.shared.state.lock();
Shared::record_request(&mut state, index, asserted);
}
if asserted {
self.shared.service(index);
}
}
#[must_use]
pub fn address(&self, channel: u8) -> Option<u16> {
let index = self.index_of(channel)?;
Some(self.shared.state.lock().ch[index].addr)
}
#[must_use]
pub fn count(&self, channel: u8) -> Option<u16> {
let index = self.index_of(channel)?;
Some(self.shared.state.lock().ch[index].count)
}
#[must_use]
pub fn masked(&self, channel: u8) -> Option<bool> {
let index = self.index_of(channel)?;
Some(self.shared.state.lock().ch[index].masked)
}
}
pub static CLASS: DeviceClass = DeviceClass {
name: CLASS_NAME,
version: STATE_VERSION,
summary: "Intel 8237A DMA controller, with the PC's page-register latches",
properties: &[
PropertySpec {
name: "mode",
kind: ValueKind::Str,
required: false,
summary: "\"byte\" for the 8-bit controller, \"word\" for the 16-bit one \
(default \"byte\")",
},
PropertySpec {
name: "base",
kind: ValueKind::Uint,
required: false,
summary: "the number of the first channel, 0 or 4 (default 0)",
},
],
construct: |props| Ok(Box::new(Dma8237::new(props)?)),
};
impl Device for Dma8237 {
fn class(&self) -> &'static DeviceClass {
&CLASS
}
fn realize(&self, _ctx: &mut RealizeCtx<'_>) -> Result<()> {
Ok(())
}
fn reset(&self, kind: ResetKind) {
{
let mut state = self.shared.state.lock();
state.master_clear();
if kind == ResetKind::Cold {
for ch in &mut state.ch {
*ch = Channel {
masked: true,
..Channel::default()
};
}
state.pages = [0; 16];
}
}
for index in 0..CHANNELS {
self.shared.drive_dack(index, false);
}
let out = self.shared.eop.lock().clone();
if let Some(out) = out {
out.set(Level::High);
}
}
fn region(&self, name: &str) -> Option<RegionRef> {
match name {
"" | "regs" => Some(Arc::clone(&self.regs)),
"pages" => Some(Arc::clone(&self.pages)),
_ => None,
}
}
fn sink(&self, port: &str, sources: &[WireId]) -> Option<SinkPin> {
let index = self.pin_index(port, "dreq")?;
let pin = Arc::new(RequestPin {
shared: Arc::clone(&self.shared),
index,
inputs: FanIn::new(sources),
});
self.pins.lock().push(Arc::clone(&pin));
Some(SinkPin {
sink: pin,
line: index as u32,
})
}
fn attach_dma_peripheral(&self, port: &str, peer: Weak<dyn DmaPeripheral>) {
if let Some(index) = self.pin_index(port, "dreq") {
*self.shared.peers[index].lock() = Some(peer);
}
}
fn connect(&self, port: &str, source: WireSource) -> Result<()> {
if port == "eop" {
*self.shared.eop.lock() = Some(source);
return Ok(());
}
match self.pin_index(port, "dack") {
Some(index) => {
*self.shared.dack[index].lock() = Some(source);
Ok(())
}
None => Err(Error::Config {
at: port.to_string(),
message: format!(
"an 8237A drives `dack{}`..`dack{}` and `eop`",
self.shared.cfg.base,
usize::from(self.shared.cfg.base) + CHANNELS - 1
),
}),
}
}
fn announce(&self, port: &str) {
if port == "eop" {
let out = self.shared.eop.lock().clone();
if let Some(out) = out {
out.set(Level::High);
}
} else if let Some(index) = self.pin_index(port, "dack") {
self.shared.drive_dack(index, false);
}
}
fn save(&self, w: &mut ChunkWriter<'_>) -> Result<()> {
let state = self.shared.state.lock();
for ch in &state.ch {
w.write_u16(ch.addr)?;
w.write_u16(ch.base_addr)?;
w.write_u16(ch.count)?;
w.write_u16(ch.base_count)?;
w.write_u8(ch.mode)?;
w.write_bool(ch.masked)?;
}
w.write_u8(state.command)?;
w.write_u8(state.tc)?;
w.write_u8(state.request)?;
w.write_bool(state.flipflop)?;
w.write_u8(state.rotate)?;
w.write_u8(state.temp)?;
w.write_all(&state.pages)
}
fn load(&self, r: &mut ChunkReader<'_>) -> Result<()> {
let mut ch = [Channel::default(); CHANNELS];
for slot in &mut ch {
slot.addr = r.read_u16()?;
slot.base_addr = r.read_u16()?;
slot.count = r.read_u16()?;
slot.base_count = r.read_u16()?;
slot.mode = r.read_u8()?;
slot.masked = r.read_bool()?;
}
let command = r.read_u8()?;
let tc = r.read_u8()?;
let request = r.read_u8()?;
let flipflop = r.read_bool()?;
let rotate = r.read_u8()?;
if usize::from(rotate) >= CHANNELS {
return Err(Error::State(format!(
"rotating priority position {rotate} is not a channel of a \
{CHANNELS}-channel controller"
)));
}
let temp = r.read_u8()?;
let mut pages = [0u8; 16];
for byte in &mut pages {
*byte = r.read_u8()?;
}
let mut state = self.shared.state.lock();
state.ch = ch;
state.command = command;
state.tc = tc;
state.request = request;
state.flipflop = flipflop;
state.rotate = rotate;
state.temp = temp;
state.pages = pages;
Ok(())
}
}
impl Instance for Dma8237 {
fn bind(&self, ctx: &BindCtx<'_>) -> Result<()> {
let space = ctx.space().ok_or_else(|| Error::Config {
at: String::from(ctx.path()),
message: String::from(
"an 8237A masters the bus it transfers across: add `space = mem` to the \
object that declares it",
),
})?;
self.attach_bus(space, ctx.requester());
Ok(())
}
}
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(Dma8237::new(props)?)))
}
#[must_use]
pub fn schema() -> ClassSchema {
use crate::machine::validate::{PortDir, PropSchema};
let mut schema = ClassSchema::new(CLASS_NAME)
.prop(PropSchema::new("mode", ValueKind::Str).values(&["byte", "word"]))
.prop(PropSchema::new("base", ValueKind::Uint).range(0, 4))
.region("")
.region("regs")
.region("pages")
.port("eop", PortDir::Out);
for channel in 0..8u8 {
schema = schema
.port(format!("dreq{channel}"), PortDir::In)
.port(format!("dack{channel}"), PortDir::Out);
}
schema
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::space::RamStore;
use crate::core::state::{MachineShape, Migrations, StateReader, StateWriter};
use alloc::collections::VecDeque;
const RAM_LEN: u64 = 0x10_0000;
#[derive(Debug)]
struct Peer {
supply: Mutex<VecDeque<u8>>,
taken: Mutex<Vec<u8>>,
terminals: Mutex<u32>,
served: Mutex<u32>,
ready: bool,
}
impl Peer {
fn new(ready: bool) -> Peer {
Peer {
supply: Mutex::with_rank(LockRank::LEAF, VecDeque::new()),
taken: Mutex::with_rank(LockRank::LEAF, Vec::new()),
terminals: Mutex::with_rank(LockRank::LEAF, 0),
served: Mutex::with_rank(LockRank::LEAF, 0),
ready,
}
}
fn supplying_one_at_a_time(bytes: &[u8]) -> Peer {
let peer = Peer::new(false);
peer.supply.lock().extend(bytes.iter().copied());
peer
}
fn supplying(bytes: &[u8]) -> Peer {
let peer = Peer::new(true);
peer.supply.lock().extend(bytes.iter().copied());
peer
}
}
impl DmaPeripheral for Peer {
fn dma_read(&self, terminal: bool) -> u8 {
*self.served.lock() += 1;
if terminal {
*self.terminals.lock() += 1;
}
self.supply.lock().pop_front().unwrap_or(0xee)
}
fn dma_write(&self, byte: u8, terminal: bool) {
*self.served.lock() += 1;
if terminal {
*self.terminals.lock() += 1;
}
self.taken.lock().push(byte);
}
fn dma_ready(&self) -> bool {
self.ready
}
}
struct Rig {
space: Arc<AddressSpace>,
dma: Dma8237,
regs: ControlBlock,
pages: PageLatches,
peer: Arc<Peer>,
channel: u8,
}
impl Rig {
fn new(word: bool, base: u8, channel: u8, peer: Peer) -> Rig {
let space = Arc::new(AddressSpace::new("mem", 24));
{
let mut topo = space.topology();
topo.map(
Arc::new(Region::ram("ram", Arc::new(RamStore::new(RAM_LEN)))),
0,
)
.expect("maps");
}
let dma = Dma8237::with_config(word, base).expect("a legal controller");
dma.attach_bus(&space, RequesterId::ANONYMOUS);
let peer = Arc::new(peer);
dma.attach_dma_peripheral(
&format!("dreq{channel}"),
Arc::downgrade(&peer) as Weak<dyn DmaPeripheral>,
);
let regs = ControlBlock {
shared: Arc::clone(&dma.shared),
};
let pages = PageLatches {
shared: Arc::clone(&dma.shared),
};
Rig {
space,
dma,
regs,
pages,
peer,
channel,
}
}
fn byte(channel: u8, peer: Peer) -> Rig {
Rig::new(false, 0, channel, peer)
}
fn poke(&self, index: u64, value: u8) {
let offset = if self.dma.is_word() { index * 2 } else { index };
self.regs
.write(offset, &[value], MemAttrs::DEFAULT)
.expect("a byte write is legal");
}
fn peek(&self, index: u64) -> u8 {
self.peek_attrs(index, MemAttrs::DEFAULT)
}
fn peek_attrs(&self, index: u64, attrs: MemAttrs) -> u8 {
let offset = if self.dma.is_word() { index * 2 } else { index };
let mut byte = [0u8; 1];
self.regs
.read(offset, &mut byte, attrs)
.expect("a byte read is legal");
byte[0]
}
fn set_page(&self, page: u8) {
let offset = u64::from(PAGE_OFFSET[usize::from(self.channel)]);
self.pages
.write(offset, &[page], MemAttrs::DEFAULT)
.expect("a byte write is legal");
}
fn program(&self, page: u8, addr: u16, count: u16, mode: u8) {
let local = u64::from(self.channel - self.dma.channel_base());
self.poke(0xa, 0x04 | (local as u8));
self.set_page(page);
self.poke(0xc, 0); self.poke(local * 2, addr as u8);
self.poke(local * 2, (addr >> 8) as u8);
self.poke(local * 2 + 1, count as u8);
self.poke(local * 2 + 1, (count >> 8) as u8);
self.poke(0xb, mode | (local as u8));
self.poke(0xa, local as u8); }
fn ram(&self, addr: u64) -> u8 {
self.space
.read(addr, Width::U8, MemAttrs::DEFAULT)
.expect("mapped") as u8
}
fn set_ram(&self, addr: u64, value: u8) {
self.space
.write(addr, Width::U8, u64::from(value), MemAttrs::DEFAULT)
.expect("mapped");
}
}
fn mode(xfer: u8, select: u8, flags: u8) -> u8 {
(xfer << 2) | (select << 6) | flags
}
#[test]
fn two_writes_through_the_flip_flop_make_one_sixteen_bit_address() {
let rig = Rig::byte(1, Peer::new(true));
rig.poke(0xc, 0);
rig.poke(2, 0x34);
rig.poke(2, 0x12);
assert_eq!(rig.dma.address(1), Some(0x1234));
rig.poke(0xc, 0);
assert_eq!(rig.peek(2), 0x34);
assert_eq!(rig.peek(2), 0x12);
rig.poke(2, 0xff);
rig.poke(0xc, 0);
rig.poke(2, 0x78);
rig.poke(2, 0x56);
assert_eq!(rig.dma.address(1), Some(0x5678));
}
#[test]
fn a_write_to_memory_transfer_lands_at_the_page_shifted_up_by_sixteen() {
let rig = Rig::byte(2, Peer::supplying(&[0xde, 0xad, 0xbe, 0xef]));
rig.program(0x02, 0x1234, 3, mode(XFER_WRITE, SELECT_BLOCK, 0));
rig.dma.request(2, true);
for (i, expected) in [0xde, 0xad, 0xbe, 0xefu8].into_iter().enumerate() {
assert_eq!(rig.ram(0x0002_1234 + i as u64), expected, "byte {i}");
}
assert_eq!(*rig.peer.terminals.lock(), 1, "TC on the last byte only");
}
#[test]
fn a_read_from_memory_transfer_hands_the_bytes_to_the_peripheral() {
let rig = Rig::byte(3, Peer::new(true));
for (i, value) in [1u8, 2, 3, 4].into_iter().enumerate() {
rig.set_ram(0x0003_0100 + i as u64, value);
}
rig.program(0x03, 0x0100, 3, mode(XFER_READ, SELECT_BLOCK, 0));
rig.dma.request(3, true);
assert_eq!(*rig.peer.taken.lock(), alloc::vec![1, 2, 3, 4]);
assert_eq!(*rig.peer.terminals.lock(), 1);
}
#[test]
fn the_address_counts_down_when_the_mode_says_so() {
let rig = Rig::byte(1, Peer::supplying(&[0xaa, 0xbb, 0xcc]));
rig.program(
0x01,
0x0200,
2,
mode(XFER_WRITE, SELECT_BLOCK, MODE_DECREMENT),
);
rig.dma.request(1, true);
assert_eq!(rig.ram(0x0001_0200), 0xaa);
assert_eq!(rig.ram(0x0001_01ff), 0xbb);
assert_eq!(rig.ram(0x0001_01fe), 0xcc);
}
#[test]
fn terminal_count_sets_a_status_bit_masks_the_channel_and_the_read_clears_it() {
let rig = Rig::byte(2, Peer::supplying(&[0x11]));
rig.program(0x00, 0x0040, 0, mode(XFER_WRITE, SELECT_SINGLE, 0));
rig.dma.request(2, true);
assert_eq!(rig.ram(0x40), 0x11);
assert_eq!(rig.dma.masked(2), Some(true), "TC masks without autoinit");
assert_eq!(rig.peek_attrs(8, MemAttrs::DEBUG) & 0x0f, 0x04);
assert_eq!(rig.peek_attrs(8, MemAttrs::DEBUG) & 0x0f, 0x04);
assert_eq!(rig.peek(8) & 0x0f, 0x04);
assert_eq!(rig.peek(8) & 0x0f, 0x00, "reading the status clears it");
}
#[test]
fn autoinitialise_reloads_the_base_registers_and_leaves_the_channel_open() {
let rig = Rig::byte(1, Peer::supplying_one_at_a_time(&[0x01, 0x02]));
rig.program(
0x00,
0x0300,
0,
mode(XFER_WRITE, SELECT_SINGLE, MODE_AUTOINIT),
);
rig.dma.request(1, true);
assert_eq!(rig.ram(0x300), 0x01);
assert_eq!(rig.dma.masked(1), Some(false), "autoinit does not mask");
assert_eq!(rig.dma.address(1), Some(0x0300), "address reloaded");
assert_eq!(rig.dma.count(1), Some(0), "count reloaded");
rig.dma.request(1, true);
assert_eq!(rig.ram(0x300), 0x02, "and it starts over");
}
#[test]
fn a_masked_channel_does_not_transfer() {
let rig = Rig::byte(2, Peer::supplying(&[0x77]));
rig.program(0x00, 0x0500, 0, mode(XFER_WRITE, SELECT_SINGLE, 0));
rig.poke(0xa, 0x04 | 2); rig.dma.request(2, true);
assert_eq!(rig.ram(0x500), 0x00);
assert_eq!(*rig.peer.served.lock(), 0);
rig.poke(0xe, 0);
assert_eq!(rig.ram(0x500), 0x77);
}
#[test]
fn the_page_table_is_the_ats_wiring_and_not_a_numeric_one() {
assert_eq!(PAGE_OFFSET[2], 0x1, "channel 2's page latch is port 0x81");
assert_eq!(PAGE_OFFSET[0], 0x7);
assert_eq!(PAGE_OFFSET[1], 0x3);
assert_eq!(PAGE_OFFSET[3], 0x2);
let rig = Rig::byte(2, Peer::supplying(&[0x5a]));
rig.pages
.write(0x1, &[0x05], MemAttrs::DEFAULT)
.expect("a latch takes a byte");
rig.poke(0xc, 0);
rig.poke(4, 0x00);
rig.poke(4, 0x00);
rig.poke(5, 0x00);
rig.poke(5, 0x00);
rig.poke(0xb, mode(XFER_WRITE, SELECT_SINGLE, 0) | 2);
rig.poke(0xa, 2);
rig.dma.request(2, true);
assert_eq!(rig.ram(0x0005_0000), 0x5a);
let mut byte = [0u8; 1];
rig.pages
.read(0x1, &mut byte, MemAttrs::DEFAULT)
.expect("a latch reads back");
assert_eq!(byte[0], 0x05);
}
#[test]
fn the_word_controller_moves_two_bytes_and_shifts_its_address_left() {
let rig = Rig::new(true, 4, 6, Peer::supplying(&[0x21, 0x43, 0x65, 0x87]));
rig.program(0x02, 0x1000, 1, mode(XFER_WRITE, SELECT_BLOCK, 0));
rig.dma.request(6, true);
assert_eq!(rig.ram(0x0002_2000), 0x21);
assert_eq!(rig.ram(0x0002_2001), 0x43);
assert_eq!(rig.ram(0x0002_2002), 0x65);
assert_eq!(rig.ram(0x0002_2003), 0x87);
assert_eq!(*rig.peer.served.lock(), 4, "four bytes, two requests");
assert_eq!(*rig.peer.terminals.lock(), 1, "TC on the last byte only");
assert_eq!(rig.dma.address(6), Some(0x1002));
}
#[test]
fn a_word_controllers_registers_answer_only_at_even_offsets() {
let rig = Rig::new(true, 4, 5, Peer::new(true));
let mut byte = [0u8; 1];
rig.regs
.read(0x1, &mut byte, MemAttrs::DEFAULT)
.expect("the access completes");
assert_eq!(byte[0], 0xff, "nothing is decoded at an odd offset");
assert_eq!(rig.dma.regs.len(), WORD_REGISTER_WINDOW_LEN);
}
#[test]
fn channel_four_is_the_cascade_and_never_transfers() {
let rig = Rig::new(true, 4, 4, Peer::supplying(&[0x99]));
rig.program(0x00, 0x0000, 0, mode(XFER_WRITE, SELECT_BLOCK, 0));
rig.dma.request(4, true);
assert_eq!(*rig.peer.served.lock(), 0);
}
#[test]
fn a_burst_against_a_peripheral_that_never_stops_asking_hits_the_bound() {
let rig = Rig::byte(0, Peer::new(true));
rig.program(
0x00,
0x0000,
0x00ff,
mode(XFER_WRITE, SELECT_BLOCK, MODE_AUTOINIT),
);
rig.dma.request(0, true);
assert_eq!(*rig.peer.served.lock(), MAX_BURST_UNITS);
}
#[test]
fn a_burst_lasts_as_long_as_the_peripheral_keeps_asking() {
let rig = Rig::byte(1, Peer::supplying_one_at_a_time(&[0x01, 0x02, 0x03]));
rig.program(0x00, 0x0700, 0x00ff, mode(XFER_WRITE, SELECT_SINGLE, 0));
rig.dma.request(1, true);
assert_eq!(*rig.peer.served.lock(), 1);
rig.dma.request(1, true);
assert_eq!(*rig.peer.served.lock(), 2);
assert_eq!(rig.ram(0x700), 0x01);
assert_eq!(rig.ram(0x701), 0x02);
let held = Rig::byte(1, Peer::supplying(&[0xaa, 0xbb, 0xcc, 0xdd]));
held.program(0x00, 0x0700, 3, mode(XFER_WRITE, SELECT_SINGLE, 0));
held.dma.request(1, true);
assert_eq!(*held.peer.served.lock(), 4, "the whole programmed count");
assert_eq!(held.ram(0x703), 0xdd);
assert_eq!(held.dma.masked(1), Some(true), "and it reached TC");
}
#[test]
fn a_verify_transfer_counts_without_moving_anything() {
let rig = Rig::byte(2, Peer::supplying(&[0x42, 0x43]));
rig.program(0x00, 0x0800, 1, mode(XFER_VERIFY, SELECT_BLOCK, 0));
rig.dma.request(2, true);
assert_eq!(rig.ram(0x800), 0x00, "verify moves no data");
assert_eq!(*rig.peer.served.lock(), 0, "and touches no data pin");
assert_eq!(rig.dma.masked(2), Some(true), "but it still reached TC");
}
#[test]
fn an_illegal_transfer_type_and_a_disabled_controller_both_refuse() {
let rig = Rig::byte(1, Peer::supplying(&[0x01]));
rig.program(0x00, 0x0900, 0, mode(XFER_ILLEGAL, SELECT_SINGLE, 0));
rig.dma.request(1, true);
assert_eq!(*rig.peer.served.lock(), 0, "the fourth encoding is illegal");
rig.dma.request(1, false);
rig.poke(8, CMD_DISABLE);
rig.program(0x00, 0x0900, 0, mode(XFER_WRITE, SELECT_SINGLE, 0));
rig.dma.request(1, true);
assert_eq!(*rig.peer.served.lock(), 0);
rig.dma.request(1, false);
rig.poke(8, 0);
rig.dma.request(1, true);
assert_eq!(*rig.peer.served.lock(), 1);
assert_eq!(rig.ram(0x900), 0x01);
}
#[test]
fn a_cascade_channel_moves_nothing_of_its_own() {
let rig = Rig::byte(3, Peer::supplying(&[0x01]));
rig.program(0x00, 0x0a00, 0, mode(XFER_WRITE, SELECT_CASCADE, 0));
rig.dma.request(3, true);
assert_eq!(*rig.peer.served.lock(), 0);
}
#[test]
fn a_debug_write_is_refused_rather_than_made_harmless() {
let rig = Rig::byte(1, Peer::new(true));
assert!(rig.regs.write(0xb, &[0x45], MemAttrs::DEBUG).is_err());
assert!(rig.pages.write(0x3, &[0x01], MemAttrs::DEBUG).is_err());
rig.poke(0xc, 0);
rig.poke(2, 0xcd);
rig.poke(2, 0xab);
rig.poke(0xc, 0);
assert_eq!(rig.peek_attrs(2, MemAttrs::DEBUG), 0xcd);
assert_eq!(
rig.peek_attrs(2, MemAttrs::DEBUG),
0xcd,
"still the low half"
);
assert_eq!(rig.peek(2), 0xcd);
assert_eq!(rig.peek(2), 0xab, "a real read did advance it");
}
#[test]
fn an_access_that_is_not_a_single_byte_is_refused() {
let rig = Rig::byte(0, Peer::new(true));
assert!(rig.regs.read(0, &mut [0u8; 2], MemAttrs::DEFAULT).is_err());
assert!(rig.regs.write(0, &[0u8; 4], MemAttrs::DEFAULT).is_err());
assert!(rig.pages.read(0, &mut [0u8; 2], MemAttrs::DEFAULT).is_err());
}
#[test]
fn a_master_clear_masks_every_channel_and_clears_the_byte_pointer() {
let rig = Rig::byte(1, Peer::new(true));
rig.program(0x00, 0x0a00, 5, mode(XFER_WRITE, SELECT_BLOCK, 0));
rig.poke(2, 0x11); rig.poke(0xd, 0); assert_eq!(rig.dma.masked(1), Some(true));
assert_eq!(rig.dma.count(1), Some(5));
rig.poke(2, 0x22);
assert_eq!(
rig.dma.address(1).map(|a| a & 0xff),
Some(0x22),
"the byte pointer came back to the low half"
);
}
#[test]
fn properties_are_checked_rather_than_ignored() {
let word = Dma8237::new(&Props::new().with("mode", "word").with("base", 4u64))
.expect("the AT's second controller");
assert!(word.is_word());
assert_eq!(word.channel_base(), 4);
assert!(Dma8237::new(&Props::new().with("mode", "nibble")).is_err());
assert!(Dma8237::new(&Props::new().with("base", 2u64)).is_err());
assert!(Dma8237::new(&Props::new().with("bass", 4u64)).is_err());
let default = Dma8237::new(&Props::new()).expect("no properties is legal");
assert!(!default.is_word());
assert_eq!(default.channel_base(), 0);
}
#[test]
fn the_pins_are_the_ones_this_controller_owns() {
let second = Dma8237::with_config(true, 4).expect("legal");
assert!(second.sink("dreq5", &[]).is_some());
assert!(second.sink("dreq1", &[]).is_none(), "not its channel");
assert!(second.sink("dack5", &[]).is_none(), "an output, not a sink");
assert!(second.region("pages").is_some());
assert!(second.region("nothing").is_none());
}
#[test]
fn state_round_trips_byte_for_byte() {
fn image(dev: &Dma8237) -> Vec<u8> {
let mut shape = MachineShape::new();
shape.add_device("dma", CLASS_NAME).expect("unique path");
let mut writer = StateWriter::new(shape);
{
let mut chunk = writer
.chunk("dma", CLASS_NAME, STATE_VERSION)
.expect("one chunk");
dev.save(&mut chunk).expect("saves");
}
writer.to_vec().expect("encodes")
}
let rig = Rig::byte(2, Peer::supplying(&[0x10, 0x20]));
rig.program(
0x02,
0x4321,
1,
mode(XFER_READ, SELECT_SINGLE, MODE_AUTOINIT),
);
rig.dma.request(2, true);
rig.dma.request(2, false);
rig.poke(8, CMD_ROTATING | CMD_DREQ_ACTIVE_LOW);
rig.poke(9, 0x04 | 1);
rig.poke(0xc, 0);
rig.poke(6, 0x99);
let saved = image(&rig.dma);
let restored = Dma8237::with_config(false, 0).expect("legal");
let reader = StateReader::new(&saved).expect("decodes");
let chunk = reader
.load("dma", CLASS_NAME, STATE_VERSION, &Migrations::new())
.expect("finds the chunk");
restored.load(&mut chunk.reader()).expect("loads");
assert_eq!(image(&restored), saved, "the two images must be identical");
assert_eq!(restored.address(2), rig.dma.address(2));
assert_eq!(restored.count(2), rig.dma.count(2));
assert_eq!(restored.masked(2), rig.dma.masked(2));
}
#[test]
fn a_cold_reset_returns_every_register_to_a_documented_value() {
let rig = Rig::byte(1, Peer::new(true));
rig.program(0x07, 0x1111, 4, mode(XFER_WRITE, SELECT_BLOCK, 0));
rig.dma.reset(ResetKind::Cold);
assert_eq!(rig.dma.address(1), Some(0));
assert_eq!(rig.dma.count(1), Some(0));
assert_eq!(rig.dma.masked(1), Some(true));
let mut byte = [0u8; 1];
rig.pages
.read(u64::from(PAGE_OFFSET[1]), &mut byte, MemAttrs::DEFAULT)
.expect("reads");
assert_eq!(byte[0], 0, "the board's latches go too");
rig.program(0x00, 0x0b00, 0, mode(XFER_WRITE, SELECT_SINGLE, 0));
rig.dma.request(1, true);
assert_eq!(*rig.peer.served.lock(), 1);
}
}