use alloc::boxed::Box;
use alloc::format;
use alloc::string::ToString;
use alloc::sync::Arc;
use alloc::vec::Vec;
use core::fmt;
use crate::core::device::{Device, DeviceClass, RealizeCtx, ResetKind, SinkPin};
use crate::core::error::{BusError, Error, Result};
use crate::core::props::Props;
use crate::core::sched::{AccessKind, LazyHandle};
use crate::core::space::{AccessConstraints, MemAttrs, MemOps, MemResult, Region, RegionRef};
use crate::core::state::{ChunkReader, ChunkWriter, Sink, Source};
use crate::core::sync::{AtomicU64, LockRank, Mutex, Ordering};
use crate::core::value::{Endian, Width};
use crate::core::wire::{FanIn, Level, Resolve, WireId, WireSink, WireSource};
use crate::machine::realize::Instance;
use crate::machine::validate::ClassSchema;
pub const CLASS_NAME: &str = "pc.pit";
const STATE_VERSION: u32 = 1;
pub const REGISTER_WINDOW_LEN: u64 = 4;
pub const COUNTERS: usize = 3;
const OPEN_BUS: u8 = 0xff;
#[derive(Debug, Clone, PartialEq, Eq)]
struct Counter {
reload: u16,
count: u32,
mode: u8,
access: u8,
bcd: bool,
output: bool,
gate: bool,
loaded: bool,
null_count: bool,
pending_load: bool,
armed: bool,
latched_count: Option<u16>,
latched_status: Option<u8>,
read_high: bool,
write_high: bool,
write_low: u8,
}
impl Default for Counter {
fn default() -> Counter {
Counter {
reload: 0,
count: 0,
mode: 0,
access: 3,
bcd: false,
output: false,
gate: true,
loaded: false,
null_count: true,
pending_load: false,
armed: false,
latched_count: None,
latched_status: None,
read_high: false,
write_high: false,
write_low: 0,
}
}
}
impl Counter {
fn modulus(&self) -> u32 {
if self.bcd { 10_000 } else { 65_536 }
}
fn initial(&self) -> u32 {
let m = self.modulus();
let raw = if self.bcd {
bcd_to_bin(self.reload)
} else {
u32::from(self.reload)
};
match raw % m {
0 => m,
n => n,
}
}
fn element(&self) -> u16 {
let v = self.count % self.modulus();
if self.bcd { bin_to_bcd(v) } else { v as u16 }
}
fn status(&self) -> u8 {
(u8::from(self.output) << 7)
| (u8::from(self.null_count) << 6)
| (self.access << 4)
| (self.mode << 1)
| u8::from(self.bcd)
}
fn counting(&self) -> bool {
match self.mode {
1 | 5 => true,
_ => self.gate,
}
}
fn ticks_to_zero(&self) -> u64 {
match self.count {
0 => u64::from(self.modulus()),
c => u64::from(c),
}
}
fn ticks_to_toggle(&self) -> u64 {
let c = self.count;
if c.is_multiple_of(2) {
return u64::from(c / 2).max(1);
}
let first = if self.output { 1 } else { 3 };
if c <= first {
return 1;
}
u64::from(1 + (c - first) / 2)
}
fn next_event(&self) -> Option<u64> {
if self.pending_load {
return Some(1);
}
if !self.loaded || !self.counting() {
return None;
}
match self.mode {
0 | 1 => self.armed.then(|| self.ticks_to_zero()),
2 => Some(if self.output {
u64::from(self.count).saturating_sub(1).max(1)
} else {
1
}),
3 => Some(self.ticks_to_toggle()),
4 | 5 => {
if !self.output {
Some(1)
} else if self.armed {
Some(self.ticks_to_zero())
} else {
None
}
}
_ => None,
}
}
fn load(&mut self) {
self.pending_load = false;
self.count = self.initial();
self.null_count = false;
self.loaded = true;
self.armed = true;
match self.mode {
1 => self.output = false,
2 | 3 => self.output = true,
_ => {}
}
}
fn run(&mut self, ticks: u64) {
if ticks == 0 {
return;
}
if self.mode == 3 {
let mut n = ticks;
if !self.count.is_multiple_of(2) {
let first = if self.output { 1 } else { 3 };
self.count = self.count.saturating_sub(first);
n -= 1;
}
self.count = self.count.saturating_sub((2 * n) as u32);
return;
}
if self.mode == 2 && !self.output {
return;
}
let m = u64::from(self.modulus());
let c = u64::from(self.count);
let d = ticks % m;
self.count = if c >= d {
(c - d) as u32
} else {
(c + m - d) as u32
};
}
fn fire(&mut self) {
match self.mode {
0 | 1 => {
self.output = true;
self.armed = false;
}
2 => {
if self.output {
self.output = false;
} else {
self.count = self.initial();
self.output = true;
self.null_count = false;
}
}
3 => {
self.output = !self.output;
self.count = self.initial();
self.null_count = false;
}
4 | 5 => {
if self.output {
self.output = false;
} else {
self.output = true;
self.armed = false;
}
}
_ => {}
}
}
fn advance(&mut self, ticks: u64) {
if ticks == 0 {
return;
}
if self.pending_load {
self.load();
return;
}
if !self.loaded || !self.counting() {
return;
}
let fires = self.next_event() == Some(ticks);
self.run(ticks);
if fires {
self.fire();
}
}
fn set_gate(&mut self, level: bool) {
if level == self.gate {
return;
}
self.gate = level;
match self.mode {
1 | 5 => {
if level {
self.pending_load = true;
}
}
2 | 3 => {
if level {
if self.loaded {
self.pending_load = true;
}
} else {
self.output = true;
}
}
_ => {}
}
}
fn program(&mut self, word: u8) {
self.access = (word >> 4) & 3;
self.mode = match (word >> 1) & 7 {
6 => 2,
7 => 3,
m => m,
};
self.bcd = word & 1 != 0;
self.loaded = false;
self.armed = false;
self.pending_load = false;
self.null_count = true;
self.read_high = false;
self.write_high = false;
self.latched_count = None;
self.latched_status = None;
self.output = self.mode != 0;
}
fn latch_count(&mut self) {
if self.latched_count.is_none() {
self.latched_count = Some(self.element());
}
}
fn latch_status(&mut self) {
if self.latched_status.is_none() {
self.latched_status = Some(self.status());
}
}
fn read(&mut self, debug: bool) -> u8 {
if let Some(status) = self.latched_status {
if !debug {
self.latched_status = None;
}
return status;
}
let value = self.latched_count.unwrap_or_else(|| self.element());
match self.access {
1 => {
if !debug {
self.latched_count = None;
}
value as u8
}
2 => {
if !debug {
self.latched_count = None;
}
(value >> 8) as u8
}
_ => {
if self.read_high {
if !debug {
self.read_high = false;
self.latched_count = None;
}
(value >> 8) as u8
} else {
if !debug {
self.read_high = true;
}
value as u8
}
}
}
}
fn write(&mut self, value: u8) {
let complete = match self.access {
1 => Some(u16::from(value)),
2 => Some(u16::from(value) << 8),
_ => {
if self.write_high {
self.write_high = false;
Some(u16::from_le_bytes([self.write_low, value]))
} else {
self.write_low = value;
self.write_high = true;
if self.mode == 0 {
self.output = false;
self.loaded = false;
self.armed = false;
self.pending_load = false;
}
None
}
}
};
let Some(reload) = complete else {
return;
};
self.reload = reload;
self.null_count = true;
match self.mode {
0 => {
self.output = false;
self.pending_load = true;
}
4 => self.pending_load = true,
2 | 3 if !self.loaded => self.pending_load = true,
_ => {}
}
}
}
fn bcd_to_bin(value: u16) -> u32 {
let mut out = 0;
let mut place = 1;
for shift in [0, 4, 8, 12] {
out += u32::from((value >> shift) & 0xf) * place;
place *= 10;
}
out
}
fn bin_to_bcd(value: u32) -> u16 {
let mut out = 0u16;
let mut v = value % 10_000;
for shift in [0, 4, 8, 12] {
out |= ((v % 10) as u16) << shift;
v /= 10;
}
out
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
struct State {
counters: [Counter; COUNTERS],
tick: u64,
}
impl State {
fn next_event(&self) -> Option<u64> {
self.counters.iter().filter_map(Counter::next_event).min()
}
fn step(&mut self, ticks: u64) {
for counter in &mut self.counters {
counter.advance(ticks);
}
self.tick += ticks;
}
fn levels(&self) -> [bool; COUNTERS] {
[
self.counters[0].output,
self.counters[1].output,
self.counters[2].output,
]
}
fn control(&mut self, word: u8) {
let select = (word >> 6) as usize;
if select == COUNTERS {
self.read_back(word);
return;
}
let counter = &mut self.counters[select];
if (word >> 4) & 3 == 0 {
counter.latch_count();
} else {
counter.program(word);
}
}
fn read_back(&mut self, word: u8) {
let want_count = word & 0x20 == 0;
let want_status = word & 0x10 == 0;
for (i, counter) in self.counters.iter_mut().enumerate() {
if word & (2 << i) == 0 {
continue;
}
if want_status {
counter.latch_status();
}
if want_count {
counter.latch_count();
}
}
}
}
struct Registers {
state: Mutex<State>,
outs: Mutex<[Option<WireSource>; COUNTERS]>,
lazy: Mutex<Option<LazyHandle>>,
tick: AtomicU64,
next_event: AtomicU64,
}
impl fmt::Debug for Registers {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut s = f.debug_struct("Registers");
s.field("tick", &self.tick.load(Ordering::Relaxed));
match self.state.try_lock() {
Some(state) => s.field("counters", &state.counters).finish(),
None => s.field("counters", &"<in use>").finish(),
}
}
}
impl Registers {
fn publish(&self, state: &State) {
self.tick.store(state.tick, Ordering::Relaxed);
let at = match state.next_event() {
Some(d) => state.tick.saturating_add(d),
None => u64::MAX,
};
self.next_event.store(at, Ordering::Relaxed);
}
fn drive(&self, levels: [bool; COUNTERS]) {
let sources = self.outs.lock().clone();
for (source, level) in sources.iter().zip(levels) {
if let Some(source) = source {
source.set(Level::from_bool(level));
}
}
}
fn advance_to(&self, target: u64) {
loop {
let (reached, levels) = {
let mut state = self.state.lock();
if target <= state.tick {
return;
}
let span = target - state.tick;
let step = state.next_event().unwrap_or(span).clamp(1, span);
state.step(step);
self.publish(&state);
(state.tick >= target, state.levels())
};
self.drive(levels);
if reached {
return;
}
}
}
fn sync(&self, attrs: MemAttrs) {
let handle = self.lazy.lock().clone();
let Some(handle) = handle else {
return;
};
let kind = if attrs.debug {
AccessKind::Debug
} else {
AccessKind::Guest
};
let _ = handle.sync(kind);
}
fn set_gate(&self, counter: usize, level: bool) {
self.sync(MemAttrs::DEFAULT);
let levels = {
let mut state = self.state.lock();
state.counters[counter].set_gate(level);
self.publish(&state);
state.levels()
};
self.drive(levels);
}
}
impl MemOps for Registers {
fn read(&self, offset: u64, dst: &mut [u8], attrs: MemAttrs) -> MemResult {
let [byte] = dst else {
return Err(BusError::BadAccess);
};
if !attrs.debug {
self.sync(attrs);
}
let index = (offset & 3) as usize;
if index == COUNTERS {
*byte = OPEN_BUS;
return Ok(());
}
let mut state = self.state.lock();
*byte = state.counters[index].read(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);
}
self.sync(attrs);
let index = (offset & 3) as usize;
let levels = {
let mut state = self.state.lock();
if index == COUNTERS {
state.control(*value);
} else {
state.counters[index].write(*value);
}
self.publish(&state);
state.levels()
};
self.drive(levels);
Ok(())
}
fn constraints(&self) -> AccessConstraints {
AccessConstraints::word(Width::U8, Endian::Little)
}
}
#[derive(Debug)]
pub struct GatePin {
regs: Arc<Registers>,
counter: usize,
inputs: FanIn,
}
impl WireSink for GatePin {
fn set_level(&self, src: WireId, _line: u32, level: Level) {
self.inputs.set(src, level);
self.regs
.set_gate(self.counter, self.inputs.resolve(Resolve::Or).is_high());
}
}
#[derive(Debug)]
pub struct Pit8254 {
regs: Arc<Registers>,
region: RegionRef,
pins: Mutex<Vec<Arc<GatePin>>>,
}
impl Pit8254 {
pub fn new(props: &Props) -> Result<Pit8254> {
props.reader().finish()?;
Ok(Pit8254::default_device())
}
#[must_use]
pub fn default_device() -> Pit8254 {
let regs = Arc::new(Registers {
state: Mutex::with_rank(LockRank::DEVICE, State::default()),
outs: Mutex::with_rank(LockRank::LEAF, [None, None, None]),
lazy: Mutex::with_rank(LockRank::LEAF, None),
tick: AtomicU64::new(0),
next_event: AtomicU64::new(u64::MAX),
});
let region: RegionRef = Arc::new(Region::io(
CLASS_NAME,
REGISTER_WINDOW_LEN,
Arc::clone(®s) as Arc<dyn MemOps>,
));
Pit8254 {
regs,
region,
pins: Mutex::with_rank(LockRank::LEAF, Vec::new()),
}
}
#[must_use]
pub fn out(&self, counter: usize) -> bool {
if counter >= COUNTERS {
return false;
}
self.regs.sync(MemAttrs::DEFAULT);
self.regs.state.lock().counters[counter].output
}
pub fn advance_to(&self, tick: u64) {
self.regs.advance_to(tick);
}
#[must_use]
pub fn tick(&self) -> u64 {
self.regs.tick.load(Ordering::Relaxed)
}
}
pub static CLASS: DeviceClass = DeviceClass {
name: CLASS_NAME,
version: STATE_VERSION,
summary: "Intel 8254 programmable interval timer",
properties: &[],
construct: |props| Ok(Box::new(Pit8254::new(props)?)),
};
impl Device for Pit8254 {
fn class(&self) -> &'static DeviceClass {
&CLASS
}
fn realize(&self, _ctx: &mut RealizeCtx<'_>) -> Result<()> {
Ok(())
}
fn reset(&self, _kind: ResetKind) {
let levels = {
let mut state = self.regs.state.lock();
let gates = state.counters.each_ref().map(|c| c.gate);
*state = State::default();
for (counter, gate) in state.counters.iter_mut().zip(gates) {
counter.gate = gate;
}
self.regs.publish(&state);
state.levels()
};
self.regs.drive(levels);
}
fn region(&self, name: &str) -> Option<RegionRef> {
matches!(name, "" | "regs").then(|| Arc::clone(&self.region))
}
fn connect(&self, port: &str, source: WireSource) -> Result<()> {
let index = out_pin(port).ok_or_else(|| unknown_pin(port))?;
self.regs.outs.lock()[index] = Some(source);
Ok(())
}
fn announce(&self, port: &str) {
let Some(index) = out_pin(port) else {
return;
};
let level = self.regs.state.lock().counters[index].output;
let source = self.regs.outs.lock()[index].clone();
if let Some(source) = source {
source.set(Level::from_bool(level));
}
}
fn sink(&self, port: &str, sources: &[WireId]) -> Option<SinkPin> {
if port != "gate2" {
return None;
}
let pin = Arc::new(GatePin {
regs: Arc::clone(&self.regs),
counter: 2,
inputs: FanIn::new(sources),
});
{
let level = pin.inputs.resolve(Resolve::Or).is_high();
let mut state = self.regs.state.lock();
state.counters[pin.counter].gate = level;
}
self.pins.lock().push(Arc::clone(&pin));
Some(SinkPin { sink: pin, line: 2 })
}
fn is_lazy(&self) -> bool {
true
}
fn current_tick(&self) -> u64 {
self.regs.tick.load(Ordering::Relaxed)
}
fn advance_to(&self, tick: u64) {
self.regs.advance_to(tick);
}
fn next_event_tick(&self) -> Option<u64> {
match self.regs.next_event.load(Ordering::Relaxed) {
u64::MAX => None,
at => Some(at),
}
}
fn attach_lazy(&self, handle: LazyHandle) {
*self.regs.lazy.lock() = Some(handle);
}
fn save(&self, w: &mut ChunkWriter<'_>) -> Result<()> {
let state = self.regs.state.lock();
w.write_seq_len(COUNTERS as u64)?;
for c in &state.counters {
w.write_u16(c.reload)?;
w.write_u32(c.count)?;
w.write_u8(c.mode)?;
w.write_u8(c.access)?;
for flag in [
c.bcd,
c.output,
c.gate,
c.loaded,
c.null_count,
c.pending_load,
c.armed,
] {
w.write_bool(flag)?;
}
match c.latched_count {
None => w.write_bool(false)?,
Some(v) => {
w.write_bool(true)?;
w.write_u16(v)?;
}
}
match c.latched_status {
None => w.write_bool(false)?,
Some(v) => {
w.write_bool(true)?;
w.write_u8(v)?;
}
}
w.write_bool(c.read_high)?;
w.write_bool(c.write_high)?;
w.write_u8(c.write_low)?;
}
w.write_u64(state.tick)
}
fn load(&self, r: &mut ChunkReader<'_>) -> Result<()> {
let count = r.read_seq_len(12)? as usize;
if count != COUNTERS {
return Err(Error::State(format!(
"snapshot has {count} counter(s) of 8254 state, this chip has {COUNTERS}"
)));
}
let mut state = State::default();
for c in &mut state.counters {
c.reload = r.read_u16()?;
c.count = r.read_u32()?;
c.mode = r.read_u8()?;
c.access = r.read_u8()?;
c.bcd = r.read_bool()?;
c.output = r.read_bool()?;
c.gate = r.read_bool()?;
c.loaded = r.read_bool()?;
c.null_count = r.read_bool()?;
c.pending_load = r.read_bool()?;
c.armed = r.read_bool()?;
c.latched_count = if r.read_bool()? {
Some(r.read_u16()?)
} else {
None
};
c.latched_status = if r.read_bool()? {
Some(r.read_u8()?)
} else {
None
};
c.read_high = r.read_bool()?;
c.write_high = r.read_bool()?;
c.write_low = r.read_u8()?;
if c.mode > 5 || c.access == 0 || c.access > 3 {
return Err(Error::State(format!(
"snapshot has an 8254 counter in mode {} with access mode {}",
c.mode, c.access
)));
}
if c.count > c.modulus() {
return Err(Error::State(format!(
"snapshot has an 8254 counting element of {} past its modulus",
c.count
)));
}
}
state.tick = r.read_u64()?;
let levels = {
let mut live = self.regs.state.lock();
*live = state;
self.regs.publish(&live);
live.levels()
};
self.regs.drive(levels);
Ok(())
}
}
impl Instance for Pit8254 {}
fn out_pin(port: &str) -> Option<usize> {
match port {
"out0" => Some(0),
"out1" => Some(1),
"out2" => Some(2),
_ => None,
}
}
fn unknown_pin(port: &str) -> Error {
Error::Config {
at: port.to_string(),
message: format!("an 8254 drives `out0`, `out1` and `out2`; `{port}` is none of them"),
}
}
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(Pit8254::new(props)?)))
}
#[must_use]
pub fn schema() -> ClassSchema {
use crate::machine::validate::PortDir;
ClassSchema::new(CLASS_NAME)
.region("")
.region("regs")
.port("out0", PortDir::Out)
.port("out1", PortDir::Out)
.port("out2", PortDir::Out)
.port("gate2", PortDir::In)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::state::{MachineShape, Migrations, StateReader, StateWriter};
use crate::core::wire::{Wire, WireIdAllocator};
const fn latch(counter: u8) -> u8 {
counter << 6
}
const fn control(counter: u8, access: u8, mode: u8, bcd: bool) -> u8 {
(counter << 6) | (access << 4) | (mode << 1) | (bcd as u8)
}
fn peek(pit: &Pit8254, port: u64) -> u8 {
let mut byte = [0u8; 1];
pit.regs
.read(port, &mut byte, MemAttrs::DEFAULT)
.expect("a byte read is legal");
byte[0]
}
fn peek_debug(pit: &Pit8254, port: u64) -> u8 {
let mut byte = [0u8; 1];
pit.regs
.read(port, &mut byte, MemAttrs::DEBUG)
.expect("a debugger may look");
byte[0]
}
fn poke(pit: &Pit8254, port: u64, value: u8) {
pit.regs
.write(port, &[value], MemAttrs::DEFAULT)
.expect("a byte write is legal");
}
fn program(pit: &Pit8254, counter: u8, mode: u8, reload: u16) -> u64 {
poke(pit, 3, control(counter, 3, mode, false));
poke(pit, u64::from(counter), reload as u8);
poke(pit, u64::from(counter), (reload >> 8) as u8);
let at = pit.tick() + 1;
pit.advance_to(at);
at
}
fn samples(pit: &Pit8254, counter: usize, n: u64) -> alloc::vec::Vec<bool> {
let start = pit.tick();
let mut out = alloc::vec::Vec::new();
for i in 0..n {
pit.advance_to(start + i);
out.push(pit.out(counter));
}
out
}
fn gate2(pit: &Pit8254) -> WireSource {
let ids = WireIdAllocator::new();
let id = ids.alloc();
let pin = pit.sink("gate2", &[id]).expect("counter 2 has a gate pin");
let wire = Wire::builder()
.source(id)
.sink(pin.sink, pin.line)
.build_shared();
let source = WireSource::new(wire, id);
source.raise();
source
}
#[test]
fn mode_3_is_a_square_wave_and_an_odd_count_spends_the_extra_tick_high() {
let pit = Pit8254::default_device();
assert_eq!(program(&pit, 0, 3, 6), 1);
assert_eq!(
samples(&pit, 0, 12),
[
true, true, true, false, false, false, true, true, true, false, false, false
],
"an even count splits evenly, over several periods"
);
let pit = Pit8254::default_device();
program(&pit, 0, 3, 5);
assert_eq!(
samples(&pit, 0, 10),
[
true, true, true, false, false, true, true, true, false, false
],
"five is three high and two low, and the period is still five"
);
}
#[test]
fn mode_2_pulses_low_for_one_tick_of_each_period() {
let pit = Pit8254::default_device();
program(&pit, 0, 2, 4);
assert_eq!(
samples(&pit, 0, 9),
[true, true, true, false, true, true, true, false, true],
"one low tick every four, which is what makes IRQ0 a rate"
);
}
#[test]
fn a_mode_2_reload_of_zero_means_65536() {
let pit = Pit8254::default_device();
let loaded = program(&pit, 0, 2, 0);
pit.advance_to(loaded + 65_534);
assert!(pit.out(0), "still counting");
pit.advance_to(loaded + 65_535);
assert!(!pit.out(0), "the count reached one");
pit.advance_to(loaded + 65_536);
assert!(pit.out(0), "and the period is 65536 ticks, not zero");
}
#[test]
fn mode_0_goes_high_at_terminal_count_and_stays_there() {
let pit = Pit8254::default_device();
let loaded = program(&pit, 0, 0, 3);
assert!(!pit.out(0), "mode 0 takes OUT low on the control word");
pit.advance_to(loaded + 2);
assert!(!pit.out(0));
pit.advance_to(loaded + 3);
assert!(pit.out(0), "terminal count");
pit.advance_to(loaded + 3 + 70_000);
assert!(pit.out(0), "and it stays high while the counter wraps");
assert_eq!(
Device::next_event_tick(&pit),
None,
"a spent mode-0 counter has nothing left to do"
);
}
#[test]
fn the_low_then_high_access_mode_takes_two_writes_and_two_reads() {
let pit = Pit8254::default_device();
poke(&pit, 3, control(0, 3, 2, false));
poke(&pit, 0, 0x34);
assert_eq!(
Device::next_event_tick(&pit),
None,
"half a count is not a count: nothing is loaded yet"
);
pit.advance_to(50);
poke(&pit, 0, 0x12);
assert_eq!(
Device::next_event_tick(&pit),
Some(51),
"and the second byte arms it for the next clock"
);
pit.advance_to(51);
poke(&pit, 3, latch(0));
assert_eq!(peek(&pit, 0), 0x34);
assert_eq!(peek(&pit, 0), 0x12);
}
#[test]
fn the_latch_command_freezes_what_a_later_read_returns() {
let pit = Pit8254::default_device();
let loaded = program(&pit, 0, 2, 1_000);
pit.advance_to(loaded + 100);
poke(&pit, 3, latch(0));
pit.advance_to(loaded + 400);
let low = peek(&pit, 0);
let high = peek(&pit, 0);
assert_eq!(u16::from_le_bytes([low, high]), 900, "the latched value");
poke(&pit, 3, latch(0));
let live = u16::from_le_bytes([peek(&pit, 0), peek(&pit, 0)]);
assert_eq!(live, 600, "and the counter never stopped");
}
#[test]
fn the_read_back_command_reports_the_output_and_the_null_count() {
let pit = Pit8254::default_device();
poke(&pit, 3, control(0, 3, 2, false));
poke(&pit, 0, 4);
poke(&pit, 0, 0);
poke(&pit, 3, 0xc0 | 0x20 | 0x02);
let status = peek(&pit, 0);
assert_eq!(status & 0x40, 0x40, "written but not yet loaded");
assert_eq!(status & 0x80, 0x80, "mode 2 idles OUT high");
assert_eq!(status & 0x0f, control(0, 0, 2, false) & 0x0f);
assert_eq!((status >> 4) & 3, 3, "the access mode, read back");
pit.advance_to(pit.tick() + 1);
poke(&pit, 3, 0xc0 | 0x20 | 0x02);
assert_eq!(peek(&pit, 0) & 0x40, 0, "loaded now");
poke(&pit, 3, 0xc0 | 0x02);
assert_eq!(peek(&pit, 0) & 0x80, 0x80, "the status byte first");
assert_eq!(peek(&pit, 0), 4, "then the latched count, low byte first");
}
#[test]
fn a_low_gate_stops_counter_2_in_the_periodic_modes() {
for mode in [2u8, 3] {
let pit = Pit8254::default_device();
let gate = gate2(&pit);
let loaded = program(&pit, 2, mode, 10);
pit.advance_to(loaded + 4);
poke(&pit, 3, latch(2));
let running = u16::from_le_bytes([peek(&pit, 2), peek(&pit, 2)]);
gate.lower();
pit.advance_to(loaded + 400);
assert!(pit.out(2), "a low gate forces OUT high in modes 2 and 3");
poke(&pit, 3, latch(2));
let stopped = u16::from_le_bytes([peek(&pit, 2), peek(&pit, 2)]);
assert_eq!(stopped, running, "and the counting element is frozen");
gate.raise();
pit.advance_to(pit.tick() + 1);
poke(&pit, 3, latch(2));
let restarted = u16::from_le_bytes([peek(&pit, 2), peek(&pit, 2)]);
assert_eq!(restarted, 10, "a rising gate reloads");
pit.advance_to(pit.tick() + 3);
poke(&pit, 3, latch(2));
let moved = u16::from_le_bytes([peek(&pit, 2), peek(&pit, 2)]);
assert!(moved < 10, "and the counter is running again: {moved}");
}
}
#[test]
fn a_debug_read_consumes_neither_the_byte_toggle_nor_the_latch() {
let pit = Pit8254::default_device();
let loaded = program(&pit, 0, 2, 0x1234);
pit.advance_to(loaded + 0x34);
poke(&pit, 3, latch(0));
assert_eq!(peek_debug(&pit, 0), 0x00);
assert_eq!(peek_debug(&pit, 0), 0x00);
assert_eq!(peek_debug(&pit, 0), 0x00);
assert_eq!(peek(&pit, 0), 0x00);
assert_eq!(peek(&pit, 0), 0x12);
assert_eq!(peek_debug(&pit, 3), OPEN_BUS);
assert!(pit.regs.write(3, &[0], MemAttrs::DEBUG).is_err());
assert!(pit.regs.write(0, &[0], MemAttrs::DEBUG).is_err());
}
#[test]
fn a_debug_read_advances_nothing() {
let pit = Pit8254::default_device();
program(&pit, 0, 2, 100);
let before = pit.tick();
let _ = peek_debug(&pit, 0);
assert_eq!(pit.tick(), before);
}
#[test]
fn the_next_event_is_always_ahead_of_the_current_tick() {
let pit = Pit8254::default_device();
assert_eq!(
Device::next_event_tick(&pit),
None,
"an unprogrammed chip has nothing to do"
);
for mode in [0u8, 1, 2, 3, 4, 5] {
let pit = Pit8254::default_device();
let gate = gate2(&pit);
program(&pit, 2, mode, 7);
gate.lower();
gate.raise();
for _ in 0..40 {
let now = Device::current_tick(&pit);
let Some(at) = Device::next_event_tick(&pit) else {
break;
};
assert!(at > now, "mode {mode}: {at} must be past {now}");
pit.advance_to(at);
}
}
}
#[test]
fn the_control_port_is_write_only_and_only_bytes_are_taken() {
let pit = Pit8254::default_device();
assert_eq!(peek(&pit, 3), OPEN_BUS);
assert!(pit.regs.read(0, &mut [0u8; 2], MemAttrs::DEFAULT).is_err());
assert!(pit.regs.write(0, &[0u8; 4], MemAttrs::DEFAULT).is_err());
assert!(pit.region("").is_some());
assert!(pit.region("regs").is_some());
assert!(pit.region("nope").is_none());
}
#[test]
fn a_snapshot_round_trips_every_counter() {
let saved = Pit8254::default_device();
program(&saved, 0, 2, 0);
program(&saved, 1, 3, 18);
poke(&saved, 3, control(2, 1, 0, true));
poke(&saved, 2, 0x25);
saved.advance_to(saved.tick() + 5_000);
poke(&saved, 3, latch(0));
let _ = peek(&saved, 0);
poke(&saved, 3, control(1, 3, 2, false));
poke(&saved, 1, 0x99);
let bytes = save_bytes(&saved);
let restored = Pit8254::default_device();
let reader = StateReader::new(&bytes).unwrap();
let chunk = reader
.load("pit", CLASS.name, CLASS.version, &Migrations::new())
.unwrap();
restored.load(&mut chunk.reader()).unwrap();
assert_eq!(
save_bytes(&restored),
bytes,
"the two images are byte-identical"
);
assert_eq!(
Device::current_tick(&restored),
Device::current_tick(&saved)
);
saved.advance_to(saved.tick() + 1_000);
restored.advance_to(restored.tick() + 1_000);
assert_eq!(save_bytes(&restored), save_bytes(&saved));
}
fn save_bytes(pit: &Pit8254) -> alloc::vec::Vec<u8> {
let mut shape = MachineShape::new();
shape.add_device("pit", CLASS.name).unwrap();
let mut w = StateWriter::new(shape);
{
let mut chunk = w.chunk("pit", CLASS.name, CLASS.version).unwrap();
pit.save(&mut chunk).unwrap();
}
w.to_vec().unwrap()
}
#[test]
fn a_reset_stops_every_counter() {
let pit = Pit8254::default_device();
program(&pit, 0, 2, 12);
pit.advance_to(100);
pit.reset(ResetKind::Cold);
assert_eq!(Device::next_event_tick(&pit), None);
assert!(!pit.out(0), "and every output idles low again");
assert_eq!(Device::current_tick(&pit), 0);
}
#[test]
fn connecting_the_gate_pin_lowers_a_gate_that_was_only_pulled_up() {
let pit = Pit8254::default_device();
let ids = WireIdAllocator::new();
let id = ids.alloc();
let pin = pit.sink("gate2", &[id]).expect("counter 2 has a gate pin");
let wire = Wire::builder()
.source(id)
.sink(pin.sink, pin.line)
.build_shared();
let source = WireSource::new(wire, id);
assert!(!source.level().is_high(), "a fresh net idles low");
let loaded = program(&pit, 2, 3, 4);
pit.advance_to(loaded + 100);
assert!(
pit.out(2),
"counter 2 counted through a gate the board is holding low"
);
source.raise();
assert_eq!(
samples(&pit, 2, 9),
[true, true, true, false, false, true, true, false, false],
"a raised gate reloads and starts the square wave"
);
}
#[test]
fn a_reset_does_not_lift_a_gate_the_board_is_holding_low() {
let pit = Pit8254::default_device();
let gate = gate2(&pit);
gate.lower();
pit.reset(ResetKind::Cold);
let loaded = program(&pit, 2, 3, 4);
pit.advance_to(loaded + 100);
assert!(pit.out(2), "the gate came back high across the reset");
gate.raise();
pit.reset(ResetKind::Cold);
program(&pit, 2, 3, 4);
assert_eq!(
samples(&pit, 2, 9),
[true, true, false, false, true, true, false, false, true],
"and a high gate is not lost either"
);
}
#[test]
fn properties_are_checked_rather_than_ignored() {
assert!(Pit8254::new(&Props::new()).is_ok());
assert!(Pit8254::new(&Props::new().with("frequency", 1u64)).is_err());
}
#[test]
fn bcd_counts_in_decimal() {
let pit = Pit8254::default_device();
poke(&pit, 3, control(0, 3, 0, true));
poke(&pit, 0, 0x00);
poke(&pit, 0, 0x01);
let loaded = pit.tick() + 1;
pit.advance_to(loaded);
poke(&pit, 3, latch(0));
assert_eq!(
u16::from_le_bytes([peek(&pit, 0), peek(&pit, 0)]),
0x0100,
"a hundred, in packed BCD"
);
pit.advance_to(loaded + 1);
poke(&pit, 3, latch(0));
assert_eq!(
u16::from_le_bytes([peek(&pit, 0), peek(&pit, 0)]),
0x0099,
"and it borrows in decimal, not in binary"
);
pit.advance_to(loaded + 100);
assert!(pit.out(0), "terminal count after a hundred clocks");
}
}