feat(serial): clock disconnected normal transfers and completion IRQs
This commit is contained in:
parent
d329c76dc3
commit
0b36ef1e24
17 changed files with 860 additions and 196 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
195
crates/gba-core/src/io/serial.rs
Normal file
195
crates/gba-core/src/io/serial.rs
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
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())?;
|
||||
Self::validate_io_values(&mut audio, address, &value.to_le_bytes())?;
|
||||
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)?;
|
||||
|
|
|
|||
214
crates/gba-core/src/memory/serial_step_tests.rs
Normal file
214
crates/gba-core/src/memory/serial_step_tests.rs
Normal 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);
|
||||
}
|
||||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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,26 +12,13 @@ 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();
|
||||
bus.write8(SIOCNT, low).unwrap();
|
||||
assert_eq!(
|
||||
bus.read16(SIOCNT).unwrap(),
|
||||
(previous & 0x5000) | u16::from(low & 0x8b) | 4
|
||||
|
|
@ -41,7 +28,6 @@ fn every_low_control_byte_retains_external_busy_but_rejects_internal_start_atomi
|
|||
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);
|
||||
|
|
|
|||
231
crates/gba-core/tests/serial_internal.rs
Normal file
231
crates/gba-core/tests/serial_internal.rs
Normal 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);
|
||||
}
|
||||
|
|
@ -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/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/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/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/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/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 |
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
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.
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -1,17 +1,19 @@
|
|||
# Disconnected serial subset
|
||||
|
||||
The core supports normal-mode data registers, disconnected 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.
|
||||
The core supports disconnected normal-mode transfers, external-clock waiting, and general-purpose pins.
|
||||
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.
|
||||
|
||||
## Registers
|
||||
|
||||
- `SIODATA32` at `0x04000120` is a four-byte latch in the supported normal-mode subset.
|
||||
- `SIODATA8` at `0x0400012a` retains its low byte; its upper byte reads zero.
|
||||
- `SIODATA32` at `0x04000120` holds the 32-bit shift register. Byte lanes and halfword accesses are supported.
|
||||
- `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.
|
||||
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.
|
||||
Setting both bits 7 and 0 returns an internally-clocked-transfer diagnostic. Multiplayer/UART mode bit 13 remains unsupported.
|
||||
Start bit 7 requests a transfer. Bit 0 selects external or internal clock; bit 1 selects the internal rate.
|
||||
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`.
|
||||
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.
|
||||
|
|
@ -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.
|
||||
|
||||
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.
|
||||
|
||||
## 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.
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## 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
|
||||
|
||||
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.
|
||||
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.
|
||||
Inactive configuration bits do not wake HALT. RCNT word accesses still reject the unmapped padding at `0x04000136`.
|
||||
|
||||
## References
|
||||
|
||||
- [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.
|
||||
- [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 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.
|
||||
- [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.
|
||||
- [mGBA BIOS implementation at 3a5e34be](https://github.com/mgba-emu/mgba/blob/3a5e34be33dc7f8f707e5bc9db69e8a430046f21/src/gba/bios.c): functional serial reset defaults, not copied source.
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
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.
|
||||
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.
|
||||
`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.
|
||||
|
|
@ -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.
|
||||
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.
|
||||
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::StopIdle` identifies a stopped system with no clock progress.
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
Successful steps commit timers, the shared divider phase, timer IF bits, and audio state.
|
||||
Failed steps discard staged counters, divider progress, timer/audio writes, FIFO consumption, and requests.
|
||||
Successful steps commit timers, the shared divider phase, audio/serial state, and timer/serial IF bits.
|
||||
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 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.
|
||||
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.
|
||||
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
|
|
@ -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.
|
||||
A matching condition already present at entry prevents a sustained stop.
|
||||
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.
|
||||
It does not consume the normal running-state keypad polling history.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
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
|
||||
Steps: 426523; instructions: 424753; IRQ entries: 2; DMA units: 1768; HALT idle: 0
|
||||
Nominal cycles: 1416980
|
||||
PC=0x082e6eb8
|
||||
unsupported internally clocked serial transfer: write 0x85 at 0x04000128
|
||||
Steps: 955967; instructions: 953287; IRQ entries: 80; DMA units: 2600; HALT idle: 0
|
||||
Nominal cycles: 3514542
|
||||
PC=0x082e4358
|
||||
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.
|
||||
The window run does not reach its requested frame limit and correctly exits with failure.
|
||||
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.
|
||||
The pinned Pong debug/release reports match.
|
||||
|
||||
The next requirement is internally clocked normal serial communication, including switching from a pending external-clock request.
|
||||
Implement nominal shift periods, disconnected input sampling, completion, and IRQ behavior with deterministic tests.
|
||||
Keep completion consistent with HALT, STOP, instruction timing, and diagnostic rollback.
|
||||
Do not invent a connected partner or immediate completion to suppress the diagnostic.
|
||||
The external-clock waiting result does not establish linked gameplay or GPIO interrupt support.
|
||||
The next requirement is disconnected multiplayer-mode configuration and status, starting with SIOCNT mode value `0x2000`.
|
||||
Research mode selection, absent-partner input levels, ready/error flags, and start behavior before extending the model.
|
||||
Do not invent connected players or successful linked transfers to suppress the diagnostic.
|
||||
The normal-mode 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.
|
||||
Sweep applies only to channel 1. Direct Sound has FIFO clocks and nominal DMA requests.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
|
|
@ -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.
|
||||
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.
|
||||
The [serial subset](hardware/serial.md) supports normal data, disconnected external-clock waiting, and general-purpose pins.
|
||||
External requests retain start/busy without completion or IRQ. Internally clocked transfers and connected links remain unsupported.
|
||||
The [serial subset](hardware/serial.md) supports disconnected normal 8/32-bit transfers, external-clock waiting, and general-purpose pins.
|
||||
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.
|
||||
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.
|
||||
|
|
@ -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 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 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.
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
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.
|
||||
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 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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue