feat(cartridge): implement RTC calendar transfers and clock updates

This commit is contained in:
0xrsydn 2026-10-07 05:55:31 +07:00
commit fce222eea2
22 changed files with 1227 additions and 196 deletions

View file

@ -2,6 +2,11 @@
use crate::memory::MemoryError;
mod calendar;
mod rtc;
pub use calendar::{RtcDateTime, RtcError};
use rtc::Rtc;
pub const GPIO_DATA: u32 = 0x0800_00c4;
pub const GPIO_DIRECTION: u32 = 0x0800_00c6;
pub const GPIO_CONTROL: u32 = 0x0800_00c8;
@ -11,7 +16,7 @@ pub const GPIO_CONTROL: u32 = 0x0800_00c8;
pub enum CartridgeHardware {
#[default]
None,
/// GPIO and RTC command/control only. Calendar and interrupt commands remain diagnostic.
/// GPIO and RTC calendar/control. Time advances only through explicit caller updates.
Rtc,
}
@ -27,6 +32,29 @@ impl Cartridge {
}
}
pub(crate) fn rtc_datetime(&self) -> Option<RtcDateTime> {
self.gpio.map(|gpio| gpio.rtc.calendar)
}
pub(crate) fn set_rtc_datetime(&mut self, date: RtcDateTime) -> Result<(), RtcError> {
self.gpio
.as_mut()
.ok_or(RtcError::NotAttached)?
.rtc
.calendar = date;
Ok(())
}
pub(crate) fn advance_rtc_seconds(&mut self, seconds: u64) -> Result<(), RtcError> {
self.gpio
.as_mut()
.ok_or(RtcError::NotAttached)?
.rtc
.calendar
.advance(seconds);
Ok(())
}
pub(crate) fn mapped(&self, address: u32) -> bool {
self.gpio.is_some() && (GPIO_DATA..GPIO_CONTROL + 2).contains(&address)
}
@ -97,141 +125,3 @@ impl Gpio {
(self.latch & self.direction) | (u8::from(self.rtc.output) << 1 & !self.direction)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Rtc {
control: u8,
selected: bool,
clock: bool,
sample: bool,
output: bool,
phase: Phase,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Phase {
Command { byte: u8, bits: u8 },
WriteControl { byte: u8, bits: u8 },
ReadControl { byte: u8, bits: u8 },
Done,
}
impl Default for Rtc {
fn default() -> Self {
Self {
control: 0x40, // Deterministic powered 24-hour mode, not a sampled host clock.
selected: false,
clock: true,
sample: true,
output: true,
phase: Phase::Command { byte: 0, bits: 0 },
}
}
}
impl Rtc {
fn update(&mut self, pins: u8, direction: u8) -> Result<(), &'static str> {
let selected = pins & 4 != 0;
let clock = pins & 1 != 0;
let data = pins & 2 != 0;
if !selected {
self.selected = false;
self.clock = true;
self.output = true;
self.phase = Phase::Command { byte: 0, bits: 0 };
return Ok(());
}
if !self.selected {
if !clock {
return Err("RTC select without high clock");
}
self.selected = true;
self.clock = true;
return Ok(());
}
if !clock {
match self.phase {
Phase::Command { .. } | Phase::WriteControl { .. } => {
if direction & 2 == 0 {
return Err("RTC command/data write with SIO input");
}
self.sample = data;
}
Phase::ReadControl { byte, bits } if self.clock => {
if direction & 2 != 0 {
return Err("RTC read with SIO output");
}
self.output = byte & (1 << bits) != 0;
self.phase = if bits == 7 {
Phase::Done
} else {
Phase::ReadControl {
byte,
bits: bits + 1,
}
};
}
Phase::Done if self.clock => return Err("RTC clocks beyond command length"),
_ => {}
}
} else if !self.clock {
match self.phase {
Phase::Command { byte, bits } | Phase::WriteControl { byte, bits } => {
if direction & 2 == 0 || data != self.sample {
return Err("RTC SIO change at sampling edge");
}
let byte = byte | (u8::from(self.sample) << bits);
let command = matches!(self.phase, Phase::Command { .. });
self.phase = if bits != 7 {
if command {
Phase::Command {
byte,
bits: bits + 1,
}
} else {
Phase::WriteControl {
byte,
bits: bits + 1,
}
}
} else if command {
self.command(byte)?
} else {
// Bits 1/3/5 lack an implemented IRQ/control model.
if byte & 0x2a != 0 {
return Err("RTC interrupt/unknown control bits");
}
self.control = byte & 0x40;
Phase::Done
};
}
_ => {}
}
}
self.clock = clock;
Ok(())
}
fn command(&mut self, byte: u8) -> Result<Phase, &'static str> {
// The first four wire bits are 0,1,1,0. Translate GBATEK's LSB-first
// representation to its MSB-first command numbering; parameters stay LSB first.
if byte & 15 != 6 {
return Err("RTC command encoding");
}
let command = byte.reverse_bits();
match (command >> 1) & 7 {
0 => {
self.control = 0;
Ok(Phase::Done)
}
1 if command & 1 != 0 => Ok(Phase::ReadControl {
byte: self.control,
bits: 0,
}),
1 => Ok(Phase::WriteControl { byte: 0, bits: 0 }),
2 | 3 => Err("RTC calendar access"),
6 => Err("RTC force interrupt"),
_ => Err("RTC unused command"),
}
}
}

View 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;

View 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);
}
}

View 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,
})
}
}
}

View file

@ -27,7 +27,7 @@ use iwram_bus::IwramBus;
use crate::{
audio::{Audio, StereoLevel},
cartridge::{Cartridge, CartridgeHardware},
cartridge::{Cartridge, CartridgeHardware, RtcDateTime, RtcError},
cpu::InstructionSet,
display::{DisplayPosition, CYCLES_PER_LINE, HBLANK_START, VISIBLE_LINES},
dma::{DmaError, DMA_BASE, DMA_END},
@ -193,6 +193,30 @@ impl Memory {
self.cartridge = Cartridge::new(hardware);
}
/// Inspect the live calendar, not an in-progress serial read snapshot.
pub fn rtc_datetime(&self) -> Option<RtcDateTime> {
self.cartridge_state().rtc_datetime()
}
/// Set the RTC calendar between machine steps. Existing serial read snapshots stay latched.
pub fn set_rtc_datetime(&mut self, date: RtcDateTime) -> Result<(), RtcError> {
assert!(
self.cartridge_step.get().is_none(),
"active cartridge transaction"
);
self.cartridge.set_rtc_datetime(date)
}
/// Supply battery-clock elapsed seconds independently of CPU cycles, HALT, and STOP.
/// Call between machine steps. This requests no IRQ and never wakes the CPU.
pub fn advance_rtc_seconds(&mut self, seconds: u64) -> Result<(), RtcError> {
assert!(
self.cartridge_step.get().is_none(),
"active cartridge transaction"
);
self.cartridge.advance_rtc_seconds(seconds)
}
fn cartridge_state(&self) -> Cartridge {
self.cartridge_step.get().unwrap_or(self.cartridge)
}
@ -215,14 +239,15 @@ impl Memory {
}
/// Total elapsed emulated clock cycles, wrapping at u64::MAX. Reads/writes
/// alone consume no cycles. STOP discards externally supplied clock advances.
/// alone consume no cycles. STOP discards supplied GBA-cycle advances, not independent RTC time.
pub fn cycles(&self) -> u64 {
self.cycles
}
/// Advance devices and optional row capture. Multiple unserviced DMA requests
/// coalesce. No CPU or DMA work is executed by this method. STOP freezes all
/// coalesce. No CPU or DMA work is executed by this method. STOP freezes GBA-clock
/// progress: supplied cycles are ignored until an external wake condition.
/// The independently supplied RTC battery clock is not advanced by this method.
/// This device-only API does not advance opcode prefetch. CPU/DMA/HALT steps
/// account for their own cartridge-bus time before updating device clocks.
pub fn advance_cycles(&mut self, cycles: u32) {

View file

@ -36,9 +36,9 @@ fn rejected_rtc_command_edge_rolls_back_arm_and_thumb_steps() {
memory.write16(GPIO_DATA, 1).unwrap();
memory.write16(GPIO_DIRECTION, 7).unwrap();
memory.write16(GPIO_DATA, 5).unwrap();
// All but the final rising edge of an original date/time read command (0x65).
// All but the final rising edge of an unsupported force-interrupt command (0x6d).
for bit in (1..8).rev() {
let pins = 4 | (((0x65 >> bit) & 1) << 1);
let pins = 4 | (((0x6d >> bit) & 1) << 1);
memory.write16(GPIO_DATA, pins).unwrap();
memory.write16(GPIO_DATA, pins | 1).unwrap();
}
@ -47,7 +47,7 @@ fn rejected_rtc_command_edge_rolls_back_arm_and_thumb_steps() {
let mut machine = Machine::new(cpu, memory);
let cpu = machine.cpu().clone();
let error = machine.step().unwrap_err();
assert!(error.to_string().contains("RTC calendar access"));
assert!(error.to_string().contains("RTC force interrupt"));
assert_eq!(machine.step(), Err(error));
assert_eq!(machine.cpu(), &cpu);
assert_eq!(machine.memory().cartridge, cartridge);
@ -56,6 +56,86 @@ fn rejected_rtc_command_edge_rolls_back_arm_and_thumb_steps() {
}
}
fn prepare_calendar_final_edge(memory: &mut Memory, invalid: bool) {
memory.set_cartridge_hardware(CartridgeHardware::Rtc);
memory.write16(GPIO_CONTROL, 1).unwrap();
memory.write16(GPIO_DATA, 1).unwrap();
memory.write16(GPIO_DIRECTION, 7).unwrap();
memory.write16(GPIO_DATA, 5).unwrap();
let mut bits: Vec<bool> = (0..8).rev().map(|i| 0x64 & (1 << i) != 0).collect();
for byte in [
0x24,
2,
0x29,
4,
0x12,
0x34,
if invalid { 0x6a } else { 0x56 },
] {
bits.extend((0..8).map(|i| byte & (1 << i) != 0));
}
for (i, bit) in bits.into_iter().enumerate() {
let pins = 4 | (u16::from(bit) << 1);
memory.write16(GPIO_DATA, pins).unwrap();
if i != 63 {
memory.write16(GPIO_DATA, pins | 1).unwrap();
}
}
}
#[test]
fn calendar_payload_commits_at_final_arm_or_thumb_edge_and_rejects_invalid_data_atomically() {
for thumb in [false, true] {
for invalid in [false, true] {
let (cpu, mut memory) = prepared(
thumb,
&[if thumb { 0x8008 } else { 0xe1c100b0 }],
&[(0, 5), (1, GPIO_DATA)],
);
prepare_calendar_final_edge(&mut memory, invalid);
let before = memory.cartridge;
let mut machine = Machine::new(cpu, memory);
let cpu = machine.cpu().clone();
if invalid {
let error = machine.step().unwrap_err();
assert!(error.to_string().contains("invalid calendar data"));
assert_eq!(machine.step(), Err(error));
assert_eq!(machine.cpu(), &cpu);
assert_eq!(machine.memory().cartridge, before);
assert_eq!(machine.cycles(), 0);
} else {
machine.step().unwrap();
assert_eq!(
machine.memory().rtc_datetime().unwrap().components(),
[24, 2, 29, 4, 12, 34, 56]
);
assert_eq!(machine.last_timing().data_cycles, 5);
}
}
}
}
#[test]
fn later_block_diagnostic_discards_an_earlier_calendar_commit() {
let (_, mut memory) = prepared(false, &[0xe1a00000], &[]);
prepare_calendar_final_edge(&mut memory, false);
let before = memory.cartridge;
assert!(memory
.write_words(&[
(GPIO_DATA, 0x00070005),
(0x02000000, 0x12345678),
(crate::io::SIOCNT, 0x3000)
])
.is_err());
assert_eq!(memory.cartridge, before);
assert_eq!(memory.read32(0x02000000).unwrap(), 0);
memory.write_words(&[(GPIO_DATA, 0x00070005)]).unwrap();
assert_eq!(
memory.rtc_datetime().unwrap().components(),
[24, 2, 29, 4, 12, 34, 56]
);
}
#[test]
fn block_store_failure_retains_gpio_and_cpu_state() {
// First word targets DATA/DIRECTION. Second reaches unmapped padding after CONTROL.

View file

@ -1,4 +1,6 @@
//! Original GPIO and RTC control transactions. No game code or host clock.
//! Original GPIO and RTC transactions. No game code or host clock.
#[path = "cartridge_gpio/calendar.rs"]
mod calendar;
use gba_core::{
cartridge::{CartridgeHardware, GPIO_CONTROL, GPIO_DATA, GPIO_DIRECTION},
cpu::Cpu,
@ -180,10 +182,6 @@ fn dropping_select_aborts_partial_commands_and_parameters() {
#[test]
fn unsupported_commands_and_control_bits_fail_at_the_last_edge_without_partial_writes() {
for (byte, description) in [
(0x64, "calendar"),
(0x65, "calendar"),
(0x66, "calendar"),
(0x67, "calendar"),
(0x6c, "force interrupt"),
(0x68, "unused command"),
(0xff, "command encoding"),

View 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);
}
}