feat(serial): support disconnected Joybus configuration and local registers
This commit is contained in:
parent
8b3b7ae62d
commit
7546d17630
15 changed files with 462 additions and 62 deletions
|
|
@ -8,8 +8,8 @@ use crate::audio::Audio;
|
|||
pub use crate::audio::{FIFO_A, FIFO_B, SOUNDBIAS, SOUNDCNT_H, SOUNDCNT_X, SOUND_START, WAVE_RAM};
|
||||
pub(crate) use serial::Serial;
|
||||
pub use serial::{
|
||||
JOYCNT, JOY_RECV, JOY_TRANS, RCNT, SIOCNT, SIODATA32, SIODATA8, SIOMLT_SEND, SIOMULTI0,
|
||||
SIOMULTI1, SIOMULTI2, SIOMULTI3,
|
||||
JOYCNT, JOYSTAT, JOY_RECV, JOY_TRANS, RCNT, SIOCNT, SIODATA32, SIODATA8, SIOMLT_SEND,
|
||||
SIOMULTI0, SIOMULTI1, SIOMULTI2, SIOMULTI3,
|
||||
};
|
||||
mod timer_step;
|
||||
pub(crate) use timer_step::TimerStep;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
//! Disconnected serial: normal shifting and idle multiplayer child configuration.
|
||||
//! Disconnected serial: normal shifting, idle multiplayer, and local Joybus registers.
|
||||
use super::replace_byte;
|
||||
|
||||
pub const SIODATA32: u32 = 0x0400_0120;
|
||||
|
|
@ -13,6 +13,7 @@ pub const RCNT: u32 = 0x0400_0134;
|
|||
pub const JOYCNT: u32 = 0x0400_0140;
|
||||
pub const JOY_RECV: u32 = 0x0400_0150;
|
||||
pub const JOY_TRANS: u32 = 0x0400_0154;
|
||||
pub const JOYSTAT: u32 = 0x0400_0158;
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) struct Serial {
|
||||
|
|
@ -21,6 +22,10 @@ pub(crate) struct Serial {
|
|||
send: u16, // Shared low byte; high byte is accessible only in multiplayer format.
|
||||
control: u16,
|
||||
rcnt: u16,
|
||||
joy_control: u8, // IRQ enable only; no external master can set event flags.
|
||||
joy_receive: u32,
|
||||
joy_transmit: u32,
|
||||
joy_status: u8, // Software flags and transmit pending; no remote reception.
|
||||
bits_left: u8,
|
||||
until_bit: u32,
|
||||
}
|
||||
|
|
@ -30,28 +35,28 @@ impl Serial {
|
|||
matches!(address,
|
||||
SIODATA32..=0x0400_012b |
|
||||
RCNT..=0x0400_0135 | JOYCNT..=0x0400_0141 |
|
||||
JOY_RECV..=0x0400_0157)
|
||||
JOY_RECV..=0x0400_0159)
|
||||
}
|
||||
|
||||
pub(crate) fn unsupported(address: u32, value: u8) -> Option<&'static str> {
|
||||
match address {
|
||||
0x0400_0129 if value & 0x30 == 0x30 => Some("UART serial mode"),
|
||||
0x0400_0135 if value & 0xc0 == 0xc0 => Some("Joybus serial mode"),
|
||||
0x0400_0135 if value & 0x81 == 0x81 => Some("GPIO serial interrupt enable"),
|
||||
JOYCNT if value & 0x40 != 0 => Some("Joybus interrupt enable"),
|
||||
JOY_RECV..=0x0400_0157 if value != 0 => Some("Joybus data access beyond reset"),
|
||||
0x0400_0135 if value & 0xc1 == 0x81 => Some("GPIO serial interrupt enable"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn gpio(&self) -> bool {
|
||||
self.rcnt & 0x8000 != 0
|
||||
self.rcnt & 0xc000 == 0x8000
|
||||
}
|
||||
fn joybus(&self) -> bool {
|
||||
self.rcnt & 0xc000 == 0xc000
|
||||
}
|
||||
fn multiplayer_format(&self) -> bool {
|
||||
self.control & 0x3000 == 0x2000
|
||||
}
|
||||
fn normal(&self) -> bool {
|
||||
!self.gpio() && !self.multiplayer_format()
|
||||
self.rcnt & 0x8000 == 0 && !self.multiplayer_format()
|
||||
}
|
||||
fn busy(&self) -> bool {
|
||||
self.control & 0x80 != 0
|
||||
|
|
@ -137,7 +142,14 @@ impl Serial {
|
|||
.multiplayer_format()
|
||||
.then(|| self.multi_extra.to_le_bytes()[(address - SIOMULTI2) as usize]);
|
||||
}
|
||||
for (base, data) in [(JOY_RECV, self.joy_receive), (JOY_TRANS, self.joy_transmit)] {
|
||||
if (base..base + 4).contains(&address) {
|
||||
// No master can set receive status, so its read-clear bit is already zero.
|
||||
return Some(data.to_le_bytes()[(address - base) as usize]);
|
||||
}
|
||||
}
|
||||
let value = match address & !1 {
|
||||
SIOCNT if self.joybus() => self.control, // Unused; do not invent pin samples.
|
||||
SIOCNT => {
|
||||
let pins = if self.gpio() { self.gpio_pins() } else { 15 };
|
||||
// SI is pulled high without a cable. Multiplayer drives SD high while idle.
|
||||
|
|
@ -163,7 +175,8 @@ impl Serial {
|
|||
}
|
||||
(self.rcnt & !15) | self.gpio_pins()
|
||||
}
|
||||
JOYCNT | JOY_RECV..=0x0400_0156 => 0,
|
||||
JOYCNT => u16::from(self.joy_control),
|
||||
JOYSTAT => u16::from(self.joy_status),
|
||||
_ => return None,
|
||||
};
|
||||
Some(value.to_le_bytes()[(address & 1) as usize])
|
||||
|
|
@ -198,8 +211,22 @@ impl Serial {
|
|||
let shift = (at - SIOMULTI2) * 8;
|
||||
self.multi_extra =
|
||||
(self.multi_extra & !(255 << shift)) | (u32::from(value) << shift);
|
||||
} else if (JOY_RECV..JOY_TRANS).contains(&at) {
|
||||
// GBATEK describes R/W data latches. CPU writes are not remote events.
|
||||
let shift = (at - JOY_RECV) * 8;
|
||||
self.joy_receive =
|
||||
(self.joy_receive & !(255 << shift)) | (u32::from(value) << shift);
|
||||
} else if (JOY_TRANS..JOYSTAT).contains(&at) {
|
||||
let shift = (at - JOY_TRANS) * 8;
|
||||
self.joy_transmit =
|
||||
(self.joy_transmit & !(255 << shift)) | (u32::from(value) << shift);
|
||||
self.joy_status |= 2; // Data available, not transmission complete.
|
||||
} else {
|
||||
match at & !1 {
|
||||
JOYCNT if at & 1 == 0 => self.joy_control = value & 0x40,
|
||||
JOYSTAT if at & 1 == 0 => {
|
||||
self.joy_status = (self.joy_status & 2) | (value & 0x30);
|
||||
}
|
||||
SIODATA8 if self.multiplayer_format() || at & 1 == 0 => {
|
||||
self.send = replace_byte(self.send, at, value);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ mod iwram_bus;
|
|||
#[cfg(test)]
|
||||
mod iwram_history_tests;
|
||||
#[cfg(test)]
|
||||
mod joybus_step_tests;
|
||||
#[cfg(test)]
|
||||
mod multiplayer_step_tests;
|
||||
#[cfg(test)]
|
||||
mod prefetch_tests;
|
||||
|
|
|
|||
91
crates/gba-core/src/memory/joybus_step_tests.rs
Normal file
91
crates/gba-core/src/memory/joybus_step_tests.rs
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
//! Original Joybus transaction, CPU access, and preflight regressions.
|
||||
use super::{timing_event_tests::prepared, *};
|
||||
use crate::{
|
||||
io::{IF, JOYCNT, JOYSTAT, JOY_RECV, JOY_TRANS, RCNT, SIOCNT},
|
||||
machine::Machine,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn arm_and_thumb_transmit_stores_commit_pending_status_once() {
|
||||
for thumb in [false, true] {
|
||||
let (cpu, mut memory) = prepared(
|
||||
thumb,
|
||||
&[if thumb { 0x6008 } else { 0xe5810000 }], // STR r0,[r1]
|
||||
&[(0, 0x12345678), (1, JOY_TRANS)],
|
||||
);
|
||||
memory.write16(RCNT, 0xc000).unwrap();
|
||||
memory.write16(JOYCNT, 0x40).unwrap();
|
||||
let mut machine = Machine::new(cpu, memory);
|
||||
machine.step().unwrap();
|
||||
assert_eq!(machine.memory().read32(JOY_TRANS).unwrap(), 0x12345678);
|
||||
assert_eq!(machine.memory().read16(JOYSTAT).unwrap(), 2);
|
||||
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
|
||||
assert_eq!(machine.memory().io.serial.next_event_cycles(), None);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn joybus_mode_selection_observes_the_normal_completion_bus_phase() {
|
||||
for elapsed in [56, 57] {
|
||||
let (cpu, mut memory) = prepared(false, &[0xe5c10000], &[(0, 0xc1), (1, RCNT + 1)]);
|
||||
memory.write16(SIOCNT, 0x4083).unwrap();
|
||||
memory.advance_cycles(elapsed);
|
||||
let serial = memory.io.serial;
|
||||
let mut machine = Machine::new(cpu, memory);
|
||||
let cpu = machine.cpu().clone();
|
||||
if elapsed == 57 {
|
||||
machine.step().unwrap();
|
||||
assert_eq!(machine.cycles(), 64);
|
||||
assert_eq!(machine.memory().read8(RCNT + 1).unwrap(), 0xc1);
|
||||
assert_eq!(machine.memory().read16(IF).unwrap(), 0x80);
|
||||
assert_eq!(machine.memory().io.serial.next_event_cycles(), None);
|
||||
} else {
|
||||
let error = machine.step().unwrap_err();
|
||||
assert!(error.to_string().contains("serial reconfiguration"));
|
||||
assert_eq!(machine.step(), Err(error));
|
||||
assert_eq!(machine.cpu(), &cpu);
|
||||
assert_eq!(machine.cycles(), 56);
|
||||
assert_eq!(machine.memory().io.serial, serial);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn later_block_padding_failure_discards_data_pending_status_and_writeback() {
|
||||
let (cpu, mut memory) = prepared(
|
||||
false,
|
||||
&[0xe8a0000e], // STMIA r0!,{r1-r3}; final word hits JOYSTAT padding.
|
||||
&[(0, JOY_RECV), (1, 0x12345678), (2, 0xabcdef90), (3, 0x30)],
|
||||
);
|
||||
memory.write16(RCNT, 0xc000).unwrap();
|
||||
let serial = memory.io.serial;
|
||||
let mut machine = Machine::new(cpu, memory);
|
||||
let cpu = machine.cpu().clone();
|
||||
let error = machine.step().unwrap_err();
|
||||
assert!(error.to_string().contains("0x0400015a"));
|
||||
assert_eq!(machine.step(), Err(error));
|
||||
assert_eq!(machine.cpu(), &cpu);
|
||||
assert_eq!(machine.cycles(), 0);
|
||||
assert_eq!(machine.memory().io.serial, serial);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn staged_reads_see_transmit_pending_before_commit_and_discard_restores_every_lane() {
|
||||
let (_, mut memory) = prepared(false, &[0xe1a00000], &[]);
|
||||
let serial = memory.io.serial;
|
||||
memory.begin_timer_step();
|
||||
memory.write16(RCNT, 0xc100).unwrap();
|
||||
memory.write16(JOYCNT, 0x40).unwrap();
|
||||
memory.write32(JOY_TRANS, 0x12345678).unwrap();
|
||||
memory.write32(JOY_RECV, 0xabcdef90).unwrap();
|
||||
memory.write16(JOYSTAT, 0x30).unwrap();
|
||||
assert_eq!(memory.read32(JOY_TRANS).unwrap(), 0x12345678);
|
||||
assert_eq!(memory.read32(JOY_RECV).unwrap(), 0xabcdef90);
|
||||
assert_eq!(memory.read16(JOYSTAT).unwrap(), 0x32);
|
||||
assert_eq!(memory.read16(JOYCNT).unwrap(), 0x40);
|
||||
assert_eq!(memory.read8(RCNT), Err(MemoryError::Unmapped(RCNT)));
|
||||
assert_eq!(memory.io.serial, serial);
|
||||
memory.discard_timer_step();
|
||||
assert_eq!(memory.io.serial, serial);
|
||||
assert_eq!(memory.read16(JOYSTAT).unwrap(), 0);
|
||||
}
|
||||
|
|
@ -288,6 +288,37 @@ fn serial_sound_reset_flags_are_selective_and_all_devices_return() {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serial_reset_clears_joybus_data_and_irq_enable_without_fabricating_a_master() {
|
||||
use gba_core::io::{JOYCNT, JOYSTAT, JOY_RECV, JOY_TRANS, RCNT};
|
||||
for thumb in [false, true] {
|
||||
for flags in [0, 0x20, 0xff] {
|
||||
let (mut m, pc) = prepare(flags, thumb, 0x1f);
|
||||
m.memory_mut().write16(RCNT, 0xc100).unwrap();
|
||||
m.memory_mut().write16(JOYCNT, 0x40).unwrap();
|
||||
m.memory_mut().write32(JOY_RECV, MARKER).unwrap();
|
||||
m.memory_mut().write32(JOY_TRANS, MARKER).unwrap();
|
||||
m.memory_mut().write16(JOYSTAT, 0x30).unwrap();
|
||||
complete(&mut m, pc);
|
||||
let selected = flags & 0x20 != 0;
|
||||
for address in [JOY_RECV, JOY_TRANS] {
|
||||
assert_eq!(
|
||||
m.memory().read32(address).unwrap(),
|
||||
if selected { 0 } else { MARKER }
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
m.memory().read16(JOYCNT).unwrap(),
|
||||
if selected { 0 } else { 0x40 }
|
||||
);
|
||||
// The replacement writes zero to TRANS, but does not write JOYSTAT.
|
||||
// Pending data is not a completed transfer and cannot request an IRQ.
|
||||
assert_eq!(m.memory().read16(JOYSTAT).unwrap(), 0x32);
|
||||
assert_eq!(m.memory().read16(IF).unwrap() & 0x80, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serial_reset_cancels_an_external_clock_request_only_when_selected() {
|
||||
use gba_core::io::{RCNT, SIOCNT};
|
||||
|
|
|
|||
|
|
@ -2,10 +2,7 @@
|
|||
use gba_core::{
|
||||
cpu::Cpu,
|
||||
dma::DMA_BASE,
|
||||
io::{
|
||||
JOYCNT, JOY_RECV, RCNT, SIOCNT, SIODATA32, SIODATA8, SOUNDBIAS, SOUNDCNT_H, SOUNDCNT_X,
|
||||
WAVE_RAM,
|
||||
},
|
||||
io::{JOYCNT, RCNT, SIOCNT, SIODATA32, SIODATA8, SOUNDBIAS, SOUNDCNT_H, SOUNDCNT_X, WAVE_RAM},
|
||||
machine::Machine,
|
||||
memory::{Memory, MemoryError, ROM_START},
|
||||
};
|
||||
|
|
@ -81,10 +78,7 @@ fn unsupported_operations_have_specific_retryable_diagnostics() {
|
|||
let mut bus = memory();
|
||||
for (address, value, description) in [
|
||||
(SIOCNT + 1, 0x30, "UART serial mode"),
|
||||
(RCNT + 1, 0xc0, "Joybus serial mode"),
|
||||
(RCNT + 1, 0x81, "GPIO serial interrupt"),
|
||||
(JOYCNT, 0x40, "Joybus interrupt"),
|
||||
(JOY_RECV, 1, "Joybus data access"),
|
||||
] {
|
||||
let error = bus.write8(address, value).unwrap_err();
|
||||
assert!(matches!(error, MemoryError::UnsupportedIo { .. }));
|
||||
|
|
@ -92,7 +86,7 @@ fn unsupported_operations_have_specific_retryable_diagnostics() {
|
|||
assert_eq!(bus.write8(address, value), Err(error));
|
||||
}
|
||||
bus.write16(RCNT, 0x8055).unwrap();
|
||||
assert!(bus.write16(RCNT, 0xc0aa).is_err());
|
||||
assert!(bus.write16(RCNT, 0x81aa).is_err());
|
||||
assert_eq!(bus.read16(RCNT).unwrap(), 0x805f); // Low byte was not committed.
|
||||
bus.write16(SIOCNT, 0x4008).unwrap();
|
||||
assert!(bus.write32(SIOCNT, 0xaabb3080).is_err());
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ fn memory() -> Memory {
|
|||
#[test]
|
||||
fn rcnt_high_byte_selection_is_independent_of_previous_mode_and_low_byte() {
|
||||
let mut bus = memory();
|
||||
for previous in [0x8000, 0x80f5, 0x0100, 0x4100] {
|
||||
for previous in [0x8000, 0x80f5, 0x0100, 0x4100, 0xc155] {
|
||||
for low in [0, 0x55, 0xff] {
|
||||
for high in 0..=u8::MAX {
|
||||
for halfword in [false, true] {
|
||||
|
|
@ -28,25 +28,21 @@ fn rcnt_high_byte_selection_is_independent_of_previous_mode_and_low_byte() {
|
|||
// Decode individual mode bits, separately from the implementation masks.
|
||||
let gpio_or_joybus = high & 0x80 != 0;
|
||||
let joybus = gpio_or_joybus && high & 0x40 != 0;
|
||||
let gpio_irq = gpio_or_joybus && high & 1 != 0;
|
||||
if joybus || gpio_irq {
|
||||
let gpio_irq = gpio_or_joybus && !joybus && high & 1 != 0;
|
||||
if gpio_irq {
|
||||
assert_eq!(
|
||||
result,
|
||||
Err(MemoryError::UnsupportedIo {
|
||||
address: RCNT + 1,
|
||||
value: high,
|
||||
operation: if joybus {
|
||||
"Joybus serial mode"
|
||||
} else {
|
||||
"GPIO serial interrupt enable"
|
||||
},
|
||||
operation: "GPIO serial interrupt enable",
|
||||
})
|
||||
);
|
||||
assert_eq!((bus.read8(RCNT), bus.read8(RCNT + 1)), before);
|
||||
} else {
|
||||
result.unwrap();
|
||||
assert_eq!(bus.read8(RCNT + 1).unwrap(), high & 0xc1);
|
||||
if gpio_or_joybus {
|
||||
if gpio_or_joybus && !joybus {
|
||||
let latch = if halfword { low } else { previous as u8 };
|
||||
let outputs = latch >> 4;
|
||||
let pins = (latch & outputs | !outputs) & 15;
|
||||
|
|
@ -109,17 +105,13 @@ fn cpu_byte_and_halfword_writes_accept_inactive_bits_but_reject_active_modes_ato
|
|||
for high in [1, 0x41, 0x81, 0xc0, 0xc1] {
|
||||
let mut machine = prepared_store(instruction, address, high << shift);
|
||||
let cpu = machine.cpu().clone();
|
||||
if high & 0x80 == 0 {
|
||||
if high != 0x81 {
|
||||
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
|
||||
assert_eq!(machine.memory().read8(RCNT + 1).unwrap(), high as u8);
|
||||
assert!(machine.cycles() > 0);
|
||||
} else {
|
||||
let error = machine.step().unwrap_err();
|
||||
assert!(error.to_string().contains(if high & 0x40 != 0 {
|
||||
"Joybus serial mode"
|
||||
} else {
|
||||
"GPIO serial interrupt enable"
|
||||
}));
|
||||
assert!(error.to_string().contains("GPIO serial interrupt enable"));
|
||||
assert_eq!(machine.cpu(), &cpu);
|
||||
assert_eq!(machine.cycles(), 0);
|
||||
assert_eq!(machine.memory().read16(RCNT).unwrap(), 0x80f5);
|
||||
|
|
@ -132,7 +124,7 @@ fn cpu_byte_and_halfword_writes_accept_inactive_bits_but_reject_active_modes_ato
|
|||
|
||||
#[test]
|
||||
fn dma_halfword_rejection_is_retryable_then_inactive_configuration_completes() {
|
||||
for high in [0x81, 0xc0, 0xc1] {
|
||||
for high in [0x81, 0x83, 0xbf] {
|
||||
let mut bus = memory();
|
||||
bus.write16(RCNT, 0x80f5).unwrap();
|
||||
bus.write16(0x02000000, high << 8).unwrap();
|
||||
|
|
|
|||
142
crates/gba-core/tests/serial_joybus.rs
Normal file
142
crates/gba-core/tests/serial_joybus.rs
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
//! Original disconnected Joybus probes. No external commands or linked transfers.
|
||||
use gba_core::{
|
||||
cpu::Cpu,
|
||||
dma::DMA_BASE,
|
||||
io::{HALTCNT, IE, IF, IME, JOYCNT, JOYSTAT, JOY_RECV, JOY_TRANS, RCNT, SIOCNT, SIODATA8},
|
||||
machine::{Machine, StepKind},
|
||||
memory::{Memory, MemoryError, ROM_START},
|
||||
};
|
||||
|
||||
fn memory() -> Memory {
|
||||
Memory::new(0xeaff_fffe_u32.to_le_bytes().to_vec()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_register_masks_do_not_fabricate_remote_events_in_any_mode() {
|
||||
let mut bus = memory();
|
||||
for mode in [0, 0x8000, 0xc000, 0xc100, 0x4100] {
|
||||
bus.write16(RCNT, mode).unwrap();
|
||||
for value in 0..=255 {
|
||||
bus.write16(JOYCNT, 0xff00 | value).unwrap();
|
||||
bus.write16(JOYSTAT, 0xff00 | value).unwrap();
|
||||
assert_eq!(bus.read16(JOYCNT).unwrap(), value & 0x40);
|
||||
assert_eq!(bus.read16(JOYSTAT).unwrap(), value & 0x30);
|
||||
bus.write8(JOYCNT + 1, 0xff).unwrap();
|
||||
bus.write8(JOYSTAT + 1, 0xff).unwrap();
|
||||
assert_eq!(bus.read16(JOYCNT).unwrap(), value & 0x40);
|
||||
assert_eq!(bus.read16(JOYSTAT).unwrap(), value & 0x30);
|
||||
}
|
||||
bus.advance_cycles(1000000);
|
||||
assert_eq!(bus.read16(IF).unwrap(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_lanes_retain_words_and_transmit_writes_set_pending_not_completion() {
|
||||
for mode in [0, 0x8000, 0xc000] {
|
||||
for base in [JOY_RECV, JOY_TRANS] {
|
||||
let mut bus = memory();
|
||||
bus.write16(RCNT, mode).unwrap();
|
||||
for lane in 0..4 {
|
||||
bus.write8(base + lane, (0x12 + lane * 0x22) as u8).unwrap();
|
||||
}
|
||||
assert_eq!(bus.read32(base).unwrap(), 0x78563412);
|
||||
bus.write16(base + 2, 0xabcd).unwrap();
|
||||
assert_eq!(bus.read32(base).unwrap(), 0xabcd3412);
|
||||
bus.write32(base, 0x98765432).unwrap();
|
||||
assert_eq!(bus.read16(base).unwrap(), 0x5432);
|
||||
assert_eq!(bus.read16(base + 2).unwrap(), 0x9876);
|
||||
let pending = if base == JOY_TRANS { 2 } else { 0 };
|
||||
assert_eq!(bus.read16(JOYSTAT).unwrap(), pending);
|
||||
bus.write16(JOYSTAT, 0xffff).unwrap();
|
||||
assert_eq!(bus.read16(JOYSTAT).unwrap(), pending | 0x30);
|
||||
bus.write16(JOYSTAT, 0).unwrap(); // Software cannot clear transmit pending.
|
||||
bus.write16(JOYCNT, 0x47).unwrap();
|
||||
bus.read32(JOY_RECV).unwrap();
|
||||
bus.advance_cycles(1000000);
|
||||
assert_eq!(bus.read16(JOYSTAT).unwrap(), pending);
|
||||
assert_eq!(bus.read16(JOYCNT).unwrap(), 0x40);
|
||||
assert_eq!(bus.read16(IF).unwrap(), 0);
|
||||
bus.write16(RCNT, 0x8000).unwrap();
|
||||
bus.write16(RCNT, 0xc100).unwrap();
|
||||
assert_eq!(bus.read32(base).unwrap(), 0x98765432);
|
||||
assert_eq!(bus.read16(JOYSTAT).unwrap(), pending);
|
||||
}
|
||||
}
|
||||
// Writing zero also makes a reply available; zero is not a reset strobe.
|
||||
let mut bus = memory();
|
||||
bus.write8(JOY_TRANS + 3, 0).unwrap();
|
||||
assert_eq!(bus.read16(JOYSTAT).unwrap(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn joybus_does_not_run_normal_clocks_or_wake_halt_or_stop() {
|
||||
for stop in [false, true] {
|
||||
let mut bus = memory();
|
||||
bus.write16(RCNT, 0xc100).unwrap(); // Bit 8 is inactive, not a GPIO IRQ enable.
|
||||
bus.write16(SIOCNT, 0x4083).unwrap(); // Unused in Joybus, even with internal start retained.
|
||||
bus.write8(SIODATA8, 0x12).unwrap();
|
||||
bus.write16(JOYCNT, 0x40).unwrap();
|
||||
bus.write32(JOY_TRANS, 0xaabbccdd).unwrap();
|
||||
bus.write16(IE, 0x80).unwrap();
|
||||
bus.write16(IME, 1).unwrap();
|
||||
bus.write8(HALTCNT, if stop { 0x80 } else { 0 }).unwrap();
|
||||
bus.advance_cycles(1000000);
|
||||
assert_eq!(bus.stopped(), stop);
|
||||
assert_eq!(bus.halted(), !stop);
|
||||
assert_eq!(bus.read8(SIODATA8).unwrap(), 0x12);
|
||||
assert_eq!(bus.read16(SIOCNT).unwrap(), 0x4083);
|
||||
assert_eq!(bus.read16(JOYCNT).unwrap(), 0x40);
|
||||
assert_eq!(bus.read16(JOYSTAT).unwrap(), 2);
|
||||
assert_eq!(bus.read16(IF).unwrap(), 0);
|
||||
assert_eq!(bus.read8(RCNT), Err(MemoryError::Unmapped(RCNT)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn live_normal_transfer_requires_completion_or_explicit_cancellation_before_joybus() {
|
||||
let mut bus = memory();
|
||||
bus.write16(SIOCNT, 0x4083).unwrap();
|
||||
for elapsed in [0, 8] {
|
||||
bus.advance_cycles(elapsed);
|
||||
let error = bus.write16(RCNT, 0xc100).unwrap_err();
|
||||
assert!(error.to_string().contains("serial reconfiguration"));
|
||||
assert_eq!(bus.write16(RCNT, 0xc100), Err(error));
|
||||
assert_eq!(bus.read8(RCNT + 1).unwrap(), 0);
|
||||
}
|
||||
bus.write16(SIOCNT, 0).unwrap();
|
||||
bus.write16(RCNT, 0xc100).unwrap();
|
||||
bus.advance_cycles(1000000);
|
||||
assert_eq!(bus.read16(IF).unwrap(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn joy_control_and_status_word_padding_fail_without_partial_side_effects() {
|
||||
let mut bus = memory();
|
||||
for address in [JOYCNT, JOYSTAT] {
|
||||
assert_eq!(
|
||||
bus.write32(address, u32::MAX),
|
||||
Err(MemoryError::Unmapped(address + 2))
|
||||
);
|
||||
assert_eq!(bus.read32(address), Err(MemoryError::Unmapped(address + 2)));
|
||||
assert_eq!(bus.read16(address).unwrap(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dma_transmit_write_has_only_dma_completion_irq() {
|
||||
let mut bus = memory();
|
||||
bus.write16(RCNT, 0xc000).unwrap();
|
||||
bus.write16(JOYCNT, 0x40).unwrap();
|
||||
bus.write32(0x02000000, 0x12345678).unwrap();
|
||||
bus.write32(DMA_BASE, 0x02000000).unwrap();
|
||||
bus.write32(DMA_BASE + 4, JOY_TRANS).unwrap();
|
||||
bus.write32(DMA_BASE + 8, 0xc4000001).unwrap();
|
||||
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
|
||||
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 0 });
|
||||
assert_eq!(machine.memory().read32(JOY_TRANS).unwrap(), 0x12345678);
|
||||
assert_eq!(machine.memory().read16(JOYSTAT).unwrap(), 2);
|
||||
assert_eq!(machine.memory().read16(IF).unwrap(), 0x100);
|
||||
machine.memory_mut().advance_cycles(1000000);
|
||||
assert_eq!(machine.memory().read16(IF).unwrap(), 0x100);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue