use alloc::boxed::Box;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::sync::Arc;
use core::fmt;
use crate::core::device::{Device, DeviceClass, PropertySpec, RealizeCtx, ResetKind};
use crate::core::error::{BusError, Error, Result};
use crate::core::props::{Props, ValueKind};
use crate::core::sched::{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::{Level, WireSource};
use crate::machine::realize::Instance;
use crate::machine::validate::{ClassSchema, PortDir, PropSchema};
pub const CLASS_NAME: &str = "pc.rtc";
const STATE_VERSION: u32 = 1;
pub const REGISTER_WINDOW_LEN: u64 = 2;
pub const CMOS_BYTES: usize = 128;
pub const TICKS_PER_SECOND: u64 = 32_768;
pub const DEFAULT_TIME: &str = "2026-01-01T00:00:00";
const UIP_TICKS: u64 = 8;
const REG_SECONDS: u8 = 0x00;
const REG_SECONDS_ALARM: u8 = 0x01;
const REG_MINUTES: u8 = 0x02;
const REG_MINUTES_ALARM: u8 = 0x03;
const REG_HOURS: u8 = 0x04;
const REG_HOURS_ALARM: u8 = 0x05;
const REG_WEEKDAY: u8 = 0x06;
const REG_DAY: u8 = 0x07;
const REG_MONTH: u8 = 0x08;
const REG_YEAR: u8 = 0x09;
const REG_STATUS_A: u8 = 0x0a;
const REG_STATUS_B: u8 = 0x0b;
const REG_STATUS_C: u8 = 0x0c;
const REG_STATUS_D: u8 = 0x0d;
const REG_FLOPPY: usize = 0x10;
const REG_EQUIPMENT: usize = 0x14;
const REG_BASE_MEM: usize = 0x15;
const REG_EXT_MEM: usize = 0x17;
const CHECKSUM_FIRST: usize = 0x10;
const CHECKSUM_LAST: usize = 0x2d;
const REG_CHECKSUM: usize = 0x2e;
const REG_EXT_MEM_MIRROR: usize = 0x30;
const REG_CENTURY: u8 = 0x32;
const REG_HIGH_MEM: usize = 0x34;
const A_UIP: u8 = 0x80;
const A_DIVIDER: u8 = 0x70;
const A_DIVIDER_32KHZ: u8 = 0x20;
const A_RATE: u8 = 0x0f;
const B_SET: u8 = 0x80;
const B_PIE: u8 = 0x40;
const B_AIE: u8 = 0x20;
const B_UIE: u8 = 0x10;
const B_SQWE: u8 = 0x08;
const B_DM: u8 = 0x04;
const B_24H: u8 = 0x02;
const B_DSE: u8 = 0x01;
const B_RESET_CLEARS: u8 = B_PIE | B_AIE | B_UIE | B_SQWE;
const B_RESET_KEEPS: u8 = B_SET | B_DM | B_24H | B_DSE;
const _: () = assert!(B_RESET_CLEARS | B_RESET_KEEPS == 0xff);
const C_IRQF: u8 = 0x80;
const C_PF: u8 = 0x40;
const C_AF: u8 = 0x20;
const C_UF: u8 = 0x10;
const D_VRT: u8 = 0x80;
const NMI_DISABLE: u8 = 0x80;
const INDEX_MASK: u8 = 0x7f;
const INDEX_READS_AS: u8 = 0xff;
const ALARM_DONT_CARE: u8 = 0xc0;
const DEFAULT_STATUS_A: u8 = A_DIVIDER_32KHZ | 0x06;
const DEFAULT_STATUS_B: u8 = B_24H;
const DEFAULT_BASE_MEM: u64 = 640 * 1024;
const DEFAULT_EQUIPMENT: u8 = 0x2d;
const DEFAULT_FLOPPY: u8 = 0x40;
const EXT_MEM_MAX_KIB: u64 = 65_280;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Calendar {
second: u8,
minute: u8,
hour: u8,
weekday: u8,
day: u8,
month: u8,
year: u16,
}
impl Calendar {
fn is_leap(year: u16) -> bool {
year.is_multiple_of(4) && (!year.is_multiple_of(100) || year.is_multiple_of(400))
}
fn days_in_month(year: u16, month: u8) -> u8 {
match month {
2 => {
if Calendar::is_leap(year) {
29
} else {
28
}
}
4 | 6 | 9 | 11 => 30,
_ => 31,
}
}
fn weekday_of(year: u16, month: u8, day: u8) -> u8 {
let (m, y) = if month <= 2 {
(i32::from(month) + 12, i32::from(year) - 1)
} else {
(i32::from(month), i32::from(year))
};
let k = y % 100;
let j = y / 100;
let h = (i32::from(day) + (13 * (m + 1)) / 5 + k + k / 4 + j / 4 + 5 * j) % 7;
if h == 0 { 7 } else { h as u8 }
}
fn advance_second(&mut self) {
self.second += 1;
if self.second < 60 {
return;
}
self.second = 0;
self.minute += 1;
if self.minute < 60 {
return;
}
self.minute = 0;
self.hour += 1;
if self.hour < 24 {
return;
}
self.hour = 0;
self.weekday = self.weekday % 7 + 1;
self.day += 1;
if self.day <= Calendar::days_in_month(self.year, self.month) {
return;
}
self.day = 1;
self.month += 1;
if self.month <= 12 {
return;
}
self.month = 1;
self.year = self.year.wrapping_add(1);
}
}
fn parse_time(text: &str) -> Result<Calendar> {
let bad = || {
Error::Property(format!(
"property `time`: expected a date and time like \"{DEFAULT_TIME}\", found \"{text}\""
))
};
let bytes = text.as_bytes();
if bytes.len() != 19 {
return Err(bad());
}
let sep = |at: usize, want: u8| bytes[at] == want;
if !sep(4, b'-') || !sep(7, b'-') || !(sep(10, b'T') || sep(10, b' ')) {
return Err(bad());
}
if !sep(13, b':') || !sep(16, b':') {
return Err(bad());
}
let field = |from: usize, to: usize| -> Option<u32> {
let mut value = 0u32;
for byte in &bytes[from..to] {
if !byte.is_ascii_digit() {
return None;
}
value = value * 10 + u32::from(byte - b'0');
}
Some(value)
};
let (Some(year), Some(month), Some(day), Some(hour), Some(minute), Some(second)) = (
field(0, 4),
field(5, 7),
field(8, 10),
field(11, 13),
field(14, 16),
field(17, 19),
) else {
return Err(bad());
};
if !(1..=9999).contains(&year) || !(1..=12).contains(&month) {
return Err(bad());
}
let year = year as u16;
let month = month as u8;
if day < 1 || day > u32::from(Calendar::days_in_month(year, month)) {
return Err(Error::Property(format!(
"property `time`: \"{text}\" names day {day} of a month with {} of them",
Calendar::days_in_month(year, month)
)));
}
if hour > 23 || minute > 59 || second > 59 {
return Err(bad());
}
let day = day as u8;
Ok(Calendar {
second: second as u8,
minute: minute as u8,
hour: hour as u8,
weekday: Calendar::weekday_of(year, month, day),
day,
month,
year,
})
}
fn to_bcd(value: u8) -> u8 {
let value = value % 100;
((value / 10) << 4) | (value % 10)
}
fn from_bcd(value: u8) -> u8 {
(value >> 4) * 10 + (value & 0x0f)
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct State {
cmos: [u8; CMOS_BYTES],
index: u8,
now: Calendar,
flags: u8,
tick: u64,
}
impl State {
fn status_a(&self) -> u8 {
self.cmos[REG_STATUS_A as usize]
}
fn status_b(&self) -> u8 {
self.cmos[REG_STATUS_B as usize]
}
fn running(&self) -> bool {
self.status_a() & A_DIVIDER == A_DIVIDER_32KHZ
}
fn frozen(&self) -> bool {
self.status_b() & B_SET != 0
}
fn periodic_period(&self) -> Option<u64> {
match self.status_a() & A_RATE {
0 => None,
1 => Some(TICKS_PER_SECOND / 256),
2 => Some(TICKS_PER_SECOND / 128),
rate => Some(1u64 << (rate - 1)),
}
}
fn binary(&self) -> bool {
self.status_b() & B_DM != 0
}
fn hour24(&self) -> bool {
self.status_b() & B_24H != 0
}
fn encode(&self, value: u8) -> u8 {
if self.binary() { value } else { to_bcd(value) }
}
fn decode(&self, value: u8) -> u8 {
if self.binary() {
value
} else {
from_bcd(value)
}
}
fn encode_hour(&self, hour: u8) -> u8 {
if self.hour24() {
return self.encode(hour);
}
let pm = hour >= 12;
let twelve = match hour % 12 {
0 => 12,
h => h,
};
self.encode(twelve) | if pm { 0x80 } else { 0 }
}
fn decode_hour(&self, value: u8) -> u8 {
if self.hour24() {
return self.decode(value).min(23);
}
let pm = value & 0x80 != 0;
let twelve = self.decode(value & 0x7f) % 12;
if pm { twelve + 12 } else { twelve }
}
fn alarm_field_matches(&self, raw: u8, actual: u8) -> bool {
raw >= ALARM_DONT_CARE || self.decode(raw) == actual
}
fn alarm_matches(&self) -> bool {
let hours = self.cmos[REG_HOURS_ALARM as usize];
self.alarm_field_matches(self.cmos[REG_SECONDS_ALARM as usize], self.now.second)
&& self.alarm_field_matches(self.cmos[REG_MINUTES_ALARM as usize], self.now.minute)
&& (hours >= ALARM_DONT_CARE || self.decode_hour(hours) == self.now.hour)
}
fn irq(&self) -> bool {
let b = self.status_b();
(self.flags & C_PF != 0 && b & B_PIE != 0)
|| (self.flags & C_AF != 0 && b & B_AIE != 0)
|| (self.flags & C_UF != 0 && b & B_UIE != 0)
}
fn status_c(&self) -> u8 {
self.flags | if self.irq() { C_IRQF } else { 0 }
}
fn uip(&self) -> bool {
self.running()
&& !self.frozen()
&& self.tick % TICKS_PER_SECOND >= TICKS_PER_SECOND - UIP_TICKS
}
fn next_event(&self) -> Option<u64> {
if !self.running() {
return None;
}
let update = (self.tick / TICKS_PER_SECOND + 1) * TICKS_PER_SECOND;
match self.periodic_period() {
Some(period) => Some(update.min((self.tick / period + 1) * period)),
None => Some(update),
}
}
}
#[derive(Debug, Clone, Copy)]
struct Seed {
base_kib: u16,
ext_kib: u16,
high_units: u16,
equipment: u8,
floppy: u8,
}
impl Default for Seed {
fn default() -> Seed {
Seed {
base_kib: (DEFAULT_BASE_MEM / 1024) as u16,
ext_kib: 0,
high_units: 0,
equipment: DEFAULT_EQUIPMENT,
floppy: DEFAULT_FLOPPY,
}
}
}
fn put16(cmos: &mut [u8; CMOS_BYTES], at: usize, value: u16) {
cmos[at] = value as u8;
cmos[at + 1] = (value >> 8) as u8;
}
fn seed_cmos(seed: &Seed) -> [u8; CMOS_BYTES] {
let mut cmos = [0u8; CMOS_BYTES];
cmos[REG_STATUS_A as usize] = DEFAULT_STATUS_A;
cmos[REG_STATUS_B as usize] = DEFAULT_STATUS_B;
cmos[REG_STATUS_D as usize] = D_VRT;
cmos[REG_FLOPPY] = seed.floppy;
cmos[REG_EQUIPMENT] = seed.equipment;
put16(&mut cmos, REG_BASE_MEM, seed.base_kib);
put16(&mut cmos, REG_EXT_MEM, seed.ext_kib);
put16(&mut cmos, REG_EXT_MEM_MIRROR, seed.ext_kib);
put16(&mut cmos, REG_HIGH_MEM, seed.high_units);
let sum =
(CHECKSUM_FIRST..=CHECKSUM_LAST).fold(0u16, |acc, i| acc.wrapping_add(cmos[i].into()));
cmos[REG_CHECKSUM] = (sum >> 8) as u8;
cmos[REG_CHECKSUM + 1] = sum as u8;
cmos
}
#[derive(Debug, Default)]
struct Outputs {
irq: Option<WireSource>,
nmi_mask: Option<WireSource>,
}
struct Registers {
state: Mutex<State>,
outs: Mutex<Outputs>,
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("state", &*state).finish(),
None => s.field("state", &"<in use>").finish(),
}
}
}
impl Registers {
fn republish(&self, state: &State) {
self.tick.store(state.tick, Ordering::Relaxed);
self.next_event
.store(state.next_event().unwrap_or(u64::MAX), Ordering::Relaxed);
}
fn drive_irq(&self, asserted: bool) {
let out = self.outs.lock().irq.clone();
if let Some(out) = out {
out.set(Level::from_bool(asserted));
}
}
fn drive_nmi_mask(&self, disabled: bool) {
let out = self.outs.lock().nmi_mask.clone();
if let Some(out) = out {
out.set(Level::from_bool(disabled));
}
}
fn refresh_irq(&self) {
let asserted = self.state.lock().irq();
self.drive_irq(asserted);
}
fn sync(&self) {
let handle = self.lazy.lock().clone();
let Some(handle) = handle else {
return;
};
let _ = handle.sync(AccessKind::Guest);
}
fn advance_to(&self, target: u64) {
let asserted = {
let mut state = self.state.lock();
if target <= state.tick {
return;
}
if state.running() {
if let Some(period) = state.periodic_period()
&& target / period > state.tick / period
{
state.flags |= C_PF;
}
if !state.frozen() {
let mut boundary = state.tick / TICKS_PER_SECOND;
let last = target / TICKS_PER_SECOND;
while boundary < last {
boundary += 1;
state.now.advance_second();
state.flags |= C_UF;
if state.alarm_matches() {
state.flags |= C_AF;
}
}
}
}
state.tick = target;
self.republish(&state);
state.irq()
};
self.drive_irq(asserted);
}
fn read_register(&self, debug: bool) -> u8 {
let mut state = self.state.lock();
let index = state.index & INDEX_MASK;
match index {
REG_SECONDS => state.encode(state.now.second),
REG_MINUTES => state.encode(state.now.minute),
REG_HOURS => state.encode_hour(state.now.hour),
REG_WEEKDAY => state.encode(state.now.weekday),
REG_DAY => state.encode(state.now.day),
REG_MONTH => state.encode(state.now.month),
REG_YEAR => state.encode((state.now.year % 100) as u8),
REG_STATUS_A => {
let uip = if state.uip() { A_UIP } else { 0 };
(state.status_a() & !A_UIP) | uip
}
REG_STATUS_C => {
let value = state.status_c();
if !debug {
state.flags = 0;
}
value
}
REG_STATUS_D => D_VRT,
REG_CENTURY => to_bcd((state.now.year / 100) as u8),
_ => state.cmos[index as usize],
}
}
fn write_register(&self, value: u8) {
let asserted = {
let mut state = self.state.lock();
let index = state.index & INDEX_MASK;
match index {
REG_SECONDS => state.now.second = state.decode(value).min(59),
REG_MINUTES => state.now.minute = state.decode(value).min(59),
REG_HOURS => state.now.hour = state.decode_hour(value),
REG_WEEKDAY => state.now.weekday = state.decode(value).clamp(1, 7),
REG_DAY => state.now.day = state.decode(value).clamp(1, 31),
REG_MONTH => state.now.month = state.decode(value).clamp(1, 12),
REG_YEAR => {
let century = state.now.year / 100;
state.now.year = century * 100 + u16::from(state.decode(value) % 100);
}
REG_STATUS_A => state.cmos[index as usize] = value & !A_UIP,
REG_STATUS_B => state.cmos[index as usize] = value,
REG_STATUS_C | REG_STATUS_D => {}
REG_CENTURY => {
let century = from_bcd(value);
if century <= 99 {
state.now.year = u16::from(century) * 100 + state.now.year % 100;
}
}
_ => state.cmos[index as usize] = value,
}
self.republish(&state);
state.irq()
};
self.drive_irq(asserted);
}
fn write_index(&self, value: u8) {
{
self.state.lock().index = value;
}
self.drive_nmi_mask(value & NMI_DISABLE != 0);
}
}
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();
}
*byte = if offset & 1 == 0 {
INDEX_READS_AS
} else {
self.read_register(attrs.debug)
};
if !attrs.debug {
self.refresh_irq();
}
Ok(())
}
fn write(&self, offset: u64, src: &[u8], attrs: MemAttrs) -> MemResult {
let [value] = src else {
return Err(BusError::BadAccess);
};
if attrs.debug {
return Err(BusError::BadAccess);
}
self.sync();
if offset & 1 == 0 {
self.write_index(*value);
} else {
self.write_register(*value);
}
Ok(())
}
fn constraints(&self) -> AccessConstraints {
AccessConstraints::word(Width::U8, Endian::Little)
}
}
#[derive(Debug)]
pub struct Rtc146818 {
regs: Arc<Registers>,
region: RegionRef,
}
impl Rtc146818 {
pub fn new(props: &Props) -> Result<Rtc146818> {
let mut r = props.reader();
let time = r.or_str("time", DEFAULT_TIME)?;
let base_mem = r.or_size("basemem", DEFAULT_BASE_MEM)?;
let ext_mem = r.or_size("extmem", 0)?;
let high_mem = r.or_size("highmem", 0)?;
let equipment = r.or_range("equipment", u64::from(DEFAULT_EQUIPMENT), 0..=255)?;
let floppy = r.or_range("floppy", u64::from(DEFAULT_FLOPPY), 0..=255)?;
let century: Option<u64> = r.optional("century")?;
r.finish()?;
let base_kib = base_mem / 1024;
if base_kib > u64::from(u16::MAX) {
return Err(Error::Property(format!(
"property `basemem`: {base_kib} KiB does not fit the two CMOS bytes that report \
it; base memory is at most {} KiB",
u16::MAX
)));
}
let ext_kib = (ext_mem / 1024).min(EXT_MEM_MAX_KIB) as u16;
let high_units = (high_mem / (64 * 1024)).min(u64::from(u16::MAX)) as u16;
let mut now = parse_time(time)?;
if let Some(century) = century {
if century > 99 {
return Err(Error::Property(format!(
"property `century`: the CMOS century byte holds two digits, not {century}"
)));
}
now.year = (century as u16) * 100 + now.year % 100;
now.weekday = Calendar::weekday_of(now.year, now.month, now.day);
}
Ok(Rtc146818::build(
now,
&Seed {
base_kib: base_kib as u16,
ext_kib,
high_units,
equipment: equipment as u8,
floppy: floppy as u8,
},
))
}
#[must_use]
pub fn default_device() -> Rtc146818 {
Rtc146818::at(DEFAULT_TIME).expect("the default time is a date this calendar has")
}
pub fn at(time: &str) -> Result<Rtc146818> {
Ok(Rtc146818::build(parse_time(time)?, &Seed::default()))
}
fn build(now: Calendar, seed: &Seed) -> Rtc146818 {
let regs = Arc::new(Registers {
state: Mutex::with_rank(
LockRank::DEVICE,
State {
cmos: seed_cmos(seed),
index: 0,
now,
flags: 0,
tick: 0,
},
),
outs: Mutex::with_rank(LockRank::LEAF, Outputs::default()),
lazy: Mutex::with_rank(LockRank::LEAF, None),
tick: AtomicU64::new(0),
next_event: AtomicU64::new(u64::MAX),
});
{
let state = regs.state.lock();
regs.republish(&state);
}
let region: RegionRef = Arc::new(Region::io(
CLASS_NAME,
REGISTER_WINDOW_LEN,
Arc::clone(®s) as Arc<dyn MemOps>,
));
Rtc146818 { regs, region }
}
#[must_use]
pub fn cmos(&self, index: u8) -> u8 {
let saved = self.regs.state.lock().index;
self.regs.state.lock().index = index & INDEX_MASK;
let value = self.regs.read_register(true);
self.regs.state.lock().index = saved;
value
}
#[must_use]
pub fn irq_asserted(&self) -> bool {
self.regs.state.lock().irq()
}
#[must_use]
pub fn nmi_disabled(&self) -> bool {
self.regs.state.lock().index & NMI_DISABLE != 0
}
pub fn advance_to(&self, tick: u64) {
self.regs.advance_to(tick);
}
}
pub static CLASS: DeviceClass = DeviceClass {
name: CLASS_NAME,
version: STATE_VERSION,
summary: "MC146818 real-time clock and CMOS RAM",
properties: &[
PropertySpec {
name: "time",
kind: ValueKind::Str,
required: false,
summary: "the date the clock starts at, \"YYYY-MM-DDTHH:MM:SS\" (never the host's)",
},
PropertySpec {
name: "basemem",
kind: ValueKind::Size,
required: false,
summary: "memory below 640K, reported in kilobytes at CMOS 0x15/0x16 (default 640K)",
},
PropertySpec {
name: "extmem",
kind: ValueKind::Size,
required: false,
summary: "memory above 1M, in kilobytes at CMOS 0x17/0x18 and 0x30/0x31, capped at \
65280K",
},
PropertySpec {
name: "highmem",
kind: ValueKind::Size,
required: false,
summary: "memory above 16M, in 64K units at CMOS 0x34/0x35 (default 0)",
},
PropertySpec {
name: "equipment",
kind: ValueKind::Uint,
required: false,
summary: "the equipment byte at CMOS 0x14 (default 0x2d)",
},
PropertySpec {
name: "floppy",
kind: ValueKind::Uint,
required: false,
summary: "the floppy drive types byte at CMOS 0x10 (default 0x40, one 1.44M as A)",
},
PropertySpec {
name: "century",
kind: ValueKind::Uint,
required: false,
summary: "the century, read back from CMOS 0x32 as BCD (default: the one in `time`)",
},
],
construct: |props| Ok(Box::new(Rtc146818::new(props)?)),
};
impl Device for Rtc146818 {
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.cmos[REG_STATUS_B as usize] &= !B_RESET_CLEARS;
state.flags = 0;
state.index = 0;
self.regs.republish(&state);
}
self.regs.drive_irq(false);
self.regs.drive_nmi_mask(false);
}
fn region(&self, name: &str) -> Option<RegionRef> {
matches!(name, "" | "regs").then(|| Arc::clone(&self.region))
}
fn connect(&self, port: &str, source: WireSource) -> Result<()> {
let mut outs = self.regs.outs.lock();
match port {
"irq" => outs.irq = Some(source),
"nmi_mask" => outs.nmi_mask = Some(source),
_ => {
return Err(Error::Config {
at: port.to_string(),
message: String::from(
"an MC146818 drives `irq` and the board's `nmi_mask`; nothing else",
),
});
}
}
Ok(())
}
fn announce(&self, port: &str) {
match port {
"irq" => self.regs.refresh_irq(),
"nmi_mask" => {
let disabled = self.nmi_disabled();
self.regs.drive_nmi_mask(disabled);
}
_ => {}
}
}
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_bytes(&state.cmos)?;
w.write_u8(state.index)?;
w.write_u8(state.now.second)?;
w.write_u8(state.now.minute)?;
w.write_u8(state.now.hour)?;
w.write_u8(state.now.weekday)?;
w.write_u8(state.now.day)?;
w.write_u8(state.now.month)?;
w.write_u16(state.now.year)?;
w.write_u8(state.flags)?;
w.write_u64(state.tick)
}
fn load(&self, r: &mut ChunkReader<'_>) -> Result<()> {
let cmos = r.read_bytes()?;
let cmos: [u8; CMOS_BYTES] = cmos.try_into().map_err(|_| {
Error::State(format!(
"snapshot has {} byte(s) of CMOS, this chip has {CMOS_BYTES}",
r.remaining()
))
})?;
let index = r.read_u8()?;
let now = Calendar {
second: r.read_u8()?,
minute: r.read_u8()?,
hour: r.read_u8()?,
weekday: r.read_u8()?,
day: r.read_u8()?,
month: r.read_u8()?,
year: r.read_u16()?,
};
let flags = r.read_u8()?;
let tick = r.read_u64()?;
if now.second > 59
|| now.minute > 59
|| now.hour > 23
|| !(1..=7).contains(&now.weekday)
|| !(1..=31).contains(&now.day)
|| !(1..=12).contains(&now.month)
{
return Err(Error::State(format!(
"snapshot holds an impossible date: {:04}-{:02}-{:02} {:02}:{:02}:{:02}, weekday {}",
now.year, now.month, now.day, now.hour, now.minute, now.second, now.weekday
)));
}
let (asserted, nmi) = {
let mut state = self.regs.state.lock();
*state = State {
cmos,
index,
now,
flags,
tick,
};
self.regs.republish(&state);
(state.irq(), state.index & NMI_DISABLE != 0)
};
self.regs.drive_irq(asserted);
self.regs.drive_nmi_mask(nmi);
Ok(())
}
}
impl Instance for Rtc146818 {}
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(Rtc146818::new(props)?)))
}
#[must_use]
pub fn schema() -> ClassSchema {
ClassSchema::new(CLASS_NAME)
.prop(PropSchema::new("time", ValueKind::Str))
.prop(PropSchema::new("basemem", ValueKind::Size))
.prop(PropSchema::new("extmem", ValueKind::Size))
.prop(PropSchema::new("highmem", ValueKind::Size))
.prop(PropSchema::new("equipment", ValueKind::Uint).range(0, 255))
.prop(PropSchema::new("floppy", ValueKind::Uint).range(0, 255))
.prop(PropSchema::new("century", ValueKind::Uint).range(0, 99))
.region("")
.region("regs")
.port("irq", PortDir::Out)
.port("nmi_mask", PortDir::Out)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::props::Value;
use crate::core::state::{MachineShape, Migrations, StateReader, StateWriter};
use crate::core::sync::AtomicU32;
use crate::core::wire::{Wire, WireId, WireIdAllocator, WireSink};
use alloc::vec::Vec;
fn rtc() -> Rtc146818 {
Rtc146818::default_device()
}
fn peek(d: &Rtc146818, offset: u64) -> u8 {
let mut byte = [0u8; 1];
d.regs
.read(offset, &mut byte, MemAttrs::DEFAULT)
.expect("a byte read is legal");
byte[0]
}
fn poke(d: &Rtc146818, offset: u64, value: u8) {
d.regs
.write(offset, &[value], MemAttrs::DEFAULT)
.expect("a byte write is legal");
}
fn get(d: &Rtc146818, index: u8) -> u8 {
poke(d, 0, index);
peek(d, 1)
}
fn set(d: &Rtc146818, index: u8, value: u8) {
poke(d, 0, index);
poke(d, 1, value);
}
#[derive(Debug, Default)]
struct Probe {
level: AtomicU32,
}
impl WireSink for Probe {
fn set_level(&self, _src: WireId, _line: u32, level: Level) {
self.level
.store(u32::from(level.is_high()), Ordering::Relaxed);
}
}
impl Probe {
fn high(&self) -> bool {
self.level.load(Ordering::Relaxed) != 0
}
}
fn wired_at(time: &str) -> (Rtc146818, Arc<Probe>, Arc<Probe>) {
let d = Rtc146818::at(time).expect("a date this calendar has");
let ids = WireIdAllocator::new();
let attach = |port: &str| {
let id = ids.alloc();
let probe = Arc::new(Probe::default());
let wire = Wire::builder()
.source(id)
.sink(Arc::clone(&probe) as Arc<dyn WireSink>, 0)
.build_shared();
d.connect(port, WireSource::new(wire, id))
.expect("the chip drives this pin");
probe
};
let irq = attach("irq");
let nmi = attach("nmi_mask");
(d, irq, nmi)
}
fn wired() -> (Rtc146818, Arc<Probe>, Arc<Probe>) {
wired_at(DEFAULT_TIME)
}
fn date(d: &Rtc146818) -> [u8; 7] {
[
get(d, REG_YEAR),
get(d, REG_MONTH),
get(d, REG_DAY),
get(d, REG_HOURS),
get(d, REG_MINUTES),
get(d, REG_SECONDS),
get(d, REG_WEEKDAY),
]
}
fn image(d: &Rtc146818) -> Vec<u8> {
let mut shape = MachineShape::new();
shape.add_device("rtc", CLASS.name).unwrap();
let mut w = StateWriter::new(shape);
{
let mut chunk = w.chunk("rtc", CLASS.name, CLASS.version).unwrap();
d.save(&mut chunk).unwrap();
}
w.to_vec().unwrap()
}
#[test]
fn one_second_is_thirty_two_thousand_seven_hundred_and_sixty_eight_ticks() {
let d = rtc();
assert_eq!(get(&d, REG_SECONDS), 0x00);
d.advance_to(TICKS_PER_SECOND - 1);
assert_eq!(get(&d, REG_SECONDS), 0x00, "not yet");
d.advance_to(TICKS_PER_SECOND);
assert_eq!(get(&d, REG_SECONDS), 0x01);
d.advance_to(TICKS_PER_SECOND * 59);
assert_eq!(get(&d, REG_SECONDS), 0x59, "BCD, so 59 reads as 0x59");
d.advance_to(TICKS_PER_SECOND * 60);
assert_eq!(get(&d, REG_SECONDS), 0x00);
assert_eq!(get(&d, REG_MINUTES), 0x01);
d.advance_to(0);
assert_eq!(get(&d, REG_MINUTES), 0x01);
}
#[test]
fn the_calendar_carries_through_minutes_hours_days_months_and_years() {
let d = Rtc146818::at("2026-12-31T23:59:59").unwrap();
assert_eq!(date(&d), [0x26, 0x12, 0x31, 0x23, 0x59, 0x59, 5]);
d.advance_to(TICKS_PER_SECOND);
assert_eq!(
date(&d),
[0x27, 0x01, 0x01, 0x00, 0x00, 0x00, 6],
"every field carried at once, and the weekday advanced with them"
);
}
#[test]
fn february_has_a_twenty_ninth_only_in_a_leap_year() {
let d = Rtc146818::at("2024-02-28T23:59:59").unwrap();
d.advance_to(TICKS_PER_SECOND);
assert_eq!(get(&d, REG_MONTH), 0x02);
assert_eq!(get(&d, REG_DAY), 0x29);
let d = Rtc146818::at("1900-02-28T23:59:59").unwrap();
assert_eq!(get(&d, REG_CENTURY), 0x19);
d.advance_to(TICKS_PER_SECOND);
assert_eq!(get(&d, REG_MONTH), 0x03);
assert_eq!(get(&d, REG_DAY), 0x01);
let d = Rtc146818::at("2000-02-28T23:59:59").unwrap();
assert_eq!(get(&d, REG_YEAR), 0x00);
assert_eq!(get(&d, REG_CENTURY), 0x20);
d.advance_to(TICKS_PER_SECOND);
assert_eq!(get(&d, REG_DAY), 0x29);
let d = Rtc146818::at("1999-12-31T23:59:59").unwrap();
d.advance_to(TICKS_PER_SECOND);
assert_eq!(get(&d, REG_YEAR), 0x00);
assert_eq!(get(&d, REG_CENTURY), 0x20);
}
#[test]
fn bcd_and_binary_read_back_what_was_written() {
let d = rtc();
set(&d, REG_HOURS, 0x17);
set(&d, REG_MINUTES, 0x45);
assert_eq!(get(&d, REG_HOURS), 0x17);
assert_eq!(get(&d, REG_MINUTES), 0x45);
set(&d, REG_STATUS_B, B_24H | B_DM);
assert_eq!(get(&d, REG_HOURS), 17);
assert_eq!(get(&d, REG_MINUTES), 45);
set(&d, REG_HOURS, 23);
assert_eq!(get(&d, REG_HOURS), 23);
}
#[test]
fn twelve_hour_mode_carries_the_afternoon_in_bit_seven() {
let d = rtc();
set(&d, REG_STATUS_B, 0);
set(&d, REG_HOURS, 0x80 | 0x12);
assert_eq!(get(&d, REG_HOURS), 0x92, "still 12 PM");
set(&d, REG_STATUS_B, B_24H);
assert_eq!(get(&d, REG_HOURS), 0x12, "which is hour 12");
set(&d, REG_STATUS_B, 0);
set(&d, REG_HOURS, 0x12);
assert_eq!(get(&d, REG_HOURS), 0x12);
set(&d, REG_STATUS_B, B_24H);
assert_eq!(get(&d, REG_HOURS), 0x00, "which is hour 0");
set(&d, REG_HOURS, 0x13);
set(&d, REG_STATUS_B, 0);
assert_eq!(get(&d, REG_HOURS), 0x81);
set(&d, REG_STATUS_B, B_DM);
assert_eq!(get(&d, REG_HOURS), 0x80 | 1);
}
#[test]
fn the_periodic_flag_follows_the_rate_and_the_interrupt_follows_pie() {
let (d, irq, _nmi) = wired();
set(&d, REG_STATUS_A, A_DIVIDER_32KHZ | 15);
d.advance_to(16_383);
assert_eq!(get(&d, REG_STATUS_C) & C_PF, 0, "not yet");
d.advance_to(16_384);
assert_eq!(get(&d, REG_STATUS_C) & C_PF, C_PF);
assert!(!irq.high(), "the flag sets, but PIE is clear");
set(&d, REG_STATUS_A, A_DIVIDER_32KHZ | 6);
set(&d, REG_STATUS_B, B_24H | B_PIE);
assert_eq!(Device::next_event_tick(&d), Some(16_384 + 32));
d.advance_to(16_384 + 31);
assert!(!irq.high());
d.advance_to(16_384 + 32);
assert!(irq.high());
assert_eq!(get(&d, REG_STATUS_C) & (C_IRQF | C_PF), C_IRQF | C_PF);
assert!(!irq.high(), "and reading status C is the acknowledgement");
set(&d, REG_STATUS_A, A_DIVIDER_32KHZ);
d.advance_to(16_384 + 32 + 4096);
assert_eq!(get(&d, REG_STATUS_C) & C_PF, 0);
}
#[test]
fn a_stopped_divider_stops_the_clock_and_the_taps() {
let d = rtc();
set(&d, REG_STATUS_A, 0x76); assert_eq!(Device::next_event_tick(&d), None);
d.advance_to(TICKS_PER_SECOND * 10);
assert_eq!(get(&d, REG_SECONDS), 0x00, "the counter chain is stopped");
assert_eq!(get(&d, REG_STATUS_C) & C_PF, 0);
assert_eq!(get(&d, REG_STATUS_A) & A_UIP, 0, "and no update is coming");
}
#[test]
fn the_set_bit_freezes_the_time_registers() {
let d = rtc();
set(&d, REG_STATUS_B, B_24H | B_SET);
d.advance_to(TICKS_PER_SECOND * 5);
assert_eq!(get(&d, REG_SECONDS), 0x00);
assert_eq!(get(&d, REG_STATUS_C) & C_UF, 0, "no update ended");
assert_eq!(get(&d, REG_STATUS_A) & A_UIP, 0, "and UIP stays clear");
set(&d, REG_STATUS_B, B_24H);
d.advance_to(TICKS_PER_SECOND * 6);
assert_eq!(get(&d, REG_SECONDS), 0x01, "and it resumes where it stood");
}
#[test]
fn uip_rises_two_hundred_and_forty_four_microseconds_before_an_update() {
let d = rtc();
d.advance_to(TICKS_PER_SECOND - UIP_TICKS - 1);
assert_eq!(get(&d, REG_STATUS_A) & A_UIP, 0);
d.advance_to(TICKS_PER_SECOND - UIP_TICKS);
assert_eq!(get(&d, REG_STATUS_A) & A_UIP, A_UIP);
d.advance_to(TICKS_PER_SECOND);
assert_eq!(get(&d, REG_STATUS_A) & A_UIP, 0, "the update is done");
}
#[test]
fn the_alarm_fires_on_a_match_and_dont_care_matches_anything() {
let (d, irq, _nmi) = wired();
set(&d, REG_SECONDS_ALARM, 0x05);
set(&d, REG_MINUTES_ALARM, 0x00);
set(&d, REG_HOURS_ALARM, 0x00);
set(&d, REG_STATUS_B, B_24H | B_AIE);
d.advance_to(TICKS_PER_SECOND * 4);
assert!(!irq.high());
d.advance_to(TICKS_PER_SECOND * 5);
assert!(irq.high(), "00:00:05");
assert_eq!(get(&d, REG_STATUS_C) & C_AF, C_AF);
assert!(!irq.high());
set(&d, REG_SECONDS_ALARM, 0xff);
set(&d, REG_MINUTES_ALARM, 0xc0);
set(&d, REG_HOURS_ALARM, 0xc0);
d.advance_to(TICKS_PER_SECOND * 6);
assert!(irq.high());
assert_eq!(get(&d, REG_STATUS_C) & C_AF, C_AF);
set(&d, REG_SECONDS_ALARM, 0xc0);
set(&d, REG_MINUTES_ALARM, 0x01);
set(&d, REG_HOURS_ALARM, 0x00);
d.advance_to(TICKS_PER_SECOND * 59);
assert_eq!(get(&d, REG_STATUS_C) & C_AF, 0, "still minute 0");
d.advance_to(TICKS_PER_SECOND * 60);
assert_eq!(get(&d, REG_STATUS_C) & C_AF, C_AF, "minute 1");
}
#[test]
fn the_update_ended_flag_needs_uie_to_reach_the_pin() {
let (d, irq, _nmi) = wired();
d.advance_to(TICKS_PER_SECOND);
assert!(!irq.high());
assert_eq!(get(&d, REG_STATUS_C) & C_UF, C_UF);
set(&d, REG_STATUS_B, B_24H | B_UIE);
d.advance_to(TICKS_PER_SECOND * 2);
assert!(irq.high());
assert_eq!(get(&d, REG_STATUS_C) & (C_IRQF | C_UF), C_IRQF | C_UF);
assert!(!irq.high());
}
#[test]
fn a_debug_read_of_status_c_eats_nothing() {
let (d, irq, _nmi) = wired();
set(&d, REG_STATUS_B, B_24H | B_UIE);
d.advance_to(TICKS_PER_SECOND);
assert!(irq.high());
poke(&d, 0, REG_STATUS_C);
let mut byte = [0u8; 1];
d.regs
.read(1, &mut byte, MemAttrs::DEBUG)
.expect("a debugger may look");
assert_eq!(byte[0] & (C_IRQF | C_UF), C_IRQF | C_UF);
assert!(irq.high(), "the guest's interrupt is still there");
assert_eq!(
get(&d, REG_STATUS_C) & C_UF,
C_UF,
"and the flag was not eaten"
);
assert!(!irq.high(), "only the real read acknowledges");
}
#[test]
fn the_index_latch_carries_the_nmi_mask() {
let (d, _irq, nmi) = wired();
poke(&d, 0, REG_STATUS_D);
assert!(!nmi.high(), "NMI enabled");
assert!(!d.nmi_disabled());
assert_eq!(peek(&d, 1), D_VRT, "and the low bits still select");
poke(&d, 0, NMI_DISABLE | REG_STATUS_D);
assert!(nmi.high(), "NMI masked");
assert!(d.nmi_disabled());
assert_eq!(peek(&d, 1), D_VRT, "the same register, still");
poke(&d, 0, REG_STATUS_D);
assert!(!nmi.high());
}
#[test]
fn the_index_port_reads_as_ones() {
let d = rtc();
poke(&d, 0, 0x0a);
assert_eq!(peek(&d, 0), INDEX_READS_AS, "the latch is write-only");
}
#[test]
fn the_seeded_checksum_is_the_sum_of_the_at_range() {
let d = rtc();
let sum = (CHECKSUM_FIRST..=CHECKSUM_LAST)
.fold(0u16, |acc, i| acc.wrapping_add(u16::from(d.cmos(i as u8))));
let stored = u16::from(d.cmos(REG_CHECKSUM as u8)) << 8 | u16::from(d.cmos(0x2f));
assert_eq!(sum, stored);
assert_eq!(d.cmos(REG_FLOPPY as u8), DEFAULT_FLOPPY);
assert_eq!(d.cmos(REG_EQUIPMENT as u8), DEFAULT_EQUIPMENT);
assert_eq!(d.cmos(REG_BASE_MEM as u8), 640u16 as u8);
assert_eq!(d.cmos(REG_BASE_MEM as u8 + 1), (640u16 >> 8) as u8);
assert_ne!(sum, 0);
}
#[test]
fn the_memory_properties_land_where_the_bios_looks_for_them() {
let d = Rtc146818::new(
&Props::new()
.with("extmem", Value::Size(64 * 1024 * 1024))
.with("highmem", Value::Size(1024 * 1024 * 1024))
.with("time", "1999-12-31T23:59:59"),
)
.expect("a legal configuration");
let ext = u16::from(d.cmos(REG_EXT_MEM as u8)) | u16::from(d.cmos(0x18)) << 8;
assert_eq!(u64::from(ext), EXT_MEM_MAX_KIB);
let mirror = u16::from(d.cmos(REG_EXT_MEM_MIRROR as u8)) | u16::from(d.cmos(0x31)) << 8;
assert_eq!(ext, mirror, "0x30/0x31 is extended memory, not base memory");
let high = u16::from(d.cmos(REG_HIGH_MEM as u8)) | u16::from(d.cmos(0x35)) << 8;
assert_eq!(u64::from(high) * 64 * 1024, 1024 * 1024 * 1024);
assert_eq!(d.cmos(REG_CENTURY), 0x19);
let board = Rtc146818::new(
&Props::new()
.with("time", DEFAULT_TIME)
.with("basemem", Value::Size(640 * 1024))
.with("extmem", Value::Size(15 * 1024 * 1024)),
)
.expect("the board's configuration");
let ext = u16::from(board.cmos(REG_EXT_MEM as u8)) | u16::from(board.cmos(0x18)) << 8;
assert_eq!(ext, 15 * 1024);
assert_eq!(board.cmos(REG_BASE_MEM as u8), 640u16 as u8);
}
#[test]
fn properties_are_checked_rather_than_ignored() {
assert!(Rtc146818::new(&Props::new().with("time", "2026-02-30T00:00:00")).is_err());
assert!(Rtc146818::new(&Props::new().with("time", "yesterday")).is_err());
assert!(Rtc146818::new(&Props::new().with("time", "2026-01-01T24:00:00")).is_err());
assert!(Rtc146818::new(&Props::new().with("century", 100u64)).is_err());
assert!(
Rtc146818::new(&Props::new().with("basemme", Value::Size(1024))).is_err(),
"a typo is not silently ignored"
);
let d = Rtc146818::new(&Props::new().with("time", "2026-01-01 12:34:56"))
.expect("a space for the T");
assert_eq!(get(&d, REG_HOURS), 0x12);
}
#[test]
fn an_access_the_chip_cannot_answer_is_refused() {
let d = rtc();
assert!(d.regs.read(0, &mut [0u8; 2], MemAttrs::DEFAULT).is_err());
assert!(d.regs.write(1, &[0u8; 2], MemAttrs::DEFAULT).is_err());
assert!(d.regs.write(0, &[0x0a], MemAttrs::DEBUG).is_err());
assert!(d.regs.write(1, &[0x00], MemAttrs::DEBUG).is_err());
}
#[test]
fn a_reset_clears_the_enables_and_keeps_the_battery_backed_state() {
let (d, irq, nmi) = wired();
set(&d, 0x40, 0xa5); set(&d, REG_STATUS_B, B_24H | B_PIE | B_UIE);
poke(&d, 0, NMI_DISABLE | 0x0a);
d.advance_to(TICKS_PER_SECOND);
assert!(irq.high());
assert!(nmi.high());
d.reset(ResetKind::Cold);
assert!(!irq.high());
assert!(!nmi.high(), "the latch has no battery");
assert_eq!(get(&d, REG_STATUS_B), B_24H, "the enables are cleared");
assert_eq!(get(&d, REG_SECONDS), 0x01, "but the time survives");
assert_eq!(get(&d, 0x40), 0xa5, "and so does the RAM");
}
#[test]
fn a_snapshot_round_trips_byte_for_byte() {
let saved = rtc();
saved.advance_to(TICKS_PER_SECOND * 3661 + 777);
set(&saved, REG_STATUS_B, B_24H | B_PIE | B_AIE);
set(&saved, REG_STATUS_A, A_DIVIDER_32KHZ | 4);
set(&saved, REG_SECONDS_ALARM, 0xc0);
set(&saved, 0x37, 0x5a);
poke(&saved, 0, NMI_DISABLE | REG_STATUS_B);
saved.advance_to(TICKS_PER_SECOND * 3662);
assert!(saved.irq_asserted());
let first = image(&saved);
let restored = rtc();
let reader = StateReader::new(&first).unwrap();
let chunk = reader
.load("rtc", CLASS.name, CLASS.version, &Migrations::new())
.unwrap();
restored.load(&mut chunk.reader()).unwrap();
assert_eq!(image(&restored), first, "the two images are identical");
assert!(restored.nmi_disabled(), "the mask bit came back too");
assert!(restored.irq_asserted(), "and so did the pending interrupt");
assert_eq!(date(&restored), date(&saved));
assert_eq!(
Device::current_tick(&restored),
Device::current_tick(&saved)
);
assert_eq!(restored.cmos(0x37), 0x5a);
}
#[test]
fn a_snapshot_holding_an_impossible_date_is_refused() {
let mut shape = MachineShape::new();
shape.add_device("rtc", CLASS.name).unwrap();
let mut w = StateWriter::new(shape);
{
let mut chunk = w.chunk("rtc", CLASS.name, CLASS.version).unwrap();
chunk.write_bytes(&[0u8; CMOS_BYTES]).unwrap();
for byte in [0u8, 0, 0, 0, 1, 1, 200] {
chunk.write_u8(byte).unwrap();
}
chunk.write_u16(2026).unwrap();
chunk.write_u8(0).unwrap();
chunk.write_u64(0).unwrap();
}
let bytes = w.to_vec().unwrap();
let restored = rtc();
let reader = StateReader::new(&bytes).unwrap();
let chunk = reader
.load("rtc", CLASS.name, CLASS.version, &Migrations::new())
.unwrap();
let e = restored
.load(&mut chunk.reader())
.expect_err("month 200 is not a month")
.to_string();
assert!(e.contains("impossible date"), "{e}");
}
#[test]
fn the_next_event_is_the_sooner_of_the_tap_and_the_update() {
let d = rtc();
set(&d, REG_STATUS_A, A_DIVIDER_32KHZ | 15);
assert_eq!(Device::next_event_tick(&d), Some(16_384));
d.advance_to(16_384);
assert_eq!(
Device::next_event_tick(&d),
Some(TICKS_PER_SECOND),
"the update is next, and the one that fired is not reported again"
);
set(&d, REG_STATUS_A, A_DIVIDER_32KHZ);
assert_eq!(Device::next_event_tick(&d), Some(TICKS_PER_SECOND));
assert!(
Device::next_event_tick(&d).unwrap() > Device::current_tick(&d),
"and it is always in the future, or catch-up would stall"
);
}
#[test]
fn daylight_saving_is_stored_and_deliberately_does_nothing() {
let d = rtc();
set(&d, REG_STATUS_B, B_24H | B_DSE);
assert_eq!(get(&d, REG_STATUS_B), B_24H | B_DSE);
d.advance_to(TICKS_PER_SECOND);
assert_eq!(get(&d, REG_SECONDS), 0x01, "and the clock is unaffected");
}
#[test]
fn the_cmos_ram_is_ordinary_memory() {
let d = rtc();
for index in [0x0eu8, 0x33, 0x7f] {
set(&d, index, 0x5a);
assert_eq!(get(&d, index), 0x5a);
}
set(&d, 0x0e, 0x11);
poke(&d, 0, NMI_DISABLE | 0x0e);
assert_eq!(peek(&d, 1), 0x11);
}
}