feat(cartridge): implement RTC calendar transfers and clock updates
This commit is contained in:
parent
692be36349
commit
fce222eea2
22 changed files with 1227 additions and 196 deletions
|
|
@ -1,5 +1,6 @@
|
|||
//! Host-only raw ROM loading, execution modes, and diagnostics.
|
||||
|
||||
pub(crate) mod rtc_clock;
|
||||
pub mod suite;
|
||||
pub mod window;
|
||||
|
||||
|
|
@ -13,7 +14,7 @@ use std::{
|
|||
|
||||
use gba_core::{
|
||||
bios,
|
||||
cartridge::CartridgeHardware,
|
||||
cartridge::{CartridgeHardware, RtcError},
|
||||
machine::{Machine, MachineError, StepKind},
|
||||
memory::ROM_CAPACITY,
|
||||
};
|
||||
|
|
@ -176,9 +177,23 @@ struct Report {
|
|||
reason: Reason,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn run_bounded(machine: &mut Machine, limit: u64) -> Report {
|
||||
run_bounded_with_clock(machine, limit, None).unwrap()
|
||||
}
|
||||
|
||||
fn run_bounded_with_clock(
|
||||
machine: &mut Machine,
|
||||
limit: u64,
|
||||
mut clock: Option<&mut rtc_clock::RtcHostClock>,
|
||||
) -> Result<Report, RtcError> {
|
||||
let mut stats = Stats::default();
|
||||
let reason = loop {
|
||||
if stats.steps % 4096 == 0 {
|
||||
if let Some(clock) = clock.as_deref_mut() {
|
||||
clock.sync(machine.memory_mut())?;
|
||||
}
|
||||
}
|
||||
// STOP has no host input source here. Do not spin with a frozen clock.
|
||||
// Check before the budget so STOP on the last step is reported correctly.
|
||||
if machine.stopped() {
|
||||
|
|
@ -193,7 +208,10 @@ fn run_bounded(machine: &mut Machine, limit: u64) -> Report {
|
|||
Err(error) => break Reason::Diagnostic(error),
|
||||
}
|
||||
};
|
||||
Report { stats, reason }
|
||||
if let Some(clock) = clock {
|
||||
clock.sync(machine.memory_mut())?;
|
||||
}
|
||||
Ok(Report { stats, reason })
|
||||
}
|
||||
|
||||
fn write_report(writer: &mut impl Write, machine: &Machine, report: &Report) -> io::Result<()> {
|
||||
|
|
@ -244,7 +262,7 @@ pub fn execute(options: Options, writer: &mut impl Write) -> Result<(), Box<dyn
|
|||
"Original BIOS replacement; no commercial-game compatibility claim."
|
||||
)?;
|
||||
if options.hardware == CartridgeHardware::Rtc {
|
||||
writeln!(writer, "RTC selected: GPIO and command/control only; calendar and IRQ commands remain diagnostic.")?;
|
||||
writeln!(writer, "RTC selected: UTC at startup, then host elapsed time; no persistence or RTC interrupts.")?;
|
||||
}
|
||||
let steps = match options.mode {
|
||||
Mode::Terminal { steps } => steps,
|
||||
|
|
@ -257,7 +275,12 @@ pub fn execute(options: Options, writer: &mut impl Write) -> Result<(), Box<dyn
|
|||
machine
|
||||
.memory_mut()
|
||||
.set_cartridge_hardware(options.hardware);
|
||||
let report = run_bounded(&mut machine, steps);
|
||||
let mut clock = if options.hardware == CartridgeHardware::Rtc {
|
||||
Some(rtc_clock::RtcHostClock::new(machine.memory_mut())?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let report = run_bounded_with_clock(&mut machine, steps, clock.as_mut())?;
|
||||
write_report(writer, &machine, &report)?;
|
||||
writer.flush()?;
|
||||
if let Reason::Diagnostic(error) = report.reason {
|
||||
|
|
|
|||
122
crates/desktop/src/cartridge/rtc_clock.rs
Normal file
122
crates/desktop/src/cartridge/rtc_clock.rs
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
//! Host-only clock adapter. UTC is sampled once; later changes use monotonic elapsed seconds.
|
||||
use gba_core::{
|
||||
cartridge::{RtcDateTime, RtcError},
|
||||
memory::Memory,
|
||||
};
|
||||
use std::{
|
||||
io,
|
||||
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
const UNIX_2000: u64 = 946_684_800;
|
||||
|
||||
pub(crate) struct RtcHostClock {
|
||||
started: Instant,
|
||||
supplied: u64,
|
||||
}
|
||||
|
||||
impl RtcHostClock {
|
||||
pub(crate) fn new(memory: &mut Memory) -> io::Result<Self> {
|
||||
let unix = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map_err(io::Error::other)?
|
||||
.as_secs();
|
||||
Self::from_unix_seconds(memory, unix).map_err(io::Error::other)
|
||||
}
|
||||
|
||||
fn from_unix_seconds(memory: &mut Memory, unix: u64) -> Result<Self, RtcError> {
|
||||
let seconds = unix
|
||||
.checked_sub(UNIX_2000)
|
||||
.ok_or(RtcError::InvalidDateTime)?;
|
||||
memory.set_rtc_datetime(RtcDateTime::from_seconds_since_2000(seconds)?)?;
|
||||
Ok(Self {
|
||||
started: Instant::now(),
|
||||
supplied: 0,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn sync(&mut self, memory: &mut Memory) -> Result<(), RtcError> {
|
||||
self.supply(memory, self.started.elapsed())
|
||||
}
|
||||
|
||||
fn supply(&mut self, memory: &mut Memory, elapsed: Duration) -> Result<(), RtcError> {
|
||||
let seconds = elapsed.as_secs();
|
||||
// Ignore a backwards test/source sample without moving the high-water mark.
|
||||
if seconds > self.supplied {
|
||||
memory.advance_rtc_seconds(seconds - self.supplied)?;
|
||||
self.supplied = seconds;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gba_core::{cartridge::CartridgeHardware, io::HALTCNT};
|
||||
fn memory() -> Memory {
|
||||
let mut memory = Memory::new(vec![0; 4]).unwrap();
|
||||
memory.set_cartridge_hardware(CartridgeHardware::Rtc);
|
||||
memory
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn epoch_seed_is_utc_and_range_checked_without_changing_state_on_error() {
|
||||
let mut memory = memory();
|
||||
RtcHostClock::from_unix_seconds(&mut memory, UNIX_2000).unwrap();
|
||||
assert_eq!(
|
||||
memory.rtc_datetime().unwrap().components(),
|
||||
[0, 1, 1, 6, 0, 0, 0]
|
||||
);
|
||||
for invalid in [UNIX_2000 - 1, UNIX_2000 + 3_155_760_000, u64::MAX] {
|
||||
assert!(RtcHostClock::from_unix_seconds(&mut memory, invalid).is_err());
|
||||
assert_eq!(
|
||||
memory.rtc_datetime().unwrap().components(),
|
||||
[0, 1, 1, 6, 0, 0, 0]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn elapsed_seconds_are_supplied_once_preserve_guest_time_and_continue_in_stop() {
|
||||
let mut memory = memory();
|
||||
let mut clock = RtcHostClock::from_unix_seconds(&mut memory, UNIX_2000).unwrap();
|
||||
for ms in [999, 1000, 1000, 700, 1999, 2000] {
|
||||
clock
|
||||
.supply(&mut memory, Duration::from_millis(ms))
|
||||
.unwrap();
|
||||
}
|
||||
assert_eq!(
|
||||
memory.rtc_datetime().unwrap().components(),
|
||||
[0, 1, 1, 6, 0, 0, 2]
|
||||
);
|
||||
memory
|
||||
.set_rtc_datetime(RtcDateTime::new([24, 2, 29, 3, 23, 59, 59]).unwrap())
|
||||
.unwrap();
|
||||
memory.write8(HALTCNT, 128).unwrap();
|
||||
clock.supply(&mut memory, Duration::from_secs(3)).unwrap();
|
||||
assert_eq!(
|
||||
memory.rtc_datetime().unwrap().components(),
|
||||
[24, 3, 1, 4, 0, 0, 0]
|
||||
);
|
||||
assert!(memory.stopped());
|
||||
assert_eq!(memory.cycles(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_tick_does_not_lose_elapsed_seconds() {
|
||||
let mut memory = memory();
|
||||
let mut clock = RtcHostClock::from_unix_seconds(&mut memory, UNIX_2000).unwrap();
|
||||
memory.set_cartridge_hardware(CartridgeHardware::None);
|
||||
assert_eq!(
|
||||
clock.supply(&mut memory, Duration::from_secs(5)),
|
||||
Err(RtcError::NotAttached)
|
||||
);
|
||||
memory.set_cartridge_hardware(CartridgeHardware::Rtc);
|
||||
clock.supply(&mut memory, Duration::from_secs(5)).unwrap();
|
||||
assert_eq!(
|
||||
memory.rtc_datetime().unwrap().components(),
|
||||
[0, 1, 1, 0, 0, 0, 5]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -50,6 +50,11 @@ impl Session {
|
|||
self.machine.memory_mut().set_cartridge_hardware(hardware);
|
||||
}
|
||||
|
||||
/// Host adapters may supply independent cartridge time between execution slices.
|
||||
pub fn memory_mut(&mut self) -> &mut gba_core::memory::Memory {
|
||||
self.machine.memory_mut()
|
||||
}
|
||||
|
||||
pub fn frame(&self) -> &Framebuffer {
|
||||
&self.frame
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,13 @@ pub fn run(
|
|||
) -> Result<(), Box<dyn Error>> {
|
||||
let mut session = Session::new(bytes)?;
|
||||
session.set_cartridge_hardware(hardware);
|
||||
let mut rtc_clock = if hardware == gba_core::cartridge::CartridgeHardware::Rtc {
|
||||
Some(crate::cartridge::rtc_clock::RtcHostClock::new(
|
||||
session.memory_mut(),
|
||||
)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
writeln!(
|
||||
writer,
|
||||
"ROM window: Arrows=D-pad; Z/X=A/B; Q/W=L/R; Enter=Start; Backspace=Select; Escape exits."
|
||||
|
|
@ -38,6 +45,9 @@ pub fn run(
|
|||
// Show a black buffer and pump initial events before boot starts.
|
||||
window.update_with_buffer(session.frame().pixels(), WIDTH, HEIGHT)?;
|
||||
while window.is_open() && !window.is_key_down(Key::Escape) {
|
||||
if let Some(clock) = rtc_clock.as_mut() {
|
||||
clock.sync(session.memory_mut())?;
|
||||
}
|
||||
let buttons = read_buttons(window.is_active(), |key| window.is_key_down(key));
|
||||
let update = session.update(buttons)?;
|
||||
let stopped = update == Update::Stopped;
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ fn run() -> Result<(), Box<dyn Error>> {
|
|||
--rom PATH --steps COUNT Run a raw ROM in the terminal (1..=100000000 steps)\n\
|
||||
--rom PATH --window Open a raw ROM with keyboard input\n\
|
||||
--frames COUNT Exit ROM window after 1..=100000 captured frames\n\
|
||||
--rtc Select RTC GPIO/control subset for --rom (no calendar yet)\n\
|
||||
--rtc Select cartridge RTC (UTC startup, host elapsed time)\n\
|
||||
--test-suite PATH.json Run bounded ROM assertions; write a JSON report\n\
|
||||
--cpu-demo Run the terminal-only ARM/Thumb instruction demo\n\
|
||||
--timer-demo Run the timer IRQ demo with nominal cycle costs\n\
|
||||
|
|
|
|||
|
|
@ -84,6 +84,64 @@ fn rtc_setup() -> Vec<u32> {
|
|||
]
|
||||
}
|
||||
|
||||
fn rtc_calendar_setup() -> Vec<u32> {
|
||||
// Original unrolled ARM GPIO driver. Write 2024-02-29 12:34:56, then read the hour.
|
||||
// Keep executable instructions past the GPIO overlay at ROM offsets c4..c9.
|
||||
let mut code = vec![0xe1a00000; 64];
|
||||
code[0] = 0xea00003e; // B 0x08000100
|
||||
code.extend([0xe59f1000, 0xea000000, 0x080000c4]); // LDR r1; skip literal.
|
||||
fn store(code: &mut Vec<u32>, value: u32, offset: u32) {
|
||||
code.extend([0xe3a00000 | value, 0xe1c100b0 | offset]);
|
||||
}
|
||||
fn send(code: &mut Vec<u32>, byte: u8, command: bool) {
|
||||
for bit in 0..8 {
|
||||
let shift = if command { 7 - bit } else { bit };
|
||||
let pins = 4 | (u32::from((byte >> shift) & 1) << 1);
|
||||
store(code, pins, 0);
|
||||
store(code, pins | 1, 0);
|
||||
}
|
||||
}
|
||||
store(&mut code, 1, 4); // Read enable.
|
||||
store(&mut code, 1, 0);
|
||||
store(&mut code, 7, 2);
|
||||
store(&mut code, 5, 0);
|
||||
send(&mut code, 0x64, true);
|
||||
for byte in [0x24, 2, 0x29, 4, 0x12, 0x34, 0x56] {
|
||||
send(&mut code, byte, false);
|
||||
}
|
||||
store(&mut code, 1, 0); // End write and begin time read.
|
||||
store(&mut code, 5, 0);
|
||||
send(&mut code, 0x67, true);
|
||||
store(&mut code, 5, 2);
|
||||
code.push(0xe3a02000); // MOV r2,#0
|
||||
for bit in 0..8 {
|
||||
store(&mut code, 4, 0);
|
||||
store(&mut code, 5, 0);
|
||||
code.extend([0xe1d130b0, 0xe1a030a3, 0xe2033001, 0xe1822003 | (bit << 7)]);
|
||||
}
|
||||
store(&mut code, 1, 0); // Aborting the remaining read bytes preserves calendar state.
|
||||
code.push(0xeafffffe);
|
||||
code
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rtc_calendar_commands_run_through_original_arm_code_with_host_clock_enabled() {
|
||||
let fixture = Fixture::new();
|
||||
let path = fixture.rom(&rtc_calendar_setup());
|
||||
let output = command()
|
||||
.arg("--rom")
|
||||
.arg(&path)
|
||||
.args(["--rtc", "--steps", "5000"])
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{}", stderr(&output));
|
||||
assert!(
|
||||
stdout(&output).contains("r2=0x00000092"),
|
||||
"{}",
|
||||
stdout(&output)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rtc_selection_is_explicit_and_does_not_modify_the_rom_file() {
|
||||
let fixture = Fixture::new();
|
||||
|
|
@ -100,7 +158,7 @@ fn rtc_selection_is_explicit_and_does_not_modify_the_rom_file() {
|
|||
.output()
|
||||
.unwrap();
|
||||
assert!(selected.status.success(), "{}", stderr(&selected));
|
||||
assert!(stdout(&selected).contains("RTC selected: GPIO and command/control only"));
|
||||
assert!(stdout(&selected).contains("RTC selected: UTC at startup"));
|
||||
assert!(stdout(&selected).contains("r2=0x00000001"));
|
||||
assert_eq!(fs::read(&path).unwrap(), original);
|
||||
let duplicate = command()
|
||||
|
|
@ -298,6 +356,15 @@ fn native_rom_window_presents_file_bytes_and_reports_stop_cpu_and_video_errors()
|
|||
assert!(rtc_output.status.success(), "{}", stderr(&rtc_output));
|
||||
assert!(stdout(&rtc_output).contains("Captured ROM frames: 3"));
|
||||
assert!(stdout(&rtc_output).contains("r2=0x00000001"));
|
||||
let calendar_path = fixture.rom(&rtc_calendar_setup());
|
||||
let calendar_output = window(&calendar_path, true);
|
||||
assert!(
|
||||
calendar_output.status.success(),
|
||||
"{}",
|
||||
stderr(&calendar_output)
|
||||
);
|
||||
assert!(stdout(&calendar_output).contains("Captured ROM frames: 3"));
|
||||
assert!(stdout(&calendar_output).contains("r2=0x00000092"));
|
||||
for (code, error) in [
|
||||
(vec![0xef03_0000, 0xeaff_fffe], "entered STOP"),
|
||||
(vec![0xee00_0000], "instruction"),
|
||||
|
|
|
|||
|
|
@ -2,6 +2,11 @@
|
|||
|
||||
use crate::memory::MemoryError;
|
||||
|
||||
mod calendar;
|
||||
mod rtc;
|
||||
pub use calendar::{RtcDateTime, RtcError};
|
||||
use rtc::Rtc;
|
||||
|
||||
pub const GPIO_DATA: u32 = 0x0800_00c4;
|
||||
pub const GPIO_DIRECTION: u32 = 0x0800_00c6;
|
||||
pub const GPIO_CONTROL: u32 = 0x0800_00c8;
|
||||
|
|
@ -11,7 +16,7 @@ pub const GPIO_CONTROL: u32 = 0x0800_00c8;
|
|||
pub enum CartridgeHardware {
|
||||
#[default]
|
||||
None,
|
||||
/// GPIO and RTC command/control only. Calendar and interrupt commands remain diagnostic.
|
||||
/// GPIO and RTC calendar/control. Time advances only through explicit caller updates.
|
||||
Rtc,
|
||||
}
|
||||
|
||||
|
|
@ -27,6 +32,29 @@ impl Cartridge {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn rtc_datetime(&self) -> Option<RtcDateTime> {
|
||||
self.gpio.map(|gpio| gpio.rtc.calendar)
|
||||
}
|
||||
|
||||
pub(crate) fn set_rtc_datetime(&mut self, date: RtcDateTime) -> Result<(), RtcError> {
|
||||
self.gpio
|
||||
.as_mut()
|
||||
.ok_or(RtcError::NotAttached)?
|
||||
.rtc
|
||||
.calendar = date;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn advance_rtc_seconds(&mut self, seconds: u64) -> Result<(), RtcError> {
|
||||
self.gpio
|
||||
.as_mut()
|
||||
.ok_or(RtcError::NotAttached)?
|
||||
.rtc
|
||||
.calendar
|
||||
.advance(seconds);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn mapped(&self, address: u32) -> bool {
|
||||
self.gpio.is_some() && (GPIO_DATA..GPIO_CONTROL + 2).contains(&address)
|
||||
}
|
||||
|
|
@ -97,141 +125,3 @@ impl Gpio {
|
|||
(self.latch & self.direction) | (u8::from(self.rtc.output) << 1 & !self.direction)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
struct Rtc {
|
||||
control: u8,
|
||||
selected: bool,
|
||||
clock: bool,
|
||||
sample: bool,
|
||||
output: bool,
|
||||
phase: Phase,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum Phase {
|
||||
Command { byte: u8, bits: u8 },
|
||||
WriteControl { byte: u8, bits: u8 },
|
||||
ReadControl { byte: u8, bits: u8 },
|
||||
Done,
|
||||
}
|
||||
|
||||
impl Default for Rtc {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
control: 0x40, // Deterministic powered 24-hour mode, not a sampled host clock.
|
||||
selected: false,
|
||||
clock: true,
|
||||
sample: true,
|
||||
output: true,
|
||||
phase: Phase::Command { byte: 0, bits: 0 },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Rtc {
|
||||
fn update(&mut self, pins: u8, direction: u8) -> Result<(), &'static str> {
|
||||
let selected = pins & 4 != 0;
|
||||
let clock = pins & 1 != 0;
|
||||
let data = pins & 2 != 0;
|
||||
if !selected {
|
||||
self.selected = false;
|
||||
self.clock = true;
|
||||
self.output = true;
|
||||
self.phase = Phase::Command { byte: 0, bits: 0 };
|
||||
return Ok(());
|
||||
}
|
||||
if !self.selected {
|
||||
if !clock {
|
||||
return Err("RTC select without high clock");
|
||||
}
|
||||
self.selected = true;
|
||||
self.clock = true;
|
||||
return Ok(());
|
||||
}
|
||||
if !clock {
|
||||
match self.phase {
|
||||
Phase::Command { .. } | Phase::WriteControl { .. } => {
|
||||
if direction & 2 == 0 {
|
||||
return Err("RTC command/data write with SIO input");
|
||||
}
|
||||
self.sample = data;
|
||||
}
|
||||
Phase::ReadControl { byte, bits } if self.clock => {
|
||||
if direction & 2 != 0 {
|
||||
return Err("RTC read with SIO output");
|
||||
}
|
||||
self.output = byte & (1 << bits) != 0;
|
||||
self.phase = if bits == 7 {
|
||||
Phase::Done
|
||||
} else {
|
||||
Phase::ReadControl {
|
||||
byte,
|
||||
bits: bits + 1,
|
||||
}
|
||||
};
|
||||
}
|
||||
Phase::Done if self.clock => return Err("RTC clocks beyond command length"),
|
||||
_ => {}
|
||||
}
|
||||
} else if !self.clock {
|
||||
match self.phase {
|
||||
Phase::Command { byte, bits } | Phase::WriteControl { byte, bits } => {
|
||||
if direction & 2 == 0 || data != self.sample {
|
||||
return Err("RTC SIO change at sampling edge");
|
||||
}
|
||||
let byte = byte | (u8::from(self.sample) << bits);
|
||||
let command = matches!(self.phase, Phase::Command { .. });
|
||||
self.phase = if bits != 7 {
|
||||
if command {
|
||||
Phase::Command {
|
||||
byte,
|
||||
bits: bits + 1,
|
||||
}
|
||||
} else {
|
||||
Phase::WriteControl {
|
||||
byte,
|
||||
bits: bits + 1,
|
||||
}
|
||||
}
|
||||
} else if command {
|
||||
self.command(byte)?
|
||||
} else {
|
||||
// Bits 1/3/5 lack an implemented IRQ/control model.
|
||||
if byte & 0x2a != 0 {
|
||||
return Err("RTC interrupt/unknown control bits");
|
||||
}
|
||||
self.control = byte & 0x40;
|
||||
Phase::Done
|
||||
};
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
self.clock = clock;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn command(&mut self, byte: u8) -> Result<Phase, &'static str> {
|
||||
// The first four wire bits are 0,1,1,0. Translate GBATEK's LSB-first
|
||||
// representation to its MSB-first command numbering; parameters stay LSB first.
|
||||
if byte & 15 != 6 {
|
||||
return Err("RTC command encoding");
|
||||
}
|
||||
let command = byte.reverse_bits();
|
||||
match (command >> 1) & 7 {
|
||||
0 => {
|
||||
self.control = 0;
|
||||
Ok(Phase::Done)
|
||||
}
|
||||
1 if command & 1 != 0 => Ok(Phase::ReadControl {
|
||||
byte: self.control,
|
||||
bits: 0,
|
||||
}),
|
||||
1 => Ok(Phase::WriteControl { byte: 0, bits: 0 }),
|
||||
2 | 3 => Err("RTC calendar access"),
|
||||
6 => Err("RTC force interrupt"),
|
||||
_ => Err("RTC unused command"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
157
crates/gba-core/src/cartridge/calendar.rs
Normal file
157
crates/gba-core/src/cartridge/calendar.rs
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
//! Validated 2000–2099 RTC calendar arithmetic. No clock sampling or timezone conversion.
|
||||
use std::{error::Error, fmt};
|
||||
|
||||
const DAYS_PER_CENTURY: u64 = 36_525;
|
||||
const SECONDS_PER_DAY: u64 = 86_400;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum RtcError {
|
||||
NotAttached,
|
||||
InvalidDateTime,
|
||||
}
|
||||
|
||||
impl fmt::Display for RtcError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(match self {
|
||||
Self::NotAttached => "no cartridge RTC is attached",
|
||||
Self::InvalidDateTime => "invalid RTC date/time (expected a valid date in 2000–2099)",
|
||||
})
|
||||
}
|
||||
}
|
||||
impl Error for RtcError {}
|
||||
|
||||
/// Decimal [year since 2000, month, day, weekday, hour, minute, second].
|
||||
/// Weekday is an independent 0–6 counter; the host chooses its naming convention.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct RtcDateTime([u8; 7]);
|
||||
|
||||
impl Default for RtcDateTime {
|
||||
fn default() -> Self {
|
||||
Self([0, 1, 1, 0, 0, 0, 0])
|
||||
}
|
||||
}
|
||||
|
||||
fn month_days(year: u8, month: u8) -> u8 {
|
||||
match month {
|
||||
2 => {
|
||||
if year.is_multiple_of(4) {
|
||||
29
|
||||
} else {
|
||||
28
|
||||
}
|
||||
}
|
||||
4 | 6 | 9 | 11 => 30,
|
||||
_ => 31,
|
||||
}
|
||||
}
|
||||
|
||||
fn bcd(value: u8) -> u8 {
|
||||
((value / 10) << 4) | (value % 10)
|
||||
}
|
||||
|
||||
fn decimal(value: u8) -> Result<u8, RtcError> {
|
||||
if value & 15 > 9 || value >> 4 > 9 {
|
||||
return Err(RtcError::InvalidDateTime);
|
||||
}
|
||||
Ok((value >> 4) * 10 + (value & 15))
|
||||
}
|
||||
|
||||
impl RtcDateTime {
|
||||
pub fn new(parts: [u8; 7]) -> Result<Self, RtcError> {
|
||||
let [year, month, day, week, hour, minute, second] = parts;
|
||||
if year > 99
|
||||
|| !(1..=12).contains(&month)
|
||||
|| day == 0
|
||||
|| day > month_days(year, month)
|
||||
|| week > 6
|
||||
|| hour > 23
|
||||
|| minute > 59
|
||||
|| second > 59
|
||||
{
|
||||
return Err(RtcError::InvalidDateTime);
|
||||
}
|
||||
Ok(Self(parts))
|
||||
}
|
||||
|
||||
pub fn components(self) -> [u8; 7] {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Checked epoch conversion. Uses Sunday=0, so 2000-01-01 is weekday 6.
|
||||
/// Unlike RTC ticking, this constructor rejects dates beyond 2099.
|
||||
pub fn from_seconds_since_2000(seconds: u64) -> Result<Self, RtcError> {
|
||||
if seconds >= DAYS_PER_CENTURY * SECONDS_PER_DAY {
|
||||
return Err(RtcError::InvalidDateTime);
|
||||
}
|
||||
let mut result = Self([0, 1, 1, 6, 0, 0, 0]);
|
||||
result.advance(seconds);
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub(super) fn encode(self, hour24: bool) -> [u8; 7] {
|
||||
let mut data = self.0.map(bcd);
|
||||
let hour = self.0[4];
|
||||
data[4] = bcd(if hour24 { hour } else { hour % 12 }) | if hour >= 12 { 0x80 } else { 0 };
|
||||
data
|
||||
}
|
||||
|
||||
pub(super) fn decode(data: [u8; 7], hour24: bool) -> Result<Self, RtcError> {
|
||||
let mut result = [0; 7];
|
||||
let masks = [255, 31, 63, 7, 63, 127, 127];
|
||||
for i in 0..7 {
|
||||
result[i] = decimal(data[i] & masks[i])?;
|
||||
}
|
||||
if !hour24 {
|
||||
if result[4] > 11 {
|
||||
return Err(RtcError::InvalidDateTime);
|
||||
}
|
||||
result[4] += if data[4] & 128 != 0 { 12 } else { 0 };
|
||||
}
|
||||
Self::new(result)
|
||||
}
|
||||
|
||||
/// Advance a bounded two-digit year calendar and the independent weekday counter.
|
||||
/// Arithmetic is bounded even for u64::MAX seconds; year 99 wraps to 00.
|
||||
pub(super) fn advance(&mut self, seconds: u64) {
|
||||
let [year, month, day, week, hour, minute, second] = self.0;
|
||||
let time = u64::from(hour) * 3600
|
||||
+ u64::from(minute) * 60
|
||||
+ u64::from(second)
|
||||
+ seconds % SECONDS_PER_DAY;
|
||||
let days = seconds / SECONDS_PER_DAY + time / SECONDS_PER_DAY;
|
||||
let mut day_index = u64::from(day - 1);
|
||||
for y in 0..year {
|
||||
day_index += if y.is_multiple_of(4) { 366 } else { 365 };
|
||||
}
|
||||
for m in 1..month {
|
||||
day_index += u64::from(month_days(year, m));
|
||||
}
|
||||
day_index = (day_index + days % DAYS_PER_CENTURY) % DAYS_PER_CENTURY;
|
||||
let mut year: u8 = 0;
|
||||
loop {
|
||||
let count = if year.is_multiple_of(4) { 366 } else { 365 };
|
||||
if day_index < count {
|
||||
break;
|
||||
}
|
||||
day_index -= count;
|
||||
year += 1;
|
||||
}
|
||||
let mut month = 1;
|
||||
while day_index >= u64::from(month_days(year, month)) {
|
||||
day_index -= u64::from(month_days(year, month));
|
||||
month += 1;
|
||||
}
|
||||
self.0 = [
|
||||
year,
|
||||
month,
|
||||
day_index as u8 + 1,
|
||||
((u64::from(week) + days % 7) % 7) as u8,
|
||||
((time / 3600) % 24) as u8,
|
||||
((time / 60) % 60) as u8,
|
||||
(time % 60) as u8,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
141
crates/gba-core/src/cartridge/calendar/tests.rs
Normal file
141
crates/gba-core/src/cartridge/calendar/tests.rs
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn calendar_validates_fields_and_epoch_range() {
|
||||
for invalid in [
|
||||
[100, 1, 1, 0, 0, 0, 0],
|
||||
[0, 0, 1, 0, 0, 0, 0],
|
||||
[0, 13, 1, 0, 0, 0, 0],
|
||||
[0, 1, 0, 0, 0, 0, 0],
|
||||
[1, 2, 29, 0, 0, 0, 0],
|
||||
[0, 4, 31, 0, 0, 0, 0],
|
||||
[0, 1, 1, 7, 0, 0, 0],
|
||||
[0, 1, 1, 0, 24, 0, 0],
|
||||
[0, 1, 1, 0, 0, 60, 0],
|
||||
[0, 1, 1, 0, 0, 0, 60],
|
||||
] {
|
||||
assert_eq!(RtcDateTime::new(invalid), Err(RtcError::InvalidDateTime));
|
||||
}
|
||||
assert_eq!(
|
||||
RtcDateTime::from_seconds_since_2000(0)
|
||||
.unwrap()
|
||||
.components(),
|
||||
[0, 1, 1, 6, 0, 0, 0]
|
||||
);
|
||||
assert_eq!(
|
||||
RtcDateTime::from_seconds_since_2000(3_155_759_999)
|
||||
.unwrap()
|
||||
.components(),
|
||||
[99, 12, 31, 4, 23, 59, 59]
|
||||
);
|
||||
assert_eq!(
|
||||
RtcDateTime::from_seconds_since_2000(3_155_760_000),
|
||||
Err(RtcError::InvalidDateTime)
|
||||
);
|
||||
assert_eq!(
|
||||
RtcDateTime::from_seconds_since_2000(u64::MAX),
|
||||
Err(RtcError::InvalidDateTime)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hour_modes_cover_every_second_and_pm_flag() {
|
||||
for hour in 0..24 {
|
||||
for minute in 0..60 {
|
||||
for second in 0..60 {
|
||||
let date = RtcDateTime::new([24, 2, 29, 3, hour, minute, second]).unwrap();
|
||||
for mode in [false, true] {
|
||||
let bytes = date.encode(mode);
|
||||
assert_eq!(bytes[4] & 128 != 0, hour >= 12);
|
||||
assert_eq!(RtcDateTime::decode(bytes, mode).unwrap(), date);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(
|
||||
RtcDateTime::new([0, 1, 1, 0, 12, 0, 0])
|
||||
.unwrap()
|
||||
.encode(false)[4],
|
||||
0x80
|
||||
);
|
||||
assert_eq!(
|
||||
RtcDateTime::new([0, 1, 1, 0, 23, 0, 0])
|
||||
.unwrap()
|
||||
.encode(true)[4],
|
||||
0xa3
|
||||
);
|
||||
assert!(RtcDateTime::decode([0, 1, 1, 0, 0x12, 0, 0], false).is_err());
|
||||
assert_eq!(
|
||||
RtcDateTime::decode([0, 1, 1, 0, 0x80, 0, 0], true)
|
||||
.unwrap()
|
||||
.components()[4],
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn weekday_advances_independently_and_all_months_roll_over() {
|
||||
// Independent month table and explicit leap-year set; test every day of the century.
|
||||
let leaps = [
|
||||
0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 68, 72, 76, 80, 84, 88,
|
||||
92, 96,
|
||||
];
|
||||
let mut clock = RtcDateTime::new([0, 1, 1, 2, 0, 0, 0]).unwrap();
|
||||
let mut week = 2;
|
||||
for year in 0..100 {
|
||||
let months = [
|
||||
31,
|
||||
if leaps.contains(&year) { 29 } else { 28 },
|
||||
31,
|
||||
30,
|
||||
31,
|
||||
30,
|
||||
31,
|
||||
31,
|
||||
30,
|
||||
31,
|
||||
30,
|
||||
31,
|
||||
];
|
||||
for (month, length) in months.into_iter().enumerate() {
|
||||
for day in 1..=length {
|
||||
assert_eq!(
|
||||
clock.components(),
|
||||
[year, month as u8 + 1, day, week, 0, 0, 0]
|
||||
);
|
||||
assert_eq!(
|
||||
RtcDateTime::decode(clock.encode(true), true).unwrap(),
|
||||
clock
|
||||
);
|
||||
clock.advance(86400);
|
||||
week = (week + 1) % 7;
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(clock.components(), [0, 1, 1, 1, 0, 0, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn large_and_split_advances_do_not_overflow_or_lose_time() {
|
||||
let mut huge = RtcDateTime::default();
|
||||
huge.advance(u64::MAX);
|
||||
// Independent Python datetime calculation using the documented two-digit-year wrap policy.
|
||||
assert_eq!(huge.components(), [90, 8, 17, 0, 7, 0, 15]);
|
||||
let mut split = RtcDateTime::default();
|
||||
split.advance(u64::MAX / 2);
|
||||
split.advance(u64::MAX - u64::MAX / 2);
|
||||
assert_eq!(huge, split);
|
||||
for start in [
|
||||
[0, 2, 28, 6, 23, 59, 59],
|
||||
[1, 2, 28, 5, 23, 59, 59],
|
||||
[99, 12, 31, 2, 23, 59, 59],
|
||||
] {
|
||||
let mut batched = RtcDateTime::new(start).unwrap();
|
||||
let mut small = batched;
|
||||
batched.advance(100000);
|
||||
for _ in 0..100000 {
|
||||
small.advance(1);
|
||||
}
|
||||
assert_eq!(batched, small);
|
||||
}
|
||||
}
|
||||
239
crates/gba-core/src/cartridge/rtc.rs
Normal file
239
crates/gba-core/src/cartridge/rtc.rs
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
//! Edge-driven RTC command framing with latched reads and complete-payload writes.
|
||||
use super::RtcDateTime;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) struct Rtc {
|
||||
control: u8,
|
||||
pub(super) calendar: RtcDateTime,
|
||||
selected: bool,
|
||||
clock: bool,
|
||||
sample: bool,
|
||||
pub(super) output: bool,
|
||||
phase: Phase,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum Register {
|
||||
Control,
|
||||
DateTime,
|
||||
Time,
|
||||
}
|
||||
|
||||
impl Register {
|
||||
fn len(self) -> u8 {
|
||||
match self {
|
||||
Self::Control => 1,
|
||||
Self::DateTime => 7,
|
||||
Self::Time => 3,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum Phase {
|
||||
Command {
|
||||
byte: u8,
|
||||
bits: u8,
|
||||
},
|
||||
Write {
|
||||
register: Register,
|
||||
data: [u8; 7],
|
||||
offset: u8,
|
||||
byte: u8,
|
||||
bits: u8,
|
||||
},
|
||||
Read {
|
||||
data: [u8; 7],
|
||||
len: u8,
|
||||
bit: u8,
|
||||
},
|
||||
Done,
|
||||
}
|
||||
|
||||
impl Default for Rtc {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
control: 0x40,
|
||||
calendar: RtcDateTime::default(),
|
||||
selected: false,
|
||||
clock: true,
|
||||
sample: true,
|
||||
output: true,
|
||||
phase: Phase::Command { byte: 0, bits: 0 },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Rtc {
|
||||
pub(super) fn update(&mut self, pins: u8, direction: u8) -> Result<(), &'static str> {
|
||||
let selected = pins & 4 != 0;
|
||||
let clock = pins & 1 != 0;
|
||||
let data = pins & 2 != 0;
|
||||
if !selected {
|
||||
self.selected = false;
|
||||
self.clock = true;
|
||||
self.output = true;
|
||||
self.phase = Phase::Command { byte: 0, bits: 0 };
|
||||
return Ok(());
|
||||
}
|
||||
if !self.selected {
|
||||
if !clock {
|
||||
return Err("RTC select without high clock");
|
||||
}
|
||||
self.selected = true;
|
||||
self.clock = true;
|
||||
return Ok(());
|
||||
}
|
||||
if !clock {
|
||||
match self.phase {
|
||||
Phase::Command { .. } | Phase::Write { .. } => {
|
||||
if direction & 2 == 0 {
|
||||
return Err("RTC command/data write with SIO input");
|
||||
}
|
||||
self.sample = data;
|
||||
}
|
||||
Phase::Read { data, len, bit } if self.clock => {
|
||||
if direction & 2 != 0 {
|
||||
return Err("RTC read with SIO output");
|
||||
}
|
||||
self.output = data[usize::from(bit / 8)] & (1 << (bit % 8)) != 0;
|
||||
self.phase = if bit + 1 == len * 8 {
|
||||
Phase::Done
|
||||
} else {
|
||||
Phase::Read {
|
||||
data,
|
||||
len,
|
||||
bit: bit + 1,
|
||||
}
|
||||
};
|
||||
}
|
||||
Phase::Done if self.clock => return Err("RTC clocks beyond command length"),
|
||||
_ => {}
|
||||
}
|
||||
} else if !self.clock && matches!(self.phase, Phase::Command { .. } | Phase::Write { .. }) {
|
||||
if direction & 2 == 0 || data != self.sample {
|
||||
return Err("RTC SIO change at sampling edge");
|
||||
}
|
||||
self.receive_bit()?;
|
||||
}
|
||||
self.clock = clock;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn receive_bit(&mut self) -> Result<(), &'static str> {
|
||||
self.phase = match self.phase {
|
||||
Phase::Command { byte, bits } => {
|
||||
let byte = byte | (u8::from(self.sample) << bits);
|
||||
if bits == 7 {
|
||||
self.command(byte)?
|
||||
} else {
|
||||
Phase::Command {
|
||||
byte,
|
||||
bits: bits + 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
Phase::Write {
|
||||
register,
|
||||
mut data,
|
||||
offset,
|
||||
byte,
|
||||
bits,
|
||||
} => {
|
||||
let byte = byte | (u8::from(self.sample) << bits);
|
||||
if bits != 7 {
|
||||
Phase::Write {
|
||||
register,
|
||||
data,
|
||||
offset,
|
||||
byte,
|
||||
bits: bits + 1,
|
||||
}
|
||||
} else {
|
||||
data[usize::from(offset)] = byte;
|
||||
if offset + 1 == register.len() {
|
||||
self.commit(register, data)?;
|
||||
Phase::Done
|
||||
} else {
|
||||
Phase::Write {
|
||||
register,
|
||||
data,
|
||||
offset: offset + 1,
|
||||
byte: 0,
|
||||
bits: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn commit(&mut self, register: Register, data: [u8; 7]) -> Result<(), &'static str> {
|
||||
let hour24 = self.control & 0x40 != 0;
|
||||
match register {
|
||||
Register::Control => {
|
||||
if data[0] & 0x2a != 0 {
|
||||
return Err("RTC interrupt/unknown control bits");
|
||||
}
|
||||
self.control = data[0] & 0x40;
|
||||
}
|
||||
Register::DateTime | Register::Time => {
|
||||
let data = if register == Register::Time {
|
||||
let mut date = self.calendar.encode(hour24);
|
||||
date[4..].copy_from_slice(&data[..3]);
|
||||
date
|
||||
} else {
|
||||
data
|
||||
};
|
||||
self.calendar =
|
||||
RtcDateTime::decode(data, hour24).map_err(|_| "RTC invalid calendar data")?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn command(&mut self, byte: u8) -> Result<Phase, &'static str> {
|
||||
// First four wire bits are 0,1,1,0. Convert to MSB-first command numbering.
|
||||
if byte & 15 != 6 {
|
||||
return Err("RTC command encoding");
|
||||
}
|
||||
let command = byte.reverse_bits();
|
||||
let register = match (command >> 1) & 7 {
|
||||
0 => {
|
||||
self.control = 0;
|
||||
self.calendar = RtcDateTime::default();
|
||||
return Ok(Phase::Done);
|
||||
}
|
||||
1 => Register::Control,
|
||||
2 => Register::DateTime,
|
||||
3 => Register::Time,
|
||||
6 => return Err("RTC force interrupt"),
|
||||
_ => return Err("RTC unused command"),
|
||||
};
|
||||
if command & 1 == 0 {
|
||||
Ok(Phase::Write {
|
||||
register,
|
||||
data: [0; 7],
|
||||
offset: 0,
|
||||
byte: 0,
|
||||
bits: 0,
|
||||
})
|
||||
} else {
|
||||
let mut data = self.calendar.encode(self.control & 0x40 != 0);
|
||||
match register {
|
||||
Register::Control => data[0] = self.control,
|
||||
Register::Time => {
|
||||
data.copy_within(4..7, 0);
|
||||
}
|
||||
Register::DateTime => {}
|
||||
}
|
||||
Ok(Phase::Read {
|
||||
data,
|
||||
len: register.len(),
|
||||
bit: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -27,7 +27,7 @@ use iwram_bus::IwramBus;
|
|||
|
||||
use crate::{
|
||||
audio::{Audio, StereoLevel},
|
||||
cartridge::{Cartridge, CartridgeHardware},
|
||||
cartridge::{Cartridge, CartridgeHardware, RtcDateTime, RtcError},
|
||||
cpu::InstructionSet,
|
||||
display::{DisplayPosition, CYCLES_PER_LINE, HBLANK_START, VISIBLE_LINES},
|
||||
dma::{DmaError, DMA_BASE, DMA_END},
|
||||
|
|
@ -193,6 +193,30 @@ impl Memory {
|
|||
self.cartridge = Cartridge::new(hardware);
|
||||
}
|
||||
|
||||
/// Inspect the live calendar, not an in-progress serial read snapshot.
|
||||
pub fn rtc_datetime(&self) -> Option<RtcDateTime> {
|
||||
self.cartridge_state().rtc_datetime()
|
||||
}
|
||||
|
||||
/// Set the RTC calendar between machine steps. Existing serial read snapshots stay latched.
|
||||
pub fn set_rtc_datetime(&mut self, date: RtcDateTime) -> Result<(), RtcError> {
|
||||
assert!(
|
||||
self.cartridge_step.get().is_none(),
|
||||
"active cartridge transaction"
|
||||
);
|
||||
self.cartridge.set_rtc_datetime(date)
|
||||
}
|
||||
|
||||
/// Supply battery-clock elapsed seconds independently of CPU cycles, HALT, and STOP.
|
||||
/// Call between machine steps. This requests no IRQ and never wakes the CPU.
|
||||
pub fn advance_rtc_seconds(&mut self, seconds: u64) -> Result<(), RtcError> {
|
||||
assert!(
|
||||
self.cartridge_step.get().is_none(),
|
||||
"active cartridge transaction"
|
||||
);
|
||||
self.cartridge.advance_rtc_seconds(seconds)
|
||||
}
|
||||
|
||||
fn cartridge_state(&self) -> Cartridge {
|
||||
self.cartridge_step.get().unwrap_or(self.cartridge)
|
||||
}
|
||||
|
|
@ -215,14 +239,15 @@ impl Memory {
|
|||
}
|
||||
|
||||
/// Total elapsed emulated clock cycles, wrapping at u64::MAX. Reads/writes
|
||||
/// alone consume no cycles. STOP discards externally supplied clock advances.
|
||||
/// alone consume no cycles. STOP discards supplied GBA-cycle advances, not independent RTC time.
|
||||
pub fn cycles(&self) -> u64 {
|
||||
self.cycles
|
||||
}
|
||||
|
||||
/// Advance devices and optional row capture. Multiple unserviced DMA requests
|
||||
/// coalesce. No CPU or DMA work is executed by this method. STOP freezes all
|
||||
/// coalesce. No CPU or DMA work is executed by this method. STOP freezes GBA-clock
|
||||
/// progress: supplied cycles are ignored until an external wake condition.
|
||||
/// The independently supplied RTC battery clock is not advanced by this method.
|
||||
/// This device-only API does not advance opcode prefetch. CPU/DMA/HALT steps
|
||||
/// account for their own cartridge-bus time before updating device clocks.
|
||||
pub fn advance_cycles(&mut self, cycles: u32) {
|
||||
|
|
|
|||
|
|
@ -36,9 +36,9 @@ fn rejected_rtc_command_edge_rolls_back_arm_and_thumb_steps() {
|
|||
memory.write16(GPIO_DATA, 1).unwrap();
|
||||
memory.write16(GPIO_DIRECTION, 7).unwrap();
|
||||
memory.write16(GPIO_DATA, 5).unwrap();
|
||||
// All but the final rising edge of an original date/time read command (0x65).
|
||||
// All but the final rising edge of an unsupported force-interrupt command (0x6d).
|
||||
for bit in (1..8).rev() {
|
||||
let pins = 4 | (((0x65 >> bit) & 1) << 1);
|
||||
let pins = 4 | (((0x6d >> bit) & 1) << 1);
|
||||
memory.write16(GPIO_DATA, pins).unwrap();
|
||||
memory.write16(GPIO_DATA, pins | 1).unwrap();
|
||||
}
|
||||
|
|
@ -47,7 +47,7 @@ fn rejected_rtc_command_edge_rolls_back_arm_and_thumb_steps() {
|
|||
let mut machine = Machine::new(cpu, memory);
|
||||
let cpu = machine.cpu().clone();
|
||||
let error = machine.step().unwrap_err();
|
||||
assert!(error.to_string().contains("RTC calendar access"));
|
||||
assert!(error.to_string().contains("RTC force interrupt"));
|
||||
assert_eq!(machine.step(), Err(error));
|
||||
assert_eq!(machine.cpu(), &cpu);
|
||||
assert_eq!(machine.memory().cartridge, cartridge);
|
||||
|
|
@ -56,6 +56,86 @@ fn rejected_rtc_command_edge_rolls_back_arm_and_thumb_steps() {
|
|||
}
|
||||
}
|
||||
|
||||
fn prepare_calendar_final_edge(memory: &mut Memory, invalid: bool) {
|
||||
memory.set_cartridge_hardware(CartridgeHardware::Rtc);
|
||||
memory.write16(GPIO_CONTROL, 1).unwrap();
|
||||
memory.write16(GPIO_DATA, 1).unwrap();
|
||||
memory.write16(GPIO_DIRECTION, 7).unwrap();
|
||||
memory.write16(GPIO_DATA, 5).unwrap();
|
||||
let mut bits: Vec<bool> = (0..8).rev().map(|i| 0x64 & (1 << i) != 0).collect();
|
||||
for byte in [
|
||||
0x24,
|
||||
2,
|
||||
0x29,
|
||||
4,
|
||||
0x12,
|
||||
0x34,
|
||||
if invalid { 0x6a } else { 0x56 },
|
||||
] {
|
||||
bits.extend((0..8).map(|i| byte & (1 << i) != 0));
|
||||
}
|
||||
for (i, bit) in bits.into_iter().enumerate() {
|
||||
let pins = 4 | (u16::from(bit) << 1);
|
||||
memory.write16(GPIO_DATA, pins).unwrap();
|
||||
if i != 63 {
|
||||
memory.write16(GPIO_DATA, pins | 1).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn calendar_payload_commits_at_final_arm_or_thumb_edge_and_rejects_invalid_data_atomically() {
|
||||
for thumb in [false, true] {
|
||||
for invalid in [false, true] {
|
||||
let (cpu, mut memory) = prepared(
|
||||
thumb,
|
||||
&[if thumb { 0x8008 } else { 0xe1c100b0 }],
|
||||
&[(0, 5), (1, GPIO_DATA)],
|
||||
);
|
||||
prepare_calendar_final_edge(&mut memory, invalid);
|
||||
let before = memory.cartridge;
|
||||
let mut machine = Machine::new(cpu, memory);
|
||||
let cpu = machine.cpu().clone();
|
||||
if invalid {
|
||||
let error = machine.step().unwrap_err();
|
||||
assert!(error.to_string().contains("invalid calendar data"));
|
||||
assert_eq!(machine.step(), Err(error));
|
||||
assert_eq!(machine.cpu(), &cpu);
|
||||
assert_eq!(machine.memory().cartridge, before);
|
||||
assert_eq!(machine.cycles(), 0);
|
||||
} else {
|
||||
machine.step().unwrap();
|
||||
assert_eq!(
|
||||
machine.memory().rtc_datetime().unwrap().components(),
|
||||
[24, 2, 29, 4, 12, 34, 56]
|
||||
);
|
||||
assert_eq!(machine.last_timing().data_cycles, 5);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn later_block_diagnostic_discards_an_earlier_calendar_commit() {
|
||||
let (_, mut memory) = prepared(false, &[0xe1a00000], &[]);
|
||||
prepare_calendar_final_edge(&mut memory, false);
|
||||
let before = memory.cartridge;
|
||||
assert!(memory
|
||||
.write_words(&[
|
||||
(GPIO_DATA, 0x00070005),
|
||||
(0x02000000, 0x12345678),
|
||||
(crate::io::SIOCNT, 0x3000)
|
||||
])
|
||||
.is_err());
|
||||
assert_eq!(memory.cartridge, before);
|
||||
assert_eq!(memory.read32(0x02000000).unwrap(), 0);
|
||||
memory.write_words(&[(GPIO_DATA, 0x00070005)]).unwrap();
|
||||
assert_eq!(
|
||||
memory.rtc_datetime().unwrap().components(),
|
||||
[24, 2, 29, 4, 12, 34, 56]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_store_failure_retains_gpio_and_cpu_state() {
|
||||
// First word targets DATA/DIRECTION. Second reaches unmapped padding after CONTROL.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
//! Original GPIO and RTC control transactions. No game code or host clock.
|
||||
//! Original GPIO and RTC transactions. No game code or host clock.
|
||||
#[path = "cartridge_gpio/calendar.rs"]
|
||||
mod calendar;
|
||||
use gba_core::{
|
||||
cartridge::{CartridgeHardware, GPIO_CONTROL, GPIO_DATA, GPIO_DIRECTION},
|
||||
cpu::Cpu,
|
||||
|
|
@ -180,10 +182,6 @@ fn dropping_select_aborts_partial_commands_and_parameters() {
|
|||
#[test]
|
||||
fn unsupported_commands_and_control_bits_fail_at_the_last_edge_without_partial_writes() {
|
||||
for (byte, description) in [
|
||||
(0x64, "calendar"),
|
||||
(0x65, "calendar"),
|
||||
(0x66, "calendar"),
|
||||
(0x67, "calendar"),
|
||||
(0x6c, "force interrupt"),
|
||||
(0x68, "unused command"),
|
||||
(0xff, "command encoding"),
|
||||
|
|
|
|||
196
crates/gba-core/tests/cartridge_gpio/calendar.rs
Normal file
196
crates/gba-core/tests/cartridge_gpio/calendar.rs
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
use super::*;
|
||||
use gba_core::{
|
||||
cartridge::{RtcDateTime, RtcError},
|
||||
io::{HALTCNT, IE, IME},
|
||||
};
|
||||
|
||||
fn setup(parts: [u8; 7]) -> Memory {
|
||||
let mut bus = memory();
|
||||
bus.write16(GPIO_CONTROL, 1).unwrap();
|
||||
bus.set_rtc_datetime(RtcDateTime::new(parts).unwrap())
|
||||
.unwrap();
|
||||
bus
|
||||
}
|
||||
fn read_byte(bus: &mut Memory) -> u8 {
|
||||
let mut byte = 0;
|
||||
for bit in 0..8 {
|
||||
bus.write16(GPIO_DATA, 4).unwrap();
|
||||
let data = bus.read16(GPIO_DATA).unwrap() & 2;
|
||||
bus.write16(GPIO_DATA, 5).unwrap();
|
||||
assert_eq!(bus.read16(GPIO_DATA).unwrap() & 2, data);
|
||||
byte |= (data as u8 >> 1) << bit;
|
||||
}
|
||||
byte
|
||||
}
|
||||
fn read(bus: &mut Memory, cmd: u8, len: usize) -> Vec<u8> {
|
||||
start(bus);
|
||||
command(bus, cmd, true).unwrap();
|
||||
bus.write16(GPIO_DIRECTION, 5).unwrap();
|
||||
let bytes = (0..len).map(|_| read_byte(bus)).collect();
|
||||
bus.write16(GPIO_DATA, 1).unwrap();
|
||||
bytes
|
||||
}
|
||||
fn write(bus: &mut Memory, cmd: u8, bytes: &[u8]) -> Result<(), MemoryError> {
|
||||
start(bus);
|
||||
command(bus, cmd, true)?;
|
||||
for &byte in bytes {
|
||||
parameter(bus, byte)?;
|
||||
}
|
||||
bus.write16(GPIO_DATA, 1)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn calendar_and_time_reads_use_bcd_and_gba_pm_bit() {
|
||||
let mut bus = setup([24, 2, 29, 4, 23, 58, 59]);
|
||||
assert_eq!(
|
||||
read(&mut bus, 0x65, 7),
|
||||
[0x24, 2, 0x29, 4, 0xa3, 0x58, 0x59]
|
||||
);
|
||||
assert_eq!(read(&mut bus, 0x67, 3), [0xa3, 0x58, 0x59]);
|
||||
write(&mut bus, 0x62, &[0]).unwrap(); // 12-hour presentation preserves the actual hour.
|
||||
assert_eq!(read(&mut bus, 0x67, 3), [0x91, 0x58, 0x59]);
|
||||
bus.advance_rtc_seconds(61).unwrap();
|
||||
assert_eq!(read(&mut bus, 0x65, 7), [0x24, 3, 1, 5, 0, 0, 0]);
|
||||
assert_eq!(bus.read16(IF).unwrap(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_writes_commit_calendar_or_time_and_mask_unused_bits() {
|
||||
let mut bus = setup([0, 1, 1, 0, 0, 0, 0]);
|
||||
write(&mut bus, 0x64, &[0x24, 0xe2, 0xe9, 0xfc, 0x63, 0xd8, 0xd9]).unwrap();
|
||||
assert_eq!(
|
||||
bus.rtc_datetime().unwrap().components(),
|
||||
[24, 2, 29, 4, 23, 58, 59]
|
||||
);
|
||||
write(&mut bus, 0x66, &[0x80, 0x12, 0x34]).unwrap(); // PM ignored in 24-hour writes.
|
||||
assert_eq!(
|
||||
bus.rtc_datetime().unwrap().components(),
|
||||
[24, 2, 29, 4, 0, 12, 34]
|
||||
);
|
||||
write(&mut bus, 0x62, &[0]).unwrap();
|
||||
write(&mut bus, 0x66, &[0x80, 0, 0]).unwrap(); // Noon is 00 with PM, not 12.
|
||||
assert_eq!(
|
||||
bus.rtc_datetime().unwrap().components(),
|
||||
[24, 2, 29, 4, 12, 0, 0]
|
||||
);
|
||||
assert_eq!(read(&mut bus, 0x67, 3), [0x80, 0, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_snapshot_survives_mid_transfer_tick_and_host_reseed() {
|
||||
let mut bus = setup([24, 12, 31, 2, 23, 59, 59]);
|
||||
start(&mut bus);
|
||||
command(&mut bus, 0x65, true).unwrap();
|
||||
bus.write16(GPIO_DIRECTION, 5).unwrap();
|
||||
assert_eq!(read_byte(&mut bus), 0x24);
|
||||
bus.advance_rtc_seconds(1).unwrap();
|
||||
assert_eq!(
|
||||
bus.rtc_datetime().unwrap().components(),
|
||||
[25, 1, 1, 3, 0, 0, 0]
|
||||
);
|
||||
bus.set_rtc_datetime(RtcDateTime::new([50, 6, 7, 1, 8, 9, 10]).unwrap())
|
||||
.unwrap();
|
||||
let tail: Vec<_> = (0..6).map(|_| read_byte(&mut bus)).collect();
|
||||
assert_eq!(tail, [0x12, 0x31, 2, 0xa3, 0x59, 0x59]);
|
||||
bus.write16(GPIO_DATA, 1).unwrap();
|
||||
assert_eq!(read(&mut bus, 0x65, 7), [0x50, 6, 7, 1, 8, 9, 0x10]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_payloads_abort_without_calendar_changes_and_time_writes_preserve_live_date() {
|
||||
let original = [24, 2, 28, 6, 23, 59, 59];
|
||||
for bits in 0..56 {
|
||||
let mut bus = setup(original);
|
||||
start(&mut bus);
|
||||
command(&mut bus, 0x64, true).unwrap();
|
||||
send_bits(&mut bus, (0..bits).map(|_| false)).unwrap();
|
||||
bus.write16(GPIO_DATA, 1).unwrap();
|
||||
assert_eq!(bus.rtc_datetime().unwrap().components(), original);
|
||||
}
|
||||
let mut bus = setup(original);
|
||||
start(&mut bus);
|
||||
command(&mut bus, 0x66, true).unwrap();
|
||||
parameter(&mut bus, 0x12).unwrap();
|
||||
bus.advance_rtc_seconds(1).unwrap();
|
||||
parameter(&mut bus, 0x34).unwrap();
|
||||
parameter(&mut bus, 0x56).unwrap();
|
||||
bus.write16(GPIO_DATA, 1).unwrap();
|
||||
assert_eq!(
|
||||
bus.rtc_datetime().unwrap().components(),
|
||||
[24, 2, 29, 0, 12, 34, 56]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_calendar_payload_is_diagnostic_retryable_and_cannot_commit() {
|
||||
for payload in [
|
||||
[0xfa, 1, 1, 0, 0, 0, 0],
|
||||
[0x23, 2, 0x29, 0, 0, 0, 0],
|
||||
[0, 0, 1, 0, 0, 0, 0],
|
||||
[0, 1, 1, 7, 0, 0, 0],
|
||||
[0, 1, 1, 0, 0x24, 0, 0],
|
||||
[0, 1, 1, 0, 0, 0x60, 0],
|
||||
[0, 1, 1, 0, 0, 0, 0x6a],
|
||||
] {
|
||||
let mut bus = setup([24, 5, 6, 3, 7, 8, 9]);
|
||||
let before = bus.rtc_datetime();
|
||||
let error = write(&mut bus, 0x64, &payload).unwrap_err();
|
||||
assert!(error.to_string().contains("invalid calendar data"));
|
||||
let pins = bus.read16(GPIO_DATA).unwrap();
|
||||
assert_eq!(pins & 1, 0);
|
||||
assert_eq!(bus.write16(GPIO_DATA, pins | 1), Err(error));
|
||||
assert_eq!(bus.rtc_datetime(), before);
|
||||
bus.write16(GPIO_DATA, 1).unwrap();
|
||||
assert_eq!(read(&mut bus, 0x65, 7), [0x24, 5, 6, 3, 7, 8, 9]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_affects_calendar_and_mode_but_not_the_gpio_overlay() {
|
||||
for cmd in [0x60, 0x61] {
|
||||
let mut bus = setup([99, 12, 31, 6, 23, 59, 59]);
|
||||
write(&mut bus, cmd, &[]).unwrap();
|
||||
assert_eq!(bus.rtc_datetime(), Some(RtcDateTime::default()));
|
||||
assert_eq!(control(&mut bus, true), 0);
|
||||
assert_eq!(read(&mut bus, 0x65, 7), [0, 1, 1, 0, 0, 0, 0]);
|
||||
assert_eq!(bus.read16(GPIO_CONTROL).unwrap(), 1);
|
||||
bus.advance_rtc_seconds(43200).unwrap();
|
||||
assert_eq!(read(&mut bus, 0x67, 3), [0x80, 0, 0]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn battery_clock_is_independent_of_cpu_cycles_halt_stop_and_device_presence() {
|
||||
let mut absent = Memory::new(vec![0; 4]).unwrap();
|
||||
assert_eq!(absent.rtc_datetime(), None);
|
||||
assert_eq!(
|
||||
absent.set_rtc_datetime(RtcDateTime::default()),
|
||||
Err(RtcError::NotAttached)
|
||||
);
|
||||
assert_eq!(absent.advance_rtc_seconds(0), Err(RtcError::NotAttached));
|
||||
for stop in [false, true] {
|
||||
let mut bus = setup([0, 1, 1, 0, 0, 0, 0]);
|
||||
bus.write16(IE, 0x2080).unwrap();
|
||||
bus.write16(IME, 1).unwrap();
|
||||
bus.write8(HALTCNT, if stop { 128 } else { 0 }).unwrap();
|
||||
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
|
||||
for _ in 0..20 {
|
||||
machine.step().unwrap();
|
||||
}
|
||||
machine.memory_mut().advance_cycles(16777216);
|
||||
assert_eq!(
|
||||
machine.memory().rtc_datetime(),
|
||||
Some(RtcDateTime::default())
|
||||
);
|
||||
let cycles = machine.cycles();
|
||||
machine.memory_mut().advance_rtc_seconds(86401).unwrap();
|
||||
assert_eq!(
|
||||
machine.memory().rtc_datetime().unwrap().components(),
|
||||
[0, 1, 2, 1, 0, 0, 1]
|
||||
);
|
||||
assert_eq!(machine.cycles(), cycles);
|
||||
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
|
||||
assert_eq!(machine.halted(), !stop);
|
||||
assert_eq!(machine.stopped(), stop);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue