1#![allow(clippy::doc_markdown)]
30
31use alloc::boxed::Box;
32
33use rustysnes_savestate::{SaveReader, SaveStateError, SaveWriter};
34
35use crate::board::{Board, Coprocessor, MappedAddr};
36
37const DAYS_IN_MONTH: [u32; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
44enum State {
45 #[default]
46 Ready,
47 Command,
48 Read,
49 Write,
50}
51
52fn calculate_weekday(year: u32, month: u32, day: u32) -> u32 {
57 let year = year.max(1000);
58 let month = month.clamp(1, 12);
59 let day = day.clamp(1, 31);
60
61 let is_leap = |y: u32| y.is_multiple_of(4) && (!y.is_multiple_of(100) || y.is_multiple_of(400));
62
63 let mut sum = 0u32;
64 let mut y = 1000u32;
65 while y < year {
66 sum += 365 + u32::from(is_leap(y));
67 y += 1;
68 }
69
70 let mut m = 1u32;
71 while m < month {
72 let days = DAYS_IN_MONTH[usize::try_from((m - 1) % 12).unwrap_or(0)];
73 let leap_month = days == 28 && is_leap(y);
74 sum += days + u32::from(leap_month);
75 m += 1;
76 }
77
78 sum += day - 1;
79 (sum + 3) % 7 }
81
82pub struct SharpRtcBoard {
84 inner: Box<dyn Board>,
85
86 second: u32,
87 minute: u32,
88 hour: u32,
89 day: u32,
90 month: u32,
91 year: u32,
92 weekday: u32,
93
94 state: State,
95 index: i32,
98}
99
100impl core::fmt::Debug for SharpRtcBoard {
101 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
102 f.debug_struct("SharpRtcBoard")
103 .field("inner", &self.inner.name())
104 .field("second", &self.second)
105 .field("minute", &self.minute)
106 .field("hour", &self.hour)
107 .field("day", &self.day)
108 .field("month", &self.month)
109 .field("year", &self.year)
110 .field("weekday", &self.weekday)
111 .field("state", &self.state)
112 .field("index", &self.index)
113 .finish()
114 }
115}
116
117fn classify(addr24: u32) -> Option<u8> {
120 let bank = (addr24 >> 16) & 0xFF;
121 let addr = addr24 & 0xFFFF;
122 (matches!(bank, 0x00..=0x3F | 0x80..=0xBF) && matches!(addr, 0x2800 | 0x2801))
123 .then_some((addr & 1) as u8)
124}
125
126impl SharpRtcBoard {
127 #[must_use]
131 pub fn new(inner: Box<dyn Board>) -> Self {
132 Self {
133 inner,
134 second: 0,
135 minute: 0,
136 hour: 0,
137 day: 0,
138 month: 0,
139 year: 0,
140 weekday: 0,
141 state: State::Read,
142 index: -1,
143 }
144 }
145
146 #[allow(clippy::missing_const_for_fn, clippy::cast_possible_truncation)]
153 fn rtc_read(&self, address: u8) -> u8 {
154 let v = match address {
155 0 => self.second % 10,
156 1 => self.second / 10,
157 2 => self.minute % 10,
158 3 => self.minute / 10,
159 4 => self.hour % 10,
160 5 => self.hour / 10,
161 6 => self.day % 10,
162 7 => self.day / 10,
163 8 => self.month,
164 9 => self.year % 10,
165 10 => (self.year / 10) % 10,
166 11 => self.year / 100,
167 12 => self.weekday,
168 _ => 0,
169 };
170 v as u8
171 }
172
173 #[allow(clippy::missing_const_for_fn)]
178 fn rtc_write(&mut self, address: u8, data: u32) {
179 match address {
180 0 => self.second = self.second / 10 * 10 + data,
181 1 => self.second = data * 10 + self.second % 10,
182 2 => self.minute = self.minute / 10 * 10 + data,
183 3 => self.minute = data * 10 + self.minute % 10,
184 4 => self.hour = self.hour / 10 * 10 + data,
185 5 => self.hour = data * 10 + self.hour % 10,
186 6 => self.day = self.day / 10 * 10 + data,
187 7 => self.day = data * 10 + self.day % 10,
188 8 => self.month = data,
189 9 => self.year = self.year / 10 * 10 + data,
190 10 => self.year = self.year / 100 * 100 + data * 10 + self.year % 10,
191 11 => self.year = data * 100 + self.year % 100,
192 12 => self.weekday = data,
193 _ => {}
194 }
195 }
196
197 fn read_register(&mut self, address: u8) -> u8 {
201 if address != 0 || self.state != State::Read {
202 return 0;
203 }
204 if self.index < 0 {
205 self.index += 1;
206 return 15;
207 }
208 if self.index > 12 {
209 self.index = -1;
210 return 15;
211 }
212 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
213 let slot = self.index as u8;
214 self.index += 1;
215 self.rtc_read(slot)
216 }
217
218 fn write_register(&mut self, address: u8, data: u8) {
220 if address != 0 {
221 return;
222 }
223 let data = data & 0xF;
224 if data == 0x0D {
225 self.state = State::Read;
226 self.index = -1;
227 return;
228 }
229 if data == 0x0E {
230 self.state = State::Command;
231 return;
232 }
233 if data == 0x0F {
234 return; }
236
237 match self.state {
238 State::Command => {
239 if data == 0 {
240 self.state = State::Write;
241 self.index = 0;
242 } else if data == 4 {
243 self.state = State::Ready;
244 self.index = -1;
245 self.second = 0;
246 self.minute = 0;
247 self.hour = 0;
248 self.day = 0;
249 self.month = 0;
250 self.year = 0;
251 self.weekday = 0;
252 } else {
253 self.state = State::Ready; }
255 }
256 State::Write => {
257 if (0..12).contains(&self.index) {
258 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
259 let slot = self.index as u8;
260 self.rtc_write(slot, u32::from(data));
261 self.index += 1;
262 if self.index == 12 {
263 self.weekday = calculate_weekday(1000 + self.year, self.month, self.day);
264 }
265 }
266 }
267 State::Ready | State::Read => {}
268 }
269 }
270}
271
272impl Board for SharpRtcBoard {
273 fn name(&self) -> &'static str {
274 "ExHiROM+S-RTC"
275 }
276
277 fn coprocessor(&self) -> Coprocessor {
278 Coprocessor::Srtc
279 }
280
281 fn map(&self, addr24: u32) -> MappedAddr {
282 if classify(addr24).is_some() {
283 MappedAddr::Coprocessor
284 } else {
285 self.inner.map(addr24)
286 }
287 }
288
289 fn read24(&mut self, addr24: u32) -> u8 {
290 match classify(addr24) {
291 Some(a) => self.read_register(a),
292 None => self.inner.read24(addr24),
293 }
294 }
295
296 fn write24(&mut self, addr24: u32, val: u8) {
297 if let Some(a) = classify(addr24) {
298 self.write_register(a, val);
299 } else {
300 self.inner.write24(addr24, val);
301 }
302 }
303
304 fn rom(&self) -> &[u8] {
305 self.inner.rom()
306 }
307
308 fn sram(&self) -> &[u8] {
309 self.inner.sram()
310 }
311
312 fn sram_mut(&mut self) -> &mut [u8] {
313 self.inner.sram_mut()
314 }
315
316 fn save_state(&self, w: &mut SaveWriter) {
317 w.section(*b"SRTC", |s| {
318 s.write_u32(self.second);
319 s.write_u32(self.minute);
320 s.write_u32(self.hour);
321 s.write_u32(self.day);
322 s.write_u32(self.month);
323 s.write_u32(self.year);
324 s.write_u32(self.weekday);
325 s.write_u8(match self.state {
326 State::Ready => 0,
327 State::Command => 1,
328 State::Read => 2,
329 State::Write => 3,
330 });
331 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
334 s.write_u8((self.index + 1) as u8);
335 });
336 self.inner.save_state(w);
337 }
338
339 fn load_state(&mut self, r: &mut SaveReader) -> Result<(), SaveStateError> {
340 let mut s = r.expect_section(*b"SRTC")?;
341 let second = s.read_u32()?;
349 let minute = s.read_u32()?;
350 let hour = s.read_u32()?;
351 let day = s.read_u32()?;
352 let month = s.read_u32()?;
353 let year = s.read_u32()?;
354 let weekday = s.read_u32()?;
355 let state_byte = s.read_u8()?;
356 let index_byte = s.read_u8()?.min(14);
361 let index = i32::from(index_byte) - 1;
362 let state = match state_byte {
363 0 => State::Ready,
364 1 => State::Command,
365 2 => State::Read,
366 3 => State::Write,
367 _ => {
368 return Err(SaveStateError::Invalid(alloc::format!(
369 "SharpRtcBoard state discriminant {state_byte} is not a valid State variant \
370 (0-3)"
371 )));
372 }
373 };
374 if s.remaining() != 0 {
375 return Err(SaveStateError::Invalid(alloc::format!(
376 "SRTC section has {} trailing byte(s)",
377 s.remaining()
378 )));
379 }
380 self.second = second;
381 self.minute = minute;
382 self.hour = hour;
383 self.day = day;
384 self.month = month;
385 self.year = year;
386 self.weekday = weekday;
387 self.state = state;
388 self.index = index;
389 self.inner.load_state(r)
390 }
391}
392
393#[cfg(test)]
394mod tests {
395 use super::*;
396 use crate::board::ExHiRom;
397 use alloc::vec;
398
399 fn board() -> SharpRtcBoard {
400 let inner = Box::new(ExHiRom::new(
401 vec![0u8; 0x40_0000].into_boxed_slice(),
402 vec![0u8; 0x2000].into_boxed_slice(),
403 ));
404 SharpRtcBoard::new(inner)
405 }
406
407 #[test]
408 fn window_classify() {
409 assert_eq!(classify(0x00_2800), Some(0));
410 assert_eq!(classify(0x3F_2801), Some(1));
411 assert_eq!(classify(0x80_2800), Some(0));
412 assert_eq!(classify(0xBF_2801), Some(1));
413 assert_eq!(classify(0x00_2802), None);
414 assert_eq!(classify(0x40_2800), None); }
416
417 #[test]
418 fn power_on_state_is_read_with_sentinel_index() {
419 let b = board();
420 assert_eq!(b.state, State::Read);
421 assert_eq!(b.index, -1);
422 }
423
424 #[test]
425 fn write_then_read_clock_roundtrip() {
426 let mut b = board();
427 b.write24(0x00_2800, 0x0E); b.write24(0x00_2800, 0x00); for digit in [5, 4, 0, 3, 2, 1, 5, 1, 6, 4, 2, 0] {
431 b.write24(0x00_2800, digit);
432 }
433 b.write24(0x00_2800, 0x0D); assert_eq!(b.read24(0x00_2800), 15); let mut got = [0u8; 13];
436 for slot in &mut got {
437 *slot = b.read24(0x00_2800);
438 }
439 assert_eq!(&got[0..12], &[5, 4, 0, 3, 2, 1, 5, 1, 6, 4, 2, 0]);
440 assert!(got[12] < 7);
442 }
443
444 #[test]
445 fn command_reset_zeroes_the_clock() {
446 let mut b = board();
447 b.write24(0x00_2800, 0x0E); b.write24(0x00_2800, 0x00); b.write24(0x00_2800, 9); b.write24(0x00_2800, 0x0E); b.write24(0x00_2800, 0x04); assert_eq!(b.second, 0);
453 assert_eq!(b.state, State::Ready);
454 }
455
456 #[test]
457 fn rom_and_sram_delegate_to_inner_board() {
458 let mut b = board();
459 assert_eq!(b.rom().len(), 0x40_0000);
460 b.write24(0x20_6000, 0x42);
462 assert_eq!(b.read24(0x20_6000), 0x42);
463 }
464
465 #[test]
466 fn clock_and_handshake_state_round_trips_through_save_state() {
467 let mut b = board();
468 b.write24(0x00_2800, 0x0E);
469 b.write24(0x00_2800, 0x00);
470 b.write24(0x00_2800, 7); let mut w = SaveWriter::new();
473 b.save_state(&mut w);
474 let bytes = w.into_bytes();
475
476 let mut fresh = board();
477 let mut r = SaveReader::new(&bytes);
478 fresh.load_state(&mut r).unwrap();
479
480 assert_eq!(fresh.second, 7);
481 assert_eq!(fresh.state, State::Write);
482 assert_eq!(fresh.index, 1);
483 assert_eq!(r.remaining(), 0);
484 }
485
486 #[test]
487 fn out_of_range_state_discriminant_is_rejected_not_panicked_on() {
488 let b = board();
489 let mut w = SaveWriter::new();
490 b.save_state(&mut w);
491 let mut bytes = w.into_bytes();
492 let offset = 8 + (4 * 7); bytes[offset] = 99;
495 let mut fresh = board();
496 let mut r = SaveReader::new(&bytes);
497 assert!(matches!(
498 fresh.load_state(&mut r),
499 Err(SaveStateError::Invalid(_))
500 ));
501 }
502
503 #[test]
504 fn weekday_epoch_is_a_wednesday() {
505 assert_eq!(calculate_weekday(1000, 1, 1), 3);
506 }
507}