WIP: adding load/store unit

This commit is contained in:
Jacob Lifshay 2026-07-10 19:16:13 -07:00
parent bc2bcf4434
commit b2c4befa21
Signed by: programmerjake
SSH key fingerprint: SHA256:HnFTLGpSm4Q4Fj502oCFisjZSoakwEuTsJJMSke63RQ
4 changed files with 670 additions and 3 deletions

View file

@ -8,6 +8,7 @@ use crate::{
MOpVariantVisitOps, MOpVariantVisitor, MOpVisitVariants, RenamedMOp, mop_enum, MOpVariantVisitOps, MOpVariantVisitor, MOpVisitVariants, RenamedMOp, mop_enum,
}, },
rename_execute_retire::ExecuteToUnitInterface, rename_execute_retire::ExecuteToUnitInterface,
unit::load_store::LoadStoreToDCacheInterface,
}; };
use fayalite::{ use fayalite::{
bundle::{Bundle, BundleType}, bundle::{Bundle, BundleType},
@ -18,6 +19,7 @@ use serde::{Deserialize, Serialize};
use std::ops::ControlFlow; use std::ops::ControlFlow;
pub mod alu_branch; pub mod alu_branch;
pub mod load_store;
macro_rules! all_units { macro_rules! all_units {
( (
@ -333,7 +335,7 @@ all_units! {
#[create_dyn_unit_fn = |config, unit_index, filter| todo!()] #[create_dyn_unit_fn = |config, unit_index, filter| todo!()]
#[extract(transformed_move_mop, transformed_move_mop_sim, transformed_move_mop_sim_ref, transformed_move_mop_sim_mut)] #[extract(transformed_move_mop, transformed_move_mop_sim, transformed_move_mop_sim_ref, transformed_move_mop_sim_mut)]
TransformedMove(TransformedMoveOp), TransformedMove(TransformedMoveOp),
#[create_dyn_unit_fn = |config, unit_index, filter| todo!()] #[create_dyn_unit_fn = |config, unit_index, filter| load_store::LoadStore::new(config, unit_index, filter).to_dyn()]
#[extract(load_store_mop, load_store_mop_sim, load_store_mop_sim_ref, load_store_mop_sim_mut)] #[extract(load_store_mop, load_store_mop_sim, load_store_mop_sim_ref, load_store_mop_sim_mut)]
LoadStore(LoadStoreMOp<DestReg, SrcReg>), LoadStore(LoadStoreMOp<DestReg, SrcReg>),
} }
@ -391,6 +393,7 @@ impl RenamedMOpFilter for () {
pub struct UnitIO { pub struct UnitIO {
pub cd: Option<Expr<ClockDomain>>, pub cd: Option<Expr<ClockDomain>>,
pub from_execute: Expr<ExecuteToUnitInterface<PhantomConst<CpuConfig>>>, pub from_execute: Expr<ExecuteToUnitInterface<PhantomConst<CpuConfig>>>,
pub to_d_cache: Option<Expr<LoadStoreToDCacheInterface<PhantomConst<CpuConfig>>>>,
} }
pub trait UnitTrait: pub trait UnitTrait:

View file

@ -1617,6 +1617,7 @@ impl UnitTrait for AluBranch {
UnitIO { UnitIO {
cd: None, cd: None,
from_execute: this.from_execute, from_execute: this.from_execute,
to_d_cache: None,
} }
} }

View file

@ -0,0 +1,659 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
// See Notices.txt for copyright information
use crate::{
config::{CpuConfig, PhantomConstCpuConfig},
instruction::{
COMMON_MOP_SRC_LEN, CommonMOp, LoadMOp, LoadStoreCommonMOp, LoadStoreMOp, PRegNum,
},
main_memory_and_io::MemoryOperationErrorKind,
next_pc::{CallStackOp, SimValueDefault},
register::PRegValue,
rename_execute_retire::{
ExecuteToUnitInterface, GlobalState, MOpId, MOpInstance, NextPcPredictorOp, RenamedMOp,
UnitCausedCancel, UnitEnqueue, UnitFinishCauseCancel, UnitInputsReady,
UnitMOpCantCauseCancel, UnitMOpIsNoLongerSpeculative, UnitOutputReady,
},
unit::{DynUnit, DynUnitWrapper, RenamedMOpFilter, UnitIO, UnitKind, UnitMOp, UnitTrait},
};
use fayalite::{
intern::Interned,
prelude::*,
ty::{SimValueDebug, SimValueDisplay, TraceAsStringSimValue},
util::ready_valid::ReadyValid,
};
use serde::{Deserialize, Serialize};
use std::{collections::VecDeque, fmt};
#[hdl]
pub struct DCacheOpKindLoad {
/// if this is `false`, then if the address refers to a valid cache line, that cache line must have any dirty data
/// written back to memory and then be set to invalid before performing the load.
pub is_cacheable: Bool,
/// if this is `true`, then the load must only look in the cache (must not propagate to the L2/L3 cache or to
/// memory), must not cause any earlier operations' timing to change because this load exists, and must not change
/// any cache state that remains after all operations finish, including LRU state, random number generators,
/// predictive prefetching state, etc.
/// Speculative loads that miss the cache must return [`DCacheFinishStatus::NotFound`].
pub is_speculative: Bool,
}
#[hdl]
pub struct DCacheOpKindStore {
/// if this is `false`, then if the address refers to a valid cache line, that cache line must have any dirty data
/// written back to memory and then be set to invalid before performing the store.
pub is_cacheable: Bool,
}
#[hdl]
pub enum DCacheOpKind {
Load(DCacheOpKindLoad),
Store(DCacheOpKindStore),
}
#[hdl]
pub struct DCacheStart<C: PhantomConstGet<CpuConfig>> {
pub kind: DCacheOpKind,
/// unaligned addresses may cause a load/store to cross a cache-line boundary
pub address: UInt<64>,
pub data_and_mask: Array<HdlOption<UInt<8>>, 8>,
pub config: C,
}
#[hdl]
pub enum DCacheFinishStatus {
/// The operation finished successfully
Success,
/// A speculative load missed the cache, so data was not returned.
NotFound,
MemoryError(MemoryOperationErrorKind),
}
#[hdl]
pub struct DCacheFinish<C: PhantomConstGet<CpuConfig>> {
pub status: DCacheFinishStatus,
pub data: Array<UInt<8>, 8>,
pub config: C,
}
#[hdl]
pub struct LoadStoreToDCacheInterface<C: PhantomConstGet<CpuConfig>> {
pub start: ReadyValid<DCacheStart<C>>,
#[hdl(flip)]
pub finish: ReadyValid<DCacheFinish<C>>,
pub config: C,
}
#[hdl(custom_debug(sim))]
struct OpDebugState {
v: SimOnly<String>,
}
impl SimValueDebug for OpDebugState {
#[hdl]
fn sim_value_debug(
value: &<Self as Type>::SimValue,
f: &mut fmt::Formatter<'_>,
) -> fmt::Result {
f.write_str(&value.v)
}
}
impl SimValueDefault for OpDebugState {
#[hdl]
fn sim_value_default(self) -> SimValue<Self> {
thread_local! {
static VALUE: SimOnlyValue<String> = SimOnlyValue::new("None".into());
}
VALUE.with(|v| {
#[hdl(sim)]
Self { v }
})
}
}
#[derive(Debug)]
struct Op<C: PhantomConstCpuConfig> {
id: SimValue<MOpId>,
pc: SimValue<UInt<64>>,
size_in_bytes: u8,
mop: SimValue<LoadStoreMOp<PRegNum<C>, PRegNum<C>>>,
is_speculative: bool,
src_values: Option<[SimValue<TraceAsString<PRegValue>>; COMMON_MOP_SRC_LEN]>,
dest_value: Option<SimValue<TraceAsString<PRegValue>>>,
sent_cant_cause_cancel: bool,
sent_output_ready: bool,
config: C,
}
impl<C: PhantomConstCpuConfig> fmt::Display for Op<C> {
#[hdl]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Self {
id,
pc,
size_in_bytes,
mop,
is_speculative,
src_values,
dest_value,
sent_cant_cause_cancel,
sent_output_ready,
config: _,
} = self;
if *is_speculative {
f.write_str("(s)")?;
}
if *sent_cant_cause_cancel {
f.write_str("(sccc)")?;
}
if *sent_output_ready {
f.write_str("(sor)")?;
}
write!(f, "id={id:?} pc={pc:?} sz={size_in_bytes}: {mop:?}")?;
if let Some(src_values) = src_values {
write!(f, "src_values={src_values:?}")?;
}
if let Some(dest_value) = dest_value {
write!(f, "dest_value={dest_value:?}")?;
}
Ok(())
}
}
impl<C: PhantomConstCpuConfig> Op<C> {
#[hdl]
fn debug_state(&self) -> SimValue<OpDebugState> {
#[hdl(sim)]
OpDebugState {
v: SimOnlyValue::new(self.to_string()),
}
}
}
#[hdl(get(|c| c.rob_size.get().next_power_of_two()))]
type OpsDebugLen<C: PhantomConstGet<CpuConfig>> = DynSize;
#[hdl(no_static)]
pub struct LoadStoreDebugState<C: PhantomConstGet<CpuConfig>> {
global_state: GlobalState,
ops: ArrayType<TraceAsString<OpDebugState>, OpsDebugLen<C>>,
canceling: Bool,
config: C,
}
#[derive(Debug)]
struct LoadStoreState<C: PhantomConstCpuConfig> {
global_state: SimValue<GlobalState>,
ops: VecDeque<Op<C>>,
canceling: bool,
config: C,
unit_index: usize,
filter: LoadStoreFilter,
}
impl<C: PhantomConstCpuConfig> LoadStoreState<C> {
fn new(config: C, unit_index: usize, filter: LoadStoreFilter) -> Self {
Self {
global_state: GlobalState.sim_value_default(),
ops: VecDeque::new(),
canceling: false,
config,
unit_index,
filter,
}
}
#[hdl]
fn debug_state(&self) -> SimValue<LoadStoreDebugState<C>> {
let Self {
ref global_state,
ops: _,
canceling,
config,
unit_index: _,
filter: _,
} = *self;
let ty = LoadStoreDebugState[config];
let mut ops = vec![OpDebugState.sim_value_default().into_trace_as_string(); ty.ops.len()];
for op in &self.ops {
ops[op.id.as_int() as usize % ty.ops.len()] = op.debug_state().into_trace_as_string();
}
#[hdl(sim)]
LoadStoreDebugState::<_> {
global_state,
ops,
canceling,
config,
}
}
#[track_caller]
fn op_by_id(&mut self, id: &<MOpId as Type>::SimValue) -> &mut Op<C> {
match self.ops.iter_mut().find(|op| *op.id == *id) {
Some(op) => op,
None => panic!("can't find load_store Op with id {id:?}"),
}
}
fn from_execute_enqueue_ready(&self) -> bool {
!self.canceling
&& self.ops.len() < self.config.get().unit_max_in_flight(self.unit_index).get()
}
#[hdl]
fn from_execute_enqueue(&mut self, enqueue: SimValue<UnitEnqueue<C>>) {
#[hdl(sim)]
let UnitEnqueue::<_> { mop, config: _ } = enqueue;
#[hdl(sim)]
let MOpInstance::<_> {
fetch_block_id: _,
id,
pc,
predicted_next_pc: _,
size_in_bytes,
is_first_mop_in_insn: _,
is_last_mop_in_insn: _,
mop,
} = mop;
let mop = #[hdl(sim)]
match SimValue::into_value(mop).into_inner() {
UnitMOp::<_, _, _>::LoadStore(mop) => mop,
_ => unreachable!(),
};
self.ops.push_back(Op {
id,
pc,
size_in_bytes: size_in_bytes.cast_to_static::<UInt<8>>().as_int(),
mop,
is_speculative: true,
src_values: None,
dest_value: None,
sent_cant_cause_cancel: false,
sent_output_ready: false,
config: self.config,
});
}
#[hdl]
fn from_execute_inputs_ready(&mut self, inputs_ready: SimValue<UnitInputsReady<C>>) {
#[hdl(sim)]
let UnitInputsReady::<_> {
mop,
src_values,
config: _,
} = inputs_ready;
self.op_by_id(&mop.id).src_values = Some(SimValue::into_value(src_values));
}
#[hdl]
fn from_execute_is_no_longer_speculative(
&mut self,
is_no_longer_speculative: SimValue<UnitMOpIsNoLongerSpeculative<C>>,
) {
#[hdl(sim)]
let UnitMOpIsNoLongerSpeculative::<_> { id, config: _ } = is_no_longer_speculative;
self.op_by_id(&id).is_speculative = false;
}
fn from_execute_cant_cause_cancel(&self) -> Option<SimValue<UnitMOpCantCauseCancel<C>>> {
// TODO: look for operations that can't cause cancels
None
}
#[hdl]
fn from_execute_sent_cant_cause_cancel(
&mut self,
cant_cause_cancel: SimValue<UnitMOpCantCauseCancel<C>>,
) {
#[hdl(sim)]
let UnitMOpCantCauseCancel::<_> { id, config: _ } = cant_cause_cancel;
self.op_by_id(&id).sent_cant_cause_cancel = true;
}
#[hdl]
fn from_execute_output_ready(&self) -> Option<SimValue<UnitOutputReady<C>>> {
todo!()
}
#[hdl]
fn from_execute_sent_output_ready(&mut self, output_ready: SimValue<UnitOutputReady<C>>) {
#[hdl(sim)]
let UnitOutputReady::<_> {
id,
dest_value: _,
predictor_op: _,
} = output_ready;
self.op_by_id(&id).sent_output_ready = true;
}
#[hdl]
fn from_execute_finish_cause_cancel(&self) -> Option<SimValue<UnitFinishCauseCancel<C>>> {
todo!()
}
#[hdl]
fn from_execute_sent_finish_cause_cancel(
&mut self,
finish_cause_cancel: SimValue<UnitFinishCauseCancel<C>>,
) {
let op = self
.ops
.pop_front_if(|front| front.id == finish_cause_cancel.id);
assert!(
op.is_some(),
"inconsistent state -- sent finish_cause_cancel but id doesn't match ops.front():\n{finish_cause_cancel:#?}\nfront={front:#?}",
front = self.ops.front(),
);
}
fn from_execute_cancel_all_ready(&self) -> bool {
true
}
#[hdl]
fn from_execute_cancel_all(&mut self) {
todo!()
}
#[hdl]
fn to_d_cache_start_data(&self) -> Option<SimValue<DCacheStart<C>>> {
todo!()
}
#[hdl]
fn to_d_cache_sent_start(&mut self, start: SimValue<DCacheStart<C>>) {
todo!()
}
#[hdl]
fn to_d_cache_finish_ready(&self) -> bool {
true
}
#[hdl]
fn to_d_cache_finish(&mut self, finish: SimValue<DCacheFinish<C>>) {
todo!()
}
}
#[hdl]
async fn load_store_impl(
config: PhantomConst<CpuConfig>,
unit_index: usize,
filter: LoadStoreFilter,
cd: Expr<ClockDomain>,
from_execute: Expr<ExecuteToUnitInterface<PhantomConst<CpuConfig>>>,
to_d_cache: Expr<LoadStoreToDCacheInterface<PhantomConst<CpuConfig>>>,
debug_state: Expr<LoadStoreDebugState<PhantomConst<CpuConfig>>>,
mut sim: ExternModuleSimulationState,
) {
let mut state = LoadStoreState::new(config, unit_index, filter);
loop {
{
#[hdl]
let ExecuteToUnitInterface::<_> {
global_state: _,
enqueue,
inputs_ready: _,
is_no_longer_speculative: _,
cant_cause_cancel,
output_ready,
finish_cause_cancel,
unit_outputs_ready: _,
cancel_all,
config: _,
} = from_execute;
sim.write(enqueue.ready, state.from_execute_enqueue_ready())
.await;
sim.write(cant_cause_cancel, state.from_execute_cant_cause_cancel())
.await;
sim.write(output_ready, state.from_execute_output_ready())
.await;
sim.write(
finish_cause_cancel,
state.from_execute_finish_cause_cancel(),
)
.await;
sim.write(cancel_all.ready, state.from_execute_cancel_all_ready())
.await;
}
{
#[hdl]
let LoadStoreToDCacheInterface::<_> {
start,
finish,
config: _,
} = to_d_cache;
sim.write(start.data, state.to_d_cache_start_data()).await;
sim.write(finish.ready, state.to_d_cache_finish_ready())
.await;
}
sim.write(debug_state, state.debug_state()).await;
sim.wait_for_clock_edge(cd.clk).await;
{
#[hdl]
let ExecuteToUnitInterface::<_> {
global_state,
enqueue,
inputs_ready,
is_no_longer_speculative,
cant_cause_cancel,
output_ready,
finish_cause_cancel,
unit_outputs_ready,
cancel_all,
config: _,
} = from_execute;
state.global_state = sim.read_past(global_state, cd.clk).await;
if sim.read_past_bool(enqueue.ready, cd.clk).await {
#[hdl(sim)]
if let HdlSome(enqueue) = sim.read_past(enqueue.data, cd.clk).await {
state.from_execute_enqueue(enqueue);
}
}
#[hdl(sim)]
if let HdlSome(inputs_ready) = sim.read_past(inputs_ready, cd.clk).await {
state.from_execute_inputs_ready(inputs_ready);
}
#[hdl(sim)]
if let HdlSome(is_no_longer_speculative) =
sim.read_past(is_no_longer_speculative, cd.clk).await
{
state.from_execute_is_no_longer_speculative(is_no_longer_speculative);
}
if sim.read_past_bool(unit_outputs_ready, cd.clk).await {
#[hdl(sim)]
if let HdlSome(cant_cause_cancel) = sim.read_past(cant_cause_cancel, cd.clk).await {
state.from_execute_sent_cant_cause_cancel(cant_cause_cancel);
}
#[hdl(sim)]
if let HdlSome(output_ready) = sim.read_past(output_ready, cd.clk).await {
state.from_execute_sent_output_ready(output_ready);
}
#[hdl(sim)]
if let HdlSome(finish_cause_cancel) =
sim.read_past(finish_cause_cancel, cd.clk).await
{
state.from_execute_sent_finish_cause_cancel(finish_cause_cancel);
}
}
if sim.read_past_bool(cancel_all.ready, cd.clk).await {
#[hdl(sim)]
if let HdlSome(cancel_all) = sim.read_past(cancel_all.data, cd.clk).await {
#[hdl]
let () = cancel_all;
state.from_execute_cancel_all();
}
}
}
{
#[hdl]
let LoadStoreToDCacheInterface::<_> {
start,
finish,
config: _,
} = to_d_cache;
if sim.read_past_bool(start.ready, cd.clk).await {
#[hdl(sim)]
if let HdlSome(start) = sim.read_past(start.data, cd.clk).await {
state.to_d_cache_sent_start(start);
}
}
if sim.read_past_bool(finish.ready, cd.clk).await {
#[hdl(sim)]
if let HdlSome(finish) = sim.read_past(finish.data, cd.clk).await {
state.to_d_cache_finish(finish);
}
}
}
}
}
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
struct LoadStoreFilter {
load: bool,
store: bool,
}
impl LoadStoreFilter {
fn new(
from_execute: ExecuteToUnitInterface<PhantomConst<CpuConfig>>,
filter: &mut impl RenamedMOpFilter,
) -> Self {
let LoadStoreMOp { Load, Store } = from_execute
.inputs_ready
.HdlSome
.mop
.mop
.inner_ty()
.LoadStore;
Self {
load: filter.should_include_ty(Load),
store: filter.should_include_ty(Store),
}
}
}
#[hdl_module(extern)]
pub fn load_store(
config: PhantomConst<CpuConfig>,
unit_index: usize,
filter: &mut impl RenamedMOpFilter,
) {
#[hdl]
let cd: ClockDomain = m.input();
#[hdl]
let from_execute: ExecuteToUnitInterface<PhantomConst<CpuConfig>> =
m.input(ExecuteToUnitInterface[config]);
#[hdl]
let to_d_cache: LoadStoreToDCacheInterface<PhantomConst<CpuConfig>> =
m.output(LoadStoreToDCacheInterface[config]);
#[hdl]
let debug_state: LoadStoreDebugState<PhantomConst<CpuConfig>> =
m.output(LoadStoreDebugState[config]);
let filter = LoadStoreFilter::new(from_execute.ty(), filter);
assert_eq!(config.get().units[unit_index].kind, UnitKind::LoadStore);
m.register_clock_for_past(cd.clk);
m.extern_module_simulation_fn(
(
config,
unit_index,
filter,
cd,
from_execute,
to_d_cache,
debug_state,
),
async |(config, unit_index, filter, cd, from_execute, to_d_cache, debug_state), mut sim| {
sim.resettable(
cd,
async |mut sim| {
#[hdl]
let ExecuteToUnitInterface::<_> {
global_state: _,
enqueue,
inputs_ready: _,
is_no_longer_speculative: _,
cant_cause_cancel,
output_ready,
finish_cause_cancel,
unit_outputs_ready: _,
cancel_all,
config: _,
} = from_execute;
sim.write(enqueue.ready, false).await;
sim.write(cant_cause_cancel, cant_cause_cancel.ty().HdlNone())
.await;
sim.write(output_ready, output_ready.ty().HdlNone()).await;
sim.write(finish_cause_cancel, finish_cause_cancel.ty().HdlNone())
.await;
sim.write(cancel_all.ready, false).await;
#[hdl]
let LoadStoreToDCacheInterface::<_> {
start,
finish,
config: _,
} = to_d_cache;
sim.write(start.data, start.ty().data.HdlNone()).await;
sim.write(finish.ready, false).await;
sim.write(
debug_state,
LoadStoreState::new(config, unit_index, filter).debug_state(),
)
.await;
},
async |sim, ()| {
load_store_impl(
config,
unit_index,
filter,
cd,
from_execute,
to_d_cache,
debug_state,
sim,
)
.await
},
)
.await
},
);
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct LoadStore {
config: PhantomConst<CpuConfig>,
module: Interned<Module<load_store>>,
}
impl LoadStore {
pub fn new(
config: PhantomConst<CpuConfig>,
unit_index: usize,
filter: &mut impl RenamedMOpFilter,
) -> Self {
Self {
config,
module: load_store(config, unit_index, filter),
}
}
}
impl UnitTrait for LoadStore {
type Type = load_store;
fn ty(&self) -> Self::Type {
self.module.io_ty()
}
fn unit_kind(&self) -> UnitKind {
UnitKind::LoadStore
}
fn module(&self) -> Interned<Module<Self::Type>> {
self.module
}
fn io(&self, this: Expr<Self::Type>) -> UnitIO {
UnitIO {
cd: Some(this.cd),
from_execute: this.from_execute,
to_d_cache: Some(this.to_d_cache),
}
}
fn to_dyn(&self) -> DynUnit {
DynUnitWrapper(*self).to_dyn()
}
}

View file

@ -94,13 +94,17 @@ fn formal_harness(
let UnitIO { let UnitIO {
cd: unit_cd, cd: unit_cd,
from_execute: unit_from_execute, from_execute: unit_from_execute,
to_d_cache,
} = dyn_unit.io(unit); } = dyn_unit.io(unit);
if let Some(unit_cd) = unit_cd {
connect(unit_cd, cd);
}
connect( connect(
unit_from_execute, unit_from_execute,
ExecuteToUnitInterfaces::unit_fields(decode_and_run.to_units)[unit_index], ExecuteToUnitInterfaces::unit_fields(decode_and_run.to_units)[unit_index],
); );
if let Some(unit_cd) = unit_cd { if let Some(to_d_cache) = to_d_cache {
connect(unit_cd, cd); unimplemented!("to_d_cache for {unit:?}: {to_d_cache:?}");
} }
} }
hdl_assert(cd.clk, !decode_and_run.error, ""); hdl_assert(cd.clk, !decode_and_run.error, "");