feat(serial): clock disconnected normal transfers and completion IRQs

This commit is contained in:
0xrsydn 2026-10-07 04:17:46 +07:00
commit 0b36ef1e24
17 changed files with 860 additions and 196 deletions

View file

@ -1,15 +1,16 @@
//! GBA I/O subset: DMA, timers, interrupts, WAITCNT, display, keypad input,
//! Direct Sound, and disconnected serial state with external-clock waiting.
//! Direct Sound, and disconnected normal serial transfers.
//! Timers share a free-running prescaler phase. Hardware startup/write delays
//! and interrupt delivery delays are not modeled.
mod inactive;
mod serial;
use crate::audio::Audio;
pub use crate::audio::{FIFO_A, FIFO_B, SOUNDBIAS, SOUNDCNT_H, SOUNDCNT_X, SOUND_START, WAVE_RAM};
pub use inactive::{JOYCNT, JOY_RECV, JOY_TRANS, RCNT, SIOCNT, SIODATA32, SIODATA8};
pub(crate) use serial::Serial;
pub use serial::{JOYCNT, JOY_RECV, JOY_TRANS, RCNT, SIOCNT, SIODATA32, SIODATA8};
mod timer_step;
pub(crate) use timer_step::TimerStep;
use timer_step::TIMER_IRQ_MASK;
use timer_step::STAGED_IRQ_MASK;
use crate::{
display::{Display, DisplayPosition, CYCLES_PER_LINE, HBLANK_START, VISIBLE_LINES},
@ -155,7 +156,7 @@ enum PowerState {
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub(crate) struct Io {
inactive: inactive::Inactive,
pub(crate) serial: Serial,
pub(crate) audio: Audio,
timers: [Timer; 4],
// Low ten system-clock bits cover every supported divider. STOP freezes this phase.
@ -182,20 +183,20 @@ pub(crate) struct Io {
impl Io {
pub(crate) fn mapped(address: u32) -> bool {
Audio::mapped(address)
|| inactive::Inactive::mapped(address)
|| Serial::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)
Serial::unsupported(address, value)
}
pub(crate) fn read8(&self, address: u32) -> Option<u8> {
if Audio::mapped(address) {
return self.audio.read8(address);
}
if inactive::Inactive::mapped(address) {
return self.inactive.read8(address);
if Serial::mapped(address) {
return self.serial.read8(address);
}
if (DMA_BASE..=DMA_END).contains(&address) {
return Some(self.dma.read8(address));
@ -235,8 +236,8 @@ impl Io {
pub(crate) fn write8(&mut self, address: u32, value: u8) {
debug_assert!(Self::mapped(address));
debug_assert!(!(KEYINPUT..=KEYCNT + 1).contains(&address));
if inactive::Inactive::mapped(address) {
self.inactive.write8(address, value);
if Serial::mapped(address) {
self.serial.write(address, &[value]);
return;
}
if (DMA_BASE..=DMA_END).contains(&address) {
@ -336,12 +337,13 @@ impl Io {
}
}
/// Bound an idle batch by the next display edge or independent timer overflow.
/// Bound idle by the next display edge, timer overflow, or serial completion.
/// Display edges keep this finite even without an enabled wake source.
pub(crate) fn next_event_cycles(&self) -> u32 {
self.timers
.iter()
.filter_map(|timer| timer.next_overflow(self.timer_phase))
.chain(self.serial.next_event_cycles())
.fold(self.display.next_event_cycles(), u32::min)
}
@ -425,10 +427,11 @@ impl Io {
pub(crate) fn commit_timer_step(&mut self, step: TimerStep) {
self.audio = step.audio;
self.serial = step.serial;
self.dma.trigger_sound(self.audio.take_requests());
self.timers = step.timers;
self.timer_phase = step.phase;
self.pending = (self.pending & !TIMER_IRQ_MASK) | step.pending;
self.pending = (self.pending & !STAGED_IRQ_MASK) | step.pending;
self.wake_if_requested();
}
@ -486,6 +489,7 @@ impl Io {
self.dma.trigger(vblank, hblank);
self.pending |= self.display.advance(cycles);
if advance_timers {
self.pending |= self.serial.advance(cycles);
self.pending |= Self::advance_timer_bank(
&mut self.timers,
&mut self.timer_phase,

View file

@ -1,94 +0,0 @@
//! Disconnected serial state, including external-clock requests that cannot progress.
use super::replace_byte;
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 {
serial_data: [u8; 4],
serial_send: u8,
serial_control: u16,
rcnt: u16,
}
impl Inactive {
pub(super) fn mapped(address: u32) -> bool {
matches!(address,
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.
/// RCNT mode and IRQ-enable bits share the high byte. Each high-byte write
/// supplies the complete selection, independent of the previous low byte.
pub(super) fn unsupported(address: u32, value: u8) -> Option<&'static str> {
match address {
// Clock source and start share the low byte. With no external
// clock edges, an external-clock request only latches start/busy.
SIOCNT if value & 0x81 == 0x81 => Some("internally clocked serial transfer"),
0x0400_0129 if value & 0x20 != 0 => Some("multiplayer/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"),
_ => 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 (SIODATA32..=SIODATA32 + 3).contains(&address) {
return Some(self.serial_data[(address - SIODATA32) as usize]);
}
let value = match address & !1 {
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 (SIODATA32..=SIODATA32 + 3).contains(&address) {
self.serial_data[(address - SIODATA32) as usize] = value;
return;
}
match address & !1 {
SIOCNT => {
self.serial_control = replace_byte(self.serial_control, address, value) & 0x508b
}
SIODATA8 if address & 1 == 0 => self.serial_send = value,
RCNT => self.rcnt = replace_byte(self.rcnt, address, value) & 0xc1ff,
_ => {} // Unused/read-only bits have no effect.
}
}
}

View file

@ -0,0 +1,195 @@
//! Disconnected normal serial shifter with nominal internal clocks.
use super::replace_byte;
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, Copy, PartialEq, Eq)]
pub(crate) struct Serial {
data: u32,
send: u8,
control: u16,
rcnt: u16,
bits_left: u8,
until_bit: u32,
}
impl Serial {
pub(crate) fn mapped(address: u32) -> bool {
matches!(address,
SIODATA32..=0x0400_0123 | SIOCNT..=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_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"),
_ => None,
}
}
fn normal(&self) -> bool {
self.rcnt & 0x8000 == 0
}
fn busy(&self) -> bool {
self.control & 0x80 != 0
}
fn clocked(&self) -> bool {
self.normal() && self.busy() && self.control & 1 != 0
}
fn width(&self) -> u8 {
if self.control & 0x1000 != 0 {
32
} else {
8
}
}
fn period(&self) -> u32 {
if self.control & 2 != 0 {
8
} else {
64
}
}
/// Validate the final access value at its bus-completion phase, not a transient byte state.
pub(crate) fn validate(&self, address: u32, bytes: &[u8]) -> Option<(u32, u8, &'static str)> {
for (offset, &value) in bytes.iter().enumerate() {
let at = address + offset as u32;
if let Some(operation) = Self::unsupported(at, value) {
return Some((at, value, operation));
}
}
let mut next = *self;
next.merge(address, bytes);
// 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() {
let changed_mode = self.normal() != next.normal();
let changed_clock = (self.control ^ next.control) & 0x1003 != 0;
let data_touched = (0..bytes.len()).any(|offset| {
let at = address + offset as u32;
if self.width() == 32 {
(SIODATA32..SIODATA32 + 4).contains(&at)
} else {
at == SIODATA8
}
});
if (self.clocked() && changed_mode)
|| (self.bits_left != self.width()
&& (changed_clock || changed_mode || data_touched))
{
return Some((
address,
bytes[0],
"serial reconfiguration during active transfer",
));
}
}
None
}
fn gpio_pins(&self) -> u16 {
let outputs = (self.rcnt >> 4) & 15;
(self.rcnt & outputs & 15) | (!outputs & 15)
}
pub(crate) fn read8(&self, address: u32) -> Option<u8> {
if (SIODATA32..SIODATA32 + 4).contains(&address) {
return Some(self.data.to_le_bytes()[(address - SIODATA32) as usize]);
}
let value = match address & !1 {
SIOCNT => {
self.control
| if self.normal() {
4
} else {
self.gpio_pins() & 4
}
}
SIODATA8 => u16::from(self.send),
RCNT => {
if self.normal() && address & 1 == 0 {
return None;
}
(self.rcnt & !15) | self.gpio_pins()
}
JOYCNT | JOY_RECV..=0x0400_0156 => 0,
_ => return None,
};
Some(value.to_le_bytes()[(address & 1) as usize])
}
fn merge(&mut self, address: u32, bytes: &[u8]) {
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 {
match at & !1 {
SIOCNT => self.control = replace_byte(self.control, at, value) & 0x508b,
SIODATA8 if at & 1 == 0 => self.send = value,
RCNT => self.rcnt = replace_byte(self.rcnt, at, value) & 0xc1ff,
_ => {}
}
}
}
}
pub(crate) fn write(&mut self, address: u32, bytes: &[u8]) {
debug_assert!(self.validate(address, bytes).is_none());
let previous = *self;
self.merge(address, bytes);
if !self.normal() || !self.busy() {
self.bits_left = 0;
self.until_bit = 0;
} else if !previous.normal()
|| !previous.busy()
|| (previous.control ^ self.control) & 0x1003 != 0
{
// New request or an unshifted clock/width change, including external -> internal.
self.bits_left = self.width();
self.until_bit = self.period();
}
}
pub(crate) fn next_event_cycles(&self) -> Option<u32> {
self.clocked()
.then(|| self.until_bit + u32::from(self.bits_left - 1) * self.period())
}
/// At most 32 edges per request. SI is pulled high without a connected partner.
pub(crate) fn advance(&mut self, mut cycles: u32) -> u16 {
if !self.clocked() {
return 0;
}
while cycles >= self.until_bit {
cycles -= self.until_bit;
if self.width() == 32 {
self.data = (self.data << 1) | 1;
} else {
self.send = (self.send << 1) | 1;
}
self.bits_left -= 1;
if self.bits_left == 0 {
self.control &= !0x80;
self.until_bit = 0;
return if self.control & 0x4000 != 0 { 0x80 } else { 0 };
}
self.until_bit = self.period();
}
self.until_bit -= cycles;
0
}
}

View file

@ -1,12 +1,13 @@
//! Staged timer and audio clocks for one instruction, IRQ entry, or DMA unit.
//! Staged timer, audio, and serial clocks for one instruction, IRQ entry, or DMA unit.
//! Other devices remain on the instruction-boundary scheduler.
use super::{Io, Timer, IF, TIMER_BASE};
pub(super) const TIMER_IRQ_MASK: u16 = 0x78;
pub(super) const STAGED_IRQ_MASK: u16 = 0xf8;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct TimerStep {
pub(crate) audio: crate::audio::Audio,
pub(crate) serial: super::Serial,
pub(super) timers: [Timer; 4],
pub(super) phase: u16,
pub(super) pending: u16,
@ -17,15 +18,17 @@ impl TimerStep {
pub(super) fn new(io: &Io) -> Self {
Self {
audio: io.audio,
serial: io.serial,
timers: io.timers,
phase: io.timer_phase,
pending: io.pending & TIMER_IRQ_MASK,
pending: io.pending & STAGED_IRQ_MASK,
elapsed: 0,
}
}
pub(crate) fn advance_to(&mut self, elapsed: u32) {
assert!(elapsed >= self.elapsed, "timer step time must be monotonic");
self.pending |= self.serial.advance(elapsed - self.elapsed);
self.pending |= Io::advance_timer_bank(
&mut self.timers,
&mut self.phase,
@ -44,7 +47,7 @@ impl TimerStep {
TIMER_BASE..=0x0400_010f => {
Some(self.timers[((address - TIMER_BASE) / 4) as usize].read8(address))
}
IF => Some((committed & !(TIMER_IRQ_MASK as u8)) | self.pending as u8),
IF => Some((committed & !(STAGED_IRQ_MASK as u8)) | self.pending as u8),
_ => None,
}
}
@ -59,7 +62,7 @@ impl TimerStep {
}
IF => {
self.pending &= !u16::from(value);
false // The normal I/O path must also acknowledge non-timer sources.
false // The normal I/O path must also acknowledge non-staged sources.
}
_ => false,
}

View file

@ -155,7 +155,7 @@ impl Machine {
/// Sample IRQ before executing the next instruction. GBA devices do not
/// generate FIQ. IRQ entry consumes a separate step and does not clear IF.
/// CPU/DMA work uses nominal costs; HALT advances exactly to the next device event.
/// Timer data accesses use staged bus-completion phases. Other device writes
/// Timer, sound, and serial accesses use staged bus-completion phases. Other device writes
/// precede their bulk clock update. IRQ synchronization delays are not modeled. Opcode
/// prefetch follows ordered nominal CPU/DMA costs, not per-cycle arbitration.
/// CPU/DMA diagnostics leave CPU, devices, and the clock unchanged for this step.

View file

@ -13,6 +13,8 @@ mod iwram_history_tests;
#[cfg(test)]
mod prefetch_tests;
#[cfg(test)]
mod serial_step_tests;
#[cfg(test)]
mod timer_step_tests;
#[cfg(test)]
mod timing_event_tests;
@ -25,7 +27,7 @@ use crate::{
display::{DisplayPosition, CYCLES_PER_LINE, HBLANK_START, VISIBLE_LINES},
dma::{DmaError, DMA_BASE, DMA_END},
input::Buttons,
io::{Io, TimerStep, HALTCNT, KEYINPUT, POSTFLG},
io::{Io, Serial, TimerStep, HALTCNT, KEYINPUT, POSTFLG},
timing::{AccessKind, AccessWidth, CpuTiming, Prefetch, StepTiming},
video::{self, sprites::pipeline, Framebuffer, VideoError},
};
@ -107,7 +109,7 @@ struct CpuAccess {
/// Successful DMA accesses drive local lanes even before the first CPU instruction.
/// Channel data remains separate; general and sub-instruction CPU bus handoff is unmodeled.
/// Instruction buffering and supported bus history consume shared fetch samples.
/// Timer and sound accesses use staged machine-step time; other per-access device timing remains unmodeled.
/// Timer, sound, and serial accesses use staged machine-step time; other per-access device timing remains unmodeled.
pub struct Memory {
external_ram: Vec<u8>,
internal_ram: Vec<u8>,
@ -177,6 +179,12 @@ impl Memory {
self.io.audio.level()
}
fn serial_state(&self) -> Serial {
self.timer_step
.get()
.map_or(self.io.serial, |step| step.serial)
}
fn audio_state(&self) -> Audio {
self.timer_step
.get()
@ -231,7 +239,7 @@ impl Memory {
.take()
.expect("machine timer transaction is active");
self.io.commit_timer_step(step);
// Timers already reached this boundary. Advance other devices exactly once.
// Staged devices already reached this boundary. Advance remaining devices exactly once.
self.advance_running_cycles_with_timers(cycles, false);
}
@ -258,7 +266,7 @@ impl Memory {
self.io.advance(cycles - advanced, advance_timers);
} else {
// Split at display and sprite-preparation edges. Video writes have
// already committed. Machine timer transactions skip this bulk timer clock.
// already committed. Staged devices skip their bulk clock on machine steps.
let mut remaining = cycles;
while remaining != 0 {
let position = self.io.display_position();
@ -689,6 +697,12 @@ impl Memory {
fn read_byte(&self, address: u32) -> Result<u8, MemoryError> {
if address >> 24 == 0x04 {
if let Some(step) = self.timer_step.get() {
if Serial::mapped(address) {
return step
.serial
.read8(address)
.ok_or(MemoryError::Unmapped(address));
}
if let Some(value) = step.read8(address, self.io.read8(address).unwrap_or(0)) {
return Ok(value);
}
@ -798,6 +812,18 @@ impl Memory {
self.iwram_bus.access(address, width, value);
}
if address >> 24 == 0x04 {
if Serial::mapped(address) {
let mut serial = self.serial_state();
Self::validate_serial(&serial, address, &bytes)?;
serial.write(address, &bytes);
if let Some(mut step) = self.timer_step.get() {
step.serial = serial;
self.timer_step.set(Some(step));
} else {
self.io.serial = serial;
}
return Ok(());
}
if Audio::mapped(address) {
if let Some(mut step) = self.timer_step.get() {
step.audio.write(address, &bytes);
@ -930,15 +956,49 @@ impl Memory {
Ok(())
}
fn validate_serial(serial: &Serial, address: u32, bytes: &[u8]) -> Result<(), MemoryError> {
if let Some((address, value, operation)) = serial.validate(address, bytes) {
return Err(MemoryError::UnsupportedIo {
address,
value,
operation,
});
}
Ok(())
}
/// 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.
/// 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> {
let mut audio = self.audio_state();
for &(address, value) in writes {
self.write_index::<4>(address, self.write_access())?;
Self::validate_io_values(&mut audio, address, &value.to_le_bytes())?;
}
let mut serial = self.serial_state();
let timing = self.cpu_timing.get();
let devices = self.timer_step.get();
// Preflight ordered data phases on temporary device/timing copies.
// A serial request can finish before a later control write reaches the bus.
let validation = (|| {
for &(address, value) in writes {
self.write_index::<4>(address, self.write_access())?;
let bytes = value.to_le_bytes();
Self::validate_io_values(&mut audio, address, &bytes)?;
self.record_access(address, AccessWidth::Word);
if Serial::mapped(address) {
if let Some(mut step) = self.timer_step.get() {
Self::validate_serial(&step.serial, address, &bytes)?;
step.serial.write(address, &bytes);
self.timer_step.set(Some(step));
} else {
Self::validate_serial(&serial, address, &bytes)?;
serial.write(address, &bytes);
}
}
}
Ok(())
})();
self.cpu_timing.set(timing);
self.timer_step.set(devices);
validation?;
for &(address, value) in writes {
// The map cannot change between validation and these writes.
self.write32(address, value)?;

View file

@ -0,0 +1,214 @@
//! Serial progress belongs to the same speculative bus phases as timers/audio.
use super::{timing_event_tests::prepared, *};
use crate::{
io::{IF, RCNT, SIOCNT, SIODATA8, SOUNDCNT_X},
machine::{Machine, StepKind},
};
fn start(memory: &mut Memory, elapsed: u32) {
memory.write16(SIOCNT, 0x4083).unwrap(); // 8 bits, eight cycles per bit.
memory.advance_cycles(elapsed);
}
#[test]
fn cpu_starts_at_data_completion_and_cpu_only_execution_does_not_shift() {
for (thumb, instruction) in [(false, 0xe1c100b0), (true, 0x8008)] {
// STRH r0,[r1]
let (mut cpu, mut memory) = prepared(thumb, &[instruction], &[(0, 0x4083), (1, SIOCNT)]);
cpu.step_timed(&mut memory).unwrap();
let started = memory.io.serial;
assert_eq!(started.next_event_cycles(), Some(64));
let (cpu, memory) = prepared(thumb, &[instruction], &[(0, 0x4083), (1, SIOCNT)]);
let mut machine = Machine::new(cpu, memory);
machine.step().unwrap();
assert_eq!(machine.memory().io.serial, started); // No preceding fetch/data cycles charged to the new request.
assert_eq!(machine.last_timing().data_cycles, 1);
machine.memory_mut().advance_cycles(63);
assert_eq!(machine.memory().read8(SIODATA8).unwrap(), 0x7f);
machine.memory_mut().advance_cycles(1);
assert_eq!(machine.memory().read16(IF).unwrap(), 0x80);
}
}
#[test]
fn arm_and_thumb_loads_sample_before_the_trailing_internal_cycle() {
for (thumb, instruction, elapsed) in [(false, 0xe1d100b0, 56), (true, 0x8808, 59)] {
let (cpu, mut memory) = prepared(thumb, &[instruction], &[(1, SIOCNT)]);
start(&mut memory, elapsed);
let mut machine = Machine::new(cpu, memory);
machine.step().unwrap();
assert_eq!(machine.cpu().registers()[0], 0x4087);
assert_eq!(machine.memory().read16(SIOCNT).unwrap(), 0x4007);
assert_eq!(machine.memory().read16(IF).unwrap(), 0x80);
assert_eq!(machine.cycles(), 64);
}
}
#[test]
fn if_acknowledgement_at_the_completion_phase_clears_serial_but_not_other_sources() {
let (cpu, mut memory) = prepared(false, &[0xe1c100b0], &[(0, 0x80), (1, IF)]);
start(&mut memory, 57);
memory.write16(crate::io::KEYCNT, 0x4001).unwrap();
memory.set_buttons(crate::input::Buttons::from_bits(1));
assert_eq!(memory.read16(IF).unwrap(), 0x1000);
let mut machine = Machine::new(cpu, memory);
machine.step().unwrap(); // Fetch six cycles + one data cycle: complete, then acknowledge.
assert_eq!(machine.cycles(), 64);
assert_eq!(machine.memory().read16(IF).unwrap(), 0x1000);
assert_eq!(machine.memory().read16(SIOCNT).unwrap(), 0x4007);
}
#[test]
fn single_and_block_store_validation_uses_completion_phase_and_does_not_double_advance() {
for instruction in [0xe5810000, 0xe8a10001] {
// STR and STMIA r1!,{r0}
for elapsed in [56, 57] {
let (cpu, mut memory) =
prepared(false, &[instruction], &[(0, 0x00204081), (1, SIOCNT)]);
start(&mut memory, elapsed);
let serial = memory.io.serial;
let mut machine = Machine::new(cpu, memory);
let cpu = machine.cpu().clone();
if elapsed == 57 {
machine.step().unwrap(); // Previous request completes at this write's phase.
assert_eq!(machine.memory().read16(IF).unwrap(), 0x80);
assert_eq!(machine.memory().read16(SIOCNT).unwrap(), 0x4085);
assert_eq!(machine.memory().read8(SIODATA8).unwrap(), 0x20);
assert_eq!(machine.memory().io.serial.next_event_cycles(), Some(512));
assert_eq!(machine.cycles(), 64);
} 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(), u64::from(elapsed));
assert_eq!(machine.memory().io.serial, serial);
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
}
assert!(machine.memory().timer_step.get().is_none());
assert!(machine.memory().cpu_timing.get().is_none());
}
}
}
#[test]
fn later_block_failure_restores_completion_and_partial_serial_writes() {
let (cpu, mut memory) = prepared(
false,
&[0xe8a10005],
&[(0, 0x00204081), (1, SIOCNT), (2, 0)],
);
start(&mut memory, 57);
let serial = memory.io.serial;
let mut machine = Machine::new(cpu, memory);
let cpu = machine.cpu().clone();
for _ in 0..2 {
assert!(machine
.step()
.unwrap_err()
.to_string()
.contains("0x0400012c"));
assert_eq!(machine.memory().io.serial, serial);
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
assert_eq!(machine.cpu(), &cpu);
assert_eq!(machine.cycles(), 57);
}
}
#[test]
fn dma_source_observes_old_busy_then_destination_phase_commits_completion() {
let (cpu, mut memory) = prepared(false, &[0xe1a00000], &[]);
start(&mut memory, 60);
memory.write32(DMA_BASE, SIOCNT).unwrap();
memory.write32(DMA_BASE + 4, 0x02000000).unwrap();
memory.write32(DMA_BASE + 8, 0xc4000001).unwrap();
let mut machine = Machine::new(cpu, memory);
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 0 });
assert_eq!(machine.memory().read32(0x02000000).unwrap(), 0x007f4087);
assert_eq!(machine.memory().read16(SIOCNT).unwrap(), 0x4007);
assert_eq!(machine.memory().read16(IF).unwrap(), 0x180); // Both sources survive their separate commits.
assert_eq!(machine.cycles(), 69);
}
#[test]
fn dma_start_excludes_source_cycles_and_failed_destination_discards_serial_progress() {
for fail in [false, true] {
let (cpu, mut memory) = prepared(false, &[0xe1a00000], &[]);
if fail {
start(&mut memory, 60);
memory.write16(SOUNDCNT_X, 0x80).unwrap();
}
memory
.write32(0x02000000, if fail { 0x8000 } else { 0x4083 })
.unwrap();
memory.write32(DMA_BASE, 0x02000000).unwrap();
memory
.write32(DMA_BASE + 4, if fail { 0x04000074 } else { SIOCNT })
.unwrap();
memory.write32(DMA_BASE + 8, 0x84000001).unwrap();
let serial = memory.io.serial;
let mut machine = Machine::new(cpu, memory);
if fail {
let cpu = machine.cpu().clone();
let error = machine.step().unwrap_err();
assert_eq!(machine.step(), Err(error));
assert_eq!(machine.memory().io.serial, serial);
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
assert_eq!(machine.cpu(), &cpu);
assert_eq!(machine.cycles(), 60);
} else {
machine.step().unwrap();
assert_eq!(machine.cycles(), 9);
assert_eq!(machine.memory().io.serial.next_event_cycles(), Some(64));
}
}
}
#[test]
fn batch_validation_carries_control_changes_without_committing_earlier_ram_on_error() {
let (_, mut memory) = prepared(false, &[0xe1a00000], &[]);
start(&mut memory, 8);
let serial = memory.io.serial;
assert!(memory
.write_words(&[(0x02000000, 0x12345678), (SIOCNT, 0x00aa4083)])
.is_err());
assert_eq!(memory.read32(0x02000000).unwrap(), 0);
assert_eq!(memory.io.serial, serial);
memory
.write_words(&[(SIOCNT, 0), (SIOCNT, 0x00aa4083)])
.unwrap();
assert_eq!(memory.read8(SIODATA8).unwrap(), 0xaa);
assert_eq!(memory.io.serial.next_event_cycles(), Some(64));
}
#[test]
fn staged_unmapped_pin_reads_do_not_fall_back_to_old_committed_gpio_state() {
let (_, mut memory) = prepared(false, &[0xe1a00000], &[]);
memory.write16(RCNT, 0x8000).unwrap();
memory.begin_timer_step();
memory.write16(RCNT, 0).unwrap();
assert_eq!(memory.read8(RCNT), Err(MemoryError::Unmapped(RCNT)));
memory.discard_timer_step();
assert_eq!(memory.read16(RCNT).unwrap(), 0x800f);
}
#[test]
fn capture_splits_and_cpu_clock_commits_do_not_duplicate_serial_edges() {
let (cpu, mut plain) = prepared(false, &[0xe1a00000; 100], &[]);
let (_, mut captured) = prepared(false, &[0xe1a00000; 100], &[]);
plain.advance_cycles(950);
captured.advance_cycles(950);
captured.set_scanline_rendering(true);
start(&mut plain, 0);
start(&mut captured, 0);
let mut plain = Machine::new(cpu.clone(), plain);
let mut captured = Machine::new(cpu, captured);
for _ in 0..20 {
plain.step().unwrap();
captured.step().unwrap();
assert_eq!(plain.memory().io.serial, captured.memory().io.serial);
assert_eq!(plain.memory().read16(IF), captured.memory().read16(IF));
assert_eq!(plain.cycles(), captured.cycles());
}
assert_eq!(plain.memory().read8(SIODATA8).unwrap(), 0xff);
}