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();
|
||||
for &(address, value) in writes {
|
||||
self.write_index::<4>(address, self.write_access())?;
|
||||
Self::validate_io_values(&mut audio, address, &value.to_le_bytes())?;
|
||||
}
|
||||
let mut serial = self.serial_state();
|
||||
let timing = self.cpu_timing.get();
|
||||
let devices = self.timer_step.get();
|
||||
// Preflight ordered data phases on temporary device/timing copies.
|
||||
// A serial request can finish before a later control write reaches the bus.
|
||||
let validation = (|| {
|
||||
for &(address, value) in writes {
|
||||
self.write_index::<4>(address, self.write_access())?;
|
||||
let bytes = value.to_le_bytes();
|
||||
Self::validate_io_values(&mut audio, address, &bytes)?;
|
||||
self.record_access(address, AccessWidth::Word);
|
||||
if Serial::mapped(address) {
|
||||
if let Some(mut step) = self.timer_step.get() {
|
||||
Self::validate_serial(&step.serial, address, &bytes)?;
|
||||
step.serial.write(address, &bytes);
|
||||
self.timer_step.set(Some(step));
|
||||
} else {
|
||||
Self::validate_serial(&serial, address, &bytes)?;
|
||||
serial.write(address, &bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})();
|
||||
self.cpu_timing.set(timing);
|
||||
self.timer_step.set(devices);
|
||||
validation?;
|
||||
for &(address, value) in writes {
|
||||
// The map cannot change between validation and these writes.
|
||||
self.write32(address, value)?;
|
||||
|
|
|
|||
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);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue