fix(timers): apply machine accesses at bus completion
This commit is contained in:
parent
8930ea93b4
commit
bfecc87e84
20 changed files with 765 additions and 137 deletions
|
|
@ -233,12 +233,13 @@ fn failed_alias_reads_preserve_register_banks_device_state_and_clock() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn alias_io_load_reads_before_device_progress_and_irq_delivery() {
|
||||
fn alias_io_load_reads_at_data_completion_before_irq_delivery() {
|
||||
let mut bus = memory(0xe5b0_0004); // LDR r0,[r0,#4]!
|
||||
bus.write32(TIMER_BASE, 0x00c0_fffa).unwrap();
|
||||
bus.write16(IE, 8).unwrap();
|
||||
bus.write16(IME, 1).unwrap();
|
||||
let expected = bus.read32(TIMER_BASE).unwrap();
|
||||
// Six source cycles overflow/reload the timer; the data cycle adds one tick.
|
||||
let expected = 0x00c0_fffb;
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = TIMER_BASE - 4;
|
||||
let mut machine = Machine::new(cpu, bus);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@
|
|||
//! Timer phase resets on enable or clock-source changes. Hardware startup delays,
|
||||
//! shared prescaler phase, audio events, and interrupt delivery delays are not modeled.
|
||||
|
||||
mod timer_step;
|
||||
pub(crate) use timer_step::TimerStep;
|
||||
use timer_step::TIMER_IRQ_MASK;
|
||||
|
||||
use crate::{
|
||||
display::{Display, DisplayPosition, CYCLES_PER_LINE, HBLANK_START, VISIBLE_LINES},
|
||||
dma::{Dma, DmaError, Transfer, DMA_BASE, DMA_END},
|
||||
|
|
@ -58,7 +62,7 @@ pub const HALTCNT: u32 = 0x0400_0301;
|
|||
|
||||
const IRQ_MASK: u16 = 0x3fff;
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
struct Timer {
|
||||
reload: u16,
|
||||
counter: u16,
|
||||
|
|
@ -67,6 +71,23 @@ struct Timer {
|
|||
}
|
||||
|
||||
impl Timer {
|
||||
fn read8(&self, address: u32) -> u8 {
|
||||
let value = if address & 2 == 0 {
|
||||
self.counter
|
||||
} else {
|
||||
u16::from(self.control)
|
||||
};
|
||||
value.to_le_bytes()[(address & 1) as usize]
|
||||
}
|
||||
|
||||
fn write8(&mut self, address: u32, value: u8, index: usize) {
|
||||
if address & 2 == 0 {
|
||||
self.reload = replace_byte(self.reload, address, value);
|
||||
} else if address & 1 == 0 {
|
||||
self.set_control(value, index);
|
||||
}
|
||||
}
|
||||
|
||||
fn enabled(&self) -> bool {
|
||||
self.control & 0x80 != 0
|
||||
}
|
||||
|
|
@ -162,6 +183,9 @@ impl Io {
|
|||
if (DMA_BASE..=DMA_END).contains(&address) {
|
||||
return Some(self.dma.read8(address));
|
||||
}
|
||||
if (TIMER_BASE..=TIMER_BASE + 15).contains(&address) {
|
||||
return Some(self.timers[((address - TIMER_BASE) / 4) as usize].read8(address));
|
||||
}
|
||||
let value = match address & !1 {
|
||||
DISPCNT => self.dispcnt,
|
||||
GREENSWAP => u16::from(self.greenswap),
|
||||
|
|
@ -176,14 +200,6 @@ impl Io {
|
|||
BLDALPHA => self.effects.alpha,
|
||||
KEYINPUT => self.keypad.keyinput(),
|
||||
KEYCNT => self.keypad.control(),
|
||||
0x0400_0100..=0x0400_010e => {
|
||||
let timer = &self.timers[((address - TIMER_BASE) / 4) as usize];
|
||||
if address & 2 == 0 {
|
||||
timer.counter
|
||||
} else {
|
||||
u16::from(timer.control)
|
||||
}
|
||||
}
|
||||
IE => self.enable,
|
||||
IF => self.pending,
|
||||
WAITCNT => self.waitcnt,
|
||||
|
|
@ -258,13 +274,7 @@ impl Io {
|
|||
}
|
||||
0x0400_0100..=0x0400_010e => {
|
||||
let index = ((address - TIMER_BASE) / 4) as usize;
|
||||
let timer = &mut self.timers[index];
|
||||
if address & 2 == 0 {
|
||||
// Merge into the reload latch, never the visible counter.
|
||||
timer.reload = replace_byte(timer.reload, address, value);
|
||||
} else if address & 1 == 0 {
|
||||
timer.set_control(value, index);
|
||||
}
|
||||
self.timers[index].write8(address, value, index);
|
||||
}
|
||||
IE => self.enable = replace_byte(self.enable, address, value) & IRQ_MASK,
|
||||
WAITCNT => self.waitcnt = replace_byte(self.waitcnt, address, value) & 0x5fff,
|
||||
|
|
@ -388,7 +398,29 @@ impl Io {
|
|||
self.wake_if_requested();
|
||||
}
|
||||
|
||||
pub(crate) fn advance(&mut self, cycles: u32) {
|
||||
pub(crate) fn timer_step(&self) -> TimerStep {
|
||||
TimerStep::new(self)
|
||||
}
|
||||
|
||||
pub(crate) fn commit_timer_step(&mut self, step: TimerStep) {
|
||||
self.timers = step.timers;
|
||||
self.pending = (self.pending & !TIMER_IRQ_MASK) | step.pending;
|
||||
self.wake_if_requested();
|
||||
}
|
||||
|
||||
fn advance_timer_bank(timers: &mut [Timer; 4], cycles: u32) -> u16 {
|
||||
let mut pending = 0;
|
||||
let mut overflows = 0;
|
||||
for (index, timer) in timers.iter_mut().enumerate() {
|
||||
overflows = timer.advance(cycles, overflows);
|
||||
if overflows != 0 && timer.control & 0x40 != 0 {
|
||||
pending |= 1 << (3 + index);
|
||||
}
|
||||
}
|
||||
pending
|
||||
}
|
||||
|
||||
pub(crate) fn advance(&mut self, cycles: u32, advance_timers: bool) {
|
||||
// Hardware state tracks time even when host-side frame capture is disabled.
|
||||
let position = self.display.position();
|
||||
let phase = u32::from(position.scanline) * CYCLES_PER_LINE + u32::from(position.line_cycle);
|
||||
|
|
@ -420,13 +452,8 @@ impl Io {
|
|||
let (vblank, hblank) = self.display.dma_events(cycles);
|
||||
self.dma.trigger(vblank, hblank);
|
||||
self.pending |= self.display.advance(cycles);
|
||||
let mut overflows = 0;
|
||||
for (index, timer) in self.timers.iter_mut().enumerate() {
|
||||
overflows = timer.advance(cycles, overflows);
|
||||
if overflows != 0 && timer.control & 0x40 != 0 {
|
||||
// Pending flags latch even if IE, IME, or CPSR.I blocks delivery.
|
||||
self.pending |= 1 << (3 + index);
|
||||
}
|
||||
if advance_timers {
|
||||
self.pending |= Self::advance_timer_bank(&mut self.timers, cycles);
|
||||
}
|
||||
self.wake_if_requested();
|
||||
}
|
||||
|
|
|
|||
55
crates/gba-core/src/io/timer_step.rs
Normal file
55
crates/gba-core/src/io/timer_step.rs
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
//! Staged timer clocks for one machine 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;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) struct TimerStep {
|
||||
pub(super) timers: [Timer; 4],
|
||||
pub(super) pending: u16,
|
||||
elapsed: u32,
|
||||
}
|
||||
|
||||
impl TimerStep {
|
||||
pub(super) fn new(io: &Io) -> Self {
|
||||
Self {
|
||||
timers: io.timers,
|
||||
pending: io.pending & TIMER_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 |= Io::advance_timer_bank(&mut self.timers, elapsed - self.elapsed);
|
||||
self.elapsed = elapsed;
|
||||
}
|
||||
|
||||
/// All lanes of a bus access observe the same staged time.
|
||||
pub(crate) fn read8(&self, address: u32, committed: u8) -> Option<u8> {
|
||||
match address {
|
||||
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),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return true only when the staged state owns the entire register byte.
|
||||
pub(crate) fn write8(&mut self, address: u32, value: u8) -> bool {
|
||||
match address {
|
||||
TIMER_BASE..=0x0400_010f => {
|
||||
let index = ((address - TIMER_BASE) / 4) as usize;
|
||||
self.timers[index].write8(address, value, index);
|
||||
true
|
||||
}
|
||||
IF => {
|
||||
self.pending &= !u16::from(value);
|
||||
false // The normal I/O path must also acknowledge non-timer sources.
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -155,8 +155,8 @@ 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.
|
||||
/// CPU writes take effect before this bulk device update. Within-instruction
|
||||
/// register timing and IRQ synchronization delays are not modeled. Opcode
|
||||
/// Timer data 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.
|
||||
/// Row-capture diagnostics are deferred to Memory::present_frame instead.
|
||||
|
|
@ -178,15 +178,21 @@ impl Machine {
|
|||
self.last_timing = timing;
|
||||
return Ok(StepKind::HaltIdle);
|
||||
}
|
||||
let (kind, timing) = if let Some(timing) = self.cpu.take_irq_timed(&mut self.memory) {
|
||||
(StepKind::IrqEntry, timing)
|
||||
self.memory.begin_timer_step();
|
||||
let result = if let Some(timing) = self.cpu.take_irq_timed(&mut self.memory) {
|
||||
Ok((StepKind::IrqEntry, timing))
|
||||
} else {
|
||||
(
|
||||
StepKind::Instruction,
|
||||
self.cpu
|
||||
.step_timed(&mut self.memory)
|
||||
.map_err(MachineError::Cpu)?,
|
||||
)
|
||||
self.cpu
|
||||
.step_timed(&mut self.memory)
|
||||
.map(|timing| (StepKind::Instruction, timing))
|
||||
.map_err(MachineError::Cpu)
|
||||
};
|
||||
let (kind, timing) = match result {
|
||||
Ok(result) => result,
|
||||
Err(error) => {
|
||||
self.memory.discard_timer_step();
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
self.memory.complete_cpu_step(timing.total());
|
||||
self.last_timing = timing;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ mod iwram_history_tests;
|
|||
#[cfg(test)]
|
||||
mod prefetch_tests;
|
||||
#[cfg(test)]
|
||||
mod timer_step_tests;
|
||||
#[cfg(test)]
|
||||
mod timing_event_tests;
|
||||
pub(crate) use fetch::InstructionFetch;
|
||||
use iwram_bus::IwramBus;
|
||||
|
|
@ -20,7 +22,7 @@ use crate::{
|
|||
display::{DisplayPosition, CYCLES_PER_LINE, HBLANK_START, VISIBLE_LINES},
|
||||
dma::{DmaError, DMA_BASE, DMA_END},
|
||||
input::Buttons,
|
||||
io::{Io, HALTCNT, KEYINPUT, POSTFLG},
|
||||
io::{Io, TimerStep, HALTCNT, KEYINPUT, POSTFLG},
|
||||
timing::{AccessKind, AccessWidth, CpuTiming, Prefetch, StepTiming},
|
||||
video::{self, sprites::pipeline, Framebuffer, VideoError},
|
||||
};
|
||||
|
|
@ -82,7 +84,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.
|
||||
/// Complete bus ownership and per-access timing remain unmodeled.
|
||||
/// Timer data accesses use staged machine-step time; other per-access device timing remains unmodeled.
|
||||
pub struct Memory {
|
||||
external_ram: Vec<u8>,
|
||||
internal_ram: Vec<u8>,
|
||||
|
|
@ -94,6 +96,7 @@ pub struct Memory {
|
|||
io: Io,
|
||||
cycles: u64,
|
||||
cpu_timing: Cell<Option<CpuTiming>>,
|
||||
timer_step: Cell<Option<TimerStep>>,
|
||||
gamepak_prefetch: Prefetch,
|
||||
#[cfg(test)]
|
||||
last_cpu_timing: Option<CpuTiming>,
|
||||
|
|
@ -122,6 +125,7 @@ impl Memory {
|
|||
io: Io::default(),
|
||||
cycles: 0,
|
||||
cpu_timing: Cell::new(None),
|
||||
timer_step: Cell::new(None),
|
||||
gamepak_prefetch: Prefetch::default(),
|
||||
#[cfg(test)]
|
||||
last_cpu_timing: None,
|
||||
|
|
@ -167,10 +171,41 @@ impl Memory {
|
|||
pub(crate) fn complete_cpu_step(&mut self, cycles: u32) {
|
||||
// IRQ entry consumes the resume too, without executing an instruction.
|
||||
self.cpu_resume_nonsequential = false;
|
||||
self.advance_running_cycles(cycles);
|
||||
self.complete_timer_step(cycles);
|
||||
}
|
||||
|
||||
pub(crate) fn begin_timer_step(&self) {
|
||||
debug_assert!(self.timer_step.get().is_none());
|
||||
self.timer_step.set(Some(self.io.timer_step()));
|
||||
}
|
||||
|
||||
pub(crate) fn discard_timer_step(&self) {
|
||||
self.timer_step.set(None);
|
||||
}
|
||||
|
||||
fn advance_timer_step(&self, cycles: u32) {
|
||||
if let Some(mut step) = self.timer_step.get() {
|
||||
step.advance_to(cycles);
|
||||
self.timer_step.set(Some(step));
|
||||
}
|
||||
}
|
||||
|
||||
fn complete_timer_step(&mut self, cycles: u32) {
|
||||
self.advance_timer_step(cycles);
|
||||
let step = self
|
||||
.timer_step
|
||||
.take()
|
||||
.expect("machine timer transaction is active");
|
||||
self.io.commit_timer_step(step);
|
||||
// Timers already reached this boundary. Advance other devices exactly once.
|
||||
self.advance_running_cycles_with_timers(cycles, false);
|
||||
}
|
||||
|
||||
fn advance_running_cycles(&mut self, cycles: u32) {
|
||||
self.advance_running_cycles_with_timers(cycles, true);
|
||||
}
|
||||
|
||||
fn advance_running_cycles_with_timers(&mut self, cycles: u32, advance_timers: bool) {
|
||||
if cycles != 0 && !self.sprite_pipeline.started {
|
||||
// Synthetic reset begins on row0, with no preceding row227 to prepare it.
|
||||
self.prepare_sprite_row(0);
|
||||
|
|
@ -182,14 +217,14 @@ impl Memory {
|
|||
u32::from(position.scanline) * CYCLES_PER_LINE + u32::from(position.line_cycle);
|
||||
let mut advanced = 0;
|
||||
for (offset, row) in pipeline::latest_events(phase, cycles).into_iter().flatten() {
|
||||
self.io.advance(offset - advanced);
|
||||
self.io.advance(offset - advanced, advance_timers);
|
||||
self.prepare_sprite_row(row);
|
||||
advanced = offset;
|
||||
}
|
||||
self.io.advance(cycles - advanced);
|
||||
self.io.advance(cycles - advanced, advance_timers);
|
||||
} else {
|
||||
// Split at display and sprite-preparation edges. CPU/DMA writes have
|
||||
// already committed before these nominal clocks; no per-access timing.
|
||||
// Split at display and sprite-preparation edges. Video writes have
|
||||
// already committed. Machine timer transactions skip this bulk timer clock.
|
||||
let mut remaining = cycles;
|
||||
while remaining != 0 {
|
||||
let position = self.io.display_position();
|
||||
|
|
@ -202,7 +237,7 @@ impl Memory {
|
|||
let until =
|
||||
display_edge.min(pipeline::next_event(position.scanline, position.line_cycle));
|
||||
let step = remaining.min(until);
|
||||
self.io.advance(step);
|
||||
self.io.advance(step, advance_timers);
|
||||
remaining -= step;
|
||||
if step == until {
|
||||
let position = self.io.display_position();
|
||||
|
|
@ -316,28 +351,6 @@ impl Memory {
|
|||
AccessWidth::Byte => unreachable!("DMA is never byte-wide"),
|
||||
}
|
||||
.map_err(map_error)?;
|
||||
let data_latch = if transfer.source < 0x0200_0000 {
|
||||
// DMA cannot read BIOS or the lower unused region. Retain this channel's
|
||||
// full word; never read firmware bytes or borrow CPU/another channel's data.
|
||||
transfer
|
||||
.data_latch
|
||||
.expect("unknown blocked source rejected above")
|
||||
} else {
|
||||
match transfer.width {
|
||||
AccessWidth::Halfword => {
|
||||
u32::from(self.read16(transfer.source).map_err(map_error)?) * 0x0001_0001
|
||||
}
|
||||
AccessWidth::Word => self.read32(transfer.source).map_err(map_error)?,
|
||||
AccessWidth::Byte => unreachable!("DMA is never byte-wide"),
|
||||
}
|
||||
};
|
||||
// A blocked halfword read keeps both old lanes. The destination selects
|
||||
// one lane; mapped halfword reads have already duplicated their source data.
|
||||
let value = match transfer.width {
|
||||
AccessWidth::Halfword => data_latch >> ((transfer.destination & 2) * 8),
|
||||
AccessWidth::Word => data_latch,
|
||||
AccessWidth::Byte => unreachable!("DMA is never byte-wide"),
|
||||
};
|
||||
let kind = if transfer.first {
|
||||
AccessKind::NonSequential
|
||||
} else {
|
||||
|
|
@ -348,19 +361,50 @@ impl Memory {
|
|||
let internal_cycles = if transfer.first { 2 } else { 0 };
|
||||
// Keep the existing whole-unit DMA startup model; no sub-instruction arbitration.
|
||||
prefetch.advance(internal_cycles);
|
||||
let source_cycles = prefetch.data(transfer.source, transfer.width, kind);
|
||||
let destination_cycles = prefetch.data(transfer.destination, transfer.width, kind);
|
||||
let timing = StepTiming {
|
||||
code_cycles: 0,
|
||||
data_cycles: prefetch.data(transfer.source, transfer.width, kind)
|
||||
+ prefetch.data(transfer.destination, transfer.width, kind),
|
||||
data_cycles: source_cycles + destination_cycles,
|
||||
internal_cycles,
|
||||
idle_cycles: 0,
|
||||
};
|
||||
match transfer.width {
|
||||
AccessWidth::Halfword => self.write16(transfer.destination, value as u16),
|
||||
AccessWidth::Word => self.write32(transfer.destination, value),
|
||||
AccessWidth::Byte => unreachable!("DMA is never byte-wide"),
|
||||
}
|
||||
.map_err(map_error)?;
|
||||
self.begin_timer_step();
|
||||
let result = (|| {
|
||||
self.advance_timer_step(internal_cycles + source_cycles);
|
||||
let data_latch = if transfer.source < 0x0200_0000 {
|
||||
// Blocked reads retain channel data, not firmware or another channel's value.
|
||||
transfer
|
||||
.data_latch
|
||||
.expect("unknown blocked source rejected above")
|
||||
} else {
|
||||
match transfer.width {
|
||||
AccessWidth::Halfword => u32::from(self.read16(transfer.source)?) * 0x0001_0001,
|
||||
AccessWidth::Word => self.read32(transfer.source)?,
|
||||
AccessWidth::Byte => unreachable!("DMA is never byte-wide"),
|
||||
}
|
||||
};
|
||||
// Blocked halfword reads retain both lanes; the destination selects a lane.
|
||||
let value = match transfer.width {
|
||||
AccessWidth::Halfword => data_latch >> ((transfer.destination & 2) * 8),
|
||||
AccessWidth::Word => data_latch,
|
||||
AccessWidth::Byte => unreachable!("DMA is never byte-wide"),
|
||||
};
|
||||
self.advance_timer_step(timing.total());
|
||||
match transfer.width {
|
||||
AccessWidth::Halfword => self.write16(transfer.destination, value as u16),
|
||||
AccessWidth::Word => self.write32(transfer.destination, value),
|
||||
AccessWidth::Byte => unreachable!("DMA is never byte-wide"),
|
||||
}?;
|
||||
Ok::<_, MemoryError>((data_latch, value))
|
||||
})();
|
||||
let (data_latch, value) = match result {
|
||||
Ok(result) => result,
|
||||
Err(error) => {
|
||||
self.discard_timer_step();
|
||||
return Err(map_error(error));
|
||||
}
|
||||
};
|
||||
// Only a successful unit commits queue progress. A WAITCNT destination can reset it.
|
||||
prefetch.configure(self.waitcnt());
|
||||
self.gamepak_prefetch = prefetch;
|
||||
|
|
@ -373,7 +417,7 @@ impl Memory {
|
|||
.dma_access(transfer.destination, transfer.width, value);
|
||||
// Keep this channel active during its cycles: another edge must not
|
||||
// queue a second block while the current block is still transferring.
|
||||
self.advance_cycles(timing.total());
|
||||
self.complete_timer_step(timing.total());
|
||||
self.io.complete_dma_unit(channel, data_latch);
|
||||
self.cpu_resume_nonsequential = true;
|
||||
Ok(Some((channel, timing)))
|
||||
|
|
@ -566,6 +610,7 @@ impl Memory {
|
|||
if let Some(mut trace) = self.cpu_timing.get() {
|
||||
trace.code(address, width, kind);
|
||||
self.cpu_timing.set(Some(trace));
|
||||
self.advance_timer_step(trace.total.total());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -576,6 +621,7 @@ impl Memory {
|
|||
if let Some(mut trace) = self.cpu_timing.get() {
|
||||
trace.internal(cycles);
|
||||
self.cpu_timing.set(Some(trace));
|
||||
self.advance_timer_step(trace.total.total());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -583,12 +629,13 @@ impl Memory {
|
|||
if let Some(mut trace) = self.cpu_timing.get() {
|
||||
trace.data(self.waitcnt(), address, width);
|
||||
self.cpu_timing.set(Some(trace));
|
||||
self.advance_timer_step(trace.total.total());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read8(&self, address: u32) -> Result<u8, MemoryError> {
|
||||
let value = self.read_byte(address)?;
|
||||
self.record_access(address, AccessWidth::Byte);
|
||||
let value = self.read_byte(address)?;
|
||||
self.iwram_bus
|
||||
.access(address, AccessWidth::Byte, u32::from(value));
|
||||
Ok(value)
|
||||
|
|
@ -596,6 +643,13 @@ impl Memory {
|
|||
|
||||
// Multi-byte reads use this helper to avoid charging each byte as a bus access.
|
||||
fn read_byte(&self, address: u32) -> Result<u8, MemoryError> {
|
||||
if address >> 24 == 0x04 {
|
||||
if let Some(step) = self.timer_step.get() {
|
||||
if let Some(value) = step.read8(address, self.io.read8(address).unwrap_or(0)) {
|
||||
return Ok(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
if address < BIOS_SIZE as u32
|
||||
&& self.bios.is_some()
|
||||
&& self
|
||||
|
|
@ -694,6 +748,13 @@ impl Memory {
|
|||
if matches!(byte_address, POSTFLG | HALTCNT) && !self.can_write_power_control() {
|
||||
continue; // BIOS-only CPU writes; ignored elsewhere, including STOP requests.
|
||||
}
|
||||
if let Some(mut step) = self.timer_step.get() {
|
||||
let handled = step.write8(byte_address, byte);
|
||||
self.timer_step.set(Some(step));
|
||||
if handled {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
self.io.write8(byte_address, byte);
|
||||
}
|
||||
if address & !3 == crate::io::WAITCNT {
|
||||
|
|
@ -786,8 +847,8 @@ impl Memory {
|
|||
if address & 1 != 0 {
|
||||
return Err(MemoryError::Unaligned(address));
|
||||
}
|
||||
let value = u16::from_le_bytes([self.read_byte(address)?, self.read_byte(address + 1)?]);
|
||||
self.record_access(address, AccessWidth::Halfword);
|
||||
let value = u16::from_le_bytes([self.read_byte(address)?, self.read_byte(address + 1)?]);
|
||||
self.iwram_bus
|
||||
.access(address, AccessWidth::Halfword, u32::from(value));
|
||||
Ok(value)
|
||||
|
|
@ -798,13 +859,13 @@ impl Memory {
|
|||
if address & 3 != 0 {
|
||||
return Err(MemoryError::Unaligned(address));
|
||||
}
|
||||
self.record_access(address, AccessWidth::Word);
|
||||
let value = u32::from_le_bytes([
|
||||
self.read_byte(address)?,
|
||||
self.read_byte(address + 1)?,
|
||||
self.read_byte(address + 2)?,
|
||||
self.read_byte(address + 3)?,
|
||||
]);
|
||||
self.record_access(address, AccessWidth::Word);
|
||||
self.iwram_bus.access(address, AccessWidth::Word, value);
|
||||
Ok(value)
|
||||
}
|
||||
|
|
|
|||
346
crates/gba-core/src/memory/timer_step_tests.rs
Normal file
346
crates/gba-core/src/memory/timer_step_tests.rs
Normal file
|
|
@ -0,0 +1,346 @@
|
|||
//! Original checks of timer transaction ownership and bus-completion sampling.
|
||||
use super::{timing_event_tests::prepared, *};
|
||||
use crate::{
|
||||
cpu::Cpu,
|
||||
display::HBLANK_START,
|
||||
dma::DMA_STRIDE,
|
||||
io::{DISPSTAT, HALTCNT, IE, IF, IME, TIMER_BASE, WAITCNT},
|
||||
machine::{Machine, StepKind},
|
||||
};
|
||||
|
||||
const NOP: u32 = 0xe1a0_0000;
|
||||
const RAM: u32 = 0x0300_1000;
|
||||
|
||||
fn start(memory: &mut Memory, index: u32, reload: u16, control: u16) {
|
||||
memory
|
||||
.write32(
|
||||
TIMER_BASE + index * 4,
|
||||
u32::from(reload) | u32::from(control) << 16,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn dma(memory: &mut Memory, source: u32, destination: u32, count: u16) {
|
||||
let base = DMA_BASE + 3 * DMA_STRIDE;
|
||||
memory.write32(base, source).unwrap();
|
||||
memory.write32(base + 4, destination).unwrap();
|
||||
memory
|
||||
.write32(base + 8, 0x8400_0000 | u32::from(count))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arm_and_thumb_timer_loads_observe_source_and_data_but_not_trailing_internal_cycles() {
|
||||
for (thumb, instruction, sample, final_count) in
|
||||
[(false, 0xe591_0000, 7, 8), (true, 0x6808, 4, 5)]
|
||||
{
|
||||
let (cpu, mut memory) = prepared(thumb, &[instruction], &[(1, TIMER_BASE)]);
|
||||
start(&mut memory, 0, 0, 0x80);
|
||||
let mut machine = Machine::new(cpu, memory);
|
||||
machine.step().unwrap();
|
||||
assert_eq!(machine.cpu().registers()[0], 0x0080_0000 | sample);
|
||||
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), final_count);
|
||||
assert_eq!(machine.cycles(), u64::from(final_count));
|
||||
assert!(machine.memory().timer_step.get().is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn byte_halfword_and_word_lanes_share_one_completed_access() {
|
||||
for (instruction, address, expected) in [
|
||||
(0xe5d1_0000, TIMER_BASE, 6),
|
||||
(0xe5d1_0000, TIMER_BASE + 1, 1),
|
||||
(0xe1d1_00b0, TIMER_BASE, 0x106),
|
||||
(0xe591_0000, TIMER_BASE, 0x0080_0106),
|
||||
] {
|
||||
let (cpu, mut memory) = prepared(false, &[instruction], &[(1, address)]);
|
||||
start(&mut memory, 0, 0xff, 0x80);
|
||||
let mut machine = Machine::new(cpu, memory);
|
||||
machine.step().unwrap();
|
||||
assert_eq!(machine.cpu().registers()[0], expected);
|
||||
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 0x107);
|
||||
assert_eq!(machine.cycles(), 8);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn starting_and_stopping_timers_take_effect_after_the_store_bus_cycle() {
|
||||
for running in [false, true] {
|
||||
let control = if running { 0 } else { 0x0080_0000 };
|
||||
let (cpu, mut memory) =
|
||||
prepared(false, &[0xe581_0000, NOP], &[(0, control), (1, TIMER_BASE)]);
|
||||
if running {
|
||||
start(&mut memory, 0, 0, 0x80);
|
||||
}
|
||||
let mut machine = Machine::new(cpu, memory);
|
||||
machine.step().unwrap();
|
||||
assert_eq!(machine.cycles(), 7);
|
||||
assert_eq!(
|
||||
machine.memory().read16(TIMER_BASE).unwrap(),
|
||||
if running { 7 } else { 0 }
|
||||
);
|
||||
machine.step().unwrap(); // Store left N: this ROM fetch costs eight cycles.
|
||||
assert_eq!(
|
||||
machine.memory().read16(TIMER_BASE).unwrap(),
|
||||
if running { 7 } else { 8 }
|
||||
);
|
||||
assert_eq!(machine.cycles(), 15);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_timer_accesses_keep_distinct_word_phases() {
|
||||
let (cpu, mut memory) = prepared(false, &[0xe891_0005], &[(1, TIMER_BASE)]); // LDM r1,{r0,r2}
|
||||
start(&mut memory, 0, 0, 0x80);
|
||||
start(&mut memory, 1, 0, 0x80);
|
||||
let mut machine = Machine::new(cpu, memory);
|
||||
machine.step().unwrap();
|
||||
assert_eq!(machine.cpu().registers()[0], 0x0080_0007);
|
||||
assert_eq!(machine.cpu().registers()[2], 0x0080_0008);
|
||||
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 9);
|
||||
assert_eq!(machine.memory().read16(TIMER_BASE + 4).unwrap(), 9);
|
||||
|
||||
let (cpu, memory) = prepared(
|
||||
false,
|
||||
&[0xe880_0006],
|
||||
&[(0, TIMER_BASE), (1, 0x0080_0000), (2, 0x0080_0000)],
|
||||
);
|
||||
let mut machine = Machine::new(cpu, memory);
|
||||
machine.step().unwrap();
|
||||
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 1);
|
||||
assert_eq!(machine.memory().read16(TIMER_BASE + 4).unwrap(), 0);
|
||||
assert_eq!(machine.cycles(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reload_changes_preserve_pre_write_overflows_and_prescaler_phase() {
|
||||
// Overflow during source fetch uses the old reload, not the value stored afterward.
|
||||
let (cpu, mut memory) = prepared(false, &[0xe1c1_00b0], &[(0, 0x1234), (1, TIMER_BASE)]);
|
||||
start(&mut memory, 0, 0xfffa, 0xc0);
|
||||
let mut machine = Machine::new(cpu, memory);
|
||||
machine.step().unwrap();
|
||||
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 0xfffb);
|
||||
assert_eq!(machine.memory().read16(IF).unwrap(), 8);
|
||||
machine.memory_mut().advance_cycles(5);
|
||||
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 0x1234);
|
||||
|
||||
// A read crossing a prescaler edge must retain the remainder for the next step.
|
||||
let (cpu, mut memory) = prepared(false, &[0xe591_0000], &[(1, TIMER_BASE)]);
|
||||
start(&mut memory, 0, 0, 0x81);
|
||||
memory.advance_cycles(60);
|
||||
let mut machine = Machine::new(cpu, memory);
|
||||
machine.step().unwrap();
|
||||
assert_eq!(machine.cpu().registers()[0], 0x0081_0001);
|
||||
machine.memory_mut().advance_cycles(59);
|
||||
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 1);
|
||||
machine.memory_mut().advance_cycles(1);
|
||||
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cascades_and_timer_irq_bits_are_visible_at_the_read_phase() {
|
||||
let (cpu, mut memory) = prepared(false, &[0xe591_0000], &[(1, TIMER_BASE + 4)]);
|
||||
start(&mut memory, 0, 0xfffe, 0xc0);
|
||||
start(&mut memory, 1, 0xfffe, 0xc4);
|
||||
let mut machine = Machine::new(cpu, memory);
|
||||
machine.step().unwrap(); // Source+data=7 gives three timer0 pulses and one timer1 overflow.
|
||||
assert_eq!(machine.cpu().registers()[0], 0x00c4_ffff);
|
||||
assert_eq!(machine.memory().read16(TIMER_BASE + 4).unwrap(), 0xfffe);
|
||||
assert_eq!(machine.memory().read16(IF).unwrap(), 0x18);
|
||||
|
||||
let (cpu, mut memory) = prepared(false, &[0xe591_0000], &[(1, IE)]);
|
||||
start(&mut memory, 0, 0xfffa, 0xc0);
|
||||
memory.write16(IE, 8).unwrap();
|
||||
memory.write16(IME, 1).unwrap();
|
||||
let mut machine = Machine::new(cpu, memory);
|
||||
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
|
||||
assert_eq!(machine.cpu().registers()[0], 0x0008_0008); // Word IE+IF, coherent timer flag.
|
||||
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn if_acknowledgement_clears_earlier_timer_events_but_not_later_ones() {
|
||||
for (instruction, reload, expected) in [(0xe1c1_00b0, 0xfffa, 0), (0xe141_0092, 0xffff, 8)] {
|
||||
let (cpu, mut memory) = prepared(false, &[instruction], &[(0, 8), (1, IF), (2, 8)]);
|
||||
start(&mut memory, 0, reload, 0xc0);
|
||||
let mut machine = Machine::new(cpu, memory);
|
||||
machine.step().unwrap();
|
||||
assert_eq!(machine.memory().read16(IF).unwrap(), expected);
|
||||
if instruction == 0xe141_0092 {
|
||||
// SWPB acknowledges at the second data access; its final I cycle raises IF again.
|
||||
assert_eq!(machine.cpu().registers()[0], 8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_instructions_discard_partial_timer_progress_and_pending_irqs() {
|
||||
for (instruction, base) in [
|
||||
(0xe591_0000, 0x0e00_0000),
|
||||
(0xe891_0005, TIMER_BASE + 12),
|
||||
(0xe881_0005, TIMER_BASE + 12),
|
||||
(0xffff_ffff, 0),
|
||||
] {
|
||||
let (cpu, mut memory) = prepared(false, &[instruction], &[(1, base)]);
|
||||
start(&mut memory, 0, 0xfffc, 0xc0);
|
||||
let before = memory.io.timer_step();
|
||||
let cpu_before = cpu.clone();
|
||||
let mut machine = Machine::new(cpu, memory);
|
||||
for _ in 0..2 {
|
||||
assert!(machine.step().is_err());
|
||||
assert_eq!(machine.memory().io.timer_step(), before);
|
||||
assert_eq!(machine.cpu(), &cpu_before);
|
||||
assert_eq!(machine.cycles(), 0);
|
||||
assert!(machine.memory().timer_step.get().is_none());
|
||||
assert!(machine.memory().cpu_timing.get().is_none());
|
||||
}
|
||||
}
|
||||
// A missing current instruction fails before CpuTiming starts, but must still discard TimerStep.
|
||||
let mut memory = Memory::new(Vec::new()).unwrap();
|
||||
start(&mut memory, 0, 0xffff, 0xc0);
|
||||
let mut machine = Machine::new(Cpu::new(ROM_START), memory);
|
||||
assert!(machine.step().is_err());
|
||||
assert!(machine.memory().timer_step.get().is_none());
|
||||
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 0xffff);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cpu_only_apis_and_host_reads_do_not_advance_timer_devices() {
|
||||
for prefetch in [0, 0x4000] {
|
||||
for timed in [false, true] {
|
||||
let (mut cpu, mut memory) = prepared(false, &[0xe591_0000], &[(1, TIMER_BASE)]);
|
||||
memory.write16(WAITCNT, prefetch).unwrap();
|
||||
start(&mut memory, 0, 123, 0x80);
|
||||
let before = memory.io.timer_step();
|
||||
if timed {
|
||||
cpu.step_timed(&mut memory).unwrap();
|
||||
} else {
|
||||
cpu.step(&mut memory).unwrap();
|
||||
}
|
||||
assert_eq!(cpu.registers()[0], 0x0080_007b);
|
||||
assert_eq!(memory.read16(TIMER_BASE).unwrap(), 123);
|
||||
assert_eq!(memory.io.timer_step(), before);
|
||||
assert_eq!(memory.cycles(), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dma_samples_timers_after_startup_and_source_and_writes_after_destination() {
|
||||
let mut memory = Memory::new(Vec::new()).unwrap();
|
||||
start(&mut memory, 0, 0, 0x80);
|
||||
dma(&mut memory, TIMER_BASE, RAM, 1);
|
||||
memory.step_dma().unwrap().unwrap();
|
||||
assert_eq!(memory.read32(RAM).unwrap(), 0x0080_0003);
|
||||
assert_eq!(memory.read16(TIMER_BASE).unwrap(), 4);
|
||||
assert_eq!(memory.cycles(), 4);
|
||||
|
||||
let mut memory = Memory::new(Vec::new()).unwrap();
|
||||
memory.write32(RAM, 0x0080_0000).unwrap();
|
||||
memory.write32(RAM + 4, 0x0080_0000).unwrap();
|
||||
dma(&mut memory, RAM, TIMER_BASE, 2);
|
||||
memory.step_dma().unwrap().unwrap();
|
||||
assert_eq!(memory.read16(TIMER_BASE).unwrap(), 0);
|
||||
memory.step_dma().unwrap().unwrap();
|
||||
assert_eq!(memory.read16(TIMER_BASE).unwrap(), 2);
|
||||
assert_eq!(memory.read16(TIMER_BASE + 4).unwrap(), 0);
|
||||
assert_eq!(memory.cycles(), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dma_failures_preserve_timers_and_if_and_clear_the_transaction() {
|
||||
for (source, destination) in [(0x0e00_0000, RAM), (TIMER_BASE, ROM_START)] {
|
||||
let mut memory = Memory::new(Vec::new()).unwrap();
|
||||
start(&mut memory, 0, 0xffff, 0xc0);
|
||||
let before = memory.io.timer_step();
|
||||
dma(&mut memory, source, destination, 1);
|
||||
assert!(memory.step_dma().is_err());
|
||||
assert_eq!(memory.io.timer_step(), before);
|
||||
assert!(memory.timer_step.get().is_none());
|
||||
assert_eq!(memory.cycles(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_capture_splits_do_not_advance_committed_timers_twice() {
|
||||
for capture in [false, true] {
|
||||
let (cpu, mut memory) = prepared(false, &[0xe891_007d], &[(1, 0x0200_1000)]);
|
||||
memory.set_scanline_rendering(capture);
|
||||
memory.advance_cycles(HBLANK_START - 2);
|
||||
start(&mut memory, 0, 0, 0x80);
|
||||
let mut machine = Machine::new(cpu, memory);
|
||||
machine.step().unwrap(); // Source 6, six EWRAM words 36, internal 1.
|
||||
assert_eq!(machine.last_timing().total(), 43);
|
||||
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 43);
|
||||
assert_eq!(machine.cycles(), u64::from(HBLANK_START - 2 + 43));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timer_flag_commit_preserves_other_irq_sources() {
|
||||
for (instruction, expected) in [(0xe1d1_00b0, 0xa), (0xe1c1_00b0, 2)] {
|
||||
let (cpu, mut memory) = prepared(false, &[instruction], &[(0, 8), (1, IF)]);
|
||||
memory.write16(DISPSTAT, 0x10).unwrap();
|
||||
memory.advance_cycles(HBLANK_START);
|
||||
assert_eq!(memory.read16(IF).unwrap(), 2);
|
||||
start(&mut memory, 0, 0xfffa, 0xc0);
|
||||
let mut machine = Machine::new(cpu, memory);
|
||||
machine.step().unwrap();
|
||||
assert_eq!(machine.memory().read16(IF).unwrap(), expected);
|
||||
if instruction == 0xe1d1_00b0 {
|
||||
assert_eq!(machine.cpu().registers()[0], 0xa);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bios_power_store_pays_its_cycles_and_new_timer_irq_wakes_only_halt() {
|
||||
for stop in [false, true] {
|
||||
let (mut cpu, mut memory) = prepared(
|
||||
false,
|
||||
&[0xe12f_ff1c],
|
||||
&[(0, if stop { 0x80 } else { 0 }), (1, HALTCNT), (12, 0x100)],
|
||||
);
|
||||
memory.bios.as_mut().unwrap()[0x100..0x104].copy_from_slice(&0xe5c1_0000_u32.to_le_bytes());
|
||||
cpu.step(&mut memory).unwrap(); // BX into original BIOS STRB r0,[r1].
|
||||
start(&mut memory, 0, 0xffff, 0xc0);
|
||||
memory.write16(IE, 8).unwrap();
|
||||
let mut machine = Machine::new(cpu, memory);
|
||||
machine.step().unwrap();
|
||||
assert_eq!(machine.cycles(), 2);
|
||||
assert_eq!(machine.memory().read16(IF).unwrap(), 8);
|
||||
assert!(!machine.halted());
|
||||
assert_eq!(machine.stopped(), stop);
|
||||
if stop {
|
||||
let before = machine.memory().io.timer_step();
|
||||
assert_eq!(machine.step().unwrap(), StepKind::StopIdle);
|
||||
assert_eq!(machine.memory().io.timer_step(), before);
|
||||
assert_eq!(machine.cycles(), 2);
|
||||
} else {
|
||||
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn halt_and_stop_keep_their_clock_ownership() {
|
||||
for stop in [false, true] {
|
||||
let mut memory = Memory::new(Vec::new()).unwrap();
|
||||
start(&mut memory, 0, 0, 0x80);
|
||||
memory.write8(HALTCNT, if stop { 0x80 } else { 0 }).unwrap();
|
||||
let mut machine = Machine::new(Cpu::new(ROM_START), memory);
|
||||
assert_eq!(
|
||||
machine.step().unwrap(),
|
||||
if stop {
|
||||
StepKind::StopIdle
|
||||
} else {
|
||||
StepKind::HaltIdle
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
u64::from(machine.memory().read16(TIMER_BASE).unwrap()),
|
||||
machine.cycles()
|
||||
);
|
||||
assert!(machine.memory().timer_step.get().is_none());
|
||||
}
|
||||
}
|
||||
|
|
@ -93,7 +93,8 @@ pub fn bus_cycles(waitcnt: u16, address: u32, width: AccessWidth, kind: AccessKi
|
|||
}
|
||||
|
||||
/// One speculative instruction/IRQ timing transaction. Events arrive in source,
|
||||
/// data, internal, then target-pair order. Device clocks still advance in bulk.
|
||||
/// data, internal, then target-pair order. Machine timers follow these events;
|
||||
/// other device clocks still advance in bulk.
|
||||
/// Code uses the entry WAITCNT snapshot; data uses settings at each bus access.
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub(crate) struct CpuTiming {
|
||||
|
|
|
|||
|
|
@ -65,8 +65,8 @@ fn timer_demo_configures_hardware_in_cpu_code_and_handles_exactly_one_irq() {
|
|||
assert_eq!(machine.cpu().registers()[10], 1);
|
||||
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
|
||||
assert_eq!(machine.memory().read16(TIMER_BASE + 2).unwrap(), 0);
|
||||
// Persistent fetch kinds move costs between setup instructions and the running timer.
|
||||
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 0xfffb);
|
||||
// Start no longer pays its preceding nine cycles; stop now pays its two bus cycles.
|
||||
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 0xfff4);
|
||||
for _ in 0..100 {
|
||||
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,20 +42,17 @@ fn observed_extra_cycle_is_in_rom_data_cancellation_not_internal_work() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn unadjusted_timer_samples_expose_the_instruction_boundary_sampling_limit() {
|
||||
// This is a known-gap regression, not an assertion that hardware behaves this way.
|
||||
// Replace it when timer I/O moves to ordered bus phases; do not correct probe output.
|
||||
fn unadjusted_timer_samples_match_published_observations_at_the_bus_access() {
|
||||
// Keep the original program and published values unchanged. No result correction.
|
||||
for (index, expected) in PUBLISHED_READ_DELTAS.into_iter().enumerate() {
|
||||
for (waitcnt, expected) in WAITCNT_SETTINGS.into_iter().zip(expected) {
|
||||
let control = measure(waitcnt, index as u8 + 1, false).unwrap();
|
||||
let read = measure(waitcnt, index as u8 + 1, true).unwrap();
|
||||
let sampled = i64::from(read.timer_sample) - i64::from(control.timer_sample);
|
||||
let boundary = read.boundary_cycles as i64 - control.boundary_cycles as i64;
|
||||
let endpoint_fetch_difference = i64::from(read.sample_timing.code_cycles)
|
||||
- i64::from(control.sample_timing.code_cycles);
|
||||
assert_ne!(sampled, i64::from(expected));
|
||||
assert!(endpoint_fetch_difference > 0);
|
||||
assert_eq!(boundary - sampled, endpoint_fetch_difference);
|
||||
assert_eq!(sampled, i64::from(expected));
|
||||
assert_eq!(boundary, sampled);
|
||||
assert!(read.sample_timing.code_cycles > control.sample_timing.code_cycles);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue