255 lines
9.3 KiB
Rust
255 lines
9.3 KiB
Rust
//! 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);
|
|
}
|