- Start Date: 2024-03-25
- RFC PR: amaranth-lang/rfcs#62
- Amaranth Issue: amaranth-lang/amaranth#1241
The MemoryData
class
Summary
A new class, amaranth.hdl.MemoryData
, is added to represent the data and identity of a memory. It is used to reference the memory in simulation.
Motivation
It is commonly useful to access a memory in a simulation testbench without having to create a special port for it. This requires storing some kind of a reference to the memory on the elaboratables.
Currently, the object used for this is of a private MemoryIdentity
class. It is implicitly created by lib.memory.Memory
constructor, and passed to the private _MemorySim{Read|Write}
objects when __getitem__
is called. This has a few problems:
- the
Memory
needs to be instantiated in the constructor of the containing elaboratable; combined with its mutability and the occasional need to defer memory port creation toelaborate
, this results in elaboratables that break when elaborated more than once amaranth.sim
currently requires a gross hack to recognizeMemory
objects intraces
- occasionally, it is useful to have
Signal
s that are not included in the design proper, but are used to communicate between simulator processes; it could be likewise useful with memories, but it cannot work with the current code (since theMemoryIdentity
that the simulator gets doesn't have enough information to actually create backing storage for the memory) MemoryIdentity
norMemoryInstance
don't contain information about the element shape, which would require further wrappers onlib.Memory
to perform shape conversion in post-RFC 36 world
The proposed MemoryData
class:
- replaces
MemoryIdentity
and serves as the reference point for the simulator - encapsulates the memory's shape, depth, and initial value (so the simulator can use it to create backing storage)
- in the common scenario, is created by the user in elaboratable constructor, stored as an attribute on the elaboratable, then passed to
Memory
constructor inelaborate
Guide-level explanation
If the memory is to be accessible in simulation, the code to create a memory changes from:
m.submodules.memory = memory = Memory(shape=..., depth=..., init=...)
port = memory.read_port(...)
to:
# in __init__
self.mem_data = MemoryData(shape=..., depth=..., init=...)
# in elaborate
m.submodules.memory = memory = Memory(self.mem_data)
port = memory.read_port(...)
The my_component.mem_data
object can then be used in simulation to read and write memory:
addr = 0x1234
row = sim.get(mem_data[addr])
row += 1
sim.set(mem_data[addr], row)
The old way of creating memories is still supported, though somewhat less flexible.
Reference-level explanation
Two new classes are added:
amaranth.hdl.MemoryData(*, shape: ShapeLike, depth: int, init: Iterable[int | Any], name=None)
: represents a memory's data storage.name
, if not specified, defaults to the variable name used to store theMemoryData
, like it does forSignal
.__getitem__(self, addr: int) -> MemoryData._Row | ValueCastable
: creates aMemoryData._Row
object; ifself.shape
is aShapeCastable
, theMemoryData._Row
object constructed is immediately wrapped viaShapeCastable.__call__
amaranth.hdl.MemoryData._Row
(subclass ofValue
): represents a single row ofMemoryData
, has no public constructor nor operations (other than ones derived fromValue
), can only be used in simulator processes and testbenches
The MemoryData
class allows access to its constructor arguments via read-only properties.
The lib.memory.Memory.Init
class is moved to amaranth.hdl.MemoryData.Init
. It is used for the init
property of MemoryData
.
The Memory
constructor is changed to:
-
amaranth.lib.memory.Memory(data: MemoryData = None, *, shape=None, depth=None, init=None, name=None)
- either
data
, or all three ofshape
,depth
,init
need to be provided, but not both - if
data
is provided, it is used directly, and stored - if
shape
,depth
,init
(and possiblyname
) are provided, they are used to create aMemoryData
, which is then stored
- either
The MemoryData
object is accessible via a new read-only data
property on Memory
. The existing shape
, depth
, init
properties become aliases for data.shape
, data.depth
, data.init
.
MemoryInstance
constructor is likewise changed to:
amaranth.hdl.MemoryInstance(data: MemoryData, *, attrs={})
The sim.memory_read
and sim.memory_write
methods proposed by RFC 36 are removed. Instead, the new Memory._Row
simulation-only value is introduced, which can be passed to get
/set
/changed
like any other value. Masked writes can be implemented by set
with a slice of MemoryData._Row
, just like for signals.
MemoryData
and MemoryData._Row
instances (possibly wrapped in ShapeCastable
for the latter) can be added to the traces
argument when writing a VCD file with the simulator.
Using MemoryData._Row
within an elaboratable results in an immediate error, even if the design is only to be used in simulation. The only place where MemoryData._Row
is valid is within an argument to sim.get
/sim.set
/sim.changed
and similar functions.
sim.edge
remains restricted to plain Signal
and single-bit slices thereof. MemoryData._Row
is not supported.
Drawbacks
None.
Rationale and alternatives
MemoryData
having shape
, depth
, and init
is necessary to allow the simulator to create the underlying storage if the memory is not included in the design hierarchy, but is used to communicate between simulator processes.
Prior art
MemoryData
is conceptually equivalent to a 2D Signal
, for simulation purposes. It thus follows similar rules.
Unresolved questions
None.
Future possibilities
A MemoryData.Slice
class could be added, allowing a whole range of memory addresses to be get
/set
at once, monitored for changes with changes
, or added to traces
.
Support for __getitem__(Value)
could be added (currently it would be blocked on CXXRTL capabilities).