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);
}

View file

@ -80,7 +80,6 @@ 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, 0x81, "internally clocked serial transfer"),
(SIOCNT + 1, 0x20, "multiplayer/UART"),
(RCNT + 1, 0xc0, "Joybus serial mode"),
(RCNT + 1, 0x81, "GPIO serial interrupt"),

View file

@ -4,7 +4,7 @@ use gba_core::{
dma::DMA_BASE,
io::{HALTCNT, IE, IF, IME, RCNT, SIOCNT, SIODATA32, SIODATA8},
machine::{Machine, StepKind},
memory::{Memory, MemoryError, ROM_START},
memory::{Memory, ROM_START},
};
fn memory() -> Memory {
@ -12,36 +12,22 @@ fn memory() -> Memory {
}
#[test]
fn every_low_control_byte_retains_external_busy_but_rejects_internal_start_atomically() {
fn every_low_control_byte_retains_normal_control_bits_without_immediate_completion() {
let mut bus = memory();
for high in [0, 0x10, 0x40, 0x50] {
for previous in [0x0003, 0x1000, 0x4080, 0x5082] {
for low in 0..=u8::MAX {
bus.write16(SIOCNT, previous).unwrap();
let before = bus.read16(SIOCNT).unwrap();
let result = bus.write8(SIOCNT, low);
if low & 0x80 != 0 && low & 1 != 0 {
assert_eq!(
result,
Err(MemoryError::UnsupportedIo {
address: SIOCNT,
value: low,
operation: "internally clocked serial transfer",
})
);
assert_eq!(bus.read16(SIOCNT).unwrap(), before);
} else {
result.unwrap();
assert_eq!(
bus.read16(SIOCNT).unwrap(),
(previous & 0x5000) | u16::from(low & 0x8b) | 4
);
bus.write8(SIOCNT + 1, high).unwrap();
assert_eq!(
bus.read16(SIOCNT).unwrap(),
(u16::from(high) << 8) | u16::from(low & 0x8b) | 4
);
}
bus.write8(SIOCNT, low).unwrap();
assert_eq!(
bus.read16(SIOCNT).unwrap(),
(previous & 0x5000) | u16::from(low & 0x8b) | 4
);
bus.write8(SIOCNT + 1, high).unwrap();
assert_eq!(
bus.read16(SIOCNT).unwrap(),
(u16::from(high) << 8) | u16::from(low & 0x8b) | 4
);
assert_eq!(bus.read16(IF).unwrap() & 0x80, 0);
}
}
@ -85,9 +71,9 @@ fn gpio_selection_does_not_turn_a_retained_start_bit_into_a_transfer_or_irq() {
bus.advance_cycles(1000000);
assert_eq!(bus.read16(SIOCNT).unwrap(), 0x5084);
assert_eq!(bus.read16(IF).unwrap() & 0x80, 0);
// Clock-source changes with start still set remain explicit diagnostics.
assert!(bus.write8(SIOCNT, 0x81).is_err());
assert_eq!(bus.read16(SIOCNT).unwrap(), 0x5084);
// Selecting internal clock starts the pending request, but not instant completion.
bus.write8(SIOCNT, 0x81).unwrap();
assert_eq!(bus.read16(SIOCNT).unwrap(), 0x5085);
bus.write16(SIOCNT, 0x5001).unwrap(); // Cancel first, then select internal clock while idle.
assert_eq!(bus.read16(SIOCNT).unwrap(), 0x5005);
}
@ -146,7 +132,7 @@ fn prepared(thumb: bool, instruction: u32, value: u32) -> Machine {
#[test]
fn arm_and_thumb_external_starts_complete_as_instructions_not_as_serial_transfers() {
for thumb in [false, true] {
for value in [0x5084, 0x5081] {
for value in [0x5084, 0x7080] {
let mut machine = prepared(thumb, 0xe1c010b0, value); // STRH r1,[r0]
let cpu = machine.cpu().clone();
if value == 0x5084 {
@ -160,9 +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("internally clocked serial transfer"));
assert!(error.to_string().contains("multiplayer/UART serial mode"));
assert_eq!(machine.cpu(), &cpu);
assert_eq!(machine.cycles(), 0);
assert_eq!(machine.memory().read16(SIOCNT).unwrap(), 0x1007);
@ -198,15 +182,13 @@ fn invalid_word_and_block_writes_do_not_leave_a_partial_external_start() {
#[test]
fn dma_start_preserves_serial_wait_and_reports_only_dma_completion() {
let mut bus = memory();
bus.write32(0x02000000, 0x00ab5081).unwrap();
bus.write32(0x02000000, 0x00ab7080).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);
let error = machine.step().unwrap_err();
assert!(error
.to_string()
.contains("internally clocked serial transfer"));
assert!(error.to_string().contains("multiplayer/UART serial mode"));
assert_eq!(machine.step(), Err(error));
assert_eq!(machine.cycles(), 0);
assert_eq!(machine.memory().read16(SIOCNT).unwrap(), 4);

View file

@ -0,0 +1,231 @@
//! Original nominal serial clocks and disconnected-input regressions.
use gba_core::{
cpu::Cpu,
input::Buttons,
io::{HALTCNT, IE, IF, IME, KEYCNT, RCNT, SIOCNT, SIODATA32, SIODATA8},
machine::{Machine, StepKind},
memory::{Memory, ROM_START},
};
fn memory() -> Memory {
Memory::new(0xeaff_fffe_u32.to_le_bytes().to_vec()).unwrap()
}
fn value(bus: &Memory, wide: bool) -> u32 {
if wide {
bus.read32(SIODATA32).unwrap()
} else {
u32::from(bus.read8(SIODATA8).unwrap())
}
}
#[test]
fn every_shift_edge_samples_high_and_completion_occurs_after_exactly_eight_or_thirty_two_bits() {
for wide in [false, true] {
for fast in [false, true] {
for irq in [false, true] {
for seed in [0, u32::MAX, 0x12345678, 0x80000001] {
let mut bus = memory();
bus.write32(SIODATA32, seed).unwrap();
bus.write8(SIODATA8, seed as u8).unwrap();
let control = 0x81
| if wide { 0x1000 } else { 0 }
| if fast { 2 } else { 0 }
| if irq { 0x4000 } else { 0 };
bus.write16(SIOCNT, control).unwrap();
let width = if wide { 32 } else { 8 };
let period = if fast { 8 } else { 64 };
let mut bits: Vec<bool> =
(0..width).rev().map(|bit| seed & (1 << bit) != 0).collect();
for index in 0..width {
let expected = bits
.iter()
.fold(0u32, |word, bit| (word << 1) | u32::from(*bit));
bus.advance_cycles(period - 1);
assert_eq!(value(&bus, wide), expected);
assert_ne!(bus.read16(SIOCNT).unwrap() & 0x80, 0);
assert_eq!(bus.read16(IF).unwrap() & 0x80, 0);
bus.advance_cycles(1);
bits.remove(0);
bits.push(true);
let expected = bits
.iter()
.fold(0u32, |word, bit| (word << 1) | u32::from(*bit));
assert_eq!(value(&bus, wide), expected);
assert_eq!(bus.read16(SIOCNT).unwrap() & 0x80 != 0, index + 1 < width);
}
assert_eq!(bus.read16(IF).unwrap() & 0x80, if irq { 0x80 } else { 0 });
assert_eq!(bus.read16(SIOCNT).unwrap(), (control & !0x80) | 4);
if wide {
assert_eq!(bus.read8(SIODATA8).unwrap(), seed as u8);
} else {
assert_eq!(bus.read32(SIODATA32).unwrap(), seed);
}
bus.write16(IF, 0x80).unwrap();
bus.advance_cycles(u32::MAX);
assert_eq!(bus.read16(IF).unwrap() & 0x80, 0);
}
}
}
}
}
#[test]
fn bulk_and_single_cycle_advancement_match_at_every_phase_and_large_batches() {
for control in [0x4081, 0x4083, 0x5081, 0x5083] {
for cycles in [0, 1, 7, 8, 63, 64, 65, 255, 256, 511, 512, 2047, 2048, 4096] {
let mut bulk = memory();
let mut single = memory();
for bus in [&mut bulk, &mut single] {
bus.write32(SIODATA32, 0x12345678).unwrap();
bus.write8(SIODATA8, 0x96).unwrap();
bus.write16(SIOCNT, control).unwrap();
}
bulk.advance_cycles(cycles);
for _ in 0..cycles {
single.advance_cycles(1);
}
for address in [SIOCNT, SIODATA8, IF] {
assert_eq!(bulk.read16(address), single.read16(address));
}
assert_eq!(bulk.read32(SIODATA32), single.read32(SIODATA32));
}
}
}
#[test]
fn external_to_internal_transition_starts_a_fresh_period_and_same_start_writes_preserve_phase() {
let mut bus = memory();
bus.write16(SIOCNT, 0x5080).unwrap();
bus.advance_cycles(1000000);
bus.write8(SIOCNT, 0x81).unwrap();
bus.advance_cycles(63);
assert_eq!(bus.read32(SIODATA32).unwrap(), 0);
bus.write16(SIOCNT, 0x5085).unwrap(); // Read/modify/write includes read-only SI.
bus.advance_cycles(1);
assert_eq!(bus.read32(SIODATA32).unwrap(), 1);
bus.advance_cycles(64 * 31 - 1);
assert_ne!(bus.read16(SIOCNT).unwrap() & 0x80, 0);
bus.advance_cycles(1);
assert_eq!(bus.read32(SIODATA32).unwrap(), u32::MAX);
assert_eq!(bus.read16(IF).unwrap() & 0x80, 0x80);
}
#[test]
fn cancellation_allows_reconfiguration_and_irq_enable_is_sampled_at_completion() {
let mut bus = memory();
bus.write16(SIOCNT, 0x81).unwrap();
bus.advance_cycles(65);
assert_eq!(bus.read8(SIODATA8).unwrap(), 1);
let before = bus.read16(SIOCNT).unwrap();
for (address, data) in [
(SIOCNT, 0x83),
(SIOCNT + 1, 0x10),
(SIODATA8, 0x42),
(RCNT + 1, 0x80),
] {
let error = bus.write8(address, data).unwrap_err();
assert!(error.to_string().contains("serial reconfiguration"));
assert_eq!(bus.read16(SIOCNT).unwrap(), before);
assert_eq!(bus.read8(SIODATA8).unwrap(), 1);
}
bus.write8(SIOCNT + 1, 0x40).unwrap(); // IRQ enable does not restart the current bit.
bus.advance_cycles(447);
assert_eq!(bus.read16(IF).unwrap() & 0x80, 0x80);
bus.write16(IF, 0x80).unwrap();
bus.write16(SIOCNT, 0x4083).unwrap();
bus.advance_cycles(8);
bus.write32(SIOCNT, 0x00aa0001).unwrap(); // Cancel and replace data atomically.
bus.advance_cycles(1000000);
assert_eq!(bus.read8(SIODATA8).unwrap(), 0xaa);
assert_eq!(bus.read16(IF).unwrap() & 0x80, 0);
bus.write16(SIOCNT, 0x4083).unwrap();
bus.advance_cycles(63);
bus.write8(SIOCNT + 1, 0).unwrap(); // Disable IRQ one cycle before completion.
bus.advance_cycles(1);
assert_eq!(bus.read8(SIODATA8).unwrap(), 0xff);
assert_eq!(bus.read16(IF).unwrap() & 0x80, 0);
}
#[test]
fn gpio_gates_the_clock_and_leaving_normal_mode_requires_cancellation() {
let mut bus = memory();
bus.write16(RCNT, 0x8000).unwrap();
bus.write16(SIOCNT, 0x4083).unwrap();
bus.advance_cycles(1000000);
assert_eq!(bus.read8(SIODATA8).unwrap(), 0);
assert_eq!(bus.read16(IF).unwrap(), 0);
bus.write16(RCNT, 0).unwrap();
assert!(bus.write16(RCNT, 0x8000).is_err());
bus.advance_cycles(7);
assert_eq!(bus.read8(SIODATA8).unwrap(), 0);
bus.advance_cycles(1);
assert_eq!(bus.read8(SIODATA8).unwrap(), 1);
bus.write16(SIOCNT, 0).unwrap();
bus.write16(RCNT, 0x8000).unwrap();
bus.advance_cycles(1000000);
assert_eq!(bus.read8(SIODATA8).unwrap(), 1);
assert_eq!(bus.read16(IF).unwrap(), 0);
}
#[test]
fn halt_stops_at_serial_completion_and_delivery_obeys_ime() {
for ime in [0, 1] {
let mut bus = memory();
bus.write16(IE, 0x80).unwrap();
bus.write16(IME, ime).unwrap();
bus.write16(SIOCNT, 0x4083).unwrap();
bus.write8(HALTCNT, 0).unwrap();
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
assert_eq!(machine.cycles(), 64);
assert!(!machine.halted());
assert_eq!(machine.memory().read16(IF).unwrap(), 0x80);
assert_eq!(
machine.step().unwrap(),
if ime == 0 {
StepKind::Instruction
} else {
StepKind::IrqEntry
}
);
}
}
#[test]
fn serial_and_timer_requests_latch_and_acknowledge_independently() {
let mut bus = memory();
bus.write32(gba_core::io::TIMER_BASE, 0x00c0ffc0).unwrap();
bus.write16(SIOCNT, 0x4083).unwrap();
bus.advance_cycles(64);
assert_eq!(bus.read16(IF).unwrap(), 0x88);
bus.write16(IF, 0x80).unwrap();
assert_eq!(bus.read16(IF).unwrap(), 8);
bus.write16(IF, 8).unwrap();
assert_eq!(bus.read16(IF).unwrap(), 0);
bus.write16(gba_core::io::TIMER_BASE + 2, 0).unwrap();
bus.write16(SIOCNT, 0x5081).unwrap();
bus.advance_cycles(u32::MAX);
assert_eq!(bus.read32(SIODATA32).unwrap(), u32::MAX);
assert_eq!(bus.read16(IF).unwrap(), 0x80);
}
#[test]
fn stop_freezes_a_partial_bit_until_keypad_wake() {
let mut bus = memory();
bus.write16(IE, 0x1080).unwrap();
bus.write16(KEYCNT, 0x4001).unwrap();
bus.write16(SIOCNT, 0x4083).unwrap();
bus.advance_cycles(63);
bus.write8(HALTCNT, 0x80).unwrap();
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
assert_eq!(machine.step().unwrap(), StepKind::StopIdle);
machine.memory_mut().advance_cycles(u32::MAX);
assert_eq!(machine.cycles(), 63);
assert_eq!(machine.memory().read8(SIODATA8).unwrap(), 0x7f);
assert_eq!(machine.memory().read16(IF).unwrap() & 0x80, 0);
machine.memory_mut().set_buttons(Buttons::from_bits(1));
assert!(!machine.stopped());
machine.memory_mut().advance_cycles(1);
assert_eq!(machine.memory().read8(SIODATA8).unwrap(), 0xff);
assert_eq!(machine.memory().read16(IF).unwrap() & 0x80, 0x80);
}