- 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
Memoryneeds 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.simcurrently requires a gross hack to recognizeMemoryobjects intraces- occasionally, it is useful to have
Signals 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 theMemoryIdentitythat the simulator gets doesn't have enough information to actually create backing storage for the memory) MemoryIdentitynorMemoryInstancedon't contain information about the element shape, which would require further wrappers onlib.Memoryto perform shape conversion in post-RFC 36 world
The proposed MemoryData class:
- replaces
MemoryIdentityand 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
Memoryconstructor 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._Rowobject; ifself.shapeis aShapeCastable, theMemoryData._Rowobject 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,initneed to be provided, but not both - if
datais 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).