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
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)
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue