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