MemoryMod — storage across a bus
MemoryMod is the far side of the kernel boundary. It wraps a Memory (see below), makes it a
SimObj, and gives it AXI-MM endpoints — so storage the kernel reaches out to becomes a real
participant in the discrete-event simulation, with latency, contention, and transactions.
This is the class that answers “what is a bag of bytes doing in my simulation?” — nothing, until a
MemoryMod wraps it.
from waveflow.hw.memory import MemoryMod
mem = MemoryMod(name="ddr", sim=sim, clk=clk, inline=False,
word_size=64, nwords_tot=8192,
latency_init=10, latency_per_word=1)
addr = mem.alloc(256)
Two ports, two stories
s_mm is an MMIFSlave — the AXI-MM port an external master connects to. This is the path that
models latency, and the one a kernel’s m_axi reaches across.
m_mm is a directly-backed master, zero latency, for the owner’s own use. It models a
component reading its own inline block — a local C array in HLS — so no bus or access delay
applies. m_mm.array_ref() returns a direct view — the raw words, or, given an element type,
the same storage reinterpreted as those elements. It is a live view in both directions, or
a refusal: an element with no native numpy dtype is stored as its packed word, and referencing
one would have to deserialize into a fresh object whose writes reach nothing. The copying
read_array / write_array serve that case and say that they copy.
The inline flag picks which story you are telling. inline=True pre-allocates the full capacity as
one block and hands out direct views; inline=False is the external-DDR shape, where callers
alloc() regions and reach them over the bus.
A note on
inline=True. It models storage the owner treats as local. If what you actually want is storage the generated kernel emits and owns, that isHwState— codegen emits astaticfor it, whereas nothing is emitted for aMemoryMod.MemoryModis a simulation and testbench object; the kernel-side counterpart isHwState.
The latency model, and what composes
The memory models access latency; the interconnect models bus latency; the two compose
and are not double-counted. Each access on the s_mm path consumes
(latency_init + nwords * latency_per_word) / clk.freq
simulation seconds before touching the backing store. The interconnect adds its own request and return latency around that callback, so a read’s total time is
bus_request + memory_access + bus_return
half_duplex=True makes the slave’s read and write channels one shared resource, so reads and writes
to this memory mutually exclude — a single-port memory, or a DDR model that shares R/W bandwidth. The
default is full duplex, with independent AR/R and AW/W channels, which is what real AXI gives you.
In a generated testbench
A MemoryMod maps to a FlatMemory in the generated XSI testbench: the arena the AXI-MM slave
models serve out of. It is declared shared, which matters — two m_axi bundles (a gmem0 read and
a gmem1 write) backed by one memory means the emitter constructs the arena once and hands it to
both slave models, rather than making one per bundle.
Its load_segs / dump_segs are DynParams: regions loaded from burst bundles at pre_sim and
dumped back at post_sim. Both backends read the same bundles, so the pysim run and the RTL run are
driven from one scenario rather than two restatements of it.
mem.load_segs = [MemSeg(0, 0, "vectors/mem_in")]
mem.dump_segs = [MemSeg(0, nwords_tot, "vectors/out")]
The store underneath
MemoryMod wraps a Memory: a sparse word store where only the regions you allocate exist,
each backed by a NumPy array. Its surface is small, and word-typed rather than byte-typed —
addresses follow the addr_unit convention (MemMgr), but counts are always words.
addr = mem.alloc(nwords) # first-fit; returns an address in addr_unit terms
mem.free(addr) # whole segments only — the start address, not an offset
words = mem._mem.read(nwords, addr) # the raw word view
read and write raise if the range runs past the end of the allocated segment, so a wrong count
fails at the call rather than silently reading a neighbouring region. Use get_nwords to compute
the count for a typed array rather than deriving it by hand.
For moving typed arrays rather than raw words — packing a float32 array into ap_uint<W> words
and back — use the array serialization helpers, which are the same routines the HLS side generates.
See Array serialization & deserialization.
Memory is usable on its own when you want a store and no simulation; MemoryMod is what adds the
ports, the latency, and the lifecycle.
Contention is modelled where it belongs
Two masters on one memory serialize, and that is the interconnect’s job, not the memory’s — an
AXIMMCrossBarIF models the arbitration. Worth knowing: the pysim crossbar models contention, while
the XSI slave models do not. The two describe different systems on purpose, so a cycle count from one
is not a prediction of the other.
What it is not
- Not synthesizable. Nothing is emitted for a
MemoryMod; what is synthesizable is the kernel’sm_axiinterface to it. If you need storage the kernel owns and codegen emits, useHwState. - Not the allocator. Its
alloc/freeforward to the wrappedMemory, which delegates placement to aMemMgr. One policy, one implementation. - Not the bytes. That is
Memory, which aMemoryModwraps and which is perfectly usable on its own when you only need a store and no simulation.