feat(cartridge): add explicit GPIO and RTC control support

This commit is contained in:
0xrsydn 2026-10-07 05:28:52 +07:00
commit 692be36349
18 changed files with 889 additions and 26 deletions

View 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"),
}
}
}

View file

@ -2,6 +2,7 @@
pub mod audio;
pub mod bios;
pub mod cartridge;
pub mod cpu;
pub mod display;
pub mod dma;

View file

@ -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() {

View 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)
);
}

View 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);
}