feat(io): model disabled sound and disconnected serial state
This commit is contained in:
parent
786e4d6d34
commit
cb2772cef8
5 changed files with 318 additions and 6 deletions
|
|
@ -1,7 +1,13 @@
|
||||||
//! GBA I/O subset: DMA, timers, interrupts, WAITCNT, display, and keypad input.
|
//! GBA I/O subset: DMA, timers, interrupts, WAITCNT, display, keypad input,
|
||||||
|
//! disabled sound state, and disconnected serial initialization.
|
||||||
//! Timers share a free-running prescaler phase. Hardware startup/write delays,
|
//! Timers share a free-running prescaler phase. Hardware startup/write delays,
|
||||||
//! audio events, and interrupt delivery delays are not modeled.
|
//! audio events, and interrupt delivery delays are not modeled.
|
||||||
|
|
||||||
|
mod inactive;
|
||||||
|
pub use inactive::{
|
||||||
|
JOYCNT, JOY_RECV, JOY_TRANS, RCNT, SIOCNT, SIODATA32, SIODATA8, SOUNDBIAS, SOUNDCNT_H,
|
||||||
|
SOUNDCNT_X, SOUND_START, WAVE_RAM,
|
||||||
|
};
|
||||||
mod timer_step;
|
mod timer_step;
|
||||||
pub(crate) use timer_step::TimerStep;
|
pub(crate) use timer_step::TimerStep;
|
||||||
use timer_step::TIMER_IRQ_MASK;
|
use timer_step::TIMER_IRQ_MASK;
|
||||||
|
|
@ -150,6 +156,7 @@ enum PowerState {
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||||
pub(crate) struct Io {
|
pub(crate) struct Io {
|
||||||
|
inactive: inactive::Inactive,
|
||||||
timers: [Timer; 4],
|
timers: [Timer; 4],
|
||||||
// Low ten system-clock bits cover every supported divider. STOP freezes this phase.
|
// Low ten system-clock bits cover every supported divider. STOP freezes this phase.
|
||||||
timer_phase: u16,
|
timer_phase: u16,
|
||||||
|
|
@ -174,10 +181,18 @@ pub(crate) struct Io {
|
||||||
|
|
||||||
impl Io {
|
impl Io {
|
||||||
pub(crate) fn mapped(address: u32) -> bool {
|
pub(crate) fn mapped(address: u32) -> bool {
|
||||||
matches!(address, 0x0400_0000..=0x0400_0057 | DMA_BASE..=DMA_END | 0x0400_0100..=0x0400_010f | 0x0400_0130..=0x0400_0133 | 0x0400_0200..=0x0400_020b | POSTFLG..=0x0400_0303)
|
inactive::Inactive::mapped(address)
|
||||||
|
|| matches!(address, 0x0400_0000..=0x0400_0057 | DMA_BASE..=DMA_END | 0x0400_0100..=0x0400_010f | 0x0400_0130..=0x0400_0133 | 0x0400_0200..=0x0400_020b | POSTFLG..=0x0400_0303)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn unsupported_write(address: u32, value: u8) -> Option<&'static str> {
|
||||||
|
inactive::Inactive::unsupported(address, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn read8(&self, address: u32) -> Option<u8> {
|
pub(crate) fn read8(&self, address: u32) -> Option<u8> {
|
||||||
|
if inactive::Inactive::mapped(address) {
|
||||||
|
return self.inactive.read8(address);
|
||||||
|
}
|
||||||
if (DMA_BASE..=DMA_END).contains(&address) {
|
if (DMA_BASE..=DMA_END).contains(&address) {
|
||||||
return Some(self.dma.read8(address));
|
return Some(self.dma.read8(address));
|
||||||
}
|
}
|
||||||
|
|
@ -216,6 +231,10 @@ impl Io {
|
||||||
pub(crate) fn write8(&mut self, address: u32, value: u8) {
|
pub(crate) fn write8(&mut self, address: u32, value: u8) {
|
||||||
debug_assert!(Self::mapped(address));
|
debug_assert!(Self::mapped(address));
|
||||||
debug_assert!(!(KEYINPUT..=KEYCNT + 1).contains(&address));
|
debug_assert!(!(KEYINPUT..=KEYCNT + 1).contains(&address));
|
||||||
|
if inactive::Inactive::mapped(address) {
|
||||||
|
self.inactive.write8(address, value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (DMA_BASE..=DMA_END).contains(&address) {
|
if (DMA_BASE..=DMA_END).contains(&address) {
|
||||||
self.dma.write8(address, value);
|
self.dma.write8(address, value);
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
120
crates/gba-core/src/io/inactive.rs
Normal file
120
crates/gba-core/src/io/inactive.rs
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
//! Disabled sound and disconnected serial initialization state.
|
||||||
|
//! Sound enable, FIFO writes, serial transfers, UART/multiplayer/Joybus modes,
|
||||||
|
//! and GPIO interrupts remain explicit diagnostics, not silent device stubs.
|
||||||
|
|
||||||
|
use super::replace_byte;
|
||||||
|
|
||||||
|
pub const SOUND_START: u32 = 0x0400_0060;
|
||||||
|
pub const SOUNDCNT_H: u32 = 0x0400_0082;
|
||||||
|
pub const SOUNDCNT_X: u32 = 0x0400_0084;
|
||||||
|
pub const SOUNDBIAS: u32 = 0x0400_0088;
|
||||||
|
pub const WAVE_RAM: u32 = 0x0400_0090;
|
||||||
|
pub const SIODATA32: u32 = 0x0400_0120;
|
||||||
|
pub const SIOCNT: u32 = 0x0400_0128;
|
||||||
|
pub const SIODATA8: u32 = 0x0400_012a;
|
||||||
|
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;
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||||
|
pub(super) struct Inactive {
|
||||||
|
sound_control: u16,
|
||||||
|
bias: u16,
|
||||||
|
// With sound disabled, SOUND3CNT_L is zero. The CPU accesses bank 1;
|
||||||
|
// bank 0 stays zero because enabling sound/bank selection is unsupported.
|
||||||
|
wave: [u8; 16],
|
||||||
|
serial_data: [u8; 4],
|
||||||
|
serial_send: u8,
|
||||||
|
serial_control: u16,
|
||||||
|
rcnt: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Inactive {
|
||||||
|
pub(super) fn mapped(address: u32) -> bool {
|
||||||
|
matches!(address,
|
||||||
|
SOUND_START..=0x0400_008b | WAVE_RAM..=0x0400_009f |
|
||||||
|
SIODATA32..=0x0400_0123 | SIOCNT..=0x0400_012b |
|
||||||
|
RCNT..=0x0400_0135 | JOYCNT..=0x0400_0141 |
|
||||||
|
JOY_RECV..=0x0400_0157)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Value-dependent rejection is independent of device state, so batch
|
||||||
|
/// stores can validate all values before committing any RAM/I/O writes.
|
||||||
|
pub(super) fn unsupported(address: u32, value: u8) -> Option<&'static str> {
|
||||||
|
match address {
|
||||||
|
SOUNDCNT_X if value & 0x80 != 0 => {
|
||||||
|
Some("sound master enable (audio engine not implemented)")
|
||||||
|
}
|
||||||
|
SIOCNT if value & 0x80 != 0 => Some("serial transfer start"),
|
||||||
|
0x0400_0129 if value & 0x20 != 0 => Some("multiplayer/UART serial mode"),
|
||||||
|
0x0400_0135 if value & 0x40 != 0 => Some("Joybus serial mode"),
|
||||||
|
0x0400_0135 if value & 1 != 0 => 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"),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gpio_pins(&self) -> u16 {
|
||||||
|
let outputs = (self.rcnt >> 4) & 15;
|
||||||
|
(self.rcnt & outputs & 15) | (!outputs & 15) // Disconnected inputs have pull-ups.
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn read8(&self, address: u32) -> Option<u8> {
|
||||||
|
if (WAVE_RAM..=WAVE_RAM + 15).contains(&address) {
|
||||||
|
return Some(self.wave[(address - WAVE_RAM) as usize]);
|
||||||
|
}
|
||||||
|
if (SIODATA32..=SIODATA32 + 3).contains(&address) {
|
||||||
|
return Some(self.serial_data[(address - SIODATA32) as usize]);
|
||||||
|
}
|
||||||
|
let value = match address & !1 {
|
||||||
|
SOUNDCNT_H => self.sound_control,
|
||||||
|
SOUNDBIAS => self.bias,
|
||||||
|
SOUND_START..=0x0400_008a => 0, // PSG stays reset while master enable is clear.
|
||||||
|
SIOCNT => {
|
||||||
|
let si = if self.rcnt & 0x8000 != 0 {
|
||||||
|
self.gpio_pins() & 4
|
||||||
|
} else {
|
||||||
|
4
|
||||||
|
};
|
||||||
|
self.serial_control | si
|
||||||
|
}
|
||||||
|
SIODATA8 => u16::from(self.serial_send),
|
||||||
|
RCNT => {
|
||||||
|
// Normal-mode RCNT low pin samples are outside this subset.
|
||||||
|
if self.rcnt & 0x8000 == 0 && address & 1 == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
(self.rcnt & !15) | self.gpio_pins()
|
||||||
|
}
|
||||||
|
JOYCNT | JOY_RECV..=0x0400_0156 => 0, // Only reset-state Joybus accesses are supported.
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
Some(value.to_le_bytes()[(address & 1) as usize])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn write8(&mut self, address: u32, value: u8) {
|
||||||
|
debug_assert!(Self::unsupported(address, value).is_none());
|
||||||
|
if (WAVE_RAM..=WAVE_RAM + 15).contains(&address) {
|
||||||
|
self.wave[(address - WAVE_RAM) as usize] = value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (SIODATA32..=SIODATA32 + 3).contains(&address) {
|
||||||
|
self.serial_data[(address - SIODATA32) as usize] = value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
match address & !1 {
|
||||||
|
SOUNDCNT_H => {
|
||||||
|
self.sound_control = replace_byte(self.sound_control, address, value) & 0x770f
|
||||||
|
}
|
||||||
|
SOUNDBIAS => self.bias = replace_byte(self.bias, address, value) & 0xc3fe,
|
||||||
|
SIOCNT => {
|
||||||
|
self.serial_control = replace_byte(self.serial_control, address, value) & 0x500b
|
||||||
|
}
|
||||||
|
SIODATA8 if address & 1 == 0 => self.serial_send = value,
|
||||||
|
RCNT => self.rcnt = replace_byte(self.rcnt, address, value) & 0x80ff,
|
||||||
|
_ => {} // Disabled PSG writes and unused/read-only bits have no effect.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -43,11 +43,24 @@ pub enum MemoryError {
|
||||||
Unaligned(u32),
|
Unaligned(u32),
|
||||||
RomTooLarge(usize),
|
RomTooLarge(usize),
|
||||||
InvalidBiosSize(usize),
|
InvalidBiosSize(usize),
|
||||||
|
UnsupportedIo {
|
||||||
|
address: u32,
|
||||||
|
value: u8,
|
||||||
|
operation: &'static str,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for MemoryError {
|
impl fmt::Display for MemoryError {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
|
Self::UnsupportedIo {
|
||||||
|
address,
|
||||||
|
value,
|
||||||
|
operation,
|
||||||
|
} => write!(
|
||||||
|
f,
|
||||||
|
"unsupported {operation}: write {value:#04x} at {address:#010x}"
|
||||||
|
),
|
||||||
Self::Unmapped(address) => write!(f, "unmapped memory at {address:#010x}"),
|
Self::Unmapped(address) => write!(f, "unmapped memory at {address:#010x}"),
|
||||||
Self::ReadOnly(address) => write!(f, "read-only memory at {address:#010x}"),
|
Self::ReadOnly(address) => write!(f, "read-only memory at {address:#010x}"),
|
||||||
Self::Unaligned(address) => write!(f, "unaligned memory access at {address:#010x}"),
|
Self::Unaligned(address) => write!(f, "unaligned memory access at {address:#010x}"),
|
||||||
|
|
@ -714,6 +727,7 @@ impl Memory {
|
||||||
bytes: [u8; N],
|
bytes: [u8; N],
|
||||||
) -> Result<(), MemoryError> {
|
) -> Result<(), MemoryError> {
|
||||||
let index = self.write_index::<N>(address)?;
|
let index = self.write_index::<N>(address)?;
|
||||||
|
Self::validate_io_values(address, &bytes)?;
|
||||||
self.record_access(
|
self.record_access(
|
||||||
address,
|
address,
|
||||||
match N {
|
match N {
|
||||||
|
|
@ -829,12 +843,29 @@ impl Memory {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn validate_io_values(address: u32, bytes: &[u8]) -> Result<(), MemoryError> {
|
||||||
|
if address >> 24 == 0x04 {
|
||||||
|
for (offset, &value) in bytes.iter().enumerate() {
|
||||||
|
let address = address + offset as u32;
|
||||||
|
if let Some(operation) = Io::unsupported_write(address, value) {
|
||||||
|
return Err(MemoryError::UnsupportedIo {
|
||||||
|
address,
|
||||||
|
value,
|
||||||
|
operation,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Validate a batch before any RAM or I/O writes. Diagnostic errors cannot
|
/// Validate a batch before any RAM or I/O writes. Diagnostic errors cannot
|
||||||
/// leave a partial block store. This is not a model of hardware data aborts.
|
/// leave a partial block store. This is not a model of hardware data aborts.
|
||||||
/// Mapped I/O reads currently have no side effects; writes cannot fail after validation.
|
/// Mapped I/O reads currently have no side effects; writes cannot fail after validation.
|
||||||
pub(crate) fn write_words(&mut self, writes: &[(u32, u32)]) -> Result<(), MemoryError> {
|
pub(crate) fn write_words(&mut self, writes: &[(u32, u32)]) -> Result<(), MemoryError> {
|
||||||
for &(address, _) in writes {
|
for &(address, value) in writes {
|
||||||
self.write_index::<4>(address)?;
|
self.write_index::<4>(address)?;
|
||||||
|
Self::validate_io_values(address, &value.to_le_bytes())?;
|
||||||
}
|
}
|
||||||
for &(address, value) in writes {
|
for &(address, value) in writes {
|
||||||
// The map cannot change between validation and these writes.
|
// The map cannot change between validation and these writes.
|
||||||
|
|
|
||||||
142
crates/gba-core/tests/inactive_devices.rs
Normal file
142
crates/gba-core/tests/inactive_devices.rs
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
//! Original bus/CPU/DMA regressions for the disabled-device initialization subset.
|
||||||
|
use gba_core::{
|
||||||
|
cpu::Cpu,
|
||||||
|
dma::DMA_BASE,
|
||||||
|
io::{
|
||||||
|
JOYCNT, JOY_RECV, RCNT, SIOCNT, SIODATA32, SIODATA8, SOUNDBIAS, SOUNDCNT_H, SOUNDCNT_X,
|
||||||
|
WAVE_RAM,
|
||||||
|
},
|
||||||
|
machine::Machine,
|
||||||
|
memory::{Memory, MemoryError, ROM_START},
|
||||||
|
};
|
||||||
|
|
||||||
|
fn memory() -> Memory {
|
||||||
|
Memory::new(vec![0xfe, 0xff, 0xff, 0xea]).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn disabled_sound_registers_masks_lanes_and_wave_ram() {
|
||||||
|
let mut bus = memory();
|
||||||
|
for address in (0x04000060..0x04000082).step_by(2) {
|
||||||
|
bus.write16(address, 0xffff).unwrap();
|
||||||
|
assert_eq!(bus.read16(address).unwrap(), 0);
|
||||||
|
}
|
||||||
|
bus.write16(SOUNDCNT_H, 0xffff).unwrap();
|
||||||
|
assert_eq!(bus.read16(SOUNDCNT_H).unwrap(), 0x770f); // Reset strobes do not latch.
|
||||||
|
bus.write8(SOUNDCNT_H, 0).unwrap();
|
||||||
|
assert_eq!(bus.read16(SOUNDCNT_H).unwrap(), 0x7700);
|
||||||
|
bus.write32(SOUNDBIAS, u32::MAX).unwrap();
|
||||||
|
assert_eq!(bus.read32(SOUNDBIAS).unwrap(), 0xc3fe);
|
||||||
|
for index in 0..16 {
|
||||||
|
bus.write8(WAVE_RAM + index, index as u8 * 13).unwrap();
|
||||||
|
}
|
||||||
|
bus.write32(SOUNDCNT_X, 0x7f).unwrap(); // Status bits are read-only; master stays off.
|
||||||
|
assert_eq!(bus.read32(SOUNDCNT_X).unwrap(), 0);
|
||||||
|
for index in 0..16 {
|
||||||
|
assert_eq!(bus.read8(WAVE_RAM + index).unwrap(), index as u8 * 13);
|
||||||
|
}
|
||||||
|
assert_eq!(bus.read16(SOUNDCNT_H).unwrap(), 0x7700);
|
||||||
|
assert_eq!(bus.read16(SOUNDBIAS).unwrap(), 0xc3fe);
|
||||||
|
// FIFOs are not ordinary register latches.
|
||||||
|
assert!(bus.write32(0x040000a0, 0).is_err());
|
||||||
|
assert!(bus.read32(0x040000a0).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn disconnected_gpio_inputs_pull_high_and_outputs_follow_latches() {
|
||||||
|
let mut bus = memory();
|
||||||
|
bus.write16(RCNT, 0x8000).unwrap();
|
||||||
|
assert_eq!(bus.read16(RCNT).unwrap(), 0x800f);
|
||||||
|
bus.write16(RCNT, 0x80f5).unwrap();
|
||||||
|
assert_eq!(bus.read16(RCNT).unwrap(), 0x80f5);
|
||||||
|
bus.write8(RCNT, 0x20).unwrap(); // SD output low; SC/SI/SO pulled high.
|
||||||
|
assert_eq!(bus.read16(RCNT).unwrap(), 0x802d);
|
||||||
|
bus.write8(RCNT, 0x22).unwrap();
|
||||||
|
assert_eq!(bus.read16(RCNT).unwrap(), 0x802f);
|
||||||
|
bus.write16(RCNT, 0).unwrap();
|
||||||
|
assert!(bus.read8(RCNT).is_err()); // Unsupported normal-mode pin readback is not guessed.
|
||||||
|
assert_eq!(bus.read8(RCNT + 1).unwrap(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn idle_normal_serial_data_and_control_have_distinct_widths() {
|
||||||
|
let mut bus = memory();
|
||||||
|
bus.write32(SIODATA32, 0x12345678).unwrap();
|
||||||
|
assert_eq!(bus.read32(SIODATA32).unwrap(), 0x12345678);
|
||||||
|
bus.write16(SIODATA32, 0).unwrap();
|
||||||
|
assert_eq!(bus.read32(SIODATA32).unwrap(), 0x12340000);
|
||||||
|
bus.write16(SIODATA8, 0xabcd).unwrap();
|
||||||
|
assert_eq!(bus.read16(SIODATA8).unwrap(), 0xcd);
|
||||||
|
bus.write16(SIOCNT, 0x500b).unwrap();
|
||||||
|
assert_eq!(bus.read16(SIOCNT).unwrap(), 0x500f); // Disconnected SI is high.
|
||||||
|
bus.advance_cycles(1000000);
|
||||||
|
assert_eq!(bus.read16(SIOCNT).unwrap(), 0x500f); // No fabricated completion or IRQ.
|
||||||
|
assert_eq!(bus.read16(0x04000202).unwrap() & 0x80, 0);
|
||||||
|
bus.write16(JOYCNT, 7).unwrap(); // Clear empty status flags.
|
||||||
|
assert_eq!(bus.read16(JOYCNT).unwrap(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unsupported_operations_have_specific_retryable_diagnostics() {
|
||||||
|
let mut bus = memory();
|
||||||
|
for (address, value, description) in [
|
||||||
|
(SOUNDCNT_X, 0x80, "sound master enable"),
|
||||||
|
(SIOCNT, 0x80, "serial transfer start"),
|
||||||
|
(SIOCNT + 1, 0x20, "multiplayer/UART"),
|
||||||
|
(RCNT + 1, 0x40, "Joybus serial mode"),
|
||||||
|
(RCNT + 1, 1, "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 { .. }));
|
||||||
|
assert!(error.to_string().contains(description));
|
||||||
|
assert_eq!(bus.write8(address, value), Err(error));
|
||||||
|
}
|
||||||
|
bus.write16(RCNT, 0x8055).unwrap();
|
||||||
|
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_eq!(bus.read16(SIOCNT).unwrap(), 0x400c);
|
||||||
|
assert_eq!(bus.read16(SIODATA8).unwrap(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn block_store_rejects_later_activation_before_earlier_control_writes() {
|
||||||
|
let code: [u32; 7] = [
|
||||||
|
0xe3a00301, 0xe2800080, 0xe59f1008, 0xe3a02080, 0xe8a00006, 0xeafffffe, 0x770f0000,
|
||||||
|
];
|
||||||
|
let bus = Memory::new(code.into_iter().flat_map(u32::to_le_bytes).collect()).unwrap();
|
||||||
|
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
|
||||||
|
for _ in 0..4 {
|
||||||
|
machine.step().unwrap();
|
||||||
|
}
|
||||||
|
let before = machine.cpu().clone();
|
||||||
|
let cycles = machine.cycles();
|
||||||
|
let error = machine.step().unwrap_err();
|
||||||
|
assert!(error.to_string().contains("sound master enable"));
|
||||||
|
assert_eq!(machine.cpu(), &before);
|
||||||
|
assert_eq!(machine.cycles(), cycles);
|
||||||
|
assert_eq!(machine.memory().read16(SOUNDCNT_H).unwrap(), 0);
|
||||||
|
assert_eq!(machine.step(), Err(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dma_activation_error_preserves_channel_progress_and_clocks() {
|
||||||
|
let mut bus = memory();
|
||||||
|
bus.write32(0x02000000, 0x80).unwrap();
|
||||||
|
bus.write32(DMA_BASE, 0x02000000).unwrap();
|
||||||
|
bus.write32(DMA_BASE + 4, SOUNDCNT_X).unwrap();
|
||||||
|
bus.write32(DMA_BASE + 8, 0x84000001).unwrap();
|
||||||
|
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
|
||||||
|
let before = machine.cpu().clone();
|
||||||
|
let error = machine.step().unwrap_err();
|
||||||
|
assert!(error.to_string().contains("sound master enable"));
|
||||||
|
assert_eq!(machine.cycles(), 0);
|
||||||
|
assert_eq!(machine.cpu(), &before);
|
||||||
|
assert_eq!(machine.step(), Err(error));
|
||||||
|
machine.memory_mut().write32(0x02000000, 0).unwrap();
|
||||||
|
machine.step().unwrap();
|
||||||
|
assert_eq!(machine.memory().read16(DMA_BASE + 10).unwrap() & 0x8000, 0);
|
||||||
|
}
|
||||||
|
|
@ -53,7 +53,7 @@ fn register_reset_masks_byte_merges_and_combined_word_access() {
|
||||||
assert_eq!(m.read32(KEYINPUT).unwrap(), 0x0123_01fe);
|
assert_eq!(m.read32(KEYINPUT).unwrap(), 0x0123_01fe);
|
||||||
assert_eq!(m.cycles(), 0);
|
assert_eq!(m.cycles(), 0);
|
||||||
assert_eq!(m.read32(KEYCNT), Err(MemoryError::Unaligned(KEYCNT)));
|
assert_eq!(m.read32(KEYCNT), Err(MemoryError::Unaligned(KEYCNT)));
|
||||||
for address in [KEYCNT + 2, KEYCNT + 0x400] {
|
for address in [KEYCNT + 4, KEYCNT + 0x400] {
|
||||||
assert_eq!(m.read8(address), Err(MemoryError::Unmapped(address)));
|
assert_eq!(m.read8(address), Err(MemoryError::Unmapped(address)));
|
||||||
assert_eq!(m.write8(address, 0), Err(MemoryError::Unmapped(address)));
|
assert_eq!(m.write8(address, 0), Err(MemoryError::Unmapped(address)));
|
||||||
}
|
}
|
||||||
|
|
@ -371,7 +371,7 @@ fn cpu_store_samples_keys_then_irq_enters_on_the_following_step() {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn failed_block_store_does_not_commit_key_control_irq_history_or_halt_wake() {
|
fn failed_block_store_does_not_commit_key_control_irq_history_or_halt_wake() {
|
||||||
// STMIA r0!,{r1,r2}: keypad word is valid, next word (serial RCNT) is not mapped.
|
// STMIA r0!,{r1,r2}: keypad word is valid, but RCNT's upper padding is not mapped.
|
||||||
let mut m = prepared(0xe8a0_0006, KEYINPUT, 0x4001_0000);
|
let mut m = prepared(0xe8a0_0006, KEYINPUT, 0x4001_0000);
|
||||||
press(m.memory_mut(), 1);
|
press(m.memory_mut(), 1);
|
||||||
m.memory_mut().write16(IE, IRQ).unwrap();
|
m.memory_mut().write16(IE, IRQ).unwrap();
|
||||||
|
|
@ -381,7 +381,7 @@ fn failed_block_store_does_not_commit_key_control_irq_history_or_halt_wake() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
m.step(),
|
m.step(),
|
||||||
Err(MachineError::Cpu(CpuError::Memory(MemoryError::Unmapped(
|
Err(MachineError::Cpu(CpuError::Memory(MemoryError::Unmapped(
|
||||||
KEYINPUT + 4
|
KEYINPUT + 6
|
||||||
))))
|
))))
|
||||||
);
|
);
|
||||||
assert_eq!(m.cpu(), &before);
|
assert_eq!(m.cpu(), &before);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue