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, //! 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 //! Timers share a free-running prescaler phase. Hardware startup/write delays
//! and interrupt delivery delays are not modeled. //! and interrupt delivery delays are not modeled.
mod inactive; mod serial;
use crate::audio::Audio; use crate::audio::Audio;
pub use crate::audio::{FIFO_A, FIFO_B, SOUNDBIAS, SOUNDCNT_H, SOUNDCNT_X, SOUND_START, WAVE_RAM}; 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; mod timer_step;
pub(crate) use timer_step::TimerStep; pub(crate) use timer_step::TimerStep;
use timer_step::TIMER_IRQ_MASK; use timer_step::STAGED_IRQ_MASK;
use crate::{ use crate::{
display::{Display, DisplayPosition, CYCLES_PER_LINE, HBLANK_START, VISIBLE_LINES}, display::{Display, DisplayPosition, CYCLES_PER_LINE, HBLANK_START, VISIBLE_LINES},
@ -155,7 +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, pub(crate) serial: Serial,
pub(crate) audio: Audio, pub(crate) audio: Audio,
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.
@ -182,20 +183,20 @@ pub(crate) struct Io {
impl Io { impl Io {
pub(crate) fn mapped(address: u32) -> bool { pub(crate) fn mapped(address: u32) -> bool {
Audio::mapped(address) 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) || 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> { 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> { pub(crate) fn read8(&self, address: u32) -> Option<u8> {
if Audio::mapped(address) { if Audio::mapped(address) {
return self.audio.read8(address); return self.audio.read8(address);
} }
if inactive::Inactive::mapped(address) { if Serial::mapped(address) {
return self.inactive.read8(address); return self.serial.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));
@ -235,8 +236,8 @@ 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) { if Serial::mapped(address) {
self.inactive.write8(address, value); self.serial.write(address, &[value]);
return; return;
} }
if (DMA_BASE..=DMA_END).contains(&address) { 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. /// Display edges keep this finite even without an enabled wake source.
pub(crate) fn next_event_cycles(&self) -> u32 { pub(crate) fn next_event_cycles(&self) -> u32 {
self.timers self.timers
.iter() .iter()
.filter_map(|timer| timer.next_overflow(self.timer_phase)) .filter_map(|timer| timer.next_overflow(self.timer_phase))
.chain(self.serial.next_event_cycles())
.fold(self.display.next_event_cycles(), u32::min) .fold(self.display.next_event_cycles(), u32::min)
} }
@ -425,10 +427,11 @@ impl Io {
pub(crate) fn commit_timer_step(&mut self, step: TimerStep) { pub(crate) fn commit_timer_step(&mut self, step: TimerStep) {
self.audio = step.audio; self.audio = step.audio;
self.serial = step.serial;
self.dma.trigger_sound(self.audio.take_requests()); self.dma.trigger_sound(self.audio.take_requests());
self.timers = step.timers; self.timers = step.timers;
self.timer_phase = step.phase; 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(); self.wake_if_requested();
} }
@ -486,6 +489,7 @@ impl Io {
self.dma.trigger(vblank, hblank); self.dma.trigger(vblank, hblank);
self.pending |= self.display.advance(cycles); self.pending |= self.display.advance(cycles);
if advance_timers { if advance_timers {
self.pending |= self.serial.advance(cycles);
self.pending |= Self::advance_timer_bank( self.pending |= Self::advance_timer_bank(
&mut self.timers, &mut self.timers,
&mut self.timer_phase, &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. //! Other devices remain on the instruction-boundary scheduler.
use super::{Io, Timer, IF, TIMER_BASE}; 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)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct TimerStep { pub(crate) struct TimerStep {
pub(crate) audio: crate::audio::Audio, pub(crate) audio: crate::audio::Audio,
pub(crate) serial: super::Serial,
pub(super) timers: [Timer; 4], pub(super) timers: [Timer; 4],
pub(super) phase: u16, pub(super) phase: u16,
pub(super) pending: u16, pub(super) pending: u16,
@ -17,15 +18,17 @@ impl TimerStep {
pub(super) fn new(io: &Io) -> Self { pub(super) fn new(io: &Io) -> Self {
Self { Self {
audio: io.audio, audio: io.audio,
serial: io.serial,
timers: io.timers, timers: io.timers,
phase: io.timer_phase, phase: io.timer_phase,
pending: io.pending & TIMER_IRQ_MASK, pending: io.pending & STAGED_IRQ_MASK,
elapsed: 0, elapsed: 0,
} }
} }
pub(crate) fn advance_to(&mut self, elapsed: u32) { pub(crate) fn advance_to(&mut self, elapsed: u32) {
assert!(elapsed >= self.elapsed, "timer step time must be monotonic"); assert!(elapsed >= self.elapsed, "timer step time must be monotonic");
self.pending |= self.serial.advance(elapsed - self.elapsed);
self.pending |= Io::advance_timer_bank( self.pending |= Io::advance_timer_bank(
&mut self.timers, &mut self.timers,
&mut self.phase, &mut self.phase,
@ -44,7 +47,7 @@ impl TimerStep {
TIMER_BASE..=0x0400_010f => { TIMER_BASE..=0x0400_010f => {
Some(self.timers[((address - TIMER_BASE) / 4) as usize].read8(address)) 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, _ => None,
} }
} }
@ -59,7 +62,7 @@ impl TimerStep {
} }
IF => { IF => {
self.pending &= !u16::from(value); 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, _ => false,
} }

View file

@ -155,7 +155,7 @@ impl Machine {
/// Sample IRQ before executing the next instruction. GBA devices do not /// Sample IRQ before executing the next instruction. GBA devices do not
/// generate FIQ. IRQ entry consumes a separate step and does not clear IF. /// 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. /// 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 /// precede their bulk clock update. IRQ synchronization delays are not modeled. Opcode
/// prefetch follows ordered nominal CPU/DMA costs, not per-cycle arbitration. /// prefetch follows ordered nominal CPU/DMA costs, not per-cycle arbitration.
/// CPU/DMA diagnostics leave CPU, devices, and the clock unchanged for this step. /// 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)] #[cfg(test)]
mod prefetch_tests; mod prefetch_tests;
#[cfg(test)] #[cfg(test)]
mod serial_step_tests;
#[cfg(test)]
mod timer_step_tests; mod timer_step_tests;
#[cfg(test)] #[cfg(test)]
mod timing_event_tests; mod timing_event_tests;
@ -25,7 +27,7 @@ use crate::{
display::{DisplayPosition, CYCLES_PER_LINE, HBLANK_START, VISIBLE_LINES}, display::{DisplayPosition, CYCLES_PER_LINE, HBLANK_START, VISIBLE_LINES},
dma::{DmaError, DMA_BASE, DMA_END}, dma::{DmaError, DMA_BASE, DMA_END},
input::Buttons, input::Buttons,
io::{Io, TimerStep, HALTCNT, KEYINPUT, POSTFLG}, io::{Io, Serial, TimerStep, HALTCNT, KEYINPUT, POSTFLG},
timing::{AccessKind, AccessWidth, CpuTiming, Prefetch, StepTiming}, timing::{AccessKind, AccessWidth, CpuTiming, Prefetch, StepTiming},
video::{self, sprites::pipeline, Framebuffer, VideoError}, video::{self, sprites::pipeline, Framebuffer, VideoError},
}; };
@ -107,7 +109,7 @@ struct CpuAccess {
/// Successful DMA accesses drive local lanes even before the first CPU instruction. /// 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. /// Channel data remains separate; general and sub-instruction CPU bus handoff is unmodeled.
/// Instruction buffering and supported bus history consume shared fetch samples. /// 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 { pub struct Memory {
external_ram: Vec<u8>, external_ram: Vec<u8>,
internal_ram: Vec<u8>, internal_ram: Vec<u8>,
@ -177,6 +179,12 @@ impl Memory {
self.io.audio.level() 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 { fn audio_state(&self) -> Audio {
self.timer_step self.timer_step
.get() .get()
@ -231,7 +239,7 @@ impl Memory {
.take() .take()
.expect("machine timer transaction is active"); .expect("machine timer transaction is active");
self.io.commit_timer_step(step); 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); self.advance_running_cycles_with_timers(cycles, false);
} }
@ -258,7 +266,7 @@ impl Memory {
self.io.advance(cycles - advanced, advance_timers); self.io.advance(cycles - advanced, advance_timers);
} else { } else {
// Split at display and sprite-preparation edges. Video writes have // 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; let mut remaining = cycles;
while remaining != 0 { while remaining != 0 {
let position = self.io.display_position(); let position = self.io.display_position();
@ -689,6 +697,12 @@ impl Memory {
fn read_byte(&self, address: u32) -> Result<u8, MemoryError> { fn read_byte(&self, address: u32) -> Result<u8, MemoryError> {
if address >> 24 == 0x04 { if address >> 24 == 0x04 {
if let Some(step) = self.timer_step.get() { 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)) { if let Some(value) = step.read8(address, self.io.read8(address).unwrap_or(0)) {
return Ok(value); return Ok(value);
} }
@ -798,6 +812,18 @@ impl Memory {
self.iwram_bus.access(address, width, value); self.iwram_bus.access(address, width, value);
} }
if address >> 24 == 0x04 { 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 Audio::mapped(address) {
if let Some(mut step) = self.timer_step.get() { if let Some(mut step) = self.timer_step.get() {
step.audio.write(address, &bytes); step.audio.write(address, &bytes);
@ -930,15 +956,49 @@ impl Memory {
Ok(()) 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 /// 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> {
let mut audio = self.audio_state(); let mut audio = self.audio_state();
for &(address, value) in writes { let mut serial = self.serial_state();
self.write_index::<4>(address, self.write_access())?; let timing = self.cpu_timing.get();
Self::validate_io_values(&mut audio, address, &value.to_le_bytes())?; 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 { for &(address, value) in writes {
// The map cannot change between validation and these writes. // The map cannot change between validation and these writes.
self.write32(address, value)?; 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() { fn unsupported_operations_have_specific_retryable_diagnostics() {
let mut bus = memory(); let mut bus = memory();
for (address, value, description) in [ for (address, value, description) in [
(SIOCNT, 0x81, "internally clocked serial transfer"),
(SIOCNT + 1, 0x20, "multiplayer/UART"), (SIOCNT + 1, 0x20, "multiplayer/UART"),
(RCNT + 1, 0xc0, "Joybus serial mode"), (RCNT + 1, 0xc0, "Joybus serial mode"),
(RCNT + 1, 0x81, "GPIO serial interrupt"), (RCNT + 1, 0x81, "GPIO serial interrupt"),

View file

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

View file

@ -65,11 +65,13 @@ The repository is a Cargo workspace with three crates:
| `crates/gba-core/tests/pulse_sound/channel2.rs` | Channel 2 mapping, register gaps, independent state, shared clocks, and sum-before-rounding mixing | | `crates/gba-core/tests/pulse_sound/channel2.rs` | Channel 2 mapping, register gaps, independent state, shared clocks, and sum-before-rounding mixing |
| `crates/gba-core/tests/direct_sound.rs` | FIFO lanes/reset, signed mixing, timer selection, DMA refill, and HALT/STOP | | `crates/gba-core/tests/direct_sound.rs` | FIFO lanes/reset, signed mixing, timer selection, DMA refill, and HALT/STOP |
| `crates/gba-core/src/memory/audio_step_tests.rs` | Audio access phases, shadow validation, and CPU/DMA failure rollback | | `crates/gba-core/src/memory/audio_step_tests.rs` | Audio access phases, shadow validation, and CPU/DMA failure rollback |
| `crates/gba-core/src/io/inactive.rs` | Disconnected serial registers, external-clock waiting, and unsupported-activity validation | | `crates/gba-core/src/io/serial.rs` | Disconnected normal serial shifter, internal clocks, external waiting, GPIO latches, and live-write validation |
| `crates/gba-core/tests/inactive_devices.rs` | Device masks, wave RAM, GPIO pull-ups, serial data, and failed CPU/DMA write isolation | | `crates/gba-core/tests/inactive_devices.rs` | Device masks, wave RAM, GPIO pull-ups, serial data, and failed CPU/DMA write isolation |
| `crates/gba-core/tests/serial_control.rs` | RCNT mode gating, inactive bits, byte/halfword lanes, CPU/DMA diagnostics, and interrupt isolation | | `crates/gba-core/tests/serial_control.rs` | RCNT mode gating, inactive bits, byte/halfword lanes, CPU/DMA diagnostics, and interrupt isolation |
| `crates/gba-core/tests/serial_external.rs` | External-clock waiting, cancellation, HALT/STOP, ARM/Thumb/DMA starts, and atomic diagnostics | | `crates/gba-core/tests/serial_external.rs` | External-clock waiting, cancellation, HALT/STOP, ARM/Thumb/DMA starts, and atomic diagnostics |
| `crates/gba-core/src/io/timer_step.rs` | Staged timer/audio state, divider phase, timer IF bits, and sound refill requests at ordered CPU/DMA bus phases | | `crates/gba-core/tests/serial_internal.rs` | Nominal bit edges, widths/rates, cancellation, completion IRQs, HALT/STOP, and live-write limits |
| `crates/gba-core/src/memory/serial_step_tests.rs` | Serial bus-phase observations, preflight validation, DMA, IF ordering, capture isolation, and rollback |
| `crates/gba-core/src/io/timer_step.rs` | Staged timer/audio/serial state, divider phase, timer/serial IF bits, and sound refill requests at ordered CPU/DMA bus phases |
| `crates/gba-core/src/memory/timer_step_tests.rs` | Timer access phases, coherent lanes, cascades, IF ordering, rollback, DMA, idle, and clock ownership | | `crates/gba-core/src/memory/timer_step_tests.rs` | Timer access phases, coherent lanes, cascades, IF ordering, rollback, DMA, idle, and clock ownership |
| `crates/gba-core/src/display.rs` | Display clock, scanline status, comparison edges, and display IRQ events | | `crates/gba-core/src/display.rs` | Display clock, scanline status, comparison edges, and display IRQ events |
| `crates/gba-core/src/dma.rs` | DMA registers, internal pointers, retained channel data, trigger state, priority, and completion IRQs | | `crates/gba-core/src/dma.rs` | DMA registers, internal pointers, retained channel data, trigger state, priority, and completion IRQs |

View file

@ -544,5 +544,6 @@ Unsupported SWIs, unsupported exception vectors, and null/misaligned IRQ callbac
The CPU reports `CpuError::UnsupportedInstruction` with `bios::UNSUPPORTED_TRAP`, rather than silently treating a service as a no-op. The CPU reports `CpuError::UnsupportedInstruction` with `bios::UNSUPPORTED_TRAP`, rather than silently treating a service as a no-op.
Invalid arithmetic/decompression arguments use the same CPU error type with the distinct `bios::INVALID_ARGUMENT_TRAP` instruction. Invalid arithmetic/decompression arguments use the same CPU error type with the distinct `bios::INVALID_ARGUMENT_TRAP` instruction.
Prior boot/service steps remain committed on failure. Prior boot/service steps remain committed on failure.
PSG wave channel 3, host audio output, serial transfers, HardReset, and serial/Game Pak STOP wake-up remain unimplemented. PSG wave channel 3, host audio output, connected serial links, HardReset, and serial/Game Pak STOP wake-up remain unimplemented.
Disconnected normal serial transfers use the [bounded serial model](serial.md); cancel active transfers before BIOS serial reset.
This subset is not sufficient for Pokémon Emerald compatibility. This subset is not sufficient for Pokémon Emerald compatibility.

View file

@ -1,17 +1,19 @@
# Disconnected serial subset # Disconnected serial subset
The core supports normal-mode data registers, disconnected external-clock waiting, and general-purpose pins. The core supports disconnected normal-mode transfers, external-clock waiting, and general-purpose pins.
It does not support internally clocked transfers, external clock edges, multiplayer, UART, Joybus communication, or external serial interrupts. Internal clocks shift pulled-high input at nominal GBA rates. No link partner is emulated.
External clock edges, multiplayer, UART, Joybus communication, and GPIO interrupts remain unsupported.
Unsupported activity remains diagnostic rather than reporting a fabricated completion. Unsupported activity remains diagnostic rather than reporting a fabricated completion.
## Registers ## Registers
- `SIODATA32` at `0x04000120` is a four-byte latch in the supported normal-mode subset. - `SIODATA32` at `0x04000120` holds the 32-bit shift register. Byte lanes and halfword accesses are supported.
- `SIODATA8` at `0x0400012a` retains its low byte; its upper byte reads zero. - `SIODATA8` at `0x0400012a` holds the 8-bit shift register; its upper byte reads zero.
Each transfer shifts only its selected data register. The other register retains its value.
- `SIOCNT` at `0x04000128` retains normal 8/32-bit mode, IRQ configuration, clock selection, and the output-data bit. - `SIOCNT` at `0x04000128` retains normal 8/32-bit mode, IRQ configuration, clock selection, and the output-data bit.
The retained mask is `0x508b`. The disconnected serial input is high, except when a general-purpose output drives it low. The retained mask is `0x508b`. The disconnected serial input is high, except when a general-purpose output drives it low.
Start bit 7 can latch when clock-source bit 0 is clear. Software clears bit 7 to cancel the request. Start bit 7 requests a transfer. Bit 0 selects external or internal clock; bit 1 selects the internal rate.
Setting both bits 7 and 0 returns an internally-clocked-transfer diagnostic. Multiplayer/UART mode bit 13 remains unsupported. Completion or software cancellation clears start. Multiplayer/UART mode bit 13 remains unsupported.
- `RCNT` at `0x04000134` retains mode bits, interrupt configuration, output latches, and pin directions with mask `0xc1ff`. - `RCNT` at `0x04000134` retains mode bits, interrupt configuration, output latches, and pin directions with mask `0xc1ff`.
With bit 15 clear, bits 8 and 14 are writable latches without GPIO interrupt or Joybus effects. With bit 15 clear, bits 8 and 14 are writable latches without GPIO interrupt or Joybus effects.
With bit 15 set, bit 14 selects Joybus; otherwise bit 8 enables GPIO interrupts. Both active configurations remain diagnostic. With bit 15 set, bit 14 selects Joybus; otherwise bit 8 enables GPIO interrupts. Both active configurations remain diagnostic.
@ -23,7 +25,7 @@ Unsupported activity remains diagnostic rather than reporting a fabricated compl
Nonzero writes return an explicit error. Joybus status and data-transfer side effects are not implemented. Nonzero writes return an explicit error. Joybus status and data-transfer side effects are not implemented.
GPIO means general-purpose input/output. UART means universal asynchronous receiver/transmitter. GPIO means general-purpose input/output. UART means universal asynchronous receiver/transmitter.
No external serial device is connected. Elapsed cycles alone do not change serial data or generate an IRQ. No external serial device is connected. Only an enabled internal serial clock can shift data or generate a completion IRQ.
Configuration that hardware accepts in other modes can still be rejected by this intentionally limited subset. Configuration that hardware accepts in other modes can still be rejected by this intentionally limited subset.
## External-clock waiting ## External-clock waiting
@ -36,12 +38,63 @@ Repeated start writes do not complete the request. Software can cancel it by cle
General-purpose mode does not shift serial data. A retained external-clock start bit has no link-port effect there. General-purpose mode does not shift serial data. A retained external-clock start bit has no link-port effect there.
Returning to normal mode still cannot complete a transfer without external clock edges. Returning to normal mode still cannot complete a transfer without external clock edges.
Selecting internal clock while start is set remains diagnostic, including after an external request. Selecting internal clock while an unshifted request is pending starts a fresh nominal bit period.
This is a disconnected waiting model, not an implementation of a connected link or an external clock-input API. This is a disconnected waiting model, not an implementation of a connected link or an external clock-input API.
Original tests cover both widths, rate settings, control lanes, cancellation, GPIO selection, HALT/STOP, and ARM/Thumb stores. Original tests cover both widths, rate settings, control lanes, cancellation, GPIO selection, HALT/STOP, and ARM/Thumb stores.
DMA tests distinguish its own completion IRQ from a serial IRQ. Invalid word/block stores cannot leave a partial start request. DMA tests distinguish its own completion IRQ from a serial IRQ. Invalid word/block stores cannot leave a partial start request.
## Internal clocks and completion
Normal mode shifts most-significant bit first. At each nominal sampling edge, the selected register shifts left and receives a one.
The disconnected SI input is high. Completion therefore leaves `0xff` or `0xffffffff`, without inventing peer data.
Reads between edges expose the partially shifted value. SO waveforms and normal-mode RCNT pin samples remain unmodeled.
At the nominal 16,777,216 Hz system clock:
| SIOCNT bit 1 | Cycles per bit | 8-bit transfer | 32-bit transfer |
| --- | --- | --- | --- |
| 0 | 64 | 512 cycles | 2,048 cycles |
| 1 | 8 | 64 cycles | 256 cycles |
The first sample occurs one full bit period after activation. Selecting internal clock for an unshifted external request uses this same rule.
A repeated start write with unchanged width/clock fields preserves the current phase and remaining bit count.
A width/clock change before any bit has shifted starts a fresh period. Data writes before that first bit preserve the phase.
Software cancellation clears progress but retains already shifted data.
Whole-access writes merge control and data lanes before applying start/cancel behavior; byte writes remain separate accesses.
Completion clears start and latches IF bit 7 when SIOCNT bit 14 is enabled at the final edge.
IE, IME, and CPSR.I gate wake-up or delivery, not request latching. Clearing IF acknowledges the request.
A completed request does not reassert IF without another transfer.
HALT continues internal shifting and bounds its idle batch at serial completion. STOP freezes the bit phase and remaining count.
Only keypad wake is implemented for STOP; serial GPIO wake still requires its missing external-input model.
### Bus timing and validation
Serial state shares the timer/audio transaction during CPU instructions, IRQ entry, and DMA units.
Each access observes all elapsed source/data cycles through its bus-completion phase.
A start write excludes its preceding cycles. A load excludes its trailing internal cycle.
IF reads and acknowledgements include serial completion through that access, alongside timer requests.
Successful steps commit serial state and requests once. Failed steps discard speculative shifts, writes, and completion IRQs.
The display/capture scheduler does not clock serial again after that commit.
CPU-only stepping can change registers but does not advance serial clocks.
Block-store preflight simulates ordered access phases on temporary timing/device copies before committing any write.
Validation can therefore accept reconfiguration when an earlier request completes before the write reaches the bus.
The actual transfers then execute once from the original state. Other IF sources and DMA completion flags remain independent.
### Bounded behavior
After any bit has shifted, changes to width, clock selection/rate, or the selected data register remain diagnostic while start stays set.
Selecting GPIO during an internally clocked request also remains diagnostic. Clear start before these operations.
IRQ-enable and idle SO configuration writes remain supported without restarting progress.
These limits also apply to BIOS services that write serial registers; cancel active transfers before requesting serial reset.
Unverified live reconfiguration is not silently approximated.
The period arithmetic follows documented GBA rates. Activation phase, mid-transfer register visibility, and exact pin edges lack physical-hardware validation.
No external pin waveform, cable, connected partner, or serial-device protocol is implemented.
Original tests cover individual shifts, batching, cancellation, IRQ gating, HALT/STOP, bus phases, DMA, capture independence, and rollback.
## BIOS reset ## BIOS reset
RegisterRamReset always clears the low halfword of SIODATA32, even without bit 5. RegisterRamReset always clears the low halfword of SIODATA32, even without bit 5.
@ -52,15 +105,16 @@ Clearing SIOCNT cancels a pending external-clock request. Unselected serial rese
Original tests cover independent byte lanes, input pull-ups, output directions, reset flag selection, and explicit unsupported operations. Original tests cover independent byte lanes, input pull-ups, output directions, reset flag selection, and explicit unsupported operations.
Whole-access and block-store validation prevent a rejected control value from committing preceding bytes or registers. Whole-access and block-store validation prevent a rejected control value from committing preceding bytes or registers.
RCNT selection bits share the high byte. Each high-byte write replaces the complete selection, so validation needs no retained-mode shadow. Static RCNT mode rejection uses the complete high byte. Live-transfer validation additionally uses staged serial state.
Mode-gating tests cover every high-byte value, previous modes, low-byte preservation, CPU stores, DMA retries, and unchanged interrupt state. Mode-gating tests cover every high-byte value, previous modes, low-byte preservation, CPU stores, DMA retries, and unchanged interrupt state.
Inactive configuration bits do not wake HALT. RCNT word accesses still reject the unmapped padding at `0x04000136`. Inactive configuration bits do not wake HALT. RCNT word accesses still reject the unmapped padding at `0x04000136`.
## References ## References
- [GBATEK reset functions](https://problemkaputt.de/gbatek-bios-reset-functions.htm): reset flags, general-purpose selection, and the unconditional SIODATA32 side effect. - [GBATEK reset functions](https://problemkaputt.de/gbatek-bios-reset-functions.htm): reset flags, general-purpose selection, and the unconditional SIODATA32 side effect.
- [GBATEK normal serial mode](https://problemkaputt.de/gbatek-sio-normal-mode.htm): data widths, input status, writable unused RCNT bits 8/14, and external-clock slave start/wait/timeout behavior. - [GBATEK normal serial mode](https://problemkaputt.de/gbatek-sio-normal-mode.htm): data widths, MSB-first shifting, rates, pulled-high SI, start/completion, IRQ selection, and external-clock waiting.
- [mGBA serial implementation at 3a5e34be](https://github.com/mgba-emu/mgba/blob/3a5e34be33dc7f8f707e5bc9db69e8a430046f21/src/gba/sio.c): compared normal-mode control and completion paths. Its no-driver scheduled completion does not establish disconnected external-clock hardware behavior; this core does not copy that fallback. - [GBATEK mode summary](https://problemkaputt.de/gbatek-sio-control-registers-summary.htm): RCNT/SIOCNT mode selection.
- [mGBA serial implementation at 3a5e34be](https://github.com/mgba-emu/mgba/blob/3a5e34be33dc7f8f707e5bc9db69e8a430046f21/src/gba/sio.c): cross-checked normal transfer-duration arithmetic and completion IRQ handling. Its no-driver scheduled completion and zero receive data are not copied; they do not establish disconnected hardware behavior.
- [GBATEK](https://mgba-emu.github.io/gbatek/): GBA general-purpose pin directions, internal pull-ups, SI falling-edge interrupts, and the SIO mode-selection table. - [GBATEK](https://mgba-emu.github.io/gbatek/): GBA general-purpose pin directions, internal pull-ups, SI falling-edge interrupts, and the SIO mode-selection table.
- [mGBA BIOS implementation at 3a5e34be](https://github.com/mgba-emu/mgba/blob/3a5e34be33dc7f8f707e5bc9db69e8a430046f21/src/gba/bios.c): functional serial reset defaults, not copied source. - [mGBA BIOS implementation at 3a5e34be](https://github.com/mgba-emu/mgba/blob/3a5e34be33dc7f8f707e5bc9db69e8a430046f21/src/gba/bios.c): functional serial reset defaults, not copied source.

View file

@ -7,7 +7,7 @@
Otherwise it services one ready DMA data unit, keeping the CPU paused. Otherwise it services one ready DMA data unit, keeping the CPU paused.
If HALT is waiting and no DMA is ready, it advances device clocks to the next event without executing CPU code. If HALT is waiting and no DMA is ready, it advances device clocks to the next event without executing CPU code.
Otherwise, it samples the IRQ line, including the CPU's interrupt mask, then enters IRQ mode or executes one instruction. Otherwise, it samples the IRQ line, including the CPU's interrupt mask, then enters IRQ mode or executes one instruction.
Each successful step advances display, timer, and audio clocks by its nominal cost. Each successful step advances display, timer, audio, and enabled internal serial clocks by its nominal cost.
`Machine::last_timing()` reports separate code, data, internal, and idle cycle totals for the last successful step. `Machine::last_timing()` reports separate code, data, internal, and idle cycle totals for the last successful step.
`StepTiming::idle_cycles` counts device-clock cycles during HALT; it does not count CPU work. `StepTiming::idle_cycles` counts device-clock cycles during HALT; it does not count CPU work.
IRQ entry is a separate step; the vector instruction runs on the following step. IRQ entry is a separate step; the vector instruction runs on the following step.
@ -15,7 +15,7 @@ Entry charges the discarded incoming-state PC+8/PC+4 fetch and the ARM vector pa
DMA resume can make the discarded fetch non-sequential. Missing discarded bytes do not prevent IRQ entry. DMA resume can make the discarded fetch non-sequential. Missing discarded bytes do not prevent IRQ entry.
See [refill timing](../research/refill-fetch-timing.md) for history effects and remaining limits. See [refill timing](../research/refill-fetch-timing.md) for history effects and remaining limits.
A device event during a step can trigger IRQ entry once no DMA is ready and CPU masks allow delivery. A device event during a step can trigger IRQ entry once no DMA is ready and CPU masks allow delivery.
Timer and sound accesses use ordered bus-completion phases. Other device writes still take effect before their bulk clock update. Timer, sound, and serial accesses use ordered bus-completion phases. Other device writes still take effect before their bulk clock update.
`StepKind::Dma { channel }` identifies a DMA unit. `StepKind::HaltIdle` identifies one bounded HALT clock advance. `StepKind::Dma { channel }` identifies a DMA unit. `StepKind::HaltIdle` identifies one bounded HALT clock advance.
`StepKind::StopIdle` identifies a stopped system with no clock progress. `StepKind::StopIdle` identifies a stopped system with no clock progress.
The other step kinds remain `Instruction` and `IrqEntry`. The other step kinds remain `Instruction` and `IrqEntry`.
@ -31,15 +31,18 @@ Timer-start writes do not count preceding cycles. Timer-stop writes count throug
Block transfers preserve distinct word phases; combined reload/control writes merge at one phase, with reload bytes first. Block transfers preserve distinct word phases; combined reload/control writes merge at one phase, with reload bytes first.
The transaction also owns timer IF bits. Reads see overflows through the current access; acknowledgements clear earlier timer requests. The transaction also owns timer IF bits. Reads see overflows through the current access; acknowledgements clear earlier timer requests.
A later internal cycle can raise an acknowledged timer request again. Non-timer IF bits retain their existing behavior. A later internal cycle can raise an acknowledged timer request again.
The same transaction owns serial IF bit 7; serial completion and acknowledgement follow the same access boundaries.
Other IF sources retain their existing behavior.
IRQ delivery still waits until the next machine step. A committed timer request can wake HALT, but not STOP. IRQ delivery still waits until the next machine step. A committed timer request can wake HALT, but not STOP.
Successful steps commit timers, the shared divider phase, timer IF bits, and audio state. Successful steps commit timers, the shared divider phase, audio/serial state, and timer/serial IF bits.
Failed steps discard staged counters, divider progress, timer/audio writes, FIFO consumption, and requests. Failed steps discard staged counters, divider progress, device writes, FIFO consumption, serial shifts, and requests.
Display and other device clocks then advance once, without advancing timers again. Display and other device clocks then advance once, without advancing timers again.
Display and remaining devices still use the bulk scheduler. Mid-instruction display events, general bus arbitration, and IRQ synchronization delays remain unmodeled. Display and remaining devices still use the bulk scheduler. Mid-instruction display events, general bus arbitration, and IRQ synchronization delays remain unmodeled.
[Direct Sound](audio.md) consumes selected timer overflows in the timer transaction; sound DMA requests commit at step boundaries. [Direct Sound](audio.md) consumes selected timer overflows in the timer transaction; sound DMA requests commit at step boundaries.
The same transaction advances both PSG pulse oscillators, the noise counter, and their shared 512 Hz sequencer independently of the general-purpose timers. The same transaction advances both PSG pulse oscillators, the noise counter, and their shared 512 Hz sequencer independently of the general-purpose timers.
It also advances [normal serial transfers](serial.md) with nominal internal clocks. External-clock requests cannot progress without a clock source.
Host clock advances and HALT idle batches retain ordinary bulk timer advancement; STOP idle does not advance timers. Host clock advances and HALT idle batches retain ordinary bulk timer advancement; STOP idle does not advance timers.
The unchanged [prefetch cancellation probe](../research/prefetch-cancellation.md) now matches both published interval totals and actual timer samples. The unchanged [prefetch cancellation probe](../research/prefetch-cancellation.md) now matches both published interval totals and actual timer samples.
@ -95,7 +98,8 @@ See [prescaler evidence and original tests](../research/timer-prescaler.md).
Hardware startup and register-write delays are still not modeled. Writes retain the immediate bus-completion policy. Hardware startup and register-write delays are still not modeled. Writes retain the immediate bus-completion policy.
The shared-phase tests validate this bounded model, not absolute enable/reload edges on hardware. The shared-phase tests validate this bounded model, not absolute enable/reload edges on hardware.
Timers 0 and 1 can clock Direct Sound playback and request FIFO DMA independently of timer IRQ enable. Timers 0 and 1 can clock Direct Sound playback and request FIFO DMA independently of timer IRQ enable.
Interrupt sources other than timers, display events, DMA completion, and keypad input are not implemented. Implemented interrupt sources are timers, display events, DMA completion, keypad input, and normal serial completion.
Other serial modes and external serial interrupts remain unimplemented.
`--timer-demo` executes original ARM code that configures Timer 0, IE, and IME. `--timer-demo` executes original ARM code that configures Timer 0, IE, and IME.
The timer overflows after 16 supplied cycles and enters the original handler through vector `0x18`. The timer overflows after 16 supplied cycles and enters the original handler through vector `0x18`.
@ -173,7 +177,7 @@ Wake-up does not acknowledge IF. Clearing IF after wake-up does not put the CPU
On the next machine step, ready DMA still takes priority. On the next machine step, ready DMA still takes priority.
The CPU then enters an unmasked IRQ handler or resumes the next instruction. The CPU then enters an unmasked IRQ handler or resumes the next instruction.
Each `HaltIdle` step advances to the earliest display edge or independently clocked timer overflow. Each `HaltIdle` step advances to the earliest display edge, independently clocked timer overflow, or internal serial completion.
Display edges include every HBlank entry and scanline start. Cascaded timers receive pulses at their predecessor's overflow. Display edges include every HBlank entry and scanline start. Cascaded timers receive pulses at their predecessor's overflow.
These bounds prevent idle advances from skipping DMA triggers or wake-up events. These bounds prevent idle advances from skipping DMA triggers or wake-up events.
They also keep frame execution bounded when no interrupt can wake the CPU. They also keep frame execution bounded when no interrupt can wake the CPU.
@ -209,7 +213,7 @@ The same selected-button OR/AND logic applies, including the empty-mask behavior
IME and CPSR.I do not gate wake-up in this functional model. IME and CPSR.I do not gate wake-up in this functional model.
A matching condition already present at entry prevents a sustained stop. A matching condition already present at entry prevents a sustained stop.
Stale IF flags alone cannot wake STOP, including an old keypad request. Stale IF flags alone cannot wake STOP, including an old keypad request.
Timer, display, and DMA requests cannot wake STOP. Timer, display, DMA, and internal serial requests cannot wake STOP.
An input sample supplied while stopped updates KEYINPUT and the wake condition without setting IF. An input sample supplied while stopped updates KEYINPUT and the wake condition without setting IF.
It does not consume the normal running-state keypad polling history. It does not consume the normal running-state keypad polling history.

View file

@ -53,16 +53,21 @@ A local register trace found RCNT zero and a SIOCNT write of `0x5084`: 32-bit mo
With no connected clock source, this request must wait rather than complete on elapsed CPU cycles. With no connected clock source, this request must wait rather than complete on elapsed CPU cycles.
The [serial subset](../hardware/serial.md#external-clock-waiting) now retains the request without shifting data or raising a serial IRQ. The [serial subset](../hardware/serial.md#external-clock-waiting) now retains the request without shifting data or raising a serial IRQ.
Both runs pass that request and next stop when software selects internal clock while start remains set: External-clock waiting then reached internal-clock selection at PC `0x082e6eb8`, with start still set.
The serial model now handles that unshifted transition and clocks normal 8/32-bit transfers at nominal rates.
It shifts disconnected high input, clears start at completion, and requests serial IRQs through the ordinary device transaction.
No connected partner, fabricated peer data, or immediate completion was added.
Both runs now reach multiplayer mode selection:
```text ```text
Steps: 426523; instructions: 424753; IRQ entries: 2; DMA units: 1768; HALT idle: 0 Steps: 955967; instructions: 953287; IRQ entries: 80; DMA units: 2600; HALT idle: 0
Nominal cycles: 1416980 Nominal cycles: 3514542
PC=0x082e6eb8 PC=0x082e4358
unsupported internally clocked serial transfer: write 0x85 at 0x04000128 unsupported multiplayer/UART serial mode: write 0x20 at 0x04000129
``` ```
The native run captures five startup frames. These do not establish a working title screen or gameplay. The native run captures twelve startup frames. These do not establish a working title screen or gameplay.
Audio-device behavior is validated by original tests, not by audible game output. Audio-device behavior is validated by original tests, not by audible game output.
The window run does not reach its requested frame limit and correctly exits with failure. The window run does not reach its requested frame limit and correctly exits with failure.
Terminal execution is also diagnostic, not a compatibility pass. Terminal execution is also diagnostic, not a compatibility pass.
@ -74,11 +79,10 @@ Workspace debug/release tests, strict Clippy, public ARM/Thumb/memory/BIOS check
Native ROM-window tests and graphics smoke modes pass on Darwin arm64. Native ROM-window tests and graphics smoke modes pass on Darwin arm64.
The pinned Pong debug/release reports match. The pinned Pong debug/release reports match.
The next requirement is internally clocked normal serial communication, including switching from a pending external-clock request. The next requirement is disconnected multiplayer-mode configuration and status, starting with SIOCNT mode value `0x2000`.
Implement nominal shift periods, disconnected input sampling, completion, and IRQ behavior with deterministic tests. Research mode selection, absent-partner input levels, ready/error flags, and start behavior before extending the model.
Keep completion consistent with HALT, STOP, instruction timing, and diagnostic rollback. Do not invent connected players or successful linked transfers to suppress the diagnostic.
Do not invent a connected partner or immediate completion to suppress the diagnostic. The normal-mode result does not establish linked gameplay or GPIO interrupt support.
The external-clock waiting result does not establish linked gameplay or GPIO interrupt support.
Noise now has deterministic counter clocks and shares tested length/envelope logic with the independent pulse channels. Noise now has deterministic counter clocks and shares tested length/envelope logic with the independent pulse channels.
Sweep applies only to channel 1. Direct Sound has FIFO clocks and nominal DMA requests. Sweep applies only to channel 1. Direct Sound has FIFO clocks and nominal DMA requests.

View file

@ -52,7 +52,7 @@ Update this file when a feature lands or a limit is removed.
- Persistent fetch access kinds follow CPU data/internal work, compose with DMA resume, and reset after target refills. - Persistent fetch access kinds follow CPU data/internal work, compose with DMA resume, and reset after target refills.
- Game Pak wait-state control (`WAITCNT`) for all three ROM windows. - Game Pak wait-state control (`WAITCNT`) for all three ROM windows.
- Bit 14 enables a nominal eight-halfword opcode prefetch queue, with partial fetches, cancellation, and DMA/idle integration. - Bit 14 enables a nominal eight-halfword opcode prefetch queue, with partial fetches, cancellation, and DMA/idle integration.
- Staged timer clocks follow CPU and DMA bus-completion phases, including timer IF reads/acknowledgements and failed-step rollback. - Staged timer/audio/serial clocks follow CPU and DMA bus-completion phases, including timer/serial IF reads, acknowledgements, and failed-step rollback.
- The unchanged original ARM cancellation probe matches both published read interval totals and its unadjusted timer samples. - The unchanged original ARM cancellation probe matches both published read interval totals and its unadjusted timer samples.
- Sequential/non-sequential data accesses, branch refill costs, and variable multiply timing. - Sequential/non-sequential data accesses, branch refill costs, and variable multiply timing.
- Optional caller-supplied 16 KiB BIOS mapping, with CPU read protection based on retained ARM PC+8 or aligned Thumb PC+4 words. - Optional caller-supplied 16 KiB BIOS mapping, with CPU read protection based on retained ARM PC+8 or aligned Thumb PC+4 words.
@ -142,8 +142,9 @@ There is no host audio output or cartridge save support.
The [audio subset](hardware/audio.md) supports Direct Sound, both PSG pulse channels, noise channel 4, idle wave channel 3 state, and instantaneous digital mixing. The [audio subset](hardware/audio.md) supports Direct Sound, both PSG pulse channels, noise channel 4, idle wave channel 3 state, and instantaneous digital mixing.
It does not yet provide a continuous sample stream. Wave channel 3 playback and unverified sound-DMA pairings remain diagnostic. It does not yet provide a continuous sample stream. Wave channel 3 playback and unverified sound-DMA pairings remain diagnostic.
PSG timing and signed mixing are nominal; hardware startup edges and analog behavior remain unverified. PSG timing and signed mixing are nominal; hardware startup edges and analog behavior remain unverified.
The [serial subset](hardware/serial.md) supports normal data, disconnected external-clock waiting, and general-purpose pins. The [serial subset](hardware/serial.md) supports disconnected normal 8/32-bit transfers, external-clock waiting, and general-purpose pins.
External requests retain start/busy without completion or IRQ. Internally clocked transfers and connected links remain unsupported. Internal clocks shift high input at nominal rates, clear busy, and request completion IRQs. External requests retain busy without clock edges.
Connected links, multiplayer/UART, Joybus, GPIO interrupts, and unverified mid-transfer reconfiguration remain unsupported.
RCNT retains inactive bits 8/14 in normal mode. Effective GPIO interrupt enable and Joybus selection remain diagnostic. RCNT retains inactive bits 8/14 in normal mode. Effective GPIO interrupt enable and Joybus selection remain diagnostic.
Mapped I/O covers DMA, timers, interrupts, WAITCNT, display control/status, background control/scroll/affine registers, window/effect registers, KEYINPUT/KEYCNT, POSTFLG, and HALTCNT. Mapped I/O covers DMA, timers, interrupts, WAITCNT, display control/status, background control/scroll/affine registers, window/effect registers, KEYINPUT/KEYCNT, POSTFLG, and HALTCNT.
Keypad IRQs use a documented polling model; hardware retrigger edges and asynchronous input timing remain unverified. Keypad IRQs use a documented polling model; hardware retrigger edges and asynchronous input timing remain unverified.
@ -157,7 +158,7 @@ A local Emerald run now completes its all-device reset and master sound enable.
It also passes the pulse/noise triggers and normal-mode RCNT configuration. It also passes the pulse/noise triggers and normal-mode RCNT configuration.
It also completes a CpuSet call to address zero through the corrected ignored BIOS-write behavior. It also completes a CpuSet call to address zero through the corrected ignored BIOS-write behavior.
It also accepts an external-clock serial request without inventing clock edges or completion. It also accepts an external-clock serial request without inventing clock edges or completion.
It next stops when software selects internal serial clock (`0x04000128`, PC `0x082e6eb8`). It now also completes internally clocked normal transfers and next stops at multiplayer mode selection (`0x04000129`, PC `0x082e4358`).
No title-screen, gameplay, or audible-game compatibility is established. No title-screen, gameplay, or audible-game compatibility is established.
See [the startup result](research/emerald-reset.md) for the bounded run and next investigation. See [the startup result](research/emerald-reset.md) for the bounded run and next investigation.
See [cartridge loading](hardware/cartridge.md) for file limits, step budgets, and exit status. See [cartridge loading](hardware/cartridge.md) for file limits, step budgets, and exit status.

View file

@ -292,6 +292,10 @@ Regressions retain the original program and published observations without corre
Disabled-prefetch controls, delay bounds, completion PCs, and fresh-run determinism have separate checks. Disabled-prefetch controls, delay bounds, completion PCs, and fresh-run determinism have separate checks.
Instruction timing tests cover arithmetic, shifts, branches, transfers, block operations, multiply, and exception returns. Instruction timing tests cover arithmetic, shifts, branches, transfers, block operations, multiply, and exception returns.
Timed and untimed execution are compared across all 65,536 Thumb encodings with fixed initial registers. Timed and untimed execution are compared across all 65,536 Thumb encodings with fixed initial registers.
Normal serial tests cover both widths and internal rates, every bit edge, disconnected high input, batching, and completion IRQs.
They check external-clock handoff, cancellation, phase retention, HALT bounds, STOP freeze/wake, and explicit live-write limits.
Bus-phase tests cover start/load timing, IF acknowledgement order, CPU/DMA rollback, block preflight, and capture independence.
These tests validate the documented nominal serial model, not physical link-pin timing or multiplayer compatibility.
Timer/IRQ tests verify that changing WAITCNT changes when an interrupt is sampled. Timer/IRQ tests verify that changing WAITCNT changes when an interrupt is sampled.
Timer tests cover all prescalers, reload changes, start/stop behavior, byte writes, large clock advances, and cascades. Timer tests cover all prescalers, reload changes, start/stop behavior, byte writes, large clock advances, and cascades.
Ordered timer tests cover ARM/Thumb sampling, coherent byte lanes, block-transfer phases, reload/control ordering, and prescaler retention. Ordered timer tests cover ARM/Thumb sampling, coherent byte lanes, block-transfer phases, reload/control ordering, and prescaler retention.