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.
|
//! Host-only raw ROM loading, execution modes, and diagnostics.
|
||||||
|
|
||||||
|
pub(crate) mod rtc_clock;
|
||||||
pub mod suite;
|
pub mod suite;
|
||||||
pub mod window;
|
pub mod window;
|
||||||
|
|
||||||
|
|
@ -13,7 +14,7 @@ use std::{
|
||||||
|
|
||||||
use gba_core::{
|
use gba_core::{
|
||||||
bios,
|
bios,
|
||||||
cartridge::CartridgeHardware,
|
cartridge::{CartridgeHardware, RtcError},
|
||||||
machine::{Machine, MachineError, StepKind},
|
machine::{Machine, MachineError, StepKind},
|
||||||
memory::ROM_CAPACITY,
|
memory::ROM_CAPACITY,
|
||||||
};
|
};
|
||||||
|
|
@ -176,9 +177,23 @@ struct Report {
|
||||||
reason: Reason,
|
reason: Reason,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
fn run_bounded(machine: &mut Machine, limit: u64) -> Report {
|
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 mut stats = Stats::default();
|
||||||
let reason = loop {
|
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.
|
// 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.
|
// Check before the budget so STOP on the last step is reported correctly.
|
||||||
if machine.stopped() {
|
if machine.stopped() {
|
||||||
|
|
@ -193,7 +208,10 @@ fn run_bounded(machine: &mut Machine, limit: u64) -> Report {
|
||||||
Err(error) => break Reason::Diagnostic(error),
|
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<()> {
|
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."
|
"Original BIOS replacement; no commercial-game compatibility claim."
|
||||||
)?;
|
)?;
|
||||||
if options.hardware == CartridgeHardware::Rtc {
|
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 {
|
let steps = match options.mode {
|
||||||
Mode::Terminal { steps } => steps,
|
Mode::Terminal { steps } => steps,
|
||||||
|
|
@ -257,7 +275,12 @@ pub fn execute(options: Options, writer: &mut impl Write) -> Result<(), Box<dyn
|
||||||
machine
|
machine
|
||||||
.memory_mut()
|
.memory_mut()
|
||||||
.set_cartridge_hardware(options.hardware);
|
.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)?;
|
write_report(writer, &machine, &report)?;
|
||||||
writer.flush()?;
|
writer.flush()?;
|
||||||
if let Reason::Diagnostic(error) = report.reason {
|
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);
|
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 {
|
pub fn frame(&self) -> &Framebuffer {
|
||||||
&self.frame
|
&self.frame
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,13 @@ pub fn run(
|
||||||
) -> Result<(), Box<dyn Error>> {
|
) -> Result<(), Box<dyn Error>> {
|
||||||
let mut session = Session::new(bytes)?;
|
let mut session = Session::new(bytes)?;
|
||||||
session.set_cartridge_hardware(hardware);
|
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!(
|
writeln!(
|
||||||
writer,
|
writer,
|
||||||
"ROM window: Arrows=D-pad; Z/X=A/B; Q/W=L/R; Enter=Start; Backspace=Select; Escape exits."
|
"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.
|
// Show a black buffer and pump initial events before boot starts.
|
||||||
window.update_with_buffer(session.frame().pixels(), WIDTH, HEIGHT)?;
|
window.update_with_buffer(session.frame().pixels(), WIDTH, HEIGHT)?;
|
||||||
while window.is_open() && !window.is_key_down(Key::Escape) {
|
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 buttons = read_buttons(window.is_active(), |key| window.is_key_down(key));
|
||||||
let update = session.update(buttons)?;
|
let update = session.update(buttons)?;
|
||||||
let stopped = update == Update::Stopped;
|
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 --steps COUNT Run a raw ROM in the terminal (1..=100000000 steps)\n\
|
||||||
--rom PATH --window Open a raw ROM with keyboard input\n\
|
--rom PATH --window Open a raw ROM with keyboard input\n\
|
||||||
--frames COUNT Exit ROM window after 1..=100000 captured frames\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\
|
--test-suite PATH.json Run bounded ROM assertions; write a JSON report\n\
|
||||||
--cpu-demo Run the terminal-only ARM/Thumb instruction demo\n\
|
--cpu-demo Run the terminal-only ARM/Thumb instruction demo\n\
|
||||||
--timer-demo Run the timer IRQ demo with nominal cycle costs\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]
|
#[test]
|
||||||
fn rtc_selection_is_explicit_and_does_not_modify_the_rom_file() {
|
fn rtc_selection_is_explicit_and_does_not_modify_the_rom_file() {
|
||||||
let fixture = Fixture::new();
|
let fixture = Fixture::new();
|
||||||
|
|
@ -100,7 +158,7 @@ fn rtc_selection_is_explicit_and_does_not_modify_the_rom_file() {
|
||||||
.output()
|
.output()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(selected.status.success(), "{}", stderr(&selected));
|
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!(stdout(&selected).contains("r2=0x00000001"));
|
||||||
assert_eq!(fs::read(&path).unwrap(), original);
|
assert_eq!(fs::read(&path).unwrap(), original);
|
||||||
let duplicate = command()
|
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!(rtc_output.status.success(), "{}", stderr(&rtc_output));
|
||||||
assert!(stdout(&rtc_output).contains("Captured ROM frames: 3"));
|
assert!(stdout(&rtc_output).contains("Captured ROM frames: 3"));
|
||||||
assert!(stdout(&rtc_output).contains("r2=0x00000001"));
|
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 [
|
for (code, error) in [
|
||||||
(vec![0xef03_0000, 0xeaff_fffe], "entered STOP"),
|
(vec![0xef03_0000, 0xeaff_fffe], "entered STOP"),
|
||||||
(vec![0xee00_0000], "instruction"),
|
(vec![0xee00_0000], "instruction"),
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,11 @@
|
||||||
|
|
||||||
use crate::memory::MemoryError;
|
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_DATA: u32 = 0x0800_00c4;
|
||||||
pub const GPIO_DIRECTION: u32 = 0x0800_00c6;
|
pub const GPIO_DIRECTION: u32 = 0x0800_00c6;
|
||||||
pub const GPIO_CONTROL: u32 = 0x0800_00c8;
|
pub const GPIO_CONTROL: u32 = 0x0800_00c8;
|
||||||
|
|
@ -11,7 +16,7 @@ pub const GPIO_CONTROL: u32 = 0x0800_00c8;
|
||||||
pub enum CartridgeHardware {
|
pub enum CartridgeHardware {
|
||||||
#[default]
|
#[default]
|
||||||
None,
|
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,
|
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 {
|
pub(crate) fn mapped(&self, address: u32) -> bool {
|
||||||
self.gpio.is_some() && (GPIO_DATA..GPIO_CONTROL + 2).contains(&address)
|
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)
|
(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::{
|
use crate::{
|
||||||
audio::{Audio, StereoLevel},
|
audio::{Audio, StereoLevel},
|
||||||
cartridge::{Cartridge, CartridgeHardware},
|
cartridge::{Cartridge, CartridgeHardware, RtcDateTime, RtcError},
|
||||||
cpu::InstructionSet,
|
cpu::InstructionSet,
|
||||||
display::{DisplayPosition, CYCLES_PER_LINE, HBLANK_START, VISIBLE_LINES},
|
display::{DisplayPosition, CYCLES_PER_LINE, HBLANK_START, VISIBLE_LINES},
|
||||||
dma::{DmaError, DMA_BASE, DMA_END},
|
dma::{DmaError, DMA_BASE, DMA_END},
|
||||||
|
|
@ -193,6 +193,30 @@ impl Memory {
|
||||||
self.cartridge = Cartridge::new(hardware);
|
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 {
|
fn cartridge_state(&self) -> Cartridge {
|
||||||
self.cartridge_step.get().unwrap_or(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
|
/// 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 {
|
pub fn cycles(&self) -> u64 {
|
||||||
self.cycles
|
self.cycles
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Advance devices and optional row capture. Multiple unserviced DMA requests
|
/// 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.
|
/// 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
|
/// This device-only API does not advance opcode prefetch. CPU/DMA/HALT steps
|
||||||
/// account for their own cartridge-bus time before updating device clocks.
|
/// account for their own cartridge-bus time before updating device clocks.
|
||||||
pub fn advance_cycles(&mut self, cycles: u32) {
|
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_DATA, 1).unwrap();
|
||||||
memory.write16(GPIO_DIRECTION, 7).unwrap();
|
memory.write16(GPIO_DIRECTION, 7).unwrap();
|
||||||
memory.write16(GPIO_DATA, 5).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() {
|
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).unwrap();
|
||||||
memory.write16(GPIO_DATA, pins | 1).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 mut machine = Machine::new(cpu, memory);
|
||||||
let cpu = machine.cpu().clone();
|
let cpu = machine.cpu().clone();
|
||||||
let error = machine.step().unwrap_err();
|
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.step(), Err(error));
|
||||||
assert_eq!(machine.cpu(), &cpu);
|
assert_eq!(machine.cpu(), &cpu);
|
||||||
assert_eq!(machine.memory().cartridge, cartridge);
|
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]
|
#[test]
|
||||||
fn block_store_failure_retains_gpio_and_cpu_state() {
|
fn block_store_failure_retains_gpio_and_cpu_state() {
|
||||||
// First word targets DATA/DIRECTION. Second reaches unmapped padding after CONTROL.
|
// 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::{
|
use gba_core::{
|
||||||
cartridge::{CartridgeHardware, GPIO_CONTROL, GPIO_DATA, GPIO_DIRECTION},
|
cartridge::{CartridgeHardware, GPIO_CONTROL, GPIO_DATA, GPIO_DIRECTION},
|
||||||
cpu::Cpu,
|
cpu::Cpu,
|
||||||
|
|
@ -180,10 +182,6 @@ fn dropping_select_aborts_partial_commands_and_parameters() {
|
||||||
#[test]
|
#[test]
|
||||||
fn unsupported_commands_and_control_bits_fail_at_the_last_edge_without_partial_writes() {
|
fn unsupported_commands_and_control_bits_fail_at_the_last_edge_without_partial_writes() {
|
||||||
for (byte, description) in [
|
for (byte, description) in [
|
||||||
(0x64, "calendar"),
|
|
||||||
(0x65, "calendar"),
|
|
||||||
(0x66, "calendar"),
|
|
||||||
(0x67, "calendar"),
|
|
||||||
(0x6c, "force interrupt"),
|
(0x6c, "force interrupt"),
|
||||||
(0x68, "unused command"),
|
(0x68, "unused command"),
|
||||||
(0xff, "command encoding"),
|
(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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -48,7 +48,11 @@ The repository is a Cargo workspace with three crates:
|
||||||
| `crates/gba-core/src/cpu/transfer_tests.rs` | Stack, addressing, overlap, empty-list, and swap tests |
|
| `crates/gba-core/src/cpu/transfer_tests.rs` | Stack, addressing, overlap, empty-list, and swap tests |
|
||||||
| `crates/gba-core/src/cpu/tests.rs` | Condition truth tables and immediate arithmetic tests |
|
| `crates/gba-core/src/cpu/tests.rs` | Condition truth tables and immediate arithmetic tests |
|
||||||
| `crates/gba-core/src/cpu/instruction_tests.rs` | Register operations, transfers, branches, and edge cases |
|
| `crates/gba-core/src/cpu/instruction_tests.rs` | Register operations, transfers, branches, and edge cases |
|
||||||
| `crates/gba-core/src/cartridge.rs` | Explicit GPIO/RTC selection, ROM read overlay, pin directions, serial command/control, and bounded diagnostics |
|
| `crates/gba-core/src/cartridge.rs` | Explicit GPIO/RTC selection, ROM read overlay, pin directions, caller-clock routing, and bounded diagnostics |
|
||||||
|
| `crates/gba-core/src/cartridge/rtc.rs` | RTC framing, control/calendar commands, read snapshots, complete writes, and reset |
|
||||||
|
| `crates/gba-core/src/cartridge/calendar.rs` | Validated decimal calendar, BCD representation, checked epoch conversion, and bounded elapsed-time arithmetic |
|
||||||
|
| `crates/gba-core/src/cartridge/calendar/tests.rs` | Calendar field validation, century-wide rollovers, hour modes, and large/split advances |
|
||||||
|
| `crates/gba-core/tests/cartridge_gpio/calendar.rs` | Calendar wire transfers, snapshots, aborts, invalid payloads, reset, and independent HALT/STOP time |
|
||||||
| `crates/gba-core/tests/cartridge_gpio.rs` | GPIO lanes, ROM preservation, RTC framing/control/aborts, unsupported commands, and DMA3 access |
|
| `crates/gba-core/tests/cartridge_gpio.rs` | GPIO lanes, ROM preservation, RTC framing/control/aborts, unsupported commands, and DMA3 access |
|
||||||
| `crates/gba-core/src/memory/cartridge_step_tests.rs` | ARM/Thumb GPIO timing, rejected command-edge rollback, block preflight, and staged fetch/read overlays |
|
| `crates/gba-core/src/memory/cartridge_step_tests.rs` | ARM/Thumb GPIO timing, rejected command-edge rollback, block preflight, and staged fetch/read overlays |
|
||||||
| `crates/gba-core/src/memory.rs` | Memory mapping, I/O routing, write validation, and device clock |
|
| `crates/gba-core/src/memory.rs` | Memory mapping, I/O routing, write validation, and device clock |
|
||||||
|
|
@ -150,6 +154,7 @@ The repository is a Cargo workspace with three crates:
|
||||||
| `crates/desktop/src/cartridge/suite/gameplay_tests.rs` | VBlank completion, input snapshots, captured pixels, and rendering failures |
|
| `crates/desktop/src/cartridge/suite/gameplay_tests.rs` | VBlank completion, input snapshots, captured pixels, and rendering failures |
|
||||||
| `crates/desktop/tests/rom_suite_cli.rs` | File-backed suite execution, case isolation, output schema, determinism, and exit status |
|
| `crates/desktop/tests/rom_suite_cli.rs` | File-backed suite execution, case isolation, output schema, determinism, and exit status |
|
||||||
| `crates/desktop/examples/write_test_suite.rs` | Original ARM/Thumb/BIOS fixture generator with a local JSON suite |
|
| `crates/desktop/examples/write_test_suite.rs` | Original ARM/Thumb/BIOS fixture generator with a local JSON suite |
|
||||||
|
| `crates/desktop/src/cartridge/rtc_clock.rs` | Host-only UTC seed and monotonic elapsed-time adapter, with injected epoch/duration tests |
|
||||||
| `crates/desktop/src/cartridge/window.rs` | ROM-window session, bounded slices, capture, STOP/input handling, and progress limits |
|
| `crates/desktop/src/cartridge/window.rs` | ROM-window session, bounded slices, capture, STOP/input handling, and progress limits |
|
||||||
| `crates/desktop/src/cartridge/window/tests.rs` | Original ROM pixels, keyboard mapping, STOP wake, HALT, frame budgets, and diagnostics |
|
| `crates/desktop/src/cartridge/window/tests.rs` | Original ROM pixels, keyboard mapping, STOP wake, HALT, frame budgets, and diagnostics |
|
||||||
| `crates/desktop/src/desktop/rom.rs` | Main-thread ROM window, input polling, presentation pacing, and frame-limited exit |
|
| `crates/desktop/src/desktop/rom.rs` | Main-thread ROM window, input polling, presentation pacing, and frame-limited exit |
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
# Game Pak GPIO and RTC control subset
|
# Game Pak GPIO and RTC calendar subset
|
||||||
|
|
||||||
The core supports an explicitly selected cartridge GPIO interface and RTC command/control transactions.
|
The core supports an explicitly selected cartridge GPIO interface and RTC calendar/control transactions.
|
||||||
GPIO means general-purpose input/output. RTC means real-time clock.
|
GPIO means general-purpose input/output. RTC means real-time clock.
|
||||||
Calendar access, clock advancement, battery persistence, RTC interrupts, and other cartridge sensors remain unimplemented.
|
An explicit caller supplies initial time and elapsed seconds. The core never samples a host clock.
|
||||||
Unsupported operations return diagnostics. No fabricated date or successful calendar transfer is reported.
|
Battery persistence, RTC interrupts, and other cartridge sensors remain unimplemented.
|
||||||
|
Unsupported operations and invalid calendar payloads return diagnostics.
|
||||||
|
|
||||||
## Selection
|
## Selection
|
||||||
|
|
||||||
|
|
@ -18,9 +19,10 @@ direnv exec . cargo run --locked --release -- --rom roms/pokemon-emerald.gba --r
|
||||||
direnv exec . cargo run --locked --release -- --rom roms/pokemon-emerald.gba --rtc --window --frames 60
|
direnv exec . cargo run --locked --release -- --rom roms/pokemon-emerald.gba --rtc --window --frames 60
|
||||||
```
|
```
|
||||||
|
|
||||||
The report states that only GPIO and command/control are supported.
|
The report states the desktop clock policy: UTC at startup, then host elapsed time, without persistence or RTC interrupts.
|
||||||
|
UTC means Coordinated Universal Time. The host does not convert to local time.
|
||||||
The JSON suite schema does not yet select cartridge peripherals. Its default remains no attached peripheral.
|
The JSON suite schema does not yet select cartridge peripherals. Its default remains no attached peripheral.
|
||||||
There is no host-clock dependency, ROM patch, save file, or automatic device detection.
|
There is no core host-clock dependency, ROM patch, save file, or automatic device detection.
|
||||||
|
|
||||||
## GPIO registers
|
## GPIO registers
|
||||||
|
|
||||||
|
|
@ -59,27 +61,80 @@ Chip select rising with clock high begins a command. Chip select falling aborts
|
||||||
Command bits enter on rising clock edges after software establishes data while clock is low.
|
Command bits enter on rising clock edges after software establishes data while clock is low.
|
||||||
The first four command bits on the wire must be `0,1,1,0`.
|
The first four command bits on the wire must be `0,1,1,0`.
|
||||||
GBATEK's forward and reverse command-byte representations describe this same wire sequence, not interchangeable wire prefixes.
|
GBATEK's forward and reverse command-byte representations describe this same wire sequence, not interchangeable wire prefixes.
|
||||||
Control parameters transfer least-significant bit first.
|
Control and calendar parameters transfer least-significant bit first.
|
||||||
During reads, the RTC supplies each bit on the falling edge. GPIO reads do not advance the serial state.
|
During reads, the RTC supplies each bit on the falling edge. GPIO reads do not advance the serial state.
|
||||||
|
|
||||||
Using GBATEK's MSB-first command representation:
|
Using GBATEK's MSB-first command representation:
|
||||||
|
|
||||||
- `0x62` writes one control byte; `0x63` reads one control byte.
|
- `0x62` writes one control byte; `0x63` reads one control byte.
|
||||||
- `0x60` and `0x61` reset supported control state to zero. Calendar reset side effects are not implemented.
|
- `0x60` and `0x61` reset control to zero and the calendar to year 00, month/day 01, weekday/time zero.
|
||||||
- `0x64` through `0x67` return an explicit calendar-access diagnostic.
|
- `0x64` writes seven date/time bytes; `0x65` reads them.
|
||||||
|
- `0x66` writes three time bytes; `0x67` reads them.
|
||||||
- `0x6c` and `0x6d` return an explicit force-interrupt diagnostic.
|
- `0x6c` and `0x6d` return an explicit force-interrupt diagnostic.
|
||||||
- Unused commands and invalid command prefixes remain diagnostic.
|
- Unused commands and invalid command prefixes remain diagnostic.
|
||||||
|
|
||||||
Initial control is `0x40`, a deterministic powered 24-hour-mode setting.
|
Initial control is `0x40`, a deterministic powered 24-hour-mode setting.
|
||||||
Bit 6 is writable. Unused bits and read-only bit 7 do not retain writes.
|
Bit 6 is writable. Unused bits and read-only bit 7 do not retain writes.
|
||||||
Writes enabling bits 1, 3, or 5 fail because their interrupt or uncertain control behavior is not implemented.
|
Writes enabling bits 1, 3, or 5 fail because their interrupt or uncertain control behavior is not implemented.
|
||||||
No power-failure condition is synthesized. There is no calendar value behind this control setting yet.
|
No power-failure condition is synthesized. Core-only attachment initializes the calendar to the documented reset date.
|
||||||
|
Changing hour mode changes its wire representation without changing the stored time.
|
||||||
|
|
||||||
A partial control write does not commit. A complete byte commits at its final rising edge.
|
A partial control write does not commit. A complete byte commits at its final rising edge.
|
||||||
Extra clocks beyond the documented parameter length remain diagnostic rather than guessing repeated transfers.
|
Extra clocks beyond the documented parameter length remain diagnostic rather than guessing repeated transfers.
|
||||||
Selecting with clock low, driving data during read clocks, and changing data at a sampling edge also remain diagnostic.
|
Selecting with clock low, driving data during read clocks, and changing data at a sampling edge also remain diagnostic.
|
||||||
Minimum physical setup/hold durations, signal races, and exact analog behavior are not modeled.
|
Minimum physical setup/hold durations, signal races, and exact analog behavior are not modeled.
|
||||||
|
|
||||||
|
## Calendar registers and transactions
|
||||||
|
|
||||||
|
Date/time payload order is year, month, day, weekday, hour, minute, second.
|
||||||
|
Time-only payloads contain hour, minute, second. Numeric fields use binary-coded decimal (BCD).
|
||||||
|
Each BCD byte stores a decimal tens digit in its upper nibble and a units digit in its lower nibble.
|
||||||
|
The weekday remains an independent counter from zero through six; software can choose its naming convention.
|
||||||
|
|
||||||
|
The supported date range is 2000–2099. Month lengths and leap years follow that range's Gregorian calendar.
|
||||||
|
Hours use `00`–`23` in 24-hour mode and `00`–`11` in 12-hour mode.
|
||||||
|
GBA hour bit 7 indicates PM. In 24-hour reads it reflects hours 12–23; writes ignore it.
|
||||||
|
In 12-hour mode, noon is `0x80`, not `0x12`. Midnight is zero.
|
||||||
|
Documented unused bits are masked. Invalid BCD digits, out-of-range fields, and impossible dates remain diagnostic.
|
||||||
|
The S-3511A datasheet describes invalid-data corrections, including delayed seconds correction. Those behaviors are not implemented.
|
||||||
|
|
||||||
|
A read snapshots the selected registers at command completion.
|
||||||
|
Later caller time updates or reseeding do not change the current serial payload. The next command observes the updated calendar.
|
||||||
|
A write buffers its entire payload and commits only at its final rising edge.
|
||||||
|
Dropping chip select before completion discards that payload. A rejected final edge can be retried without partial calendar changes.
|
||||||
|
Time-only writes preserve the live date and weekday at commit, including a midnight crossed during the transaction.
|
||||||
|
This is a bounded transaction model, not physical verification of chip latching, partial writes, or oscillator phase.
|
||||||
|
|
||||||
|
## Clock ownership
|
||||||
|
|
||||||
|
The core exposes these APIs on `Memory`:
|
||||||
|
|
||||||
|
- `rtc_datetime()` inspects the live calendar, or returns `None` when no RTC is attached.
|
||||||
|
- `set_rtc_datetime(RtcDateTime)` replaces the calendar between machine steps without changing pending read snapshots.
|
||||||
|
- `advance_rtc_seconds(u64)` applies elapsed battery-clock seconds between machine steps.
|
||||||
|
It changes neither CPU cycles nor interrupt state, and it cannot wake HALT or STOP.
|
||||||
|
|
||||||
|
`RtcDateTime::new` validates decimal calendar fields. `from_seconds_since_2000` provides checked epoch conversion with Sunday=0.
|
||||||
|
Calendar advancement is bounded for any `u64` duration, without iterating once per second.
|
||||||
|
The two-digit year wraps from 99 to 00 while weekday continues independently. This is the bounded century-wrap policy.
|
||||||
|
Epoch construction rejects values outside 2000–2099 rather than wrapping a supplied host date.
|
||||||
|
|
||||||
|
CPU execution, `advance_cycles`, HALT, and STOP do not advance RTC time automatically.
|
||||||
|
A caller can advance the battery clock while GBA clocks are stopped. Core tests inject deterministic dates and durations.
|
||||||
|
Reset changes calendar registers but does not reset the caller's elapsed-time source or its fractional phase.
|
||||||
|
Exact oscillator startup, subsecond reset/write phase, century-wrap behavior, and mode-switch timing lack physical validation.
|
||||||
|
|
||||||
|
The desktop adapter samples `SystemTime` once for UTC startup, at whole-second resolution.
|
||||||
|
It then supplies elapsed whole seconds from `Instant`, preserving the fractional remainder in its elapsed-time measurement.
|
||||||
|
Guest time writes and reset therefore remain effective; each update does not replace them with a fresh host date.
|
||||||
|
Later system-clock adjustments do not change the running RTC. Host dates outside the supported range produce an error.
|
||||||
|
|
||||||
|
Terminal runs synchronize between bounded groups of machine steps and at exit.
|
||||||
|
Native windows synchronize between execution slices, including while waiting for input in STOP.
|
||||||
|
Host sleeping can advance the RTC on the next update but never advances CPU/device cycles.
|
||||||
|
This is a coarse host-clock adapter, not cycle-exact pin scheduling or a guarantee about system-suspend behavior.
|
||||||
|
There is no battery save file or offline catch-up. Each new `--rtc` process starts from host UTC.
|
||||||
|
|
||||||
## Bus integration and tests
|
## Bus integration and tests
|
||||||
|
|
||||||
CPU and DMA accesses retain Game Pak wait-state costs.
|
CPU and DMA accesses retain Game Pak wait-state costs.
|
||||||
|
|
@ -89,16 +144,20 @@ A later diagnostic leaves earlier RAM, GPIO, and CPU state unchanged.
|
||||||
Data reads and instruction fetches share the selected register overlay. Fetches have no serial side effects.
|
Data reads and instruction fetches share the selected register overlay. Fetches have no serial side effects.
|
||||||
|
|
||||||
Original tests cover selection, masks, directions, retained latches, read enable, widths, ROM preservation, and command framing.
|
Original tests cover selection, masks, directions, retained latches, read enable, widths, ROM preservation, and command framing.
|
||||||
They check control reads/writes/reset, aborts, both command-byte representations, edge stability, and unsupported commands.
|
They check control/calendar reads, complete writes, reset, aborts, both command-byte representations, and unsupported commands.
|
||||||
|
Calendar tests cover every day in the supported century, hour modes, PM flags, leap days, midnight, large advances, and invalid payloads.
|
||||||
|
Read snapshots, live-date time writes, independent HALT/STOP advancement, and failed final-edge rollback have separate checks.
|
||||||
ARM/Thumb, DMA3, block preflight, staged fetches, retryable errors, and nominal bus costs have separate regressions.
|
ARM/Thumb, DMA3, block preflight, staged fetches, retryable errors, and nominal bus costs have separate regressions.
|
||||||
File-backed desktop tests check explicit `--rtc` selection, unchanged files, option rejection, and native-window propagation.
|
File-backed desktop tests execute original ARM calendar writes/reads with `--rtc` in terminal and native-window modes.
|
||||||
|
The probe places executable code outside the GPIO overlay. Host-adapter tests inject epoch values and elapsed durations without sleeping.
|
||||||
These tests validate the bounded implementation, not physical RTC timing or game playability.
|
These tests validate the bounded implementation, not physical RTC timing or game playability.
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
|
- [Seiko S-3511A datasheet](https://www.rockby.com.au/DSheets/10110.pdf), sections 2-1, 3-3, and 4: BCD calendar fields, weekday counter, reset values, PM behavior, and invalid-data corrections. Chip variants and physical transfer timing remain unverified; unsupported corrections are not silently approximated.
|
||||||
- [GBATEK cartridge GPIO](https://problemkaputt.de/gbatek-gba-cart-i-o-port-gpio.htm): register addresses, masks, directions, read control, and uncertain byte-store behavior.
|
- [GBATEK cartridge GPIO](https://problemkaputt.de/gbatek-gba-cart-i-o-port-gpio.htm): register addresses, masks, directions, read control, and uncertain byte-store behavior.
|
||||||
- [GBATEK GBA RTC](https://problemkaputt.de/gbatek-gba-cart-real-time-clock-rtc.htm): control fields, reset, calendar formats, and command mapping relative to the DS chip.
|
- [GBATEK GBA RTC](https://problemkaputt.de/gbatek-gba-cart-real-time-clock-rtc.htm): control fields, reset, calendar formats, and command mapping relative to the DS chip.
|
||||||
- [GBATEK DS RTC](https://problemkaputt.de/gbatek-ds-real-time-clock-rtc.htm): the linked serial flow, chip-select sequencing, bit edges, and forward/reverse command representations. GBA register differences remain separate.
|
- [GBATEK DS RTC](https://problemkaputt.de/gbatek-ds-real-time-clock-rtc.htm): the linked serial flow, chip-select sequencing, bit edges, and forward/reverse command representations. GBA register differences remain separate.
|
||||||
- [mGBA GPIO at 3a5e34be](https://github.com/mgba-emu/mgba/blob/3a5e34be33dc7f8f707e5bc9db69e8a430046f21/src/gba/cart/gpio.c): comparison for retained outputs, direction changes, idle inputs, serial edges, and initial control. Its host-clock lookup, ROM-buffer mutation, and repeated transfers are not copied.
|
- [mGBA GPIO at 3a5e34be](https://github.com/mgba-emu/mgba/blob/3a5e34be33dc7f8f707e5bc9db69e8a430046f21/src/gba/cart/gpio.c): comparison for retained outputs, direction changes, idle inputs, serial edges, and initial control. Its host-clock lookup, ROM-buffer mutation, and repeated transfers are not copied. The calendar implementation uses the documented GBA PM bit, including 24-hour readback; the pinned implementation does not supply that flag.
|
||||||
- [mGBA memory at 3a5e34be](https://github.com/mgba-emu/mgba/blob/3a5e34be33dc7f8f707e5bc9db69e8a430046f21/src/gba/memory.c): first-window halfword GPIO write routing. This is not physical proof of all address aliases.
|
- [mGBA memory at 3a5e34be](https://github.com/mgba-emu/mgba/blob/3a5e34be33dc7f8f707e5bc9db69e8a430046f21/src/gba/memory.c): first-window halfword GPIO write routing. This is not physical proof of all address aliases.
|
||||||
- [Explaining GBA RTC](https://bmchtech.github.io/post/rtc/): introductory background surfaced during research; the author explicitly warns that details may be inaccurate. Register and protocol decisions use the references above.
|
- [Explaining GBA RTC](https://bmchtech.github.io/post/rtc/): introductory background surfaced during research; the author explicitly warns that details may be inaccurate. Register and protocol decisions use the references above.
|
||||||
|
|
|
||||||
|
|
@ -34,8 +34,9 @@ Headerless original programs are accepted.
|
||||||
Reads past supplied cartridge bytes remain explicit memory errors, not open-bus values.
|
Reads past supplied cartridge bytes remain explicit memory errors, not open-bus values.
|
||||||
All three Game Pak ROM windows use the same supplied bytes and existing wait-state rules.
|
All three Game Pak ROM windows use the same supplied bytes and existing wait-state rules.
|
||||||
Ordinary cartridge bytes remain read-only. Save devices remain unsupported.
|
Ordinary cartridge bytes remain read-only. Save devices remain unsupported.
|
||||||
Optional `--rtc` selection enables the bounded [Game Pak GPIO and RTC control interface](cartridge-gpio.md).
|
Optional `--rtc` selection enables the bounded [Game Pak GPIO and RTC calendar interface](cartridge-gpio.md).
|
||||||
It supports configuration and control transactions, not calendar access, clock advancement, or RTC interrupts.
|
It supports date/time reads, complete writes, reset, and explicit elapsed-time updates. RTC interrupts and persistence remain unsupported.
|
||||||
|
The desktop seeds UTC once, then supplies monotonic host elapsed seconds independently of GBA cycles and STOP.
|
||||||
|
|
||||||
Both modes call `bios::boot` with the loaded bytes.
|
Both modes call `bios::boot` with the loaded bytes.
|
||||||
With `--rtc`, the host then selects the cartridge peripheral before the first machine step.
|
With `--rtc`, the host then selects the cartridge peripheral before the first machine step.
|
||||||
|
|
|
||||||
|
|
@ -235,7 +235,9 @@ The current desktop demos do not enter STOP; the frontend does not yet provide a
|
||||||
|
|
||||||
Only keypad wake-up is implemented. General-purpose serial and Game Pak wake sources require their missing external devices.
|
Only keypad wake-up is implemented. General-purpose serial and Game Pak wake sources require their missing external devices.
|
||||||
Oscillator restart delays, exact entry/wake edges, and hardware-specific IF behavior remain unverified.
|
Oscillator restart delays, exact entry/wake edges, and hardware-specific IF behavior remain unverified.
|
||||||
There is no host-thread sleep or wall-clock accounting. STOP freezes the emulated timer-driven audio clock.
|
Core STOP does not sleep a host thread or account for wall time. It freezes the emulated timer-driven audio clock.
|
||||||
|
The optional [cartridge RTC](cartridge-gpio.md#clock-ownership) uses a separate caller-supplied battery clock that can advance during STOP.
|
||||||
|
Desktop `--rtc` continues supplying elapsed time while its native window waits for input. RTC updates do not wake STOP.
|
||||||
|
|
||||||
The graphics demo now uses the optional original BIOS replacement's VBlankIntrWait service.
|
The graphics demo now uses the optional original BIOS replacement's VBlankIntrWait service.
|
||||||
Its IRQ callback acknowledges VBlank and updates the BIOS RAM flag before the service returns.
|
Its IRQ callback acknowledges VBlank and updates the BIOS RAM flag before the service returns.
|
||||||
|
|
|
||||||
|
|
@ -65,17 +65,19 @@ The send register supports 16-bit data; receive latches do not become invented p
|
||||||
See [the register model and evidence limits](../hardware/serial.md#disconnected-multiplayer-configuration).
|
See [the register model and evidence limits](../hardware/serial.md#disconnected-multiplayer-configuration).
|
||||||
|
|
||||||
Multiplayer configuration then reached a Game Pak GPIO control write (`0x080000c8`, PC `0x082e29f8`).
|
Multiplayer configuration then reached a Game Pak GPIO control write (`0x080000c8`, PC `0x082e29f8`).
|
||||||
The optional [cartridge GPIO/RTC interface](../hardware/cartridge-gpio.md) now supports read enable, directions, pin latches, and RTC control transactions.
|
The optional [cartridge GPIO/RTC interface](../hardware/cartridge-gpio.md) first added read enable, directions, pin latches, and RTC control transactions.
|
||||||
Select it explicitly with `--rtc`; ordinary ROM bytes and other ROMs' default hardware remain unchanged.
|
That reached a calendar-read command at PC `0x082e28a6`.
|
||||||
No host date, RTC calendar, or successful calendar read is invented.
|
The calendar model now supports date/time reads, complete writes, reset, and caller-supplied elapsed seconds.
|
||||||
|
Core tests inject deterministic time. Desktop `--rtc` uses UTC startup and monotonic elapsed time, including during GBA STOP.
|
||||||
|
Select it explicitly; ordinary ROM bytes and other ROMs' default hardware remain unchanged.
|
||||||
|
|
||||||
Both terminal and native runs with `--rtc` now reach a calendar-read command:
|
Both terminal and native runs with `--rtc` now reach a Flash unlock write:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Steps: 957729; instructions: 955049; IRQ entries: 80; DMA units: 2600; HALT idle: 0
|
Steps: 964375; instructions: 961679; IRQ entries: 80; DMA units: 2616; HALT idle: 0
|
||||||
Nominal cycles: 3520685
|
Nominal cycles: 3546961
|
||||||
PC=0x082e28a6
|
PC=0x082e1892
|
||||||
unsupported RTC calendar access: write 0x07 at 0x080000c4
|
unmapped memory at 0x0e005555
|
||||||
```
|
```
|
||||||
|
|
||||||
The native run captures twelve startup frames. These do not establish a working title screen or gameplay.
|
The native run captures twelve startup frames. These do not establish a working title screen or gameplay.
|
||||||
|
|
@ -90,9 +92,10 @@ Workspace debug/release tests, strict Clippy, public ARM/Thumb/memory/BIOS check
|
||||||
Native ROM-window tests and graphics smoke modes pass on Darwin arm64.
|
Native ROM-window tests and graphics smoke modes pass on Darwin arm64.
|
||||||
The pinned Pong debug/release reports match.
|
The pinned Pong debug/release reports match.
|
||||||
|
|
||||||
The next requirement is RTC calendar transfers with an explicit clock-source model.
|
The next requirement is a cartridge Flash save device, beginning with command unlock and identification.
|
||||||
Implement validated date/time formats, read snapshots, writes/reset, and clock ownership without putting host time in the core.
|
The failing state contains value `0xaa` and address `0x0e005555`, matching the documented unlock sequence.
|
||||||
Keep battery persistence and cartridge IRQ behavior explicit; do not equate GBA STOP with stopping a battery-powered RTC.
|
Research device selection, IDs, banking, program/erase, busy behavior, and safe persistence before claiming save compatibility.
|
||||||
|
RTC persistence and cartridge IRQ behavior remain separate missing features.
|
||||||
Ordinary ROM writes remain read-only. Without `--rtc`, the earlier GPIO diagnostic is still expected.
|
Ordinary ROM writes remain read-only. Without `--rtc`, the earlier GPIO diagnostic is still expected.
|
||||||
This result does not establish gameplay, linked transfers, or cartridge/serial GPIO interrupt support.
|
This result does not establish gameplay, linked transfers, or cartridge/serial GPIO interrupt support.
|
||||||
|
|
||||||
|
|
@ -104,3 +107,5 @@ Do not treat digital-level tests as audible-game validation or suppress remainin
|
||||||
Cartridge saves remain a separate required feature. SRAM alone will not support Emerald's Flash save protocol.
|
Cartridge saves remain a separate required feature. SRAM alone will not support Emerald's Flash save protocol.
|
||||||
|
|
||||||
Hardware and emulator references are recorded in the linked subsystem documents.
|
Hardware and emulator references are recorded in the linked subsystem documents.
|
||||||
|
[GBATEK backup Flash](https://problemkaputt.de/gbatek-gba-cart-backup-flash-rom.htm) identifies the `0xaa`/`0x5555` unlock and chip-identification sequence.
|
||||||
|
The bounded failure establishes an access requirement, not the device's capacity or full save compatibility.
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ Update this file when a feature lands or a limit is removed.
|
||||||
- One-shot non-sequential nominal CPU code timing after successful DMA, including RAM-only transfers.
|
- One-shot non-sequential nominal CPU code timing after successful DMA, including RAM-only transfers.
|
||||||
- HALT with enabled-request wake-up, continued device clocks, and bounded idle steps.
|
- HALT with enabled-request wake-up, continued device clocks, and bounded idle steps.
|
||||||
- BIOS-only CPU writes to POSTFLG/HALTCNT, with HALT and keypad-wake STOP.
|
- BIOS-only CPU writes to POSTFLG/HALTCNT, with HALT and keypad-wake STOP.
|
||||||
- STOP freezes CPU/DMA/device clocks, retains device phases, and reports a stopped frame runner without spinning.
|
- STOP freezes CPU/DMA/GBA-device clocks, retains device phases, and reports a stopped frame runner without spinning.
|
||||||
- Machine execution to the next VBlank event, with a configurable step limit.
|
- Machine execution to the next VBlank event, with a configurable step limit.
|
||||||
- CPU/device stepping with nominal ARM7 instruction costs and memory wait states.
|
- CPU/device stepping with nominal ARM7 instruction costs and memory wait states.
|
||||||
- Ordered CPU timing transactions record source, data, internal, and refill costs, with whole-step diagnostic rollback.
|
- Ordered CPU timing transactions record source, data, internal, and refill costs, with whole-step diagnostic rollback.
|
||||||
|
|
@ -76,7 +76,9 @@ Update this file when a feature lands or a limit is removed.
|
||||||
- `SWP`/`SWPB`, including unaligned word rotation and register aliases.
|
- `SWP`/`SWPB`, including unaligned word rotation and register aliases.
|
||||||
- 256 KiB external work RAM and 32 KiB internal work RAM, including mirrors.
|
- 256 KiB external work RAM and 32 KiB internal work RAM, including mirrors.
|
||||||
- Read-only cartridge bytes in the three GBA cartridge windows.
|
- Read-only cartridge bytes in the three GBA cartridge windows.
|
||||||
- Explicit cartridge RTC selection with first-window GPIO read enable, direction/latch behavior, and serial command/control transactions.
|
- Explicit cartridge RTC selection with first-window GPIO read enable, direction/latch behavior, and serial calendar/control transactions.
|
||||||
|
- Validated RTC date/time, BCD transfers, read snapshots, complete writes/reset, and caller-supplied battery-clock advancement.
|
||||||
|
- Desktop `--rtc` seeds UTC once and supplies host elapsed seconds independently of CPU cycles and STOP.
|
||||||
- Cartridge GPIO changes participate in CPU/DMA rollback and block-store preflight without modifying ROM bytes.
|
- Cartridge GPIO changes participate in CPU/DMA rollback and block-store preflight without modifying ROM bytes.
|
||||||
- Raw ROM-file loading with original BIOS boot, terminal diagnostics, and final CPU/step reports.
|
- Raw ROM-file loading with original BIOS boot, terminal diagnostics, and final CPU/step reports.
|
||||||
- Firmware boot initializes BG2/BG3 identity scales for bitmap and affine programs that do not program their own matrices.
|
- Firmware boot initializes BG2/BG3 identity scales for bitmap and affine programs that do not program their own matrices.
|
||||||
|
|
@ -141,8 +143,9 @@ Horizontal window comparators retain four-cycle event history.
|
||||||
Sprite rows prepare one line ahead with a whole-row cycle40 sample and nominal per-row work limits.
|
Sprite rows prepare one line ahead with a whole-row cycle40 sample and nominal per-row work limits.
|
||||||
Individual fetch timing, hardware-accurate work cutoffs, video-bus contention, and per-pixel color composition remain unimplemented.
|
Individual fetch timing, hardware-accurate work cutoffs, video-bus contention, and per-pixel color composition remain unimplemented.
|
||||||
There is no host audio output or cartridge save support.
|
There is no host audio output or cartridge save support.
|
||||||
The optional [cartridge GPIO/RTC subset](hardware/cartridge-gpio.md) supports command/control only, selected with `--rtc` in ROM modes.
|
The optional [cartridge GPIO/RTC subset](hardware/cartridge-gpio.md) supports calendar/control transfers and explicit clock updates, selected with `--rtc` in ROM modes.
|
||||||
RTC calendar access, clock advancement, persistence, interrupts, GPIO aliases, and byte writes remain unsupported or unverified.
|
RTC persistence, interrupts, invalid-date correction, physical subsecond timing, GPIO aliases, and byte writes remain unsupported or unverified.
|
||||||
|
The core uses deterministic caller time. Desktop RTC runs use host UTC/elapsed time and are not fully deterministic.
|
||||||
The [audio subset](hardware/audio.md) supports Direct Sound, both PSG pulse channels, noise channel 4, idle wave channel 3 state, and instantaneous digital mixing.
|
The [audio subset](hardware/audio.md) supports Direct Sound, both PSG pulse channels, noise channel 4, idle wave channel 3 state, and instantaneous digital mixing.
|
||||||
It does not yet provide a continuous sample stream. Wave channel 3 playback and unverified sound-DMA pairings remain diagnostic.
|
It does not yet provide a continuous sample stream. Wave channel 3 playback and unverified sound-DMA pairings remain diagnostic.
|
||||||
PSG timing and signed mixing are nominal; hardware startup edges and analog behavior remain unverified.
|
PSG timing and signed mixing are nominal; hardware startup edges and analog behavior remain unverified.
|
||||||
|
|
@ -165,8 +168,8 @@ It also passes the pulse/noise triggers and normal-mode RCNT configuration.
|
||||||
It also completes a CpuSet call to address zero through the corrected ignored BIOS-write behavior.
|
It also completes a CpuSet call to address zero through the corrected ignored BIOS-write behavior.
|
||||||
It also accepts an external-clock serial request without inventing clock edges or completion.
|
It also accepts an external-clock serial request without inventing clock edges or completion.
|
||||||
It now also completes internally clocked normal transfers and selects idle multiplayer mode.
|
It now also completes internally clocked normal transfers and selects idle multiplayer mode.
|
||||||
With explicit `--rtc` selection, it also passes Game Pak GPIO configuration and reaches an RTC calendar-read command.
|
With explicit `--rtc` selection, it also passes Game Pak GPIO configuration and RTC calendar reads.
|
||||||
That command remains diagnostic (`0x080000c4`, PC `0x082e28a6`); no date is fabricated.
|
It next stops at a Flash unlock write (`0x0e005555`, PC `0x082e1892`). Cartridge save devices remain unimplemented.
|
||||||
Without `--rtc`, cartridge GPIO writes still return read-only diagnostics.
|
Without `--rtc`, cartridge GPIO writes still return read-only diagnostics.
|
||||||
No title-screen, gameplay, or audible-game compatibility is established.
|
No title-screen, gameplay, or audible-game compatibility is established.
|
||||||
See [the startup result](research/emerald-reset.md) for the bounded run and next investigation.
|
See [the startup result](research/emerald-reset.md) for the bounded run and next investigation.
|
||||||
|
|
|
||||||
|
|
@ -65,11 +65,14 @@ They verify suite selection, pinned URLs, hashes, sizes, checkpoint validation,
|
||||||
The [Pong adapter](public-pong.md) has additional offline tests for its pinned downloads, retained license, and bounded gameplay scenarios.
|
The [Pong adapter](public-pong.md) has additional offline tests for its pinned downloads, retained license, and bounded gameplay scenarios.
|
||||||
Run the external Pong suite separately; ordinary Cargo and Python tests require no public ROM or network access.
|
Run the external Pong suite separately; ordinary Cargo and Python tests require no public ROM or network access.
|
||||||
Device initialization regressions use original instructions to check disabled sound, wave RAM, disconnected serial pins, and reset flags.
|
Device initialization regressions use original instructions to check disabled sound, wave RAM, disconnected serial pins, and reset flags.
|
||||||
Cartridge GPIO tests use an explicitly attached RTC control subset and original pin sequences, never game code or host time.
|
Core cartridge GPIO tests use an explicitly attached RTC and original pin sequences, never game code or host time.
|
||||||
They cover read overlays, unchanged ROM bytes, masks, directions, widths, serial framing, control/reset, aborts, and explicit calendar/IRQ diagnostics.
|
They cover read overlays, unchanged ROM bytes, masks, directions, widths, framing, control/calendar/reset, aborts, and unsupported commands.
|
||||||
|
Calendar tests cover all dates in 2000–2099, 12/24-hour representation, PM flags, midnight, leap days, and large/split elapsed durations.
|
||||||
|
Payload tests verify read snapshots, complete writes, invalid-date diagnostics, reset, and clock advancement during HALT/STOP.
|
||||||
ARM/Thumb and DMA3 checks retain Game Pak costs; staged fetches, rejected edges, and block-store preflight retain diagnostic atomicity.
|
ARM/Thumb and DMA3 checks retain Game Pak costs; staged fetches, rejected edges, and block-store preflight retain diagnostic atomicity.
|
||||||
Desktop tests check explicit `--rtc` selection, duplicate rejection, unchanged files, and native-window configuration.
|
Desktop tests execute original ARM calendar commands in terminal and native-window modes with explicit `--rtc` selection.
|
||||||
Physical pin timing, GPIO aliases, RTC calendar values, and battery behavior remain unverified or unimplemented.
|
Host-adapter tests inject UTC epoch values and elapsed durations without sleeping. Guest time changes survive subsequent elapsed updates.
|
||||||
|
Physical pin timing, invalid-date hardware correction, GPIO aliases, and battery persistence remain unverified or unimplemented.
|
||||||
They verify explicit activation errors and atomic byte/word/block-store/DMA failure behavior.
|
They verify explicit activation errors and atomic byte/word/block-store/DMA failure behavior.
|
||||||
The optional [local Emerald startup check](research/emerald-reset.md) now reaches sound activation but still fails deliberately there.
|
The optional [local Emerald startup check](research/emerald-reset.md) now reaches sound activation but still fails deliberately there.
|
||||||
[Jev evidence review](jev-debugging.md) is optional and never changes deterministic test verdicts.
|
[Jev evidence review](jev-debugging.md) is optional and never changes deterministic test verdicts.
|
||||||
|
|
|
||||||
|
|
@ -41,10 +41,10 @@ This exposed and corrected missing firmware affine-scale initialization, without
|
||||||
Frame-based scenarios and the preparation adapter now make the result repeatable.
|
Frame-based scenarios and the preparation adapter now make the result repeatable.
|
||||||
A local Emerald run now passes its initial all-device reset, master sound enable, and pulse/noise triggers.
|
A local Emerald run now passes its initial all-device reset, master sound enable, and pulse/noise triggers.
|
||||||
It also passes normal serial transfers and idle multiplayer configuration.
|
It also passes normal serial transfers and idle multiplayer configuration.
|
||||||
With `--rtc`, it passes Game Pak GPIO setup and stops at the first unsupported RTC calendar-read command.
|
With `--rtc`, it passes Game Pak GPIO setup and RTC calendar reads, then stops at a Flash unlock write.
|
||||||
The core has tested Direct Sound, both pulse channels, and noise channel 4.
|
The core has tested Direct Sound, both pulse channels, and noise channel 4.
|
||||||
Wave channel 3, a continuous sample stream, and desktop audio output remain missing.
|
Wave channel 3, a continuous sample stream, and desktop audio output remain missing.
|
||||||
See [the runtime result](research/emerald-reset.md). RTC calendar transfers and an explicit clock-source model are the next startup requirement.
|
See [the runtime result](research/emerald-reset.md). Flash identification and its save protocol are the next demonstrated startup requirement.
|
||||||
The separate save requirement remains cartridge SRAM, followed by safe host save persistence.
|
The separate save requirement remains cartridge SRAM, followed by safe host save persistence.
|
||||||
SRAM is byte-addressable battery-backed save memory. Flash and EEPROM remain separate protocols.
|
SRAM is byte-addressable battery-backed save memory. Flash and EEPROM remain separate protocols.
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue