rustysnes_cart/coproc/armv3/board.rs
1//! The ST018 board — the ARMv3 (ARM6) coprocessor wired into a cartridge.
2//!
3//! The one confirmed commercial cart is *Hayazashi Nidan Morita Shogi 2* (SETA, 1995,
4//! Japan-only; internal title `NIDAN MORITASHOGI2`) — a LoROM cart (512 KiB ROM + 8 KiB
5//! battery-backed SRAM) using the chip's ARM core to strengthen its shogi AI. Confirmed against
6//! two independent sources: Mesen2's own header detection (`BaseCartridge::GetCoprocessorType`,
7//! `RomType` high nibble `$F` + `CartridgeType == 0x02`) and ares' heuristic detector
8//! (`mia/medium/super-famicom.cpp`, the identical `cartridgeTypeHi==0xf && cartridgeSubType==2`
9//! signature at the extended-header byte `$xFBF`) — see `docs/st018-arm-notes.md` for the full
10//! research trail, including the earlier (wrong) assumption that this chip was Star Ocean's
11//! (Star Ocean uses S-DD1 only; no ARM coprocessor). This project's own [`crate::header`]
12//! parser doesn't read `$xFBF` for the OTHER `$F`-nibble customs (CX4/SPC7110/S-RTC all resolve
13//! by title match instead, after an earlier investigation found that byte unreliable for THOSE
14//! chips against a real Mega Man X2 dump) — [`crate::header`]'s `coprocessor_from_chipset`
15//! mirrors that same title-match convention here rather than introducing a new header field this
16//! codebase has otherwise deliberately chosen not to trust.
17//!
18//! Board/bus protocol ported from Mesen2's `St018` (`Core/SNES/Coprocessors/ST018/St018.cpp`),
19//! architecturally an SA-1-style deterministic master-clock catch-up (`Run()`, called before
20//! every register access and at end-of-frame in the reference; here driven every single master
21//! tick by [`Board::coprocessor_tick`] instead — strictly more granular, functionally
22//! equivalent, and avoids needing any `rustysnes-core`-side plumbing since — unlike SA-1's
23//! second 65C816 — this ARM core is entirely self-contained within `rustysnes-cart` already).
24//! SNES-side register window `$00-3F,$80-BF:$3000-$3FFF` (the whole block, not just
25//! `$3800-38FF` as `boards.bml` implies at a glance): `$3800` (read: pull one byte the ARM
26//! placed for the SNES), `$3802` (write: push one byte to the ARM; read: clear the ack flag),
27//! `$3804` (status on read; a `1->0` write transition resets the ARM, preserving its own cycle
28//! counter). ARM-side 32-bit address space (top nibble selects region): `0x0` = 128 KiB PRG ROM,
29//! `0x4` = the same handshake registers mirrored in, `0xA` = 32 KiB data ROM, `0xE` = 16 KiB
30//! work RAM. A firmware dump is a single combined `0x28000`-byte file (PRG then data,
31//! `docs/st018-arm-notes.md`) — never bundled, user-supplied only (`docs/adr/0003`).
32
33// Chip-name jargon (ST018, ARMv3, ...) is not Rust code. The handshake state is a fixed set of
34// independent hardware flags (Mesen2 `St018State`), not a state-machine candidate for an enum.
35#![allow(clippy::doc_markdown, clippy::struct_excessive_bools)]
36
37use alloc::boxed::Box;
38use alloc::vec;
39
40use rustysnes_savestate::{SaveReader, SaveStateError, SaveWriter};
41
42use crate::board::{Board, Coprocessor, MappedAddr};
43use crate::coproc::armv3::{ArmBus, Cpu};
44
45/// ARM-side PRG ROM size (Mesen2 `St018::PrgRomSize`).
46const PRG_ROM_SIZE: usize = 0x2_0000;
47/// ARM-side data ROM size (Mesen2 `St018::DataRomSize`).
48const DATA_ROM_SIZE: usize = 0x8000;
49/// ARM-side work RAM size (Mesen2 `St018::WorkRamSize`).
50const WORK_RAM_SIZE: usize = 0x4000;
51/// The single combined firmware dump this board accepts (PRG ROM immediately followed by data
52/// ROM — Mesen2 `FirmwareHelper::LoadSt018Firmware` splits the same way).
53const FIRMWARE_SIZE: usize = PRG_ROM_SIZE + DATA_ROM_SIZE;
54
55/// Bridges [`Cpu::step`]'s [`ArmBus`] calls to the board's ROM/RAM/handshake state, charging one
56/// cycle per byte lane touched (Mesen2 `ProcessIdleCycle`/the implicit per-byte-lane cost inside
57/// `Read`/`Write` — `docs/st018-arm-notes.md`'s board-bus-protocol section). A short-lived
58/// borrow, not stored: [`St018Board::coprocessor_tick`] takes `self.cpu` out via
59/// `core::mem::take` before constructing this (mirroring `rustysnes-core`'s SA-1 catch-up's
60/// `Option::take`), so the adapter can freely borrow every OTHER field of the board.
61struct St018Bus<'a> {
62 board: &'a mut St018Board,
63}
64
65impl ArmBus for St018Bus<'_> {
66 fn read_code(&mut self, addr: u32) -> u32 {
67 self.board.arm_cycle_count += 4;
68 self.board.read_arm_word(addr)
69 }
70
71 fn read(&mut self, addr: u32, byte: bool) -> u32 {
72 if byte {
73 self.board.arm_cycle_count += 1;
74 u32::from(self.board.read_arm_byte(addr))
75 } else {
76 self.board.arm_cycle_count += 4;
77 self.board.read_arm_word(addr)
78 }
79 }
80
81 fn write(&mut self, addr: u32, value: u32, byte: bool) {
82 if byte {
83 self.board.arm_cycle_count += 1;
84 #[allow(clippy::cast_possible_truncation)]
85 self.board.write_arm_byte(addr, value as u8);
86 } else {
87 self.board.arm_cycle_count += 4;
88 self.board.write_arm_word(addr, value);
89 }
90 }
91
92 fn idle(&mut self) {
93 self.board.arm_cycle_count += 1;
94 }
95}
96
97/// Classify a 24-bit CPU address into the SNES-side handshake window (Mesen2 `St018::Read`/
98/// `Write`'s `addr & 0xFF06` dispatch over the registered `$3000-$3FFF` block).
99#[allow(clippy::cast_possible_truncation)] // `addr & 0xFF06` is always <= 0xFFFF.
100fn classify(addr24: u32) -> Option<u16> {
101 let bank = (addr24 >> 16) & 0xFF;
102 let addr = addr24 & 0xFFFF;
103 (matches!(bank, 0x00..=0x3F | 0x80..=0xBF) && (0x3000..=0x3FFF).contains(&addr))
104 .then_some((addr & 0xFF06) as u16)
105}
106
107/// A cartridge carrying an ST018 (Hayazashi Nidan Morita Shogi 2's ARMv3 coprocessor).
108pub struct St018Board {
109 inner: Box<dyn Board>,
110 cpu: Cpu,
111
112 prg_rom: Box<[u8]>,
113 data_rom: Box<[u8]>,
114 work_ram: Box<[u8]>,
115 firmware_loaded: bool,
116
117 // SNES<->ARM handshake (Mesen2 `St018State`).
118 has_data_for_snes: bool,
119 data_snes: u8,
120 ack: bool,
121 has_data_for_arm: bool,
122 data_arm: u8,
123 arm_reset: bool,
124
125 /// The ARM's own catch-up target, incremented by one on every [`Board::coprocessor_tick`]
126 /// call — mathematically equivalent to Mesen2's `_memoryManager->GetMasterClock()` (both
127 /// start at 0 and advance 1:1 with the master clock), just accumulated locally instead of
128 /// read from a shared clock each time.
129 target_cycle: u64,
130 /// The ARM's own elapsed-cycle counter (Mesen2 `ArmV3CpuState::CycleCount`), advanced by
131 /// [`St018Bus`] on every bus access/idle cycle.
132 arm_cycle_count: u64,
133}
134
135impl core::fmt::Debug for St018Board {
136 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
137 f.debug_struct("St018Board")
138 .field("inner", &self.inner.name())
139 .field("cpu", &self.cpu)
140 .field("firmware_loaded", &self.firmware_loaded)
141 .field("arm_reset", &self.arm_reset)
142 .finish_non_exhaustive()
143 }
144}
145
146impl St018Board {
147 /// Wrap a base board (`inner`, the cart's own LoROM ROM/SRAM decode) with an ST018. Inert
148 /// (the ARM never steps) until [`Board::load_firmware`] supplies the combined chip dump
149 /// (`docs/adr/0003`).
150 #[must_use]
151 pub fn new(inner: Box<dyn Board>) -> Self {
152 let mut board = Self {
153 inner,
154 cpu: Cpu::default(),
155 prg_rom: vec![0u8; PRG_ROM_SIZE].into_boxed_slice(),
156 data_rom: vec![0u8; DATA_ROM_SIZE].into_boxed_slice(),
157 work_ram: vec![0u8; WORK_RAM_SIZE].into_boxed_slice(),
158 firmware_loaded: false,
159 has_data_for_snes: false,
160 data_snes: 0,
161 ack: false,
162 has_data_for_arm: false,
163 data_arm: 0,
164 arm_reset: false,
165 target_cycle: 0,
166 arm_cycle_count: 0,
167 };
168 board.power_on_arm();
169 board
170 }
171
172 /// Power on the ARM core, re-priming its pipeline (Mesen2 `PowerOn(forReset=false)`, used at
173 /// construction and whenever firmware is (re)loaded). The priming reads' own bus-cycle cost
174 /// is charged to `arm_cycle_count` like any other access — there is no prior state to
175 /// preserve at these call sites (see [`Self::reset_arm`] for the one call site that does).
176 fn power_on_arm(&mut self) {
177 let mut cpu = core::mem::take(&mut self.cpu);
178 cpu.power_on(&mut St018Bus { board: self });
179 self.cpu = cpu;
180 }
181
182 /// A true ARM reset (Mesen2 `PowerOn(forReset=true)`, the SNES-side `$3804` 1->0 edge):
183 /// re-primes the pipeline exactly like [`Self::power_on_arm`], but saves `arm_cycle_count`
184 /// first and restores it afterward, discarding the priming reads' own cost — the reference
185 /// does the identical save-before/restore-after around its own `ProcessPipeline()` call.
186 fn reset_arm(&mut self) {
187 let saved = self.arm_cycle_count;
188 self.power_on_arm();
189 self.arm_cycle_count = saved;
190 }
191
192 /// `(HasDataForSnes<<0)|(Ack<<2)|(HasDataForArm<<3)|(!ArmReset<<7)` (Mesen2 `GetStatus`).
193 const fn status(&self) -> u8 {
194 (self.has_data_for_snes as u8)
195 | ((self.ack as u8) << 2)
196 | ((self.has_data_for_arm as u8) << 3)
197 | ((!self.arm_reset as u8) << 7)
198 }
199
200 /// SNES-side register read (Mesen2 `St018::Read`) — the ARM is already caught up by the time
201 /// this runs (every master tick already ran [`Board::coprocessor_tick`] first), so unlike
202 /// the reference there is no explicit `Run()` call needed here.
203 ///
204 /// Not marked `const fn`: `St018Board` carries a heap-allocated `inner: Box<dyn Board>`
205 /// field, so `const`-ness here is cosmetic and fragile against future field changes — the
206 /// same rationale `coproc::sharprtc`'s own dense register methods already document.
207 #[allow(clippy::missing_const_for_fn)]
208 fn read_register(&mut self, window: u16) -> u8 {
209 match window {
210 0x3800 => {
211 self.has_data_for_snes = false;
212 self.data_snes
213 }
214 0x3802 => {
215 self.ack = false;
216 0 // falls through to open bus in the reference; this port has no open-bus latch.
217 }
218 0x3804 => self.status(),
219 _ => 0,
220 }
221 }
222
223 /// SNES-side register write (Mesen2 `St018::Write`).
224 fn write_register(&mut self, window: u16, val: u8) {
225 match window {
226 0x3802 => {
227 self.data_arm = val;
228 self.has_data_for_arm = true;
229 }
230 0x3804 => {
231 let new_reset = val != 0;
232 if self.arm_reset && !new_reset {
233 self.reset_arm();
234 }
235 self.arm_reset = new_reset;
236 }
237 _ => {}
238 }
239 }
240
241 /// ARM-side byte read (Mesen2 `St018::ReadCpuByte`) — the top nibble of the 32-bit ARM
242 /// address selects the region; anything unmapped reads 0 (matches the source's own
243 /// `default: return 0`).
244 fn read_arm_byte(&mut self, addr: u32) -> u8 {
245 match addr >> 28 {
246 0x0 => self.prg_rom[(addr & 0x1_FFFF) as usize],
247 0x4 => match addr & 0x3F {
248 0x10 => {
249 self.has_data_for_arm = false;
250 self.data_arm
251 }
252 0x20 => self.status(),
253 _ => 0,
254 },
255 0xA => self.data_rom[(addr & 0x7FFF) as usize],
256 0xE => self.work_ram[(addr & 0x3FFF) as usize],
257 _ => 0,
258 }
259 }
260
261 /// ARM-side byte write (Mesen2 `St018::WriteCpuByte`). PRG/data ROM are read-only from the
262 /// ARM side. The `$04:...20/24/28/2A` cases are unresolved even in the reference
263 /// implementation (commented `//??` there) — ported as no-ops rather than inventing
264 /// behavior, per `docs/st018-arm-notes.md`.
265 fn write_arm_byte(&mut self, addr: u32, val: u8) {
266 match addr >> 28 {
267 0x4 => match addr & 0x3F {
268 0x00 => {
269 self.has_data_for_snes = true;
270 self.data_snes = val;
271 }
272 0x10 => self.ack = true,
273 _ => {}
274 },
275 0xE => self.work_ram[(addr & 0x3FFF) as usize] = val,
276 _ => {}
277 }
278 }
279
280 /// Word access is 4 byte-lane accesses at `addr&!3 | {0,1,2,3}`, packed/unpacked
281 /// little-endian (Mesen2 `ReadCpu`, which just calls the byte version 4x) — no
282 /// misalignment-rotation is applied, confirmed against the real board implementation.
283 fn read_arm_word(&mut self, addr: u32) -> u32 {
284 let base = addr & !3;
285 u32::from(self.read_arm_byte(base))
286 | (u32::from(self.read_arm_byte(base | 1)) << 8)
287 | (u32::from(self.read_arm_byte(base | 2)) << 16)
288 | (u32::from(self.read_arm_byte(base | 3)) << 24)
289 }
290
291 /// The write-side mirror of [`Self::read_arm_word`] (Mesen2 `WriteCpu`).
292 #[allow(clippy::cast_possible_truncation)]
293 fn write_arm_word(&mut self, addr: u32, val: u32) {
294 let base = addr & !3;
295 self.write_arm_byte(base, val as u8);
296 self.write_arm_byte(base | 1, (val >> 8) as u8);
297 self.write_arm_byte(base | 2, (val >> 16) as u8);
298 self.write_arm_byte(base | 3, (val >> 24) as u8);
299 }
300}
301
302impl Board for St018Board {
303 fn name(&self) -> &'static str {
304 "LoROM+ST018"
305 }
306
307 fn coprocessor(&self) -> Coprocessor {
308 Coprocessor::St018
309 }
310
311 fn map(&self, addr24: u32) -> MappedAddr {
312 if classify(addr24).is_some() {
313 MappedAddr::Coprocessor
314 } else {
315 self.inner.map(addr24)
316 }
317 }
318
319 fn read24(&mut self, addr24: u32) -> u8 {
320 match classify(addr24) {
321 Some(w) => self.read_register(w),
322 None => self.inner.read24(addr24),
323 }
324 }
325
326 fn write24(&mut self, addr24: u32, val: u8) {
327 if let Some(w) = classify(addr24) {
328 self.write_register(w, val);
329 } else {
330 self.inner.write24(addr24, val);
331 }
332 }
333
334 fn rom(&self) -> &[u8] {
335 self.inner.rom()
336 }
337
338 fn sram(&self) -> &[u8] {
339 self.inner.sram()
340 }
341
342 fn sram_mut(&mut self) -> &mut [u8] {
343 self.inner.sram_mut()
344 }
345
346 /// Step the ARM core to catch up with the master clock by exactly one tick (Mesen2 `Run()`,
347 /// see the module doc for why this is driven every tick instead of lazily on access/EOF).
348 /// Inert (no-op) until firmware is loaded — the chip stays absent, never silently faked.
349 fn coprocessor_tick(&mut self) {
350 if !self.firmware_loaded {
351 return;
352 }
353 self.target_cycle += 1;
354 if self.arm_reset {
355 self.arm_cycle_count = self.target_cycle;
356 return;
357 }
358 let mut cpu = core::mem::take(&mut self.cpu);
359 while self.arm_cycle_count < self.target_cycle {
360 cpu.step(&mut St018Bus { board: self });
361 }
362 self.cpu = cpu;
363 }
364
365 fn load_firmware(&mut self, bytes: &[u8]) -> bool {
366 if bytes.len() != FIRMWARE_SIZE {
367 return false;
368 }
369 self.prg_rom.copy_from_slice(&bytes[..PRG_ROM_SIZE]);
370 self.data_rom.copy_from_slice(&bytes[PRG_ROM_SIZE..]);
371 self.firmware_loaded = true;
372 // The constructor already primed the pipeline once, against all-zero ROM (firmware
373 // hasn't loaded yet at that point) -- re-power-on now so the pipeline's `execute`/
374 // `decode` slots hold the REAL reset-vector opcodes instead of permanently-stale zero
375 // opcodes fetched before this firmware existed.
376 self.power_on_arm();
377 true
378 }
379
380 fn firmware_hint(&self) -> Option<&'static str> {
381 Some("st018.rom")
382 }
383
384 fn save_state(&self, w: &mut SaveWriter) {
385 w.section(*b"ST18", |s| {
386 self.cpu.save_state(s);
387 s.write_bytes(&self.work_ram);
388 s.write_bool(self.has_data_for_snes);
389 s.write_u8(self.data_snes);
390 s.write_bool(self.ack);
391 s.write_bool(self.has_data_for_arm);
392 s.write_u8(self.data_arm);
393 s.write_bool(self.arm_reset);
394 s.write_u64(self.target_cycle);
395 s.write_u64(self.arm_cycle_count);
396 });
397 self.inner.save_state(w);
398 }
399
400 fn load_state(&mut self, r: &mut SaveReader) -> Result<(), SaveStateError> {
401 let mut s = r.expect_section(*b"ST18")?;
402 self.cpu.load_state(&mut s)?;
403 self.work_ram.copy_from_slice(s.read_bytes(WORK_RAM_SIZE)?);
404 self.has_data_for_snes = s.read_bool()?;
405 self.data_snes = s.read_u8()?;
406 self.ack = s.read_bool()?;
407 self.has_data_for_arm = s.read_bool()?;
408 self.data_arm = s.read_u8()?;
409 self.arm_reset = s.read_bool()?;
410 self.target_cycle = s.read_u64()?;
411 self.arm_cycle_count = s.read_u64()?;
412 if s.remaining() != 0 {
413 return Err(SaveStateError::Invalid(alloc::format!(
414 "ST18 section has {} trailing byte(s)",
415 s.remaining()
416 )));
417 }
418 self.inner.load_state(r)
419 }
420}
421
422#[cfg(test)]
423mod tests {
424 use super::*;
425 use crate::board::LoRom;
426
427 fn firmware() -> alloc::vec::Vec<u8> {
428 let mut fw = vec![0u8; FIRMWARE_SIZE];
429 // A single ARM opcode at the reset vector (address 0): `MOV r0, #1` = 0xE3A00001. Enough
430 // to prove the catch-up loop actually steps the core once firmware is present.
431 fw[0..4].copy_from_slice(&0xE3A0_0001u32.to_le_bytes());
432 fw
433 }
434
435 fn board() -> St018Board {
436 let inner = Box::new(LoRom::new(
437 vec![0u8; 0x8_0000].into_boxed_slice(),
438 vec![0u8; 0x2000].into_boxed_slice(),
439 ));
440 St018Board::new(inner)
441 }
442
443 #[test]
444 fn window_classify() {
445 assert_eq!(classify(0x00_3800), Some(0x3800));
446 assert_eq!(classify(0x3F_3802), Some(0x3802));
447 assert_eq!(classify(0x80_3804), Some(0x3804));
448 assert_eq!(classify(0xBF_3FFE), Some(0x3F06)); // top of the window still folds via &0xFF06
449 assert_eq!(classify(0x00_2FFF), None); // just below the window
450 assert_eq!(classify(0x40_3800), None); // outside 00-3f/80-bf
451 }
452
453 #[test]
454 fn inert_without_firmware() {
455 let mut b = board();
456 assert!(!b.firmware_loaded);
457 // The constructor already primes the pipeline once (against all-zero ROM) regardless of
458 // firmware state; the property under test is that ticking WITHOUT firmware never grows
459 // that baseline further -- the chip stays fully inert, never silently faked.
460 let baseline = b.arm_cycle_count;
461 for _ in 0..64 {
462 b.coprocessor_tick();
463 }
464 assert_eq!(b.arm_cycle_count, baseline);
465 assert_eq!(b.read24(0x00_3804), 0x80); // status: !ArmReset only
466 }
467
468 #[test]
469 fn rejects_a_wrong_sized_firmware_dump() {
470 let mut b = board();
471 assert!(!b.load_firmware(&[0u8; 0x100]));
472 assert!(!b.firmware_loaded);
473 }
474
475 #[test]
476 fn accepts_the_combined_dump_and_splits_prg_and_data() {
477 let mut fw = firmware();
478 fw[PRG_ROM_SIZE] = 0x42; // first data-ROM byte
479 let mut b = board();
480 assert!(b.load_firmware(&fw));
481 assert!(b.firmware_loaded);
482 assert_eq!(b.prg_rom[0..4], 0xE3A0_0001u32.to_le_bytes());
483 assert_eq!(b.data_rom[0], 0x42);
484 }
485
486 #[test]
487 fn coprocessor_tick_steps_the_arm_once_firmware_is_loaded() {
488 let mut b = board();
489 assert!(b.load_firmware(&firmware()));
490 // The constructor's power-on (against all-zero ROM) plus `load_firmware`'s re-power-on
491 // (against the real dump) each spend a fixed handful of housekeeping bus cycles priming
492 // the pipeline before any real instruction can reach the Execute stage; 64 ticks is
493 // comfortably past that baseline.
494 let before = b.arm_cycle_count;
495 for _ in 0..64 {
496 b.coprocessor_tick();
497 }
498 assert!(b.arm_cycle_count > before);
499 assert_eq!(b.cpu.regs.r[0], 1); // MOV r0,#1 executed
500 }
501
502 #[test]
503 fn snes_side_handshake_round_trip() {
504 let mut b = board();
505 b.write24(0x00_3802, 0x55); // push a byte to the ARM
506 assert!(b.has_data_for_arm);
507 assert_eq!(b.data_arm, 0x55);
508 b.data_snes = 0xAA;
509 b.has_data_for_snes = true;
510 assert_eq!(b.read24(0x00_3800), 0xAA); // pull it back
511 assert!(!b.has_data_for_snes); // read clears the flag
512 }
513
514 #[test]
515 fn a_reset_edge_reinitializes_the_arm_without_resetting_its_cycle_counter() {
516 let mut b = board();
517 assert!(b.load_firmware(&firmware()));
518 for _ in 0..64 {
519 b.coprocessor_tick();
520 }
521 let cycles_before_reset = b.arm_cycle_count;
522 b.write24(0x00_3804, 1); // assert reset
523 b.write24(0x00_3804, 0); // 1->0 edge: re-initializes the ARM
524 // `power_on_arm` never touches `arm_cycle_count` -- it stays board-owned across a reset,
525 // matching Mesen2's `PowerOn(forReset=true)` preserving the cycle counter.
526 assert_eq!(b.arm_cycle_count, cycles_before_reset);
527 assert_eq!(b.cpu.regs.r[0], 0); // registers ARE re-zeroed by the reset
528 }
529
530 #[test]
531 fn state_round_trips_through_save_state() {
532 let mut b = board();
533 assert!(b.load_firmware(&firmware()));
534 for _ in 0..64 {
535 b.coprocessor_tick();
536 }
537 b.write24(0x00_3802, 0x77);
538
539 let mut w = SaveWriter::new();
540 b.save_state(&mut w);
541 let bytes = w.into_bytes();
542
543 let mut fresh = board();
544 assert!(fresh.load_firmware(&firmware()));
545 let mut r = SaveReader::new(&bytes);
546 fresh.load_state(&mut r).unwrap();
547
548 assert_eq!(fresh.arm_cycle_count, b.arm_cycle_count);
549 assert_eq!(fresh.cpu.regs.r[0], b.cpu.regs.r[0]);
550 assert_eq!(fresh.data_arm, 0x77);
551 assert!(fresh.has_data_for_arm);
552 }
553
554 #[test]
555 fn rom_and_sram_delegate_to_inner_board() {
556 let mut b = board();
557 assert_eq!(b.rom().len(), 0x8_0000);
558 b.write24(0x70_0000, 0x99); // LoROM SRAM window
559 assert_eq!(b.read24(0x70_0000), 0x99);
560 }
561}