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.