Skip to main content

rustysnes_cart/coproc/
epsonrtc.rs

1//! The Epson RTC-4513 real-time clock — the second ASIC on the one commercial cart that pairs it
2//! with SPC7110 (Far East of Eden Zero / Tengai Makyou Zero).
3//!
4//! Clean-room port of ares' `EpsonRTC` (ISC, `sfc/coprocessor/epsonrtc/`): a 3-register (`$4840`
5//! chip-select, `$4841` data, `$4842` ready-status) handshake over a 16-nibble register file (the
6//! clock fields + IRQ/mode bits). ares ticks a real wall-clock time into the register file
7//! (`EpsonRTC::synchronize`); this port instead seeds a fixed epoch and never advances it other
8//! than via explicit register writes — real wall-clock time would break this project's
9//! determinism contract (same seed + ROM + input ⇒ bit-identical output, `docs/adr`), and no game
10//! logic here depends on the clock's absolute value, only on the read/write handshake completing.
11//!
12//! This project's host-synced coprocessors (Super FX/CX4/the NEC DSP family) complete a triggered
13//! operation instantly rather than modeling ares' `wait`-cycle countdown (`Thread::step`); the RTC
14//! follows the same convention — every write/read leaves `ready` set immediately, so a game's
15//! poll-for-ready loop always succeeds on its very next check.
16
17#![allow(clippy::doc_markdown)]
18
19use rustysnes_savestate::{SaveReader, SaveStateError, SaveWriter};
20
21/// The RTC-4513's protocol state machine (ares `EpsonRTC::State`).
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
23enum State {
24    #[default]
25    Mode,
26    Seek,
27    Read,
28    Write,
29}
30
31/// The Epson RTC-4513 register file + handshake state.
32#[derive(Debug, Clone)]
33pub struct EpsonRtc {
34    // clock fields (all nibbles; see ares memory.cpp rtcRead/rtcWrite)
35    secondlo: u8,
36    secondhi: u8,
37    batteryfailure: u8,
38    minutelo: u8,
39    minutehi: u8,
40    resync: u8,
41    hourlo: u8,
42    hourhi: u8,
43    meridian: u8,
44    daylo: u8,
45    dayhi: u8,
46    dayram: u8,
47    monthlo: u8,
48    monthhi: u8,
49    monthram: u8,
50    yearlo: u8,
51    yearhi: u8,
52    weekday: u8,
53    hold: u8,
54    calendar: u8,
55    irqflag: u8,
56    roundseconds: u8,
57    irqmask: u8,
58    irqduty: u8,
59    irqperiod: u8,
60    pause: u8,
61    stop: u8,
62    atime: u8,
63    test: u8,
64
65    // handshake state
66    chipselect: u8,
67    state: State,
68    offset: u8,
69    ready: bool,
70    mdr: u8,
71}
72
73impl Default for EpsonRtc {
74    fn default() -> Self {
75        Self::new()
76    }
77}
78
79impl EpsonRtc {
80    /// Build an RTC seeded to a fixed epoch (all-zero clock fields) rather than the host's real
81    /// wall-clock time (see the module doc's determinism note).
82    #[must_use]
83    pub const fn new() -> Self {
84        Self {
85            secondlo: 0,
86            secondhi: 0,
87            batteryfailure: 1,
88            minutelo: 0,
89            minutehi: 0,
90            resync: 0,
91            hourlo: 0,
92            hourhi: 0,
93            meridian: 0,
94            daylo: 0,
95            dayhi: 0,
96            dayram: 0,
97            monthlo: 0,
98            monthhi: 0,
99            monthram: 0,
100            yearlo: 0,
101            yearhi: 0,
102            weekday: 0,
103            hold: 0,
104            calendar: 0,
105            irqflag: 0,
106            roundseconds: 0,
107            irqmask: 0,
108            irqduty: 0,
109            irqperiod: 0,
110            pause: 0,
111            stop: 0,
112            atime: 0,
113            test: 0,
114            chipselect: 0,
115            state: State::Mode,
116            offset: 0,
117            ready: false,
118            mdr: 0,
119        }
120    }
121
122    const fn rtc_reset(&mut self) {
123        self.state = State::Mode;
124        self.offset = 0;
125        self.resync = 0;
126        self.pause = 0;
127        self.test = 0;
128    }
129
130    fn rtc_read(&mut self, address: u8) -> u8 {
131        match address & 0xF {
132            0 => self.secondlo,
133            1 => self.secondhi | (self.batteryfailure << 3),
134            2 => self.minutelo,
135            3 => self.minutehi | (self.resync << 3),
136            4 => self.hourlo,
137            5 => self.hourhi | (self.meridian << 2) | (self.resync << 3),
138            6 => self.daylo,
139            7 => self.dayhi | (self.dayram << 2) | (self.resync << 3),
140            8 => self.monthlo,
141            9 => self.monthhi | (self.monthram << 1) | (self.resync << 3),
142            10 => self.yearlo,
143            11 => self.yearhi,
144            12 => self.weekday | (self.resync << 3),
145            13 => {
146                let readflag = u8::from(self.irqflag != 0 && self.irqmask == 0);
147                self.irqflag = 0;
148                self.hold | (self.calendar << 1) | (readflag << 2) | (self.roundseconds << 3)
149            }
150            14 => self.irqmask | (self.irqduty << 1) | (self.irqperiod << 2),
151            _ => self.pause | (self.stop << 1) | (self.atime << 2) | (self.test << 3),
152        }
153    }
154
155    const fn rtc_write(&mut self, address: u8, data: u8) {
156        let data = data & 0xF;
157        match address & 0xF {
158            0 => self.secondlo = data,
159            1 => {
160                self.secondhi = data & 0x7;
161                self.batteryfailure = data >> 3;
162            }
163            2 => self.minutelo = data,
164            3 => self.minutehi = data,
165            4 => self.hourlo = data,
166            5 => {
167                self.hourhi = data;
168                self.meridian = (data >> 2) & 1;
169                if self.atime == 1 {
170                    self.meridian = 0;
171                }
172                if self.atime == 0 {
173                    self.hourhi &= 1;
174                }
175            }
176            6 => self.daylo = data,
177            7 => {
178                self.dayhi = data & 0x3;
179                self.dayram = data >> 2;
180            }
181            8 => self.monthlo = data,
182            9 => {
183                self.monthhi = data & 0x1;
184                self.monthram = data >> 1;
185            }
186            10 => self.yearlo = data,
187            11 => self.yearhi = data,
188            12 => self.weekday = data,
189            13 => {
190                self.hold = data & 1;
191                self.calendar = (data >> 1) & 1;
192                self.roundseconds = data >> 3;
193            }
194            14 => {
195                self.irqmask = data & 1;
196                self.irqduty = (data >> 1) & 1;
197                self.irqperiod = data >> 2;
198            }
199            _ => {
200                self.pause = data & 1;
201                self.stop = (data >> 1) & 1;
202                self.atime = (data >> 2) & 1;
203                self.test = data >> 3;
204                if self.atime == 1 {
205                    self.meridian = 0;
206                }
207                if self.atime == 0 {
208                    self.hourhi &= 1;
209                }
210                if self.pause != 0 {
211                    self.secondlo = 0;
212                    self.secondhi = 0;
213                }
214            }
215        }
216    }
217
218    /// `$4840-$4842` register read (ares `EpsonRTC::read`, `address & 3`).
219    #[must_use]
220    pub fn read(&mut self, address: u32) -> u8 {
221        match address & 3 {
222            0 => self.chipselect,
223            1 => {
224                if self.chipselect != 1 || !self.ready {
225                    return 0;
226                }
227                match self.state {
228                    State::Write => self.mdr,
229                    State::Read => {
230                        let offset = self.offset;
231                        self.offset = self.offset.wrapping_add(1);
232                        self.rtc_read(offset)
233                    }
234                    State::Mode | State::Seek => 0,
235                }
236            }
237            2 => u8::from(self.ready) << 7,
238            _ => 0,
239        }
240    }
241
242    /// `$4840-$4842` register write (ares `EpsonRTC::write`, `address & 3`, `data & 15`).
243    pub const fn write(&mut self, address: u32, data: u8) {
244        let data = data & 0xF;
245        match address & 3 {
246            0 => {
247                self.chipselect = data;
248                if self.chipselect != 1 {
249                    self.rtc_reset();
250                }
251                self.ready = true;
252            }
253            1 => {
254                if self.chipselect != 1 || !self.ready {
255                    return;
256                }
257                match self.state {
258                    State::Mode => {
259                        if data != 0x03 && data != 0x0c {
260                            return;
261                        }
262                        self.state = State::Seek;
263                        self.mdr = data;
264                        self.ready = true; // host-sync: skip the `wait` countdown
265                    }
266                    State::Seek => {
267                        self.state = if self.mdr == 0x03 {
268                            State::Write
269                        } else {
270                            State::Read
271                        };
272                        self.offset = data;
273                        self.mdr = data;
274                        self.ready = true;
275                    }
276                    State::Write => {
277                        let offset = self.offset;
278                        self.offset = self.offset.wrapping_add(1);
279                        self.rtc_write(offset, data);
280                        self.mdr = data;
281                        self.ready = true;
282                    }
283                    State::Read => {}
284                }
285            }
286            _ => {}
287        }
288    }
289
290    /// Write every clock field + the handshake state machine into a `"RTC0"` section. There is
291    /// no firmware/chip-ROM byte here to exclude (this is a pure register-file clean-room port,
292    /// per `docs/adr/0003`).
293    pub fn save_state(&self, w: &mut SaveWriter) {
294        w.section(*b"RTC0", |s| {
295            s.write_u8(self.secondlo);
296            s.write_u8(self.secondhi);
297            s.write_u8(self.batteryfailure);
298            s.write_u8(self.minutelo);
299            s.write_u8(self.minutehi);
300            s.write_u8(self.resync);
301            s.write_u8(self.hourlo);
302            s.write_u8(self.hourhi);
303            s.write_u8(self.meridian);
304            s.write_u8(self.daylo);
305            s.write_u8(self.dayhi);
306            s.write_u8(self.dayram);
307            s.write_u8(self.monthlo);
308            s.write_u8(self.monthhi);
309            s.write_u8(self.monthram);
310            s.write_u8(self.yearlo);
311            s.write_u8(self.yearhi);
312            s.write_u8(self.weekday);
313            s.write_u8(self.hold);
314            s.write_u8(self.calendar);
315            s.write_u8(self.irqflag);
316            s.write_u8(self.roundseconds);
317            s.write_u8(self.irqmask);
318            s.write_u8(self.irqduty);
319            s.write_u8(self.irqperiod);
320            s.write_u8(self.pause);
321            s.write_u8(self.stop);
322            s.write_u8(self.atime);
323            s.write_u8(self.test);
324            s.write_u8(self.chipselect);
325            s.write_u8(match self.state {
326                State::Mode => 0,
327                State::Seek => 1,
328                State::Read => 2,
329                State::Write => 3,
330            });
331            s.write_u8(self.offset);
332            s.write_bool(self.ready);
333            s.write_u8(self.mdr);
334        });
335    }
336
337    /// The inverse of [`Self::save_state`].
338    ///
339    /// # Errors
340    /// [`SaveStateError`] on truncated/corrupt input, a section with unconsumed trailing bytes,
341    /// or [`SaveStateError::Invalid`] if the encoded `state` discriminant doesn't match one of
342    /// `State`'s four variants (a semantic enum constraint, not a hardware register width — the
343    /// same posture `Obc1Board::load_state` already applies to its own cursor fields). Every
344    /// clock/handshake field below is masked to the widest value `rtc_write`/`write` can ever
345    /// produce (nibble fields `& 0x0F`, single-bit flags `& 0x01`, the few wider-but-still-narrow
346    /// fields their own specific mask) so a hand-edited/corrupted save-state can't inject a
347    /// physically-unwritable bit pattern into a register real hardware could never hold.
348    pub fn load_state(&mut self, r: &mut SaveReader) -> Result<(), SaveStateError> {
349        let mut s = r.expect_section(*b"RTC0")?;
350        self.secondlo = s.read_u8()? & 0x0F;
351        self.secondhi = s.read_u8()? & 0x07;
352        self.batteryfailure = s.read_u8()? & 0x01;
353        self.minutelo = s.read_u8()? & 0x0F;
354        self.minutehi = s.read_u8()? & 0x0F;
355        self.resync = s.read_u8()? & 0x01;
356        self.hourlo = s.read_u8()? & 0x0F;
357        self.hourhi = s.read_u8()? & 0x0F;
358        self.meridian = s.read_u8()? & 0x01;
359        self.daylo = s.read_u8()? & 0x0F;
360        self.dayhi = s.read_u8()? & 0x03;
361        self.dayram = s.read_u8()? & 0x03;
362        self.monthlo = s.read_u8()? & 0x0F;
363        self.monthhi = s.read_u8()? & 0x01;
364        self.monthram = s.read_u8()? & 0x07;
365        self.yearlo = s.read_u8()? & 0x0F;
366        self.yearhi = s.read_u8()? & 0x0F;
367        self.weekday = s.read_u8()? & 0x0F;
368        self.hold = s.read_u8()? & 0x01;
369        self.calendar = s.read_u8()? & 0x01;
370        self.irqflag = s.read_u8()? & 0x01;
371        self.roundseconds = s.read_u8()? & 0x01;
372        self.irqmask = s.read_u8()? & 0x01;
373        self.irqduty = s.read_u8()? & 0x01;
374        self.irqperiod = s.read_u8()? & 0x03;
375        self.pause = s.read_u8()? & 0x01;
376        self.stop = s.read_u8()? & 0x01;
377        self.atime = s.read_u8()? & 0x01;
378        self.test = s.read_u8()? & 0x01;
379        self.chipselect = s.read_u8()? & 0x0F;
380        let state = s.read_u8()?;
381        self.state = match state {
382            0 => State::Mode,
383            1 => State::Seek,
384            2 => State::Read,
385            3 => State::Write,
386            _ => {
387                return Err(SaveStateError::Invalid(alloc::format!(
388                    "EpsonRtc state discriminant {state} is not a valid State variant (0-3)"
389                )));
390            }
391        };
392        self.offset = s.read_u8()? & 0x0F;
393        self.ready = s.read_bool()?;
394        self.mdr = s.read_u8()? & 0x0F;
395        if s.remaining() != 0 {
396            return Err(SaveStateError::Invalid(alloc::format!(
397                "RTC0 section has {} trailing byte(s)",
398                s.remaining()
399            )));
400        }
401        Ok(())
402    }
403}
404
405#[cfg(test)]
406mod tests {
407    use super::*;
408
409    #[test]
410    fn chipselect_then_write_read_roundtrip() {
411        let mut rtc = EpsonRtc::new();
412        rtc.write(0, 1); // chip select
413        assert_eq!(rtc.read(0), 1);
414        rtc.write(1, 0x03); // mode: write
415        rtc.write(1, 0x00); // seek to offset 0
416        rtc.write(1, 0x05); // write secondlo = 5
417        rtc.write(0, 0); // deselect
418        rtc.write(0, 1); // re-select (chipselect actually changed, so this resets the cursor)
419        rtc.write(1, 0x0c); // mode: read
420        rtc.write(1, 0x00); // seek to offset 0
421        assert_eq!(rtc.read(1), 5);
422    }
423
424    #[test]
425    fn ready_flag_reads_high_bit() {
426        let mut rtc = EpsonRtc::new();
427        rtc.write(0, 1);
428        assert_eq!(rtc.read(2), 0x80);
429    }
430
431    #[test]
432    fn clock_and_handshake_state_round_trips_through_save_state() {
433        let mut rtc = EpsonRtc::new();
434        rtc.write(0, 1);
435        rtc.write(1, 0x03); // mode: write
436        rtc.write(1, 0x00); // seek to offset 0
437        rtc.write(1, 0x05); // write secondlo = 5
438
439        let mut w = SaveWriter::new();
440        rtc.save_state(&mut w);
441        let bytes = w.into_bytes();
442
443        let mut fresh = EpsonRtc::new();
444        let mut r = SaveReader::new(&bytes);
445        fresh.load_state(&mut r).unwrap();
446
447        assert_eq!(fresh.secondlo, 5);
448        assert_eq!(fresh.state, State::Write);
449        assert_eq!(r.remaining(), 0);
450    }
451
452    #[test]
453    fn out_of_range_state_discriminant_is_rejected_not_panicked_on() {
454        let rtc = EpsonRtc::new();
455        let mut w = SaveWriter::new();
456        rtc.save_state(&mut w);
457        let mut bytes = w.into_bytes();
458        // The state discriminant follows 30 preceding u8 fields — corrupt it to a value with no
459        // matching State variant.
460        let mut r = SaveReader::new(&bytes);
461        let mut s = r.expect_section(*b"RTC0").unwrap();
462        for _ in 0..30 {
463            s.read_u8().unwrap();
464        }
465        let offset = bytes.len() - s.remaining();
466        bytes[offset] = 99;
467
468        let mut fresh = EpsonRtc::new();
469        let mut r2 = SaveReader::new(&bytes);
470        assert!(matches!(
471            fresh.load_state(&mut r2),
472            Err(SaveStateError::Invalid(_))
473        ));
474    }
475}