feat(cartridge): add explicit GPIO and RTC control support
This commit is contained in:
parent
c5e19baed4
commit
692be36349
18 changed files with 889 additions and 26 deletions
|
|
@ -13,13 +13,14 @@ use std::{
|
|||
|
||||
use gba_core::{
|
||||
bios,
|
||||
cartridge::CartridgeHardware,
|
||||
machine::{Machine, MachineError, StepKind},
|
||||
memory::ROM_CAPACITY,
|
||||
};
|
||||
|
||||
const MAX_STEPS: u64 = 100_000_000;
|
||||
const MAX_FRAMES: u64 = 100_000;
|
||||
const USAGE: &str = "usage: gameboy-rust --rom PATH (--steps COUNT | --window [--frames COUNT]); steps: 1..=100000000, frames: 1..=100000; do not combine with demo or help options";
|
||||
const USAGE: &str = "usage: gameboy-rust --rom PATH [--rtc] (--steps COUNT | --window [--frames COUNT]); steps: 1..=100000000, frames: 1..=100000; do not combine with demo or help options";
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum Mode {
|
||||
|
|
@ -31,6 +32,7 @@ enum Mode {
|
|||
pub struct Options {
|
||||
path: PathBuf,
|
||||
mode: Mode,
|
||||
hardware: CartridgeHardware,
|
||||
}
|
||||
|
||||
pub fn parse_args(args: &[OsString]) -> io::Result<Options> {
|
||||
|
|
@ -39,6 +41,7 @@ pub fn parse_args(args: &[OsString]) -> io::Result<Options> {
|
|||
let mut steps = None;
|
||||
let mut frames = None;
|
||||
let mut window = false;
|
||||
let mut hardware = CartridgeHardware::None;
|
||||
let mut args = args.iter();
|
||||
while let Some(arg) = args.next() {
|
||||
if arg == "--rom" && path.is_none() {
|
||||
|
|
@ -47,6 +50,8 @@ pub fn parse_args(args: &[OsString]) -> io::Result<Options> {
|
|||
.filter(|value| !value.is_empty())
|
||||
.ok_or_else(invalid)?;
|
||||
path = Some(PathBuf::from(value));
|
||||
} else if arg == "--rtc" && hardware == CartridgeHardware::None {
|
||||
hardware = CartridgeHardware::Rtc;
|
||||
} else if arg == "--window" && !window {
|
||||
window = true;
|
||||
} else if (arg == "--steps" && steps.is_none()) || (arg == "--frames" && frames.is_none()) {
|
||||
|
|
@ -83,6 +88,7 @@ pub fn parse_args(args: &[OsString]) -> io::Result<Options> {
|
|||
Ok(Options {
|
||||
path: path.ok_or_else(invalid)?,
|
||||
mode,
|
||||
hardware,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -237,12 +243,20 @@ pub fn execute(options: Options, writer: &mut impl Write) -> Result<(), Box<dyn
|
|||
writer,
|
||||
"Original BIOS replacement; no commercial-game compatibility claim."
|
||||
)?;
|
||||
if options.hardware == CartridgeHardware::Rtc {
|
||||
writeln!(writer, "RTC selected: GPIO and command/control only; calendar and IRQ commands remain diagnostic.")?;
|
||||
}
|
||||
let steps = match options.mode {
|
||||
Mode::Terminal { steps } => steps,
|
||||
Mode::Window { frames } => return crate::desktop::run_rom(bytes, frames, writer),
|
||||
Mode::Window { frames } => {
|
||||
return crate::desktop::run_rom(bytes, frames, options.hardware, writer)
|
||||
}
|
||||
};
|
||||
writeln!(writer, "Machine-step limit: {steps} (includes boot)")?;
|
||||
let mut machine = bios::boot(bytes)?;
|
||||
machine
|
||||
.memory_mut()
|
||||
.set_cartridge_hardware(options.hardware);
|
||||
let report = run_bounded(&mut machine, steps);
|
||||
write_report(writer, &machine, &report)?;
|
||||
writer.flush()?;
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@ fn options_require_both_flags_in_either_order() {
|
|||
parse_args(&arguments).unwrap(),
|
||||
Options {
|
||||
path: "original program.gba".into(),
|
||||
mode: Mode::Terminal { steps: count }
|
||||
mode: Mode::Terminal { steps: count },
|
||||
hardware: CartridgeHardware::None
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,10 @@ impl Session {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn set_cartridge_hardware(&mut self, hardware: gba_core::cartridge::CartridgeHardware) {
|
||||
self.machine.memory_mut().set_cartridge_hardware(hardware);
|
||||
}
|
||||
|
||||
pub fn frame(&self) -> &Framebuffer {
|
||||
&self.frame
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,9 +8,11 @@ use crate::cartridge::window::{Session, Update};
|
|||
pub fn run(
|
||||
bytes: Vec<u8>,
|
||||
frame_limit: Option<u64>,
|
||||
hardware: gba_core::cartridge::CartridgeHardware,
|
||||
writer: &mut impl Write,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let mut session = Session::new(bytes)?;
|
||||
session.set_cartridge_hardware(hardware);
|
||||
writeln!(
|
||||
writer,
|
||||
"ROM window: Arrows=D-pad; Z/X=A/B; Q/W=L/R; Enter=Start; Backspace=Select; Escape exits."
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ fn parse_args(args: &[OsString]) -> Result<RunMode, io::Error> {
|
|||
return cartridge::suite::parse_args(args).map(RunMode::TestSuite);
|
||||
}
|
||||
if args.iter().any(|arg| {
|
||||
["--rom", "--steps", "--window", "--frames"]
|
||||
["--rom", "--steps", "--window", "--frames", "--rtc"]
|
||||
.iter()
|
||||
.any(|flag| arg == flag)
|
||||
}) {
|
||||
|
|
@ -79,7 +79,7 @@ fn parse_args(args: &[OsString]) -> Result<RunMode, io::Error> {
|
|||
[arg] if arg == "--help" || arg == "-h" => Ok(RunMode::Help),
|
||||
_ => Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"usage: gameboy-rust [--cpu-demo | --timer-demo | --graphics-demo | --smoke-test | --graphics-smoke-test | --raster-demo | --raster-smoke-test | --mosaic-demo | --mosaic-smoke-test | --effects-demo | --effects-smoke-test | --tile-demo | --tile-smoke-test | --affine-demo | --affine-smoke-test | --affine-raster-demo | --affine-raster-smoke-test | --bitmap4-demo | --bitmap5-demo | --bitmap4-smoke-test | --bitmap5-smoke-test | --help]; or gameboy-rust --rom PATH (--steps COUNT | --window [--frames COUNT]); or gameboy-rust --test-suite PATH.json",
|
||||
"usage: gameboy-rust [--cpu-demo | --timer-demo | --graphics-demo | --smoke-test | --graphics-smoke-test | --raster-demo | --raster-smoke-test | --mosaic-demo | --mosaic-smoke-test | --effects-demo | --effects-smoke-test | --tile-demo | --tile-smoke-test | --affine-demo | --affine-smoke-test | --affine-raster-demo | --affine-raster-smoke-test | --bitmap4-demo | --bitmap5-demo | --bitmap4-smoke-test | --bitmap5-smoke-test | --help]; or gameboy-rust --rom PATH [--rtc] (--steps COUNT | --window [--frames COUNT]); or gameboy-rust --test-suite PATH.json",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
|
@ -125,6 +125,7 @@ fn run() -> Result<(), Box<dyn Error>> {
|
|||
--rom PATH --steps COUNT Run a raw ROM in the terminal (1..=100000000 steps)\n\
|
||||
--rom PATH --window Open a raw ROM with keyboard input\n\
|
||||
--frames COUNT Exit ROM window after 1..=100000 captured frames\n\
|
||||
--rtc Select RTC GPIO/control subset for --rom (no calendar yet)\n\
|
||||
--test-suite PATH.json Run bounded ROM assertions; write a JSON report\n\
|
||||
--cpu-demo Run the terminal-only ARM/Thumb instruction demo\n\
|
||||
--timer-demo Run the timer IRQ demo with nominal cycle costs\n\
|
||||
|
|
|
|||
|
|
@ -77,6 +77,43 @@ fn raw_original_program_runs_with_a_bounded_report_without_changing_the_file() {
|
|||
assert_eq!(fs::read(path).unwrap(), original);
|
||||
}
|
||||
|
||||
fn rtc_setup() -> Vec<u32> {
|
||||
// Original LDR/MOV/STRH/LDRH/loop: enable GPIO reads, then inspect control.
|
||||
vec![
|
||||
0xe59f100c, 0xe3a00001, 0xe1c100b0, 0xe1d120b0, 0xeafffffe, 0x080000c8,
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rtc_selection_is_explicit_and_does_not_modify_the_rom_file() {
|
||||
let fixture = Fixture::new();
|
||||
let path = fixture.rom(&rtc_setup());
|
||||
let original = fs::read(&path).unwrap();
|
||||
let absent = run(&path, "100");
|
||||
assert_eq!(absent.status.code(), Some(1));
|
||||
assert!(stderr(&absent).contains("read-only memory at 0x080000c8"));
|
||||
let selected = command()
|
||||
.arg("--rtc")
|
||||
.arg("--rom")
|
||||
.arg(&path)
|
||||
.args(["--steps", "100"])
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(selected.status.success(), "{}", stderr(&selected));
|
||||
assert!(stdout(&selected).contains("RTC selected: GPIO and command/control only"));
|
||||
assert!(stdout(&selected).contains("r2=0x00000001"));
|
||||
assert_eq!(fs::read(&path).unwrap(), original);
|
||||
let duplicate = command()
|
||||
.arg("--rom")
|
||||
.arg(&path)
|
||||
.args(["--rtc", "--rtc", "--steps", "100"])
|
||||
.output()
|
||||
.unwrap();
|
||||
assert_eq!(duplicate.status.code(), Some(1));
|
||||
assert!(stderr(&duplicate).contains("usage:"));
|
||||
assert!(duplicate.stdout.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stop_is_a_normal_bounded_outcome() {
|
||||
let fixture = Fixture::new();
|
||||
|
|
@ -216,8 +253,12 @@ fn native_rom_window_presents_file_bytes_and_reports_stop_cpu_and_video_errors()
|
|||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
fn window(path: &Path) -> Output {
|
||||
let mut child = command()
|
||||
fn window(path: &Path, rtc: bool) -> Output {
|
||||
let mut command = command();
|
||||
if rtc {
|
||||
command.arg("--rtc");
|
||||
}
|
||||
let mut child = command
|
||||
.arg("--rom")
|
||||
.arg(path)
|
||||
.args(["--window", "--frames", "3"])
|
||||
|
|
@ -243,7 +284,7 @@ fn native_rom_window_presents_file_bytes_and_reports_stop_cpu_and_video_errors()
|
|||
let fixture = Fixture::new();
|
||||
let path = fixture.0.join("original-input.gba");
|
||||
fs::write(&path, gba_demos::input_rom()).unwrap();
|
||||
let output = window(&path);
|
||||
let output = window(&path, false);
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{} {}",
|
||||
|
|
@ -252,6 +293,11 @@ fn native_rom_window_presents_file_bytes_and_reports_stop_cpu_and_video_errors()
|
|||
);
|
||||
assert!(stdout(&output).contains("Captured ROM frames: 3"));
|
||||
assert!(stdout(&output).contains("Result: window frame limit reached"));
|
||||
let rtc_path = fixture.rom(&rtc_setup());
|
||||
let rtc_output = window(&rtc_path, true);
|
||||
assert!(rtc_output.status.success(), "{}", stderr(&rtc_output));
|
||||
assert!(stdout(&rtc_output).contains("Captured ROM frames: 3"));
|
||||
assert!(stdout(&rtc_output).contains("r2=0x00000001"));
|
||||
for (code, error) in [
|
||||
(vec![0xef03_0000, 0xeaff_fffe], "entered STOP"),
|
||||
(vec![0xee00_0000], "instruction"),
|
||||
|
|
@ -267,7 +313,7 @@ fn native_rom_window_presents_file_bytes_and_reports_stop_cpu_and_video_errors()
|
|||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.unwrap();
|
||||
let output = window(&path);
|
||||
let output = window(&path, false);
|
||||
assert_eq!(output.status.code(), Some(1), "{}", stdout(&output));
|
||||
assert!(stdout(&output).contains("Result: ROM window diagnostic"));
|
||||
assert!(stdout(&output).contains("Captured ROM frames: 0"));
|
||||
|
|
|
|||
237
crates/gba-core/src/cartridge.rs
Normal file
237
crates/gba-core/src/cartridge.rs
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
//! Optional Game Pak hardware. No host clock, filesystem, or ROM-byte mutation.
|
||||
|
||||
use crate::memory::MemoryError;
|
||||
|
||||
pub const GPIO_DATA: u32 = 0x0800_00c4;
|
||||
pub const GPIO_DIRECTION: u32 = 0x0800_00c6;
|
||||
pub const GPIO_CONTROL: u32 = 0x0800_00c8;
|
||||
|
||||
/// Explicit peripheral selection; ROM contents do not select a device.
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CartridgeHardware {
|
||||
#[default]
|
||||
None,
|
||||
/// GPIO and RTC command/control only. Calendar and interrupt commands remain diagnostic.
|
||||
Rtc,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) struct Cartridge {
|
||||
gpio: Option<Gpio>,
|
||||
}
|
||||
|
||||
impl Cartridge {
|
||||
pub(crate) fn new(hardware: CartridgeHardware) -> Self {
|
||||
Self {
|
||||
gpio: (hardware == CartridgeHardware::Rtc).then(Gpio::default),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn mapped(&self, address: u32) -> bool {
|
||||
self.gpio.is_some() && (GPIO_DATA..GPIO_CONTROL + 2).contains(&address)
|
||||
}
|
||||
|
||||
/// Disabled reads expose the supplied ROM bytes, not a zero-filled replacement.
|
||||
pub(crate) fn read8(&self, address: u32) -> Option<u8> {
|
||||
let gpio = self.gpio?;
|
||||
if !self.mapped(address) || !gpio.readable {
|
||||
return None;
|
||||
}
|
||||
let value = match address & !1 {
|
||||
GPIO_DATA => gpio.pins(),
|
||||
GPIO_DIRECTION => gpio.direction,
|
||||
GPIO_CONTROL => 1,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
Some(if address & 1 == 0 { value } else { 0 })
|
||||
}
|
||||
|
||||
/// Apply one access to a copy first. A rejected second halfword changes nothing.
|
||||
pub(crate) fn write(&mut self, address: u32, bytes: &[u8]) -> Result<(), MemoryError> {
|
||||
if !self.mapped(address) {
|
||||
return Ok(()); // Non-cartridge accesses are validated by the bus.
|
||||
}
|
||||
let fail = |at, value, operation| MemoryError::UnsupportedIo {
|
||||
address: at,
|
||||
value,
|
||||
operation,
|
||||
};
|
||||
if bytes.len() == 1 {
|
||||
return Err(fail(address, bytes[0], "Game Pak GPIO byte write"));
|
||||
}
|
||||
let mut next = self.gpio.unwrap();
|
||||
for (i, pair) in bytes.chunks_exact(2).enumerate() {
|
||||
let at = address + i as u32 * 2;
|
||||
let value = pair[0]; // Registers use at most the low four bits.
|
||||
match at {
|
||||
GPIO_DATA | GPIO_DIRECTION => {
|
||||
if at == GPIO_DATA {
|
||||
next.latch = value & 15;
|
||||
} else {
|
||||
next.direction = value & 15;
|
||||
}
|
||||
next.rtc
|
||||
.update(next.latch & next.direction, next.direction)
|
||||
.map_err(|operation| fail(at, value, operation))?;
|
||||
}
|
||||
GPIO_CONTROL => next.readable = value & 1 != 0,
|
||||
_ => return Err(MemoryError::ReadOnly(at)),
|
||||
}
|
||||
}
|
||||
self.gpio = Some(next);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
struct Gpio {
|
||||
latch: u8,
|
||||
direction: u8,
|
||||
readable: bool,
|
||||
rtc: Rtc,
|
||||
}
|
||||
|
||||
impl Gpio {
|
||||
fn pins(self) -> u8 {
|
||||
// RTC input defaults: SIO high outside a read; SCK/CS/unused low.
|
||||
(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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
pub mod audio;
|
||||
pub mod bios;
|
||||
pub mod cartridge;
|
||||
pub mod cpu;
|
||||
pub mod display;
|
||||
pub mod dma;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ use std::{cell::Cell, error::Error, fmt};
|
|||
|
||||
#[cfg(test)]
|
||||
mod audio_step_tests;
|
||||
#[cfg(test)]
|
||||
mod cartridge_step_tests;
|
||||
mod fetch;
|
||||
#[cfg(test)]
|
||||
mod fetch_tests;
|
||||
|
|
@ -25,6 +27,7 @@ use iwram_bus::IwramBus;
|
|||
|
||||
use crate::{
|
||||
audio::{Audio, StereoLevel},
|
||||
cartridge::{Cartridge, CartridgeHardware},
|
||||
cpu::InstructionSet,
|
||||
display::{DisplayPosition, CYCLES_PER_LINE, HBLANK_START, VISIBLE_LINES},
|
||||
dma::{DmaError, DMA_BASE, DMA_END},
|
||||
|
|
@ -119,6 +122,8 @@ pub struct Memory {
|
|||
video_ram: Vec<u8>,
|
||||
oam: Vec<u8>,
|
||||
rom: Vec<u8>,
|
||||
cartridge: Cartridge,
|
||||
cartridge_step: Cell<Option<Cartridge>>,
|
||||
bios: Option<Vec<u8>>,
|
||||
io: Io,
|
||||
cycles: u64,
|
||||
|
|
@ -148,6 +153,8 @@ impl Memory {
|
|||
video_ram: vec![0; VRAM_SIZE],
|
||||
oam: vec![0; 1024],
|
||||
rom,
|
||||
cartridge: Cartridge::default(),
|
||||
cartridge_step: Cell::new(None),
|
||||
bios: None,
|
||||
io: Io::default(),
|
||||
cycles: 0,
|
||||
|
|
@ -176,6 +183,20 @@ impl Memory {
|
|||
Ok(memory)
|
||||
}
|
||||
|
||||
/// Replace optional cartridge hardware with its initial state. Call between machine steps.
|
||||
/// This never modifies the supplied ROM bytes or enables a host clock.
|
||||
pub fn set_cartridge_hardware(&mut self, hardware: CartridgeHardware) {
|
||||
assert!(
|
||||
self.cartridge_step.get().is_none(),
|
||||
"active cartridge transaction"
|
||||
);
|
||||
self.cartridge = Cartridge::new(hardware);
|
||||
}
|
||||
|
||||
fn cartridge_state(&self) -> Cartridge {
|
||||
self.cartridge_step.get().unwrap_or(self.cartridge)
|
||||
}
|
||||
|
||||
/// Current committed audio mixer level. This is not a host sample stream.
|
||||
pub fn audio_level(&self) -> StereoLevel {
|
||||
self.io.audio.level()
|
||||
|
|
@ -221,10 +242,12 @@ impl Memory {
|
|||
pub(crate) fn begin_timer_step(&self) {
|
||||
debug_assert!(self.timer_step.get().is_none());
|
||||
self.timer_step.set(Some(self.io.timer_step()));
|
||||
self.cartridge_step.set(Some(self.cartridge));
|
||||
}
|
||||
|
||||
pub(crate) fn discard_timer_step(&self) {
|
||||
self.timer_step.set(None);
|
||||
self.cartridge_step.set(None);
|
||||
}
|
||||
|
||||
fn advance_timer_step(&self, cycles: u32) {
|
||||
|
|
@ -241,6 +264,10 @@ impl Memory {
|
|||
.take()
|
||||
.expect("machine timer transaction is active");
|
||||
self.io.commit_timer_step(step);
|
||||
self.cartridge = self
|
||||
.cartridge_step
|
||||
.take()
|
||||
.expect("active cartridge transaction");
|
||||
// Staged devices already reached this boundary. Advance remaining devices exactly once.
|
||||
self.advance_running_cycles_with_timers(cycles, false);
|
||||
}
|
||||
|
|
@ -732,6 +759,9 @@ impl Memory {
|
|||
|
||||
// Strict lookup for host access, instruction fetches, and prefetch snapshots.
|
||||
fn read_mapped_byte(&self, address: u32) -> Result<u8, MemoryError> {
|
||||
if let Some(value) = self.cartridge_state().read8(address) {
|
||||
return Ok(value);
|
||||
}
|
||||
match address >> 24 {
|
||||
0x00 => self
|
||||
.bios
|
||||
|
|
@ -784,6 +814,8 @@ impl Memory {
|
|||
) -> Result<(), MemoryError> {
|
||||
let index = self.write_index::<N>(address, access)?;
|
||||
Self::validate_io_values(&mut self.audio_state(), address, &bytes)?;
|
||||
let mut cartridge = self.cartridge_state();
|
||||
cartridge.write(address, &bytes)?;
|
||||
self.record_access(
|
||||
address,
|
||||
match N {
|
||||
|
|
@ -793,6 +825,14 @@ impl Memory {
|
|||
_ => unreachable!("unsupported bus width"),
|
||||
},
|
||||
);
|
||||
if cartridge.mapped(address) {
|
||||
if self.cartridge_step.get().is_some() {
|
||||
self.cartridge_step.set(Some(cartridge));
|
||||
} else {
|
||||
self.cartridge = cartridge;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
if address < BIOS_SIZE as u32 {
|
||||
// Only validated CPU/DMA writes reach here. Pay the bus cost but
|
||||
// never modify the mapped ROM image or drive BIOS read history.
|
||||
|
|
@ -928,7 +968,14 @@ impl Memory {
|
|||
}
|
||||
Ok(0) // I/O does not use a RAM index.
|
||||
}
|
||||
0x08..=0x0d => Err(MemoryError::ReadOnly(address)),
|
||||
0x08..=0x0d => {
|
||||
for offset in 0..N as u32 {
|
||||
if !self.cartridge_state().mapped(address + offset) {
|
||||
return Err(MemoryError::ReadOnly(address + offset));
|
||||
}
|
||||
}
|
||||
Ok(0)
|
||||
}
|
||||
_ => Err(MemoryError::Unmapped(address)),
|
||||
}
|
||||
}
|
||||
|
|
@ -975,6 +1022,7 @@ impl Memory {
|
|||
pub(crate) fn write_words(&mut self, writes: &[(u32, u32)]) -> Result<(), MemoryError> {
|
||||
let mut audio = self.audio_state();
|
||||
let mut serial = self.serial_state();
|
||||
let mut cartridge = self.cartridge_state();
|
||||
let timing = self.cpu_timing.get();
|
||||
let devices = self.timer_step.get();
|
||||
// Preflight ordered data phases on temporary device/timing copies.
|
||||
|
|
@ -984,6 +1032,7 @@ impl Memory {
|
|||
self.write_index::<4>(address, self.write_access())?;
|
||||
let bytes = value.to_le_bytes();
|
||||
Self::validate_io_values(&mut audio, address, &bytes)?;
|
||||
cartridge.write(address, &bytes)?;
|
||||
self.record_access(address, AccessWidth::Word);
|
||||
if Serial::mapped(address) {
|
||||
if let Some(mut step) = self.timer_step.get() {
|
||||
|
|
|
|||
124
crates/gba-core/src/memory/cartridge_step_tests.rs
Normal file
124
crates/gba-core/src/memory/cartridge_step_tests.rs
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
//! Original GPIO bus-phase, fetch, and diagnostic-rollback checks.
|
||||
use super::{timing_event_tests::prepared, *};
|
||||
use crate::{
|
||||
cartridge::{CartridgeHardware, GPIO_CONTROL, GPIO_DATA, GPIO_DIRECTION},
|
||||
machine::Machine,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn arm_and_thumb_stores_pay_game_pak_costs_and_keep_rom_immutable() {
|
||||
for thumb in [false, true] {
|
||||
let (cpu, mut memory) = prepared(
|
||||
thumb,
|
||||
&[if thumb { 0x8008 } else { 0xe1c100b0 }], // STRH r0,[r1]
|
||||
&[(0, 1), (1, GPIO_CONTROL)],
|
||||
);
|
||||
memory.set_cartridge_hardware(CartridgeHardware::Rtc);
|
||||
let rom = memory.rom.clone();
|
||||
let mut machine = Machine::new(cpu, memory);
|
||||
machine.step().unwrap();
|
||||
assert_eq!(machine.memory().read16(GPIO_CONTROL).unwrap(), 1);
|
||||
assert_eq!(machine.memory().rom, rom);
|
||||
assert_eq!(machine.last_timing().data_cycles, 5); // Default non-sequential Game Pak halfword.
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejected_rtc_command_edge_rolls_back_arm_and_thumb_steps() {
|
||||
for thumb in [false, true] {
|
||||
let (cpu, mut memory) = prepared(
|
||||
thumb,
|
||||
&[if thumb { 0x8008 } else { 0xe1c100b0 }],
|
||||
&[(0, 7), (1, GPIO_DATA)],
|
||||
);
|
||||
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();
|
||||
// All but the final rising edge of an original date/time read command (0x65).
|
||||
for bit in (1..8).rev() {
|
||||
let pins = 4 | (((0x65 >> bit) & 1) << 1);
|
||||
memory.write16(GPIO_DATA, pins).unwrap();
|
||||
memory.write16(GPIO_DATA, pins | 1).unwrap();
|
||||
}
|
||||
memory.write16(GPIO_DATA, 6).unwrap();
|
||||
let cartridge = memory.cartridge;
|
||||
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_eq!(machine.step(), Err(error));
|
||||
assert_eq!(machine.cpu(), &cpu);
|
||||
assert_eq!(machine.memory().cartridge, cartridge);
|
||||
assert_eq!(machine.memory().read16(GPIO_DATA).unwrap(), 6);
|
||||
assert_eq!(machine.cycles(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_store_failure_retains_gpio_and_cpu_state() {
|
||||
// First word targets DATA/DIRECTION. Second reaches unmapped padding after CONTROL.
|
||||
let (cpu, mut memory) = prepared(
|
||||
false,
|
||||
&[0xe8a00006],
|
||||
&[(0, GPIO_DATA), (1, 0x00030002), (2, 1)],
|
||||
);
|
||||
memory.set_cartridge_hardware(CartridgeHardware::Rtc);
|
||||
memory.write16(GPIO_CONTROL, 1).unwrap();
|
||||
let cartridge = memory.cartridge;
|
||||
let mut machine = Machine::new(cpu, memory);
|
||||
let cpu = machine.cpu().clone();
|
||||
let error = machine.step().unwrap_err();
|
||||
assert_eq!(machine.step(), Err(error));
|
||||
assert_eq!(machine.cpu(), &cpu);
|
||||
assert_eq!(machine.memory().cartridge, cartridge);
|
||||
assert_eq!(machine.cycles(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preflight_applies_earlier_gpio_direction_changes_to_later_pin_validation() {
|
||||
let (_, mut memory) = prepared(false, &[0xe1a00000], &[]);
|
||||
memory.set_cartridge_hardware(CartridgeHardware::Rtc);
|
||||
memory.write16(GPIO_CONTROL, 1).unwrap();
|
||||
let cartridge = memory.cartridge;
|
||||
assert!(memory
|
||||
.write_words(&[
|
||||
(0x02000000, 0x12345678),
|
||||
(GPIO_DATA, 0x00070001), // Drive clock high, select low.
|
||||
(GPIO_DATA, 0x00070004), // CS rises with clock low: diagnostic.
|
||||
])
|
||||
.is_err());
|
||||
assert_eq!(memory.cartridge, cartridge);
|
||||
assert_eq!(memory.read32(0x02000000).unwrap(), 0);
|
||||
memory
|
||||
.write_words(&[(GPIO_DATA, 0x00070001), (GPIO_DATA, 0x00070005)])
|
||||
.unwrap();
|
||||
assert_eq!(memory.read32(GPIO_DATA).unwrap(), 0x00070005);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn staged_overlay_is_shared_by_data_and_instruction_reads_then_discarded() {
|
||||
let mut memory = Memory::new(vec![0xa5; 0x100]).unwrap();
|
||||
memory.set_cartridge_hardware(CartridgeHardware::Rtc);
|
||||
memory.begin_timer_step();
|
||||
memory.write16(GPIO_CONTROL, 1).unwrap();
|
||||
memory.write16(GPIO_DIRECTION, 3).unwrap();
|
||||
memory.write16(GPIO_DATA, 1).unwrap();
|
||||
assert_eq!(memory.read32(GPIO_DATA).unwrap(), 0x00030001);
|
||||
assert_eq!(
|
||||
memory
|
||||
.fetch_instruction(GPIO_DATA, InstructionSet::Arm)
|
||||
.instruction,
|
||||
Ok(0x00030001)
|
||||
);
|
||||
assert_eq!(memory.cartridge.read8(GPIO_DATA), None);
|
||||
memory.discard_timer_step();
|
||||
assert_eq!(memory.read32(GPIO_DATA).unwrap(), 0xa5a5a5a5);
|
||||
assert_eq!(
|
||||
memory
|
||||
.fetch_instruction(GPIO_DATA, InstructionSet::Arm)
|
||||
.instruction,
|
||||
Ok(0xa5a5a5a5)
|
||||
);
|
||||
}
|
||||
255
crates/gba-core/tests/cartridge_gpio.rs
Normal file
255
crates/gba-core/tests/cartridge_gpio.rs
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
//! Original GPIO and RTC control transactions. No game code or host clock.
|
||||
use gba_core::{
|
||||
cartridge::{CartridgeHardware, GPIO_CONTROL, GPIO_DATA, GPIO_DIRECTION},
|
||||
cpu::Cpu,
|
||||
dma::DMA_BASE,
|
||||
io::IF,
|
||||
machine::{Machine, StepKind},
|
||||
memory::{Memory, MemoryError, ROM_START},
|
||||
};
|
||||
|
||||
fn memory() -> Memory {
|
||||
let mut rom = vec![0xa5; 0x200];
|
||||
rom[..4].copy_from_slice(&0xeafffffeu32.to_le_bytes());
|
||||
let mut bus = Memory::new(rom).unwrap();
|
||||
bus.set_cartridge_hardware(CartridgeHardware::Rtc);
|
||||
bus
|
||||
}
|
||||
|
||||
fn start(bus: &mut Memory) {
|
||||
bus.write16(GPIO_DATA, 1).unwrap();
|
||||
bus.write16(GPIO_DIRECTION, 7).unwrap();
|
||||
bus.write16(GPIO_DATA, 5).unwrap();
|
||||
}
|
||||
|
||||
fn send_bits(bus: &mut Memory, bits: impl IntoIterator<Item = bool>) -> Result<(), MemoryError> {
|
||||
for bit in bits {
|
||||
let data = 4 | (u16::from(bit) << 1);
|
||||
bus.write16(GPIO_DATA, data)?;
|
||||
bus.write16(GPIO_DATA, data | 1)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn command(bus: &mut Memory, byte: u8, msb_first: bool) -> Result<(), MemoryError> {
|
||||
// GBATEK's Fwd and Rev representations describe the same physical wire sequence.
|
||||
let byte = if msb_first { byte } else { byte.reverse_bits() };
|
||||
send_bits(
|
||||
bus,
|
||||
(0..8).map(|i| byte & (1 << if msb_first { 7 - i } else { i }) != 0),
|
||||
)
|
||||
}
|
||||
|
||||
fn parameter(bus: &mut Memory, byte: u8) -> Result<(), MemoryError> {
|
||||
send_bits(bus, (0..8).map(|i| byte & (1 << i) != 0))
|
||||
}
|
||||
|
||||
fn control(bus: &mut Memory, msb_first: bool) -> u8 {
|
||||
start(bus);
|
||||
command(bus, 0x63, msb_first).unwrap();
|
||||
bus.write16(GPIO_DIRECTION, 5).unwrap();
|
||||
let mut value = 0;
|
||||
for bit in 0..8 {
|
||||
bus.write16(GPIO_DATA, 4).unwrap();
|
||||
let low = bus.read16(GPIO_DATA).unwrap();
|
||||
bus.write16(GPIO_DATA, 5).unwrap();
|
||||
let high = bus.read16(GPIO_DATA).unwrap();
|
||||
assert_eq!(low & 2, high & 2); // Output changes on falling, not rising, edges.
|
||||
assert_eq!(bus.read16(GPIO_DATA).unwrap(), high); // Reads do not shift.
|
||||
value |= ((high as u8 >> 1) & 1) << bit;
|
||||
}
|
||||
bus.write16(GPIO_DATA, 1).unwrap();
|
||||
value
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_selection_overlay_and_disable_never_patch_rom_bytes() {
|
||||
let mut bus = memory();
|
||||
for address in [GPIO_DATA, GPIO_DIRECTION, GPIO_CONTROL] {
|
||||
assert_eq!(bus.read16(address).unwrap(), 0xa5a5);
|
||||
}
|
||||
bus.write16(GPIO_CONTROL, 0xffff).unwrap();
|
||||
assert_eq!(bus.read16(GPIO_CONTROL).unwrap(), 1);
|
||||
assert_eq!(bus.read16(GPIO_DATA).unwrap(), 2); // Idle SIO high; no host time.
|
||||
assert_eq!(bus.read16(GPIO_DIRECTION).unwrap(), 0);
|
||||
assert_eq!(bus.read32(GPIO_CONTROL).unwrap(), 0xa5a50001);
|
||||
assert_eq!(bus.read8(GPIO_DATA + 1).unwrap(), 0);
|
||||
for address in [
|
||||
GPIO_DATA + 0x02000000,
|
||||
GPIO_DATA + 0x04000000,
|
||||
GPIO_DATA + 0x01000000,
|
||||
] {
|
||||
assert!(bus.write16(address, 0).is_err()); // No inferred mirror-write behavior.
|
||||
}
|
||||
for base in [0x0a000000, 0x0c000000] {
|
||||
assert_eq!(bus.read16(base + 0xc4).unwrap(), 0xa5a5);
|
||||
}
|
||||
bus.write16(GPIO_CONTROL, 0xfffe).unwrap();
|
||||
assert_eq!(bus.read16(GPIO_DATA).unwrap(), 0xa5a5);
|
||||
bus.set_cartridge_hardware(CartridgeHardware::None);
|
||||
assert_eq!(
|
||||
bus.write16(GPIO_CONTROL, 1),
|
||||
Err(MemoryError::ReadOnly(GPIO_CONTROL))
|
||||
);
|
||||
assert_eq!(bus.read16(GPIO_CONTROL).unwrap(), 0xa5a5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn masks_directions_latches_and_access_boundaries() {
|
||||
let mut bus = memory();
|
||||
bus.write16(GPIO_CONTROL, 1).unwrap();
|
||||
bus.write16(GPIO_DATA, 0xfffb).unwrap(); // CS low; data writes latch even on inputs.
|
||||
assert_eq!(bus.read16(GPIO_DATA).unwrap(), 2);
|
||||
bus.write16(GPIO_DIRECTION, 0xfffb).unwrap();
|
||||
assert_eq!(bus.read16(GPIO_DIRECTION).unwrap(), 11);
|
||||
assert_eq!(bus.read16(GPIO_DATA).unwrap(), 11);
|
||||
bus.write32(GPIO_DATA, 0x00030002).unwrap(); // Two halfwords: data, then direction.
|
||||
assert_eq!(bus.read32(GPIO_DATA).unwrap(), 0x00030002);
|
||||
assert!(bus
|
||||
.write8(GPIO_DATA, 0)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("GPIO byte write"));
|
||||
assert_eq!(
|
||||
bus.write16(GPIO_DATA + 1, 0),
|
||||
Err(MemoryError::Unaligned(GPIO_DATA + 1))
|
||||
);
|
||||
assert_eq!(
|
||||
bus.write32(GPIO_CONTROL, 0),
|
||||
Err(MemoryError::ReadOnly(GPIO_CONTROL + 2))
|
||||
);
|
||||
assert_eq!(bus.read16(GPIO_CONTROL).unwrap(), 1); // No partial disable.
|
||||
assert_eq!(
|
||||
bus.write16(GPIO_DATA - 2, 0),
|
||||
Err(MemoryError::ReadOnly(GPIO_DATA - 2))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn control_read_write_reset_and_both_command_representations() {
|
||||
for msb in [false, true] {
|
||||
let mut bus = memory();
|
||||
bus.write16(GPIO_CONTROL, 1).unwrap();
|
||||
assert_eq!(control(&mut bus, msb), 0x40);
|
||||
for reset in [0x60, 0x61] {
|
||||
// Either access direction strobes reset.
|
||||
start(&mut bus);
|
||||
command(&mut bus, reset, msb).unwrap();
|
||||
bus.write16(GPIO_DATA, 1).unwrap();
|
||||
assert_eq!(control(&mut bus, msb), 0);
|
||||
start(&mut bus);
|
||||
command(&mut bus, 0x62, msb).unwrap();
|
||||
parameter(&mut bus, 0xd5).unwrap(); // Unused and read-only bits do not stick.
|
||||
bus.write16(GPIO_DATA, 1).unwrap();
|
||||
assert_eq!(control(&mut bus, msb), 0x40);
|
||||
}
|
||||
bus.advance_cycles(1000000);
|
||||
assert_eq!(bus.read16(IF).unwrap(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabled_read_overlay_does_not_disable_serial_writes() {
|
||||
let mut bus = memory();
|
||||
start(&mut bus);
|
||||
command(&mut bus, 0x62, true).unwrap();
|
||||
parameter(&mut bus, 0).unwrap();
|
||||
bus.write16(GPIO_DATA, 1).unwrap();
|
||||
assert_eq!(bus.read16(GPIO_CONTROL).unwrap(), 0xa5a5);
|
||||
bus.write16(GPIO_CONTROL, 1).unwrap();
|
||||
assert_eq!(control(&mut bus, true), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dropping_select_aborts_partial_commands_and_parameters() {
|
||||
let mut bus = memory();
|
||||
bus.write16(GPIO_CONTROL, 1).unwrap();
|
||||
for bits in 0..8 {
|
||||
start(&mut bus);
|
||||
send_bits(&mut bus, (0..bits).map(|_| false)).unwrap();
|
||||
bus.write16(GPIO_DATA, 1).unwrap();
|
||||
assert_eq!(control(&mut bus, true), 0x40);
|
||||
start(&mut bus);
|
||||
command(&mut bus, 0x62, true).unwrap();
|
||||
send_bits(&mut bus, (0..bits).map(|_| false)).unwrap();
|
||||
bus.write16(GPIO_DATA, 1).unwrap();
|
||||
assert_eq!(control(&mut bus, true), 0x40);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_commands_and_control_bits_fail_at_the_last_edge_without_partial_writes() {
|
||||
for (byte, description) in [
|
||||
(0x64, "calendar"),
|
||||
(0x65, "calendar"),
|
||||
(0x66, "calendar"),
|
||||
(0x67, "calendar"),
|
||||
(0x6c, "force interrupt"),
|
||||
(0x68, "unused command"),
|
||||
(0xff, "command encoding"),
|
||||
(0xc6, "command encoding"), // Do not accept a reversed wire prefix as another format.
|
||||
] {
|
||||
let mut bus = memory();
|
||||
bus.write16(GPIO_CONTROL, 1).unwrap();
|
||||
start(&mut bus);
|
||||
let error = command(&mut bus, byte, true).unwrap_err();
|
||||
assert!(error.to_string().contains(description), "{error}");
|
||||
let pins = bus.read16(GPIO_DATA).unwrap();
|
||||
assert_eq!(pins & 1, 0); // Failing rising edge was not committed.
|
||||
assert_eq!(bus.write16(GPIO_DATA, pins | 1), Err(error));
|
||||
bus.write16(GPIO_DATA, 1).unwrap();
|
||||
assert_eq!(control(&mut bus, true), 0x40);
|
||||
assert_eq!(bus.read16(IF).unwrap(), 0);
|
||||
}
|
||||
for value in [2, 8, 32, 0xff] {
|
||||
let mut bus = memory();
|
||||
bus.write16(GPIO_CONTROL, 1).unwrap();
|
||||
start(&mut bus);
|
||||
command(&mut bus, 0x62, true).unwrap();
|
||||
assert!(parameter(&mut bus, value)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("control bits"));
|
||||
bus.write16(GPIO_DATA, 1).unwrap();
|
||||
assert_eq!(control(&mut bus, true), 0x40);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_edges_and_word_second_halfword_are_atomic() {
|
||||
let mut bus = memory();
|
||||
bus.write16(GPIO_CONTROL, 1).unwrap();
|
||||
// Data writes latch CS high while all pins are inputs. The following direction halfword
|
||||
// would select with SCK low, so neither halfword of this word may commit.
|
||||
assert!(bus.write32(GPIO_DATA, 0x00070004).is_err());
|
||||
assert_eq!(bus.read32(GPIO_DATA).unwrap(), 2);
|
||||
bus.write16(GPIO_DIRECTION, 7).unwrap();
|
||||
assert_eq!(bus.read16(GPIO_DATA).unwrap(), 0); // Failed word did not retain latch=4.
|
||||
bus.write16(GPIO_DATA, 1).unwrap();
|
||||
bus.write16(GPIO_DATA, 5).unwrap();
|
||||
bus.write16(GPIO_DATA, 4).unwrap();
|
||||
assert!(bus
|
||||
.write16(GPIO_DATA, 7)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("sampling edge"));
|
||||
assert_eq!(bus.read16(GPIO_DATA).unwrap(), 4);
|
||||
bus.write16(GPIO_DATA, 5).unwrap(); // Same stable data is retryable.
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dma3_word_store_uses_gpio_without_modifying_rom_or_raising_a_cartridge_irq() {
|
||||
let mut bus = memory();
|
||||
bus.write16(GPIO_CONTROL, 1).unwrap();
|
||||
bus.write32(0x02000000, 0x00030002).unwrap();
|
||||
let dma = DMA_BASE + 36;
|
||||
bus.write32(dma, 0x02000000).unwrap();
|
||||
bus.write32(dma + 4, GPIO_DATA).unwrap();
|
||||
bus.write32(dma + 8, 0xc4000001).unwrap();
|
||||
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
|
||||
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 3 });
|
||||
assert_eq!(machine.memory().read32(GPIO_DATA).unwrap(), 0x00030002);
|
||||
assert_eq!(machine.memory().read16(IF).unwrap(), 0x800);
|
||||
machine.memory_mut().write16(GPIO_CONTROL, 0).unwrap();
|
||||
assert_eq!(machine.memory().read32(GPIO_DATA).unwrap(), 0xa5a5a5a5);
|
||||
}
|
||||
|
|
@ -48,6 +48,9 @@ The repository is a Cargo workspace with three crates:
|
|||
| `crates/gba-core/src/cpu/transfer_tests.rs` | Stack, addressing, overlap, empty-list, and swap tests |
|
||||
| `crates/gba-core/src/cpu/tests.rs` | Condition truth tables and immediate arithmetic tests |
|
||||
| `crates/gba-core/src/cpu/instruction_tests.rs` | Register operations, transfers, branches, and edge cases |
|
||||
| `crates/gba-core/src/cartridge.rs` | Explicit GPIO/RTC selection, ROM read overlay, pin directions, serial command/control, and bounded diagnostics |
|
||||
| `crates/gba-core/tests/cartridge_gpio.rs` | GPIO lanes, ROM preservation, RTC framing/control/aborts, unsupported commands, and DMA3 access |
|
||||
| `crates/gba-core/src/memory/cartridge_step_tests.rs` | ARM/Thumb GPIO timing, rejected command-edge rollback, block preflight, and staged fetch/read overlays |
|
||||
| `crates/gba-core/src/memory.rs` | Memory mapping, I/O routing, write validation, and device clock |
|
||||
| `crates/gba-core/src/memory/fetch.rs` | Strict instruction samples with captured region-dependent bus observations |
|
||||
| `crates/gba-core/src/memory/fetch_tests.rs` | Fetch widths, bus lanes, captured refill/BIOS values, strict errors, and side-effect isolation |
|
||||
|
|
|
|||
104
docs/hardware/cartridge-gpio.md
Normal file
104
docs/hardware/cartridge-gpio.md
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
# Game Pak GPIO and RTC control subset
|
||||
|
||||
The core supports an explicitly selected cartridge GPIO interface and RTC command/control transactions.
|
||||
GPIO means general-purpose input/output. RTC means real-time clock.
|
||||
Calendar access, clock advancement, battery persistence, RTC interrupts, and other cartridge sensors remain unimplemented.
|
||||
Unsupported operations return diagnostics. No fabricated date or successful calendar transfer is reported.
|
||||
|
||||
## Selection
|
||||
|
||||
`Memory::set_cartridge_hardware(CartridgeHardware::Rtc)` attaches the bounded interface with initial state.
|
||||
`CartridgeHardware::None` is the default. Selecting either option replaces peripheral state, without changing ROM bytes.
|
||||
Call this API between machine steps. The core does not inspect filenames, game headers, or library signatures to select hardware.
|
||||
|
||||
The desktop enables this interface with `--rtc`, in either ROM execution mode:
|
||||
|
||||
```sh
|
||||
direnv exec . cargo run --locked --release -- --rom roms/pokemon-emerald.gba --rtc --steps 2000000
|
||||
direnv exec . cargo run --locked --release -- --rom roms/pokemon-emerald.gba --rtc --window --frames 60
|
||||
```
|
||||
|
||||
The report states that only GPIO and command/control are supported.
|
||||
The JSON suite schema does not yet select cartridge peripherals. Its default remains no attached peripheral.
|
||||
There is no host-clock dependency, ROM patch, save file, or automatic device detection.
|
||||
|
||||
## GPIO registers
|
||||
|
||||
| Address | Register | Retained bits |
|
||||
| --- | --- | --- |
|
||||
| `0x080000c4` | Data output latch and pin samples | 0–3 |
|
||||
| `0x080000c6` | Direction: 0=input, 1=output | 0–3 |
|
||||
| `0x080000c8` | Read enable: 0=ROM reads, 1=register reads | 0 |
|
||||
|
||||
All other register bits read zero when read enable is set. Initial direction, data latch, and read enable are zero.
|
||||
With read enable clear, reads return the original supplied ROM bytes, including nonzero bytes.
|
||||
Disabling reads does not disable writes or reset an RTC transaction.
|
||||
ROM bytes are never overwritten with register values.
|
||||
|
||||
Data writes retain an output latch independently of direction.
|
||||
Output pins read their driven latch bits. Changing direction can expose a retained output and create an RTC signal edge.
|
||||
The RTC mapping uses bit 0 for clock, bit 1 for bidirectional data, and bit 2 for chip select.
|
||||
The bounded input model holds clock, select, and the unused pin low; RTC data is high outside a read transaction.
|
||||
This input policy follows the pinned reference implementation, not independent physical measurements.
|
||||
No solar sensor, rumble, gyro, or cartridge interrupt source is attached.
|
||||
|
||||
Aligned halfword writes are supported. A word at `0x080000c4` applies data, then direction, as two ordered halfwords.
|
||||
A rejected second halfword leaves both registers unchanged under the core's diagnostic policy.
|
||||
A word at `0x080000c8` reaches ordinary ROM at `0x080000ca` and fails without changing control.
|
||||
Byte writes are diagnostic because the consulted bus-width description is uncertain about GBA versus DS behavior.
|
||||
Byte, halfword, and word reads use ordinary little-endian lanes, including register/ROM boundaries.
|
||||
|
||||
Only the documented first-window addresses are implemented.
|
||||
Other ROM windows still expose supplied ROM bytes; their writes remain read-only diagnostics.
|
||||
GPIO mirroring and writes at alias addresses have not been independently validated.
|
||||
Without explicit hardware selection, all these addresses remain ordinary read-only cartridge memory.
|
||||
|
||||
## RTC command and control transfers
|
||||
|
||||
Chip select rising with clock high begins a command. Chip select falling aborts the current transaction.
|
||||
Command bits enter on rising clock edges after software establishes data while clock is low.
|
||||
The first four command bits on the wire must be `0,1,1,0`.
|
||||
GBATEK's forward and reverse command-byte representations describe this same wire sequence, not interchangeable wire prefixes.
|
||||
Control parameters transfer least-significant bit first.
|
||||
During reads, the RTC supplies each bit on the falling edge. GPIO reads do not advance the serial state.
|
||||
|
||||
Using GBATEK's MSB-first command representation:
|
||||
|
||||
- `0x62` writes one control byte; `0x63` reads one control byte.
|
||||
- `0x60` and `0x61` reset supported control state to zero. Calendar reset side effects are not implemented.
|
||||
- `0x64` through `0x67` return an explicit calendar-access diagnostic.
|
||||
- `0x6c` and `0x6d` return an explicit force-interrupt diagnostic.
|
||||
- Unused commands and invalid command prefixes remain diagnostic.
|
||||
|
||||
Initial control is `0x40`, a deterministic powered 24-hour-mode setting.
|
||||
Bit 6 is writable. Unused bits and read-only bit 7 do not retain writes.
|
||||
Writes enabling bits 1, 3, or 5 fail because their interrupt or uncertain control behavior is not implemented.
|
||||
No power-failure condition is synthesized. There is no calendar value behind this control setting yet.
|
||||
|
||||
A partial control write does not commit. A complete byte commits at its final rising edge.
|
||||
Extra clocks beyond the documented parameter length remain diagnostic rather than guessing repeated transfers.
|
||||
Selecting with clock low, driving data during read clocks, and changing data at a sampling edge also remain diagnostic.
|
||||
Minimum physical setup/hold durations, signal races, and exact analog behavior are not modeled.
|
||||
|
||||
## Bus integration and tests
|
||||
|
||||
CPU and DMA accesses retain Game Pak wait-state costs.
|
||||
Machine steps stage cartridge state separately from ROM bytes. Failed steps discard pin edges and control changes.
|
||||
Block-store preflight applies earlier pin/direction changes before validating later accesses.
|
||||
A later diagnostic leaves earlier RAM, GPIO, and CPU state unchanged.
|
||||
Data reads and instruction fetches share the selected register overlay. Fetches have no serial side effects.
|
||||
|
||||
Original tests cover selection, masks, directions, retained latches, read enable, widths, ROM preservation, and command framing.
|
||||
They check control reads/writes/reset, aborts, both command-byte representations, edge stability, and unsupported commands.
|
||||
ARM/Thumb, DMA3, block preflight, staged fetches, retryable errors, and nominal bus costs have separate regressions.
|
||||
File-backed desktop tests check explicit `--rtc` selection, unchanged files, option rejection, and native-window propagation.
|
||||
These tests validate the bounded implementation, not physical RTC timing or game playability.
|
||||
|
||||
## References
|
||||
|
||||
- [GBATEK cartridge GPIO](https://problemkaputt.de/gbatek-gba-cart-i-o-port-gpio.htm): register addresses, masks, directions, read control, and uncertain byte-store behavior.
|
||||
- [GBATEK GBA RTC](https://problemkaputt.de/gbatek-gba-cart-real-time-clock-rtc.htm): control fields, reset, calendar formats, and command mapping relative to the DS chip.
|
||||
- [GBATEK DS RTC](https://problemkaputt.de/gbatek-ds-real-time-clock-rtc.htm): the linked serial flow, chip-select sequencing, bit edges, and forward/reverse command representations. GBA register differences remain separate.
|
||||
- [mGBA GPIO at 3a5e34be](https://github.com/mgba-emu/mgba/blob/3a5e34be33dc7f8f707e5bc9db69e8a430046f21/src/gba/cart/gpio.c): comparison for retained outputs, direction changes, idle inputs, serial edges, and initial control. Its host-clock lookup, ROM-buffer mutation, and repeated transfers are not copied.
|
||||
- [mGBA memory at 3a5e34be](https://github.com/mgba-emu/mgba/blob/3a5e34be33dc7f8f707e5bc9db69e8a430046f21/src/gba/memory.c): first-window halfword GPIO write routing. This is not physical proof of all address aliases.
|
||||
- [Explaining GBA RTC](https://bmchtech.github.io/post/rtc/): introductory background surfaced during research; the author explicitly warns that details may be inaccurate. Register and protocol decisions use the references above.
|
||||
|
|
@ -33,9 +33,13 @@ It does not decode archives or ELF executables. Supply a raw binary with an ARM
|
|||
Headerless original programs are accepted.
|
||||
Reads past supplied cartridge bytes remain explicit memory errors, not open-bus values.
|
||||
All three Game Pak ROM windows use the same supplied bytes and existing wait-state rules.
|
||||
Cartridge writes, save devices, and cartridge peripherals remain unsupported.
|
||||
Ordinary cartridge bytes remain read-only. Save devices remain unsupported.
|
||||
Optional `--rtc` selection enables the bounded [Game Pak GPIO and RTC control interface](cartridge-gpio.md).
|
||||
It supports configuration and control transactions, not calendar access, clock advancement, or RTC interrupts.
|
||||
|
||||
Both modes call `bios::boot` with the loaded bytes.
|
||||
With `--rtc`, the host then selects the cartridge peripheral before the first machine step.
|
||||
Without that option, no cartridge peripheral is attached. Duplicate `--rtc` options are rejected.
|
||||
The [original BIOS replacement](bios.md) executes its minimal boot before entering `0x08000000` in ARM System mode.
|
||||
There is no command-line option for external BIOS files.
|
||||
Unsupported services remain diagnostics. RegisterRamReset now accepts sound/serial reset flags, including `r0=0xff`, within the initialization subset.
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@ Byte, halfword, word, block-store, and DMA validation retain explicit errors wit
|
|||
Use a local ROM that you may lawfully use:
|
||||
|
||||
```sh
|
||||
direnv exec . cargo run --locked --release -- --rom roms/pokemon-emerald.gba --steps 1000000
|
||||
direnv exec . cargo run --locked --release -- --rom roms/pokemon-emerald.gba --window --frames 120
|
||||
direnv exec . cargo run --locked --release -- --rom roms/pokemon-emerald.gba --rtc --steps 2000000
|
||||
direnv exec . cargo run --locked --release -- --rom roms/pokemon-emerald.gba --rtc --window --frames 60
|
||||
```
|
||||
|
||||
The earlier reset-only implementation stopped at master sound enable (`0x04000084`, PC `0x082e0518`).
|
||||
|
|
@ -64,13 +64,18 @@ A child cannot start itself without a parent clock. No linked transfer, assigned
|
|||
The send register supports 16-bit data; receive latches do not become invented peer data.
|
||||
See [the register model and evidence limits](../hardware/serial.md#disconnected-multiplayer-configuration).
|
||||
|
||||
Both terminal and native runs now reach a Game Pak GPIO control write:
|
||||
Multiplayer configuration then reached a Game Pak GPIO control write (`0x080000c8`, PC `0x082e29f8`).
|
||||
The optional [cartridge GPIO/RTC interface](../hardware/cartridge-gpio.md) now supports read enable, directions, pin latches, and RTC control transactions.
|
||||
Select it explicitly with `--rtc`; ordinary ROM bytes and other ROMs' default hardware remain unchanged.
|
||||
No host date, RTC calendar, or successful calendar read is invented.
|
||||
|
||||
Both terminal and native runs with `--rtc` now reach a calendar-read command:
|
||||
|
||||
```text
|
||||
Steps: 956055; instructions: 953375; IRQ entries: 80; DMA units: 2600; HALT idle: 0
|
||||
Nominal cycles: 3514932
|
||||
PC=0x082e29f8
|
||||
read-only memory at 0x080000c8
|
||||
Steps: 957729; instructions: 955049; IRQ entries: 80; DMA units: 2600; HALT idle: 0
|
||||
Nominal cycles: 3520685
|
||||
PC=0x082e28a6
|
||||
unsupported RTC calendar access: write 0x07 at 0x080000c4
|
||||
```
|
||||
|
||||
The native run captures twelve startup frames. These do not establish a working title screen or gameplay.
|
||||
|
|
@ -85,10 +90,11 @@ Workspace debug/release tests, strict Clippy, public ARM/Thumb/memory/BIOS check
|
|||
Native ROM-window tests and graphics smoke modes pass on Darwin arm64.
|
||||
The pinned Pong debug/release reports match.
|
||||
|
||||
The next requirement is cartridge GPIO, starting with the control register at `0x080000c8`.
|
||||
Research the Game Pak data/direction/control registers and real-time clock protocol before implementing them.
|
||||
Keep ordinary ROM writes read-only. Do not hide the failure by ignoring every cartridge write.
|
||||
This serial result does not establish linked gameplay or serial GPIO interrupt support.
|
||||
The next requirement is RTC calendar transfers with an explicit clock-source model.
|
||||
Implement validated date/time formats, read snapshots, writes/reset, and clock ownership without putting host time in the core.
|
||||
Keep battery persistence and cartridge IRQ behavior explicit; do not equate GBA STOP with stopping a battery-powered RTC.
|
||||
Ordinary ROM writes remain read-only. Without `--rtc`, the earlier GPIO diagnostic is still expected.
|
||||
This result does not establish gameplay, linked transfers, or cartridge/serial GPIO interrupt support.
|
||||
|
||||
Noise now has deterministic counter clocks and shares tested length/envelope logic with the independent pulse channels.
|
||||
Sweep applies only to channel 1. Direct Sound has FIFO clocks and nominal DMA requests.
|
||||
|
|
|
|||
|
|
@ -76,6 +76,8 @@ Update this file when a feature lands or a limit is removed.
|
|||
- `SWP`/`SWPB`, including unaligned word rotation and register aliases.
|
||||
- 256 KiB external work RAM and 32 KiB internal work RAM, including mirrors.
|
||||
- Read-only cartridge bytes in the three GBA cartridge windows.
|
||||
- Explicit cartridge RTC selection with first-window GPIO read enable, direction/latch behavior, and serial command/control transactions.
|
||||
- Cartridge GPIO changes participate in CPU/DMA rollback and block-store preflight without modifying ROM bytes.
|
||||
- Raw ROM-file loading with original BIOS boot, terminal diagnostics, and final CPU/step reports.
|
||||
- Firmware boot initializes BG2/BG3 identity scales for bitmap and affine programs that do not program their own matrices.
|
||||
- RegisterRamReset supports disabled sound and disconnected serial initialization, including the common `r0=0xff` request.
|
||||
|
|
@ -139,6 +141,8 @@ Horizontal window comparators retain four-cycle event history.
|
|||
Sprite rows prepare one line ahead with a whole-row cycle40 sample and nominal per-row work limits.
|
||||
Individual fetch timing, hardware-accurate work cutoffs, video-bus contention, and per-pixel color composition remain unimplemented.
|
||||
There is no host audio output or cartridge save support.
|
||||
The optional [cartridge GPIO/RTC subset](hardware/cartridge-gpio.md) supports command/control only, selected with `--rtc` in ROM modes.
|
||||
RTC calendar access, clock advancement, persistence, interrupts, GPIO aliases, and byte writes remain unsupported or unverified.
|
||||
The [audio subset](hardware/audio.md) supports Direct Sound, both PSG pulse channels, noise channel 4, idle wave channel 3 state, and instantaneous digital mixing.
|
||||
It does not yet provide a continuous sample stream. Wave channel 3 playback and unverified sound-DMA pairings remain diagnostic.
|
||||
PSG timing and signed mixing are nominal; hardware startup edges and analog behavior remain unverified.
|
||||
|
|
@ -161,7 +165,9 @@ It also passes the pulse/noise triggers and normal-mode RCNT configuration.
|
|||
It also completes a CpuSet call to address zero through the corrected ignored BIOS-write behavior.
|
||||
It also accepts an external-clock serial request without inventing clock edges or completion.
|
||||
It now also completes internally clocked normal transfers and selects idle multiplayer mode.
|
||||
It next stops at a Game Pak GPIO control write (`0x080000c8`, PC `0x082e29f8`). Cartridge GPIO remains unimplemented.
|
||||
With explicit `--rtc` selection, it also passes Game Pak GPIO configuration and reaches an RTC calendar-read command.
|
||||
That command remains diagnostic (`0x080000c4`, PC `0x082e28a6`); no date is fabricated.
|
||||
Without `--rtc`, cartridge GPIO writes still return read-only diagnostics.
|
||||
No title-screen, gameplay, or audible-game compatibility is established.
|
||||
See [the startup result](research/emerald-reset.md) for the bounded run and next investigation.
|
||||
See [cartridge loading](hardware/cartridge.md) for file limits, step budgets, and exit status.
|
||||
|
|
@ -192,7 +198,7 @@ Host inspection and instruction fetches remain strict. Other unmapped reads retu
|
|||
Reads beyond the supplied cartridge bytes also return errors.
|
||||
Direct memory-bus halfword and word accesses require alignment.
|
||||
CPU load/store instructions apply ARM7TDMI alignment and rotation rules before accessing the bus.
|
||||
Writes to cartridge addresses return errors instead of modeling cartridge hardware.
|
||||
Ordinary ROM writes remain read-only. Only the explicitly selected cartridge GPIO register subset accepts writes.
|
||||
|
||||
## Next steps
|
||||
|
||||
|
|
|
|||
|
|
@ -65,6 +65,11 @@ They verify suite selection, pinned URLs, hashes, sizes, checkpoint validation,
|
|||
The [Pong adapter](public-pong.md) has additional offline tests for its pinned downloads, retained license, and bounded gameplay scenarios.
|
||||
Run the external Pong suite separately; ordinary Cargo and Python tests require no public ROM or network access.
|
||||
Device initialization regressions use original instructions to check disabled sound, wave RAM, disconnected serial pins, and reset flags.
|
||||
Cartridge GPIO tests use an explicitly attached RTC control subset and original pin sequences, never game code or host time.
|
||||
They cover read overlays, unchanged ROM bytes, masks, directions, widths, serial framing, control/reset, aborts, and explicit calendar/IRQ diagnostics.
|
||||
ARM/Thumb and DMA3 checks retain Game Pak costs; staged fetches, rejected edges, and block-store preflight retain diagnostic atomicity.
|
||||
Desktop tests check explicit `--rtc` selection, duplicate rejection, unchanged files, and native-window configuration.
|
||||
Physical pin timing, GPIO aliases, RTC calendar values, and battery behavior remain unverified or unimplemented.
|
||||
They verify explicit activation errors and atomic byte/word/block-store/DMA failure behavior.
|
||||
The optional [local Emerald startup check](research/emerald-reset.md) now reaches sound activation but still fails deliberately there.
|
||||
[Jev evidence review](jev-debugging.md) is optional and never changes deterministic test verdicts.
|
||||
|
|
|
|||
|
|
@ -40,10 +40,11 @@ The native Darwin window also starts and presents frames without a diagnostic.
|
|||
This exposed and corrected missing firmware affine-scale initialization, without a game-specific patch.
|
||||
Frame-based scenarios and the preparation adapter now make the result repeatable.
|
||||
A local Emerald run now passes its initial all-device reset, master sound enable, and pulse/noise triggers.
|
||||
It also passes normal serial transfers and idle multiplayer configuration, then stops at Game Pak GPIO control.
|
||||
It also passes normal serial transfers and idle multiplayer configuration.
|
||||
With `--rtc`, it passes Game Pak GPIO setup and stops at the first unsupported RTC calendar-read command.
|
||||
The core has tested Direct Sound, both pulse channels, and noise channel 4.
|
||||
Wave channel 3, a continuous sample stream, and desktop audio output remain missing.
|
||||
See [the runtime result](research/emerald-reset.md). Cartridge GPIO and its real-time clock protocol are the next demonstrated startup investigation.
|
||||
See [the runtime result](research/emerald-reset.md). RTC calendar transfers and an explicit clock-source model are the next startup requirement.
|
||||
The separate save requirement remains cartridge SRAM, followed by safe host save persistence.
|
||||
SRAM is byte-addressable battery-backed save memory. Flash and EEPROM remain separate protocols.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue