cpu/crates/cpu/tests/fetch.rs

702 lines
23 KiB
Rust

// SPDX-License-Identifier: LGPL-3.0-or-later
// See Notices.txt for copyright information
use cpu::{
config::{CpuConfig, UnitConfig},
fetch::{FetchToDecodeInterface, FetchToDecodeInterfaceInner, fetch},
main_memory_and_io::{
MemoryInterface, MemoryOperationErrorKind, MemoryOperationFinish,
MemoryOperationFinishKind, MemoryOperationKind, MemoryOperationStart,
},
next_pc::{FETCH_BLOCK_ID_WIDTH, NextPcToFetchInterface, NextPcToFetchInterfaceInner},
unit::UnitKind,
util::array_vec::ArrayVec,
};
use fayalite::{
prelude::*,
sim::vcd::VcdWriterDecls,
util::{DebugAsDisplay, RcWriter},
};
use std::{cell::RefCell, collections::VecDeque, fmt, num::NonZeroUsize};
struct Random {
index: u64,
}
impl Random {
fn next(&mut self) -> u64 {
let index = self.index;
self.index = self.index.wrapping_add(1);
// make a pseudo-random number deterministically based on index
index
.wrapping_add(1)
.wrapping_mul(0x18C49126EABE7A0D) // random prime
.rotate_left(32)
.wrapping_mul(0x92B38C197608A6B) // random prime
.rotate_right(60)
}
}
const MEMORY_QUEUE_SIZE: usize = 16;
#[hdl]
struct MemoryQueueEntry {
addr: UInt<64>,
fetch_block_id: UInt<{ FETCH_BLOCK_ID_WIDTH }>,
cycles_left: UInt<8>,
}
impl MemoryQueueEntry {
#[hdl]
fn default_sim(self) -> SimValue<Self> {
#[hdl(sim)]
Self {
addr: 0u64,
fetch_block_id: self.fetch_block_id.zero(),
cycles_left: 0u8,
}
}
fn get_next_delay(random: &mut Random) -> u8 {
if random.next() % 32 == 0 { 30 } else { 5 }
}
}
const MEMORY_DATA: &str = "Test data, testing...\nTest Test!\nSecond Cache Line\nTesting.....\n";
const MEMORY_START: u64 = 0x1000;
const MEMORY_RANGE2: std::ops::Range<u64> = 0x2000..0x3000;
const MEMORY_ERROR_RANGE: std::ops::Range<u64> = 0x10F00..0x20F00;
const MEMORY_ERROR_STEP: u64 = 0x1000;
fn read_memory(start: u64, len: usize) -> Option<&'static [u8]> {
if MEMORY_ERROR_RANGE.contains(&start) {
let start = start - MEMORY_ERROR_RANGE.start;
let fail_at = start / MEMORY_ERROR_STEP;
let offset = start % MEMORY_ERROR_STEP;
return if offset < fail_at {
[0xFFu8; MEMORY_DATA.len()].get(..len)
} else {
None
};
}
if MEMORY_RANGE2.contains(&start) {
return [0xF2u8; MEMORY_DATA.len()].get(..len);
}
MEMORY_DATA
.as_bytes()
.get(start.checked_sub(MEMORY_START)?.try_into().ok()?..)?
.get(..len)
}
#[hdl_module(extern)]
fn mock_memory(config: PhantomConst<CpuConfig>) {
#[hdl]
let cd: ClockDomain = m.input();
#[hdl]
let memory_interface: MemoryInterface<PhantomConst<CpuConfig>> =
m.input(MemoryInterface[config]);
#[hdl]
let queue_debug: ArrayVec<MemoryQueueEntry, ConstUsize<{ MEMORY_QUEUE_SIZE }>> = m.output();
m.register_clock_for_past(cd.clk);
m.extern_module_simulation_fn(
(cd, memory_interface, queue_debug),
|(cd, memory_interface, queue_debug), mut sim| async move {
// intentionally have a different sequence each time we're reset
let random = RefCell::new(Random { index: 0 });
sim.resettable(
cd,
async |mut sim| {
#[hdl]
let MemoryInterface::<_> {
start,
finish,
next_fetch_block_ids,
config: _,
} = memory_interface;
sim.write(start.ready, false).await;
sim.write(finish.data, finish.ty().data.HdlNone()).await;
sim.write(next_fetch_block_ids, next_fetch_block_ids.ty().HdlNone())
.await;
sim.write(
queue_debug,
queue_debug.ty().new_sim(MemoryQueueEntry.default_sim()),
)
.await;
},
|sim, ()| run_fn(cd, memory_interface, queue_debug, &random, sim),
)
.await;
},
);
#[hdl]
async fn run_fn(
cd: Expr<ClockDomain>,
memory_interface: Expr<MemoryInterface<PhantomConst<CpuConfig>>>,
queue_debug: Expr<ArrayVec<MemoryQueueEntry, ConstUsize<{ MEMORY_QUEUE_SIZE }>>>,
random: &RefCell<Random>,
mut sim: ExternModuleSimulationState,
) {
let mut random = random.borrow_mut();
let config = memory_interface.config.ty();
let finish_data_ty = memory_interface.finish.data.ty();
let next_fetch_block_ids_ty = memory_interface.next_fetch_block_ids.ty();
let mut queue: VecDeque<SimValue<MemoryQueueEntry>> = VecDeque::new();
loop {
for entry in &mut queue {
entry.cycles_left = entry.cycles_left.as_int().saturating_sub(1).to_sim_value();
}
let sim_queue = queue_debug
.ty()
.from_iter_sim(MemoryQueueEntry.default_sim(), &queue)
.ok()
.expect("queue is known to be small enough");
sim.write(queue_debug, sim_queue).await;
sim.write_bool(
memory_interface.start.ready,
queue.len() < MEMORY_QUEUE_SIZE && random.next() % 32 != 0,
)
.await;
sim.write(
memory_interface.next_fetch_block_ids,
#[hdl(sim)]
next_fetch_block_ids_ty.HdlSome(
next_fetch_block_ids_ty
.HdlSome
.from_iter_sim(0u8, queue.iter().map(|entry| &entry.fetch_block_id))
.ok()
.expect("queue is known to be small enough"),
),
)
.await;
let finish_data = if let Some(entry) = queue
.front()
.filter(|entry| entry.cycles_left.as_int() == 0)
{
#[hdl(sim)]
let MemoryQueueEntry {
addr,
fetch_block_id: _,
cycles_left: _,
} = entry;
let addr = addr.as_int();
let mut read_data =
repeat(0u8, finish_data_ty.HdlSome.read_data.len()).to_sim_value();
let kind = if let Some(data) = read_memory(addr, read_data.len()) {
for (l, r) in read_data.iter_mut().zip(data) {
*l = r.to_sim_value();
}
#[hdl(sim)]
MemoryOperationFinishKind.Success(
#[hdl(sim)]
MemoryOperationKind.Read(),
)
} else {
#[hdl(sim)]
MemoryOperationFinishKind.Error(
#[hdl(sim)]
MemoryOperationErrorKind.Generic(),
)
};
#[hdl(sim)]
finish_data_ty.HdlSome(
#[hdl(sim)]
MemoryOperationFinish::<_> {
kind,
read_data,
config,
},
)
} else {
#[hdl(sim)]
finish_data_ty.HdlNone()
};
sim.write(memory_interface.finish.data, &finish_data).await;
sim.wait_for_clock_edge(cd.clk).await;
println!(
"Dump mock memory queue: {:#?}",
Vec::from_iter(queue.iter().map(|v| {
DebugAsDisplay(format!(
"fid={:#x} addr={:#x}",
v.fetch_block_id.as_int(),
v.addr.as_int(),
))
}))
);
if sim
.read_past_bool(memory_interface.start.ready, cd.clk)
.await
{
#[hdl(sim)]
if let HdlSome(memory_operation_start) =
sim.read_past(memory_interface.start.data, cd.clk).await
{
#[hdl(sim)]
let MemoryOperationStart::<_> {
kind,
addr,
write_data: _,
rw_mask,
fetch_block_id,
config: _,
} = memory_operation_start;
#[hdl(sim)]
match kind {
MemoryOperationKind::Read => {}
MemoryOperationKind::Write => unreachable!(),
}
assert!(
rw_mask.iter().all(|v| **v),
"rw_mask should be all true: {rw_mask:?}"
);
let entry = #[hdl(sim)]
MemoryQueueEntry {
addr,
fetch_block_id,
cycles_left: MemoryQueueEntry::get_next_delay(&mut random),
};
println!("mock memory start: {entry:#?}");
queue.push_back(entry);
}
}
if sim
.read_past_bool(memory_interface.finish.ready, cd.clk)
.await
{
#[hdl(sim)]
if let HdlSome(finish_data) = finish_data {
let Some(entry) = queue.pop_front() else {
unreachable!();
};
#[hdl(sim)]
let MemoryOperationFinish::<_> {
kind,
read_data,
config: _,
} = finish_data;
let kind = #[hdl(sim)]
match kind {
MemoryOperationFinishKind::Error(_) => Err(()),
MemoryOperationFinishKind::Success(_) => Ok(()),
};
println!(
"mock memory finish: kind={kind:?} read_data={read_data:?} {entry:#?}"
);
}
}
}
}
}
#[hdl_module]
fn dut(config: PhantomConst<CpuConfig>) {
#[hdl]
let cd: ClockDomain = m.input();
#[hdl]
let from_next_pc: NextPcToFetchInterface<PhantomConst<CpuConfig>> =
m.input(NextPcToFetchInterface[config]);
#[hdl]
let to_decode: FetchToDecodeInterface<PhantomConst<CpuConfig>> =
m.output(FetchToDecodeInterface[config]);
#[hdl]
let fetch = instance(fetch(config));
#[hdl]
let fetch {
cd: fetch_cd,
memory_interface: fetch_memory_interface,
from_next_pc: fetch_from_next_pc,
to_decode: fetch_to_decode,
} = fetch;
connect(fetch_cd, cd);
connect(fetch_from_next_pc, from_next_pc);
connect(to_decode, fetch_to_decode);
#[hdl]
let mock_memory = instance(mock_memory(config));
#[hdl]
let mock_memory {
cd: mock_memory_cd,
memory_interface: mock_memory_interface,
queue_debug: _,
} = mock_memory;
connect(mock_memory_cd, cd);
connect(mock_memory_interface, fetch_memory_interface);
}
#[derive(Clone)]
struct FetchTestOperation {
start_pc: u64,
fetch_block_id: u8,
fetch_block_data: [u8; FETCH_WIDTH_IN_BYTES],
error: Option<SimValue<MemoryOperationErrorKind>>,
}
impl PartialEq for FetchTestOperation {
#[hdl]
fn eq(&self, other: &Self) -> bool {
let Self {
start_pc,
fetch_block_id,
fetch_block_data,
ref error,
} = *self;
if let Some(error) = error {
#[hdl(sim)]
match error {
MemoryOperationErrorKind::Generic => {}
}
}
start_pc == other.start_pc
&& fetch_block_id == other.fetch_block_id
&& fetch_block_data == other.fetch_block_data
&& error.is_some() == other.error.is_some()
}
}
impl fmt::Debug for FetchTestOperation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Self {
start_pc,
fetch_block_id,
fetch_block_data,
error,
} = self;
let mut debug_struct = f.debug_struct("FetchTestOperation");
debug_struct.field("start_pc", &format_args!("{start_pc:#x}"));
debug_struct.field("fetch_block_id", &format_args!("{fetch_block_id:#x}"));
if fetch_block_data.iter().all(|v| *v == fetch_block_data[0]) {
debug_struct.field(
"fetch_block_data",
&format_args!(
"[b'{}'; {FETCH_WIDTH_IN_BYTES}]",
fetch_block_data[0].escape_ascii(),
),
);
} else {
debug_struct.field(
"fetch_block_data",
&format_args!("b\"{}\"", fetch_block_data.escape_ascii()),
);
}
debug_struct.field("error", error).finish()
}
}
const LOG2_FETCH_WIDTH_IN_BYTES: u8 = 3;
const FETCH_WIDTH_IN_BYTES: usize = 1 << LOG2_FETCH_WIDTH_IN_BYTES;
const LOG2_CACHE_LINE_SIZE_IN_BYTES: u8 = 5;
const CACHE_LINE_SIZE_IN_BYTES: usize = 1 << LOG2_CACHE_LINE_SIZE_IN_BYTES;
// needs to be a multiple of the cache line size
const _: [(); CACHE_LINE_SIZE_IN_BYTES * 2] = [(); MEMORY_DATA.len()];
fn fetch_test_operations() -> Vec<FetchTestOperation> {
#[track_caller]
fn mem_data(r: std::ops::RangeFrom<usize>) -> [u8; FETCH_WIDTH_IN_BYTES] {
*MEMORY_DATA[r]
.as_bytes()
.first_chunk()
.expect("start should be in-range")
}
#[hdl]
fn generic_error() -> SimValue<MemoryOperationErrorKind> {
#[hdl(sim)]
MemoryOperationErrorKind.Generic()
}
let mut last_fetch_block_id = 0u8.wrapping_sub(1);
macro_rules! op {
{
$($field:ident: $value:expr,)*
} => {
FetchTestOperation {
fetch_block_id: {
last_fetch_block_id = last_fetch_block_id.wrapping_add(1);
last_fetch_block_id
},
$($field: $value,)*
}
};
}
vec![
op! {
start_pc: 0x1000,
fetch_block_data: mem_data(0..),
error: None,
},
op! {
start_pc: 0x1010,
fetch_block_data: mem_data(0x10..),
error: None,
},
op! {
start_pc: 0x1020,
fetch_block_data: mem_data(0x20..),
error: None,
},
op! {
start_pc: 0x100,
fetch_block_data: [0; _],
error: Some(generic_error()),
},
op! {
start_pc: MEMORY_ERROR_RANGE.start,
fetch_block_data: [0; _],
error: Some(generic_error()),
},
op! {
start_pc: MEMORY_ERROR_RANGE.start + MEMORY_ERROR_STEP,
fetch_block_data: [0; _],
error: Some(generic_error()),
},
op! {
start_pc: MEMORY_ERROR_RANGE.start + MEMORY_ERROR_STEP * 2,
fetch_block_data: [0; _],
error: Some(generic_error()),
},
op! {
start_pc: MEMORY_ERROR_RANGE.start + MEMORY_ERROR_STEP * 3,
fetch_block_data: [0; _],
error: Some(generic_error()),
},
op! {
start_pc: 0x1000,
fetch_block_data: mem_data(0..),
error: None,
},
op! {
start_pc: 0x1010,
fetch_block_data: mem_data(0x10..),
error: None,
},
op! {
start_pc: 0x1020,
fetch_block_data: mem_data(0x20..),
error: None,
},
op! {
start_pc: 0x1000,
fetch_block_data: mem_data(0..),
error: None,
},
op! {
start_pc: 0x1010,
fetch_block_data: mem_data(0x10..),
error: None,
},
op! {
start_pc: 0x1020,
fetch_block_data: mem_data(0x20..),
error: None,
},
op! {
start_pc: MEMORY_RANGE2.start,
fetch_block_data: [0xF2; _],
error: None,
},
op! {
start_pc: MEMORY_RANGE2.start,
fetch_block_data: [0xF2; _],
error: None,
},
op! {
start_pc: MEMORY_RANGE2.start,
fetch_block_data: [0xF2; _],
error: None,
},
op! {
start_pc: MEMORY_RANGE2.start,
fetch_block_data: [0xF2; _],
error: None,
},
op! {
start_pc: MEMORY_RANGE2.start,
fetch_block_data: [0xF2; _],
error: None,
},
op! {
start_pc: MEMORY_RANGE2.start,
fetch_block_data: [0xF2; _],
error: None,
},
op! {
start_pc: MEMORY_RANGE2.start,
fetch_block_data: [0xF2; _],
error: None,
},
op! {
start_pc: 0x1000,
fetch_block_data: mem_data(0..),
error: None,
},
op! {
start_pc: 0x1000,
fetch_block_data: mem_data(0..),
error: None,
},
op! {
start_pc: 0x1000,
fetch_block_data: mem_data(0..),
error: None,
},
op! {
start_pc: 0x1000,
fetch_block_data: mem_data(0..),
error: None,
},
op! {
start_pc: 0x1000,
fetch_block_data: mem_data(0..),
error: None,
},
op! {
start_pc: 0x1000,
fetch_block_data: mem_data(0..),
error: None,
},
]
}
#[test]
#[hdl]
fn test_fetch() {
let _n = SourceLocation::normalize_files_for_tests();
let mut config = CpuConfig::new(
vec![
UnitConfig::new(UnitKind::AluBranch),
UnitConfig::new(UnitKind::AluBranch),
],
NonZeroUsize::new(20).unwrap(),
);
config.fetch_width = NonZeroUsize::new(2).unwrap();
config.log2_fetch_width_in_bytes = LOG2_FETCH_WIDTH_IN_BYTES;
config.log2_cache_line_size_in_bytes = LOG2_CACHE_LINE_SIZE_IN_BYTES;
config.log2_l1_i_cache_line_count = 4;
let m = dut(PhantomConst::new_sized(config));
let mut sim = Simulation::new(m);
let writer = RcWriter::default();
sim.add_trace_writer(VcdWriterDecls::new(writer.clone()));
struct DumpVcdOnDrop {
writer: Option<RcWriter>,
}
impl Drop for DumpVcdOnDrop {
fn drop(&mut self) {
if let Some(mut writer) = self.writer.take() {
let vcd = String::from_utf8(writer.take()).unwrap();
println!("####### VCD:\n{vcd}\n#######");
}
}
}
let mut writer = DumpVcdOnDrop {
writer: Some(writer),
};
let from_next_pc_ty = sim.io().from_next_pc.ty();
sim.write_clock(sim.io().cd.clk, false);
sim.write_reset(sim.io().cd.rst, true);
sim.write(
sim.io().from_next_pc.cancel.data,
from_next_pc_ty.cancel.data.HdlNone(),
);
sim.write(
sim.io().from_next_pc.fetch.data,
from_next_pc_ty.fetch.data.HdlNone(),
);
sim.write(sim.io().to_decode.fetched.ready, true);
sim.write(
sim.io().to_decode.next_fetch_block_ids,
HdlSome(
sim.io()
.ty()
.to_decode
.next_fetch_block_ids
.HdlSome
.new_sim(0u8),
),
);
let operations = fetch_test_operations();
let mut started_operations = 0;
let mut finished_operations = 0;
for cycle in 0..150 {
sim.write(
sim.io().from_next_pc.fetch.data,
if let Some(op) = operations.get(started_operations) {
#[hdl(sim)]
HdlSome(
#[hdl(sim)]
NextPcToFetchInterfaceInner {
start_pc: op.start_pc,
fetch_block_id: op.fetch_block_id,
},
)
} else {
#[hdl(sim)]
HdlNone()
},
);
sim.advance_time(SimDuration::from_nanos(500));
#[hdl(sim)]
if let HdlSome(next_fetch_block_ids) = sim.read(sim.io().from_next_pc.next_fetch_block_ids)
{
let next_fetch_block_ids = ArrayVec::elements_sim_ref(&next_fetch_block_ids);
let expected_next_fetch_block_ids = Vec::from_iter(
operations
.get(finished_operations..started_operations)
.unwrap_or(&[])
.iter()
.map(|op| op.fetch_block_id.to_sim_value()),
);
println!("expected_next_fetch_block_ids={expected_next_fetch_block_ids:?}");
assert_eq!(next_fetch_block_ids, expected_next_fetch_block_ids);
}
if sim.read_bool(sim.io().from_next_pc.fetch.ready) {
#[hdl(sim)]
if let HdlSome(_) = sim.read(sim.io().from_next_pc.fetch.data) {
println!("started fetch: {:#?}", operations[started_operations]);
started_operations += 1;
}
} else {
println!("not ready to start fetch");
}
if sim.read_bool(sim.io().to_decode.fetched.ready) {
#[hdl(sim)]
if let HdlSome(fetched) = sim.read(sim.io().to_decode.fetched.data) {
#[hdl(sim)]
let FetchToDecodeInterfaceInner::<_> {
start_pc,
fetch_block_id,
fetch_block_data,
error,
config: _,
} = &fetched;
let Some(expected_op) = operations.get(finished_operations) else {
panic!("too many finished operations: {fetched:#?}");
};
let op = FetchTestOperation {
start_pc: start_pc.as_int(),
fetch_block_id: fetch_block_id.as_int(),
error: #[hdl(sim)]
match error {
HdlSome(e) => Some(e.clone()),
HdlNone => None,
},
fetch_block_data: std::array::from_fn(|i| fetch_block_data[i].as_int()),
};
println!("finished fetch: op={op:#?}");
assert_eq!(
op, *expected_op,
"cycle={cycle} finished_operations={finished_operations}",
);
finished_operations += 1;
} else {
println!("not ready to finish fetch");
}
}
println!("clock tick: {cycle}");
sim.write_clock(sim.io().cd.clk, true);
sim.advance_time(SimDuration::from_nanos(500));
sim.write_clock(sim.io().cd.clk, false);
sim.write_reset(sim.io().cd.rst, false);
}
assert_eq!(finished_operations, operations.len());
let vcd = String::from_utf8(writer.writer.take().unwrap().take()).unwrap();
println!("####### VCD:\n{vcd}\n#######");
if vcd != include_str!("expected/fetch.vcd") {
panic!();
}
}