feat(serial): model disconnected multiplayer configuration
This commit is contained in:
parent
0b36ef1e24
commit
c5e19baed4
14 changed files with 500 additions and 44 deletions
|
|
@ -7,7 +7,10 @@ mod serial;
|
|||
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};
|
||||
pub use serial::{
|
||||
JOYCNT, JOY_RECV, JOY_TRANS, RCNT, SIOCNT, SIODATA32, SIODATA8, SIOMLT_SEND, SIOMULTI0,
|
||||
SIOMULTI1, SIOMULTI2, SIOMULTI3,
|
||||
};
|
||||
mod timer_step;
|
||||
pub(crate) use timer_step::TimerStep;
|
||||
use timer_step::STAGED_IRQ_MASK;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,14 @@
|
|||
//! Disconnected normal serial shifter with nominal internal clocks.
|
||||
//! Disconnected serial: normal shifting and idle multiplayer child configuration.
|
||||
use super::replace_byte;
|
||||
|
||||
pub const SIODATA32: u32 = 0x0400_0120;
|
||||
pub const SIOMULTI0: u32 = SIODATA32;
|
||||
pub const SIOMULTI1: u32 = 0x0400_0122;
|
||||
pub const SIOMULTI2: u32 = 0x0400_0124;
|
||||
pub const SIOMULTI3: u32 = 0x0400_0126;
|
||||
pub const SIOCNT: u32 = 0x0400_0128;
|
||||
pub const SIODATA8: u32 = 0x0400_012a;
|
||||
pub const SIOMLT_SEND: u32 = SIODATA8;
|
||||
pub const RCNT: u32 = 0x0400_0134;
|
||||
pub const JOYCNT: u32 = 0x0400_0140;
|
||||
pub const JOY_RECV: u32 = 0x0400_0150;
|
||||
|
|
@ -11,8 +16,9 @@ pub const JOY_TRANS: u32 = 0x0400_0154;
|
|||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) struct Serial {
|
||||
data: u32,
|
||||
send: u8,
|
||||
data: u32, // Shared SIODATA32 / SIOMULTI0-1 lanes.
|
||||
multi_extra: u32, // SIOMULTI2-3; no connected parent can update these.
|
||||
send: u16, // Shared low byte; high byte is accessible only in multiplayer format.
|
||||
control: u16,
|
||||
rcnt: u16,
|
||||
bits_left: u8,
|
||||
|
|
@ -22,14 +28,14 @@ pub(crate) struct Serial {
|
|||
impl Serial {
|
||||
pub(crate) fn mapped(address: u32) -> bool {
|
||||
matches!(address,
|
||||
SIODATA32..=0x0400_0123 | SIOCNT..=0x0400_012b |
|
||||
SIODATA32..=0x0400_012b |
|
||||
RCNT..=0x0400_0135 | JOYCNT..=0x0400_0141 |
|
||||
JOY_RECV..=0x0400_0157)
|
||||
}
|
||||
|
||||
pub(crate) fn unsupported(address: u32, value: u8) -> Option<&'static str> {
|
||||
match address {
|
||||
0x0400_0129 if value & 0x20 != 0 => Some("multiplayer/UART serial mode"),
|
||||
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"),
|
||||
|
|
@ -38,8 +44,14 @@ impl Serial {
|
|||
}
|
||||
}
|
||||
|
||||
fn gpio(&self) -> bool {
|
||||
self.rcnt & 0x8000 != 0
|
||||
}
|
||||
fn multiplayer_format(&self) -> bool {
|
||||
self.control & 0x3000 == 0x2000
|
||||
}
|
||||
fn normal(&self) -> bool {
|
||||
self.rcnt & 0x8000 == 0
|
||||
!self.gpio() && !self.multiplayer_format()
|
||||
}
|
||||
fn busy(&self) -> bool {
|
||||
self.control & 0x80 != 0
|
||||
|
|
@ -72,9 +84,21 @@ impl Serial {
|
|||
}
|
||||
let mut next = *self;
|
||||
next.merge(address, bytes);
|
||||
if !next.multiplayer_format()
|
||||
&& (0..bytes.len())
|
||||
.any(|offset| (SIOMULTI2..SIOCNT).contains(&(address + offset as u32)))
|
||||
{
|
||||
return Some((
|
||||
address,
|
||||
bytes[0],
|
||||
"multiplayer receive registers outside multiplayer mode",
|
||||
));
|
||||
}
|
||||
// Clearing start always permits cancellation/reconfiguration. Other
|
||||
// live changes after a shifted bit lack an independently verified model.
|
||||
if self.normal() && self.busy() && next.busy() {
|
||||
// Check the written start bit before mode-specific read-only masking.
|
||||
// A high-byte mode change must not silently cancel a clocked normal transfer.
|
||||
if self.normal() && self.busy() && self.control_value(address, bytes) & 0x80 != 0 {
|
||||
let changed_mode = self.normal() != next.normal();
|
||||
let changed_clock = (self.control ^ next.control) & 0x1003 != 0;
|
||||
let data_touched = (0..bytes.len()).any(|offset| {
|
||||
|
|
@ -108,18 +132,33 @@ impl Serial {
|
|||
if (SIODATA32..SIODATA32 + 4).contains(&address) {
|
||||
return Some(self.data.to_le_bytes()[(address - SIODATA32) as usize]);
|
||||
}
|
||||
if (SIOMULTI2..SIOCNT).contains(&address) {
|
||||
return self
|
||||
.multiplayer_format()
|
||||
.then(|| self.multi_extra.to_le_bytes()[(address - SIOMULTI2) as usize]);
|
||||
}
|
||||
let value = match address & !1 {
|
||||
SIOCNT => {
|
||||
let pins = if self.gpio() { self.gpio_pins() } else { 15 };
|
||||
// SI is pulled high without a cable. Multiplayer drives SD high while idle.
|
||||
// ID is a deterministic zero placeholder until a transfer (none can occur here).
|
||||
self.control
|
||||
| if self.normal() {
|
||||
4
|
||||
| (pins & 4)
|
||||
| if self.multiplayer_format() {
|
||||
(pins & 2) << 2
|
||||
} else {
|
||||
self.gpio_pins() & 4
|
||||
0
|
||||
}
|
||||
}
|
||||
SIODATA8 => u16::from(self.send),
|
||||
SIODATA8 => {
|
||||
if self.multiplayer_format() {
|
||||
self.send
|
||||
} else {
|
||||
self.send & 255
|
||||
}
|
||||
}
|
||||
RCNT => {
|
||||
if self.normal() && address & 1 == 0 {
|
||||
if !self.gpio() && address & 1 == 0 {
|
||||
return None;
|
||||
}
|
||||
(self.rcnt & !15) | self.gpio_pins()
|
||||
|
|
@ -130,16 +169,40 @@ impl Serial {
|
|||
Some(value.to_le_bytes()[(address & 1) as usize])
|
||||
}
|
||||
|
||||
fn control_value(&self, address: u32, bytes: &[u8]) -> u16 {
|
||||
let mut control = self.control;
|
||||
for (offset, &value) in bytes.iter().enumerate() {
|
||||
let at = address + offset as u32;
|
||||
if (SIOCNT..SIOCNT + 2).contains(&at) {
|
||||
control = replace_byte(control, at, value);
|
||||
}
|
||||
}
|
||||
control
|
||||
}
|
||||
|
||||
fn merge(&mut self, address: u32, bytes: &[u8]) {
|
||||
// Select the final format first: a word can change mode and write SEND together.
|
||||
let control = self.control_value(address, bytes);
|
||||
self.control = control
|
||||
& if control & 0x3000 == 0x2000 {
|
||||
0x6f03
|
||||
} else {
|
||||
0x508b
|
||||
};
|
||||
for (offset, &value) in bytes.iter().enumerate() {
|
||||
let at = address + offset as u32;
|
||||
if (SIODATA32..SIODATA32 + 4).contains(&at) {
|
||||
let shift = (at - SIODATA32) * 8;
|
||||
self.data = (self.data & !(255 << shift)) | (u32::from(value) << shift);
|
||||
} else if (SIOMULTI2..SIOCNT).contains(&at) {
|
||||
let shift = (at - SIOMULTI2) * 8;
|
||||
self.multi_extra =
|
||||
(self.multi_extra & !(255 << shift)) | (u32::from(value) << shift);
|
||||
} else {
|
||||
match at & !1 {
|
||||
SIOCNT => self.control = replace_byte(self.control, at, value) & 0x508b,
|
||||
SIODATA8 if at & 1 == 0 => self.send = value,
|
||||
SIODATA8 if self.multiplayer_format() || at & 1 == 0 => {
|
||||
self.send = replace_byte(self.send, at, value);
|
||||
}
|
||||
RCNT => self.rcnt = replace_byte(self.rcnt, at, value) & 0xc1ff,
|
||||
_ => {}
|
||||
}
|
||||
|
|
@ -179,7 +242,7 @@ impl Serial {
|
|||
if self.width() == 32 {
|
||||
self.data = (self.data << 1) | 1;
|
||||
} else {
|
||||
self.send = (self.send << 1) | 1;
|
||||
self.send = (self.send & 0xff00) | u16::from(((self.send as u8) << 1) | 1);
|
||||
}
|
||||
self.bits_left -= 1;
|
||||
if self.bits_left == 0 {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ mod iwram_bus;
|
|||
#[cfg(test)]
|
||||
mod iwram_history_tests;
|
||||
#[cfg(test)]
|
||||
mod multiplayer_step_tests;
|
||||
#[cfg(test)]
|
||||
mod prefetch_tests;
|
||||
#[cfg(test)]
|
||||
mod serial_step_tests;
|
||||
|
|
|
|||
115
crates/gba-core/src/memory/multiplayer_step_tests.rs
Normal file
115
crates/gba-core/src/memory/multiplayer_step_tests.rs
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
//! Original multiplayer mode/alias checks at CPU bus phases and during preflight.
|
||||
use super::{timing_event_tests::prepared, *};
|
||||
use crate::{
|
||||
io::{IF, SIOCNT, SIOMLT_SEND, SIOMULTI0, SIOMULTI2},
|
||||
machine::Machine,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn mode_change_uses_normal_completion_phase_before_read_only_busy_masking() {
|
||||
for elapsed in [56, 57] {
|
||||
let (cpu, mut memory) = prepared(false, &[0xe5c10000], &[(0, 0x20), (1, SIOCNT + 1)]); // STRB r0,[r1]
|
||||
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(); // Normal completion, then multiplayer selection at cycle 64.
|
||||
assert_eq!(machine.cycles(), 64);
|
||||
assert_eq!(machine.memory().read16(SIOCNT).unwrap(), 0x200f);
|
||||
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);
|
||||
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arm_block_store_validates_receive_aliases_and_later_mode_before_committing_any_word() {
|
||||
for unsupported in [false, true] {
|
||||
let (cpu, mut memory) = prepared(
|
||||
false,
|
||||
&[0xe8a0000e],
|
||||
&[
|
||||
(0, SIOMULTI0),
|
||||
(1, 0x12345678),
|
||||
(2, 0xabcdef90),
|
||||
(3, if unsupported { 0x98763000 } else { 0x98762000 }),
|
||||
],
|
||||
);
|
||||
memory.write16(SIOCNT, 0x2000).unwrap();
|
||||
let serial = memory.io.serial;
|
||||
let mut machine = Machine::new(cpu, memory);
|
||||
let cpu = machine.cpu().clone();
|
||||
if unsupported {
|
||||
let error = machine.step().unwrap_err();
|
||||
assert!(error.to_string().contains("UART serial mode"));
|
||||
assert_eq!(machine.step(), Err(error));
|
||||
assert_eq!(machine.memory().io.serial, serial);
|
||||
assert_eq!(machine.cpu(), &cpu);
|
||||
assert_eq!(machine.cycles(), 0);
|
||||
} else {
|
||||
machine.step().unwrap();
|
||||
assert_eq!(machine.cpu().registers()[0], SIOMULTI0 + 12);
|
||||
assert_eq!(machine.memory().read32(SIOMULTI0).unwrap(), 0x12345678);
|
||||
assert_eq!(machine.memory().read32(SIOMULTI2).unwrap(), 0xabcdef90);
|
||||
assert_eq!(machine.memory().read16(SIOMLT_SEND).unwrap(), 0x9876);
|
||||
assert_eq!(machine.memory().read16(SIOCNT).unwrap(), 0x200c);
|
||||
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
|
||||
assert_eq!(machine.last_timing().data_cycles, 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arm_block_load_observes_all_multiplayer_lanes_from_staged_registers() {
|
||||
let (cpu, mut memory) = prepared(false, &[0xe8b0000e], &[(0, SIOMULTI0)]); // LDMIA r0!,{r1-r3}
|
||||
memory.write32(SIOCNT, 0x98766083).unwrap();
|
||||
memory.write32(SIOMULTI0, 0x12345678).unwrap();
|
||||
memory.write32(SIOMULTI2, 0xabcdef90).unwrap();
|
||||
let mut machine = Machine::new(cpu, memory);
|
||||
machine.step().unwrap();
|
||||
assert_eq!(
|
||||
&machine.cpu().registers()[1..4],
|
||||
&[0x12345678, 0xabcdef90, 0x9876600f]
|
||||
);
|
||||
assert_eq!(machine.cpu().registers()[0], SIOMULTI0 + 12);
|
||||
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shadow_mode_changes_control_later_access_validation_and_preserve_atomicity() {
|
||||
let (_, mut memory) = prepared(false, &[0xe1a00000], &[]);
|
||||
memory
|
||||
.write_words(&[(SIOCNT, 0x2000), (SIOMULTI2, 0x12345678), (SIOCNT, 0)])
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
memory.read32(SIOMULTI2),
|
||||
Err(MemoryError::Unmapped(SIOMULTI2))
|
||||
);
|
||||
memory.write16(SIOCNT, 0x2000).unwrap();
|
||||
assert_eq!(memory.read32(SIOMULTI2).unwrap(), 0x12345678);
|
||||
let serial = memory.io.serial;
|
||||
assert!(memory
|
||||
.write_words(&[(0x02000000, 0xabcd), (SIOCNT, 0), (SIOMULTI2, 0)])
|
||||
.is_err());
|
||||
assert_eq!(memory.io.serial, serial);
|
||||
assert_eq!(memory.read32(0x02000000).unwrap(), 0);
|
||||
// A staged read cannot fall back to committed multiplayer data after leaving the mode.
|
||||
memory.begin_timer_step();
|
||||
memory.write16(SIOCNT, 0).unwrap();
|
||||
assert_eq!(
|
||||
memory.read32(SIOMULTI2),
|
||||
Err(MemoryError::Unmapped(SIOMULTI2))
|
||||
);
|
||||
memory.discard_timer_step();
|
||||
assert_eq!(memory.read32(SIOMULTI2).unwrap(), 0x12345678);
|
||||
}
|
||||
|
|
@ -80,7 +80,7 @@ fn idle_normal_serial_data_and_control_have_distinct_widths() {
|
|||
fn unsupported_operations_have_specific_retryable_diagnostics() {
|
||||
let mut bus = memory();
|
||||
for (address, value, description) in [
|
||||
(SIOCNT + 1, 0x20, "multiplayer/UART"),
|
||||
(SIOCNT + 1, 0x30, "UART serial mode"),
|
||||
(RCNT + 1, 0xc0, "Joybus serial mode"),
|
||||
(RCNT + 1, 0x81, "GPIO serial interrupt"),
|
||||
(JOYCNT, 0x40, "Joybus interrupt"),
|
||||
|
|
@ -95,7 +95,7 @@ fn unsupported_operations_have_specific_retryable_diagnostics() {
|
|||
assert!(bus.write16(RCNT, 0xc0aa).is_err());
|
||||
assert_eq!(bus.read16(RCNT).unwrap(), 0x805f); // Low byte was not committed.
|
||||
bus.write16(SIOCNT, 0x4008).unwrap();
|
||||
assert!(bus.write32(SIOCNT, 0xaabb2080).is_err());
|
||||
assert!(bus.write32(SIOCNT, 0xaabb3080).is_err());
|
||||
assert_eq!(bus.read16(SIOCNT).unwrap(), 0x400c);
|
||||
assert_eq!(bus.read16(SIODATA8).unwrap(), 0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ fn arm_and_thumb_external_starts_complete_as_instructions_not_as_serial_transfer
|
|||
assert_eq!(machine.memory().read16(IF).unwrap() & 0x80, 0);
|
||||
} else {
|
||||
let error = machine.step().unwrap_err();
|
||||
assert!(error.to_string().contains("multiplayer/UART serial mode"));
|
||||
assert!(error.to_string().contains("UART serial mode"));
|
||||
assert_eq!(machine.cpu(), &cpu);
|
||||
assert_eq!(machine.cycles(), 0);
|
||||
assert_eq!(machine.memory().read16(SIOCNT).unwrap(), 0x1007);
|
||||
|
|
@ -161,7 +161,7 @@ fn invalid_word_and_block_writes_do_not_leave_a_partial_external_start() {
|
|||
let mut bus = memory();
|
||||
bus.write16(SIOCNT, 0x5000).unwrap();
|
||||
bus.write16(SIODATA8, 0x12).unwrap();
|
||||
assert!(bus.write32(SIOCNT, 0x00ab2080).is_err());
|
||||
assert!(bus.write32(SIOCNT, 0x00ab3080).is_err());
|
||||
assert_eq!(bus.read16(SIOCNT).unwrap(), 0x5004);
|
||||
assert_eq!(bus.read16(SIODATA8).unwrap(), 0x12);
|
||||
bus.write32(SIOCNT, 0x00ab5080).unwrap();
|
||||
|
|
@ -188,7 +188,7 @@ fn dma_start_preserves_serial_wait_and_reports_only_dma_completion() {
|
|||
bus.write32(DMA_BASE + 8, 0xc4000001).unwrap();
|
||||
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
|
||||
let error = machine.step().unwrap_err();
|
||||
assert!(error.to_string().contains("multiplayer/UART serial mode"));
|
||||
assert!(error.to_string().contains("UART serial mode"));
|
||||
assert_eq!(machine.step(), Err(error));
|
||||
assert_eq!(machine.cycles(), 0);
|
||||
assert_eq!(machine.memory().read16(SIOCNT).unwrap(), 4);
|
||||
|
|
|
|||
207
crates/gba-core/tests/serial_multiplayer.rs
Normal file
207
crates/gba-core/tests/serial_multiplayer.rs
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
//! Original disconnected multiplayer register tests; no connected peer or completed transfer.
|
||||
use gba_core::{
|
||||
cpu::Cpu,
|
||||
dma::DMA_BASE,
|
||||
io::{
|
||||
HALTCNT, IE, IF, IME, RCNT, SIOCNT, SIODATA32, SIODATA8, SIOMLT_SEND, SIOMULTI0, SIOMULTI1,
|
||||
SIOMULTI2, SIOMULTI3,
|
||||
},
|
||||
machine::{Machine, StepKind},
|
||||
memory::{Memory, MemoryError, ROM_START},
|
||||
};
|
||||
|
||||
fn memory() -> Memory {
|
||||
Memory::new(0xeaff_fffe_u32.to_le_bytes().to_vec()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_multiplayer_control_values_mask_status_and_cannot_start_a_disconnected_child() {
|
||||
let mut bus = memory();
|
||||
bus.write16(IE, 0x80).unwrap();
|
||||
bus.write16(IME, 1).unwrap();
|
||||
for value in 0..=u16::MAX {
|
||||
if value & 0x3000 != 0x2000 {
|
||||
continue;
|
||||
}
|
||||
bus.write16(SIOCNT, value).unwrap();
|
||||
// SI high (child), SD high (local idle drive); no valid assigned ID yet.
|
||||
assert_eq!(bus.read16(SIOCNT).unwrap(), (value & 0x6f03) | 12);
|
||||
assert_eq!(bus.read8(SIOCNT).unwrap() & 0xf0, 0); // ID placeholder, no error or busy.
|
||||
assert_eq!(bus.read16(IF).unwrap(), 0);
|
||||
}
|
||||
bus.advance_cycles(u32::MAX);
|
||||
assert_eq!(bus.read16(IF).unwrap(), 0);
|
||||
assert!(!bus.irq_pending());
|
||||
assert_eq!(bus.read16(SIOCNT).unwrap(), 0x6f0f);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn receive_latches_have_independent_lanes_and_do_not_become_received_peer_data() {
|
||||
let mut bus = memory();
|
||||
bus.write16(SIOCNT, 0x2000).unwrap();
|
||||
for address in [SIOMULTI0, SIOMULTI1, SIOMULTI2, SIOMULTI3] {
|
||||
assert_eq!(bus.read16(address).unwrap(), 0); // Synthetic reset latches, not completed receives.
|
||||
}
|
||||
bus.write32(SIOMULTI0, 0x12345678).unwrap();
|
||||
bus.write32(SIOMULTI2, 0x9abcdef0).unwrap();
|
||||
bus.write8(SIOMULTI1 + 1, 0x55).unwrap();
|
||||
bus.write16(SIOMULTI3, 0xaabb).unwrap();
|
||||
assert_eq!(bus.read16(SIOMULTI0).unwrap(), 0x5678);
|
||||
assert_eq!(bus.read16(SIOMULTI1).unwrap(), 0x5534);
|
||||
assert_eq!(bus.read32(SIOMULTI2).unwrap(), 0xaabbdef0);
|
||||
bus.write16(SIOMLT_SEND, 0xcdef).unwrap();
|
||||
for rate in 0..4 {
|
||||
bus.write16(SIOCNT, 0x6080 | rate).unwrap(); // Start is read-only for this disconnected child.
|
||||
bus.advance_cycles(1000000);
|
||||
assert_eq!(bus.read32(SIOMULTI0).unwrap(), 0x55345678);
|
||||
assert_eq!(bus.read32(SIOMULTI2).unwrap(), 0xaabbdef0);
|
||||
assert_eq!(bus.read16(SIOMLT_SEND).unwrap(), 0xcdef);
|
||||
assert_eq!(bus.read16(IF).unwrap(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_width_and_receive_aliases_follow_the_selected_format() {
|
||||
let mut bus = memory();
|
||||
bus.write32(SIOCNT, 0xabcd2000).unwrap(); // Final multiplayer format applies to all SEND lanes.
|
||||
assert_eq!(bus.read16(SIOMLT_SEND).unwrap(), 0xabcd);
|
||||
bus.write8(SIOMLT_SEND, 0x12).unwrap();
|
||||
bus.write8(SIOMLT_SEND + 1, 0x34).unwrap();
|
||||
assert_eq!(bus.read16(SIOMLT_SEND).unwrap(), 0x3412);
|
||||
bus.write32(SIOMULTI0, 0x12345678).unwrap();
|
||||
bus.write32(SIOMULTI2, 0x9abcdef0).unwrap();
|
||||
bus.write16(SIOCNT, 0x1000).unwrap();
|
||||
assert_eq!(bus.read32(SIODATA32).unwrap(), 0x12345678);
|
||||
assert_eq!(bus.read16(SIODATA8).unwrap(), 0x12);
|
||||
assert_eq!(bus.read32(SIOMULTI2), Err(MemoryError::Unmapped(SIOMULTI2)));
|
||||
assert!(bus
|
||||
.write32(SIOMULTI2, 0)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("outside multiplayer mode"));
|
||||
bus.write16(SIODATA8, 0xee56).unwrap(); // Existing normal mode ignores the upper write lane.
|
||||
assert_eq!(bus.read16(SIODATA8).unwrap(), 0x56);
|
||||
bus.write16(SIOCNT, 0x83).unwrap();
|
||||
bus.advance_cycles(64); // Only the normal 8-bit register shifts.
|
||||
bus.write16(SIOCNT, 0x2000).unwrap();
|
||||
assert_eq!(bus.read16(SIOMLT_SEND).unwrap(), 0x34ff);
|
||||
assert_eq!(bus.read32(SIOMULTI0).unwrap(), 0x12345678);
|
||||
assert_eq!(bus.read32(SIOMULTI2).unwrap(), 0x9abcdef0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn idle_multiplayer_neither_wakes_halt_nor_advances_stop() {
|
||||
for stop in [false, true] {
|
||||
let mut bus = memory();
|
||||
bus.write16(IE, 0x80).unwrap();
|
||||
bus.write16(IME, 1).unwrap();
|
||||
bus.write16(SIOCNT, 0x6083).unwrap();
|
||||
bus.write8(HALTCNT, if stop { 0x80 } else { 0 }).unwrap();
|
||||
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
|
||||
for _ in 0..100 {
|
||||
assert_eq!(
|
||||
machine.step().unwrap(),
|
||||
if stop {
|
||||
StepKind::StopIdle
|
||||
} else {
|
||||
StepKind::HaltIdle
|
||||
}
|
||||
);
|
||||
}
|
||||
assert_eq!(machine.halted(), !stop);
|
||||
assert_eq!(machine.stopped(), stop);
|
||||
assert_eq!(machine.cycles() == 0, stop);
|
||||
assert_eq!(machine.memory().read16(SIOCNT).unwrap(), 0x600f);
|
||||
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gpio_status_uses_pin_latches_but_cannot_create_a_multiplayer_parent_transfer() {
|
||||
let mut bus = memory();
|
||||
bus.write16(RCNT, 0x80f0).unwrap(); // All GPIO outputs low.
|
||||
bus.write16(SIOCNT, 0x2083).unwrap();
|
||||
assert_eq!(bus.read16(SIOCNT).unwrap(), 0x2003);
|
||||
bus.write16(RCNT, 0x80f6).unwrap(); // SI and SD high.
|
||||
assert_eq!(bus.read16(SIOCNT).unwrap(), 0x200f);
|
||||
bus.write16(RCNT, 0).unwrap();
|
||||
assert_eq!(bus.read16(SIOCNT).unwrap(), 0x200f);
|
||||
assert_eq!(bus.read8(RCNT), Err(MemoryError::Unmapped(RCNT))); // Do not guess SC pin samples.
|
||||
bus.advance_cycles(1000000);
|
||||
assert_eq!(bus.read16(IF).unwrap(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn active_normal_mode_cannot_be_silently_cancelled_by_read_only_multiplayer_busy_masking() {
|
||||
let mut bus = memory();
|
||||
bus.write16(SIOCNT, 0x4083).unwrap();
|
||||
bus.advance_cycles(8);
|
||||
for (address, value) in [(SIOCNT + 1, 0x20), (SIOCNT + 1, 0x60)] {
|
||||
assert!(bus
|
||||
.write8(address, value)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("serial reconfiguration"));
|
||||
assert_eq!(bus.read16(SIOCNT).unwrap(), 0x4087);
|
||||
assert_eq!(bus.read8(SIODATA8).unwrap(), 1);
|
||||
}
|
||||
assert!(bus.write16(SIOCNT, 0x2083).is_err());
|
||||
bus.write16(SIOCNT, 0x2000).unwrap(); // Explicitly clear start and select multiplayer together.
|
||||
bus.advance_cycles(1000000);
|
||||
assert_eq!(bus.read16(IF).unwrap(), 0);
|
||||
assert_eq!(bus.read16(SIOCNT).unwrap(), 0x200c);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uart_rejection_preserves_multiplayer_control_and_send_lanes() {
|
||||
let mut bus = memory();
|
||||
bus.write32(SIOCNT, 0xabcd6001).unwrap();
|
||||
let error = bus.write32(SIOCNT, 0x12343083).unwrap_err();
|
||||
assert!(error.to_string().contains("UART serial mode"));
|
||||
assert_eq!(bus.write32(SIOCNT, 0x12343083), Err(error));
|
||||
assert_eq!(bus.read16(SIOCNT).unwrap(), 0x600d);
|
||||
assert_eq!(bus.read16(SIOMLT_SEND).unwrap(), 0xabcd);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn original_arm_and_thumb_word_stores_select_multiplayer_and_write_full_send_data() {
|
||||
for thumb in [false, true] {
|
||||
let code = if thumb {
|
||||
vec![
|
||||
0xe59f0010, 0xe59f1010, 0xe28f2001, 0xe12fff12, 0xe7fe6001, 0xeafffffe, SIOCNT,
|
||||
0xabcd6083,
|
||||
]
|
||||
} else {
|
||||
vec![
|
||||
0xe59f0008, 0xe59f1008, 0xe5801000, 0xeafffffe, SIOCNT, 0xabcd6083,
|
||||
]
|
||||
};
|
||||
let mut bus = Memory::new(code.into_iter().flat_map(u32::to_le_bytes).collect()).unwrap();
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
for _ in 0..if thumb { 4 } else { 2 } {
|
||||
cpu.step(&mut bus).unwrap();
|
||||
}
|
||||
let mut machine = Machine::new(cpu, bus);
|
||||
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
|
||||
assert_eq!(machine.memory().read16(SIOCNT).unwrap(), 0x600f);
|
||||
assert_eq!(machine.memory().read16(SIOMLT_SEND).unwrap(), 0xabcd);
|
||||
machine.memory_mut().advance_cycles(1000000);
|
||||
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dma_mode_and_send_write_only_requests_its_own_completion_irq() {
|
||||
let mut bus = memory();
|
||||
bus.write32(0x02000000, 0xabcd6083).unwrap();
|
||||
bus.write32(DMA_BASE, 0x02000000).unwrap();
|
||||
bus.write32(DMA_BASE + 4, SIOCNT).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().read16(SIOCNT).unwrap(), 0x600f);
|
||||
assert_eq!(machine.memory().read16(SIOMLT_SEND).unwrap(), 0xabcd);
|
||||
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