|
| 1 | +- Start Date: 2024-03-25 |
| 2 | +- RFC PR: [amaranth-lang/rfcs#62](https://github.com/amaranth-lang/rfcs/pull/62) |
| 3 | +- Amaranth Issue: [amaranth-lang/amaranth#1241](https://github.com/amaranth-lang/amaranth/issues/1241) |
| 4 | + |
| 5 | +# The `MemoryData` class |
| 6 | + |
| 7 | +## Summary |
| 8 | +[summary]: #summary |
| 9 | + |
| 10 | +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. |
| 11 | + |
| 12 | +## Motivation |
| 13 | +[motivation]: #motivation |
| 14 | + |
| 15 | +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. |
| 16 | + |
| 17 | +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: |
| 18 | + |
| 19 | +- 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 to `elaborate`, this results in elaboratables that break when elaborated more than once |
| 20 | +- `amaranth.sim` currently requires a gross hack to recognize `Memory` objects in `traces` |
| 21 | +- 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 the `MemoryIdentity` that the simulator gets doesn't have enough information to actually create backing storage for the memory) |
| 22 | +- `MemoryIdentity` nor `MemoryInstance` don't contain information about the element shape, which would require further wrappers on `lib.Memory` to perform shape conversion in post-RFC 36 world |
| 23 | + |
| 24 | +The proposed `MemoryData` class: |
| 25 | + |
| 26 | +- replaces `MemoryIdentity` and serves as the reference point for the simulator |
| 27 | +- encapsulates the memory's shape, depth, and initial value (so the simulator can use it to create backing storage) |
| 28 | +- in the common scenario, is created by the user in elaboratable constructor, stored as an attribute on the elaboratable, then passed to `Memory` constructor in `elaborate` |
| 29 | + |
| 30 | +## Guide-level explanation |
| 31 | +[guide-level-explanation]: #guide-level-explanation |
| 32 | + |
| 33 | +If the memory is to be accessible in simulation, the code to create a memory changes from: |
| 34 | + |
| 35 | +```py |
| 36 | +m.submodules.memory = memory = Memory(shape=..., depth=..., init=...) |
| 37 | +port = memory.read_port(...) |
| 38 | +``` |
| 39 | + |
| 40 | +to: |
| 41 | + |
| 42 | +```py |
| 43 | +# in __init__ |
| 44 | +self.mem_data = MemoryData(shape=..., depth=..., init=...) |
| 45 | +# in elaborate |
| 46 | +m.submodules.memory = memory = Memory(self.mem_data) |
| 47 | +port = memory.read_port(...) |
| 48 | +``` |
| 49 | + |
| 50 | +The `my_component.mem_data` object can then be used in simulation to read and write memory: |
| 51 | + |
| 52 | +```py |
| 53 | +addr = 0x1234 |
| 54 | +row = sim.get(mem_data[addr]) |
| 55 | +row += 1 |
| 56 | +sim.set(mem_data[addr], row) |
| 57 | +``` |
| 58 | + |
| 59 | +The old way of creating memories is still supported, though somewhat less flexible. |
| 60 | + |
| 61 | +## Reference-level explanation |
| 62 | +[reference-level-explanation]: #reference-level-explanation |
| 63 | + |
| 64 | +Two new classes are added: |
| 65 | + |
| 66 | +- `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 the `MemoryData`, like it does for `Signal`. |
| 67 | + - `__getitem__(self, addr: int) -> MemoryData._Row | ValueCastable`: creates a `MemoryData._Row` object; if `self.shape` is a `ShapeCastable`, the `MemoryData._Row` object constructed is immediately wrapped via `ShapeCastable.__call__` |
| 68 | +- `amaranth.hdl.MemoryData._Row` (subclass of `Value`): represents a single row of `MemoryData`, has no public constructor nor operations (other than ones derived from `Value`), can only be used in simulator processes and testbenches |
| 69 | + |
| 70 | +The `MemoryData` class allows access to its constructor arguments via read-only properties. |
| 71 | + |
| 72 | +The `lib.memory.Memory.Init` class is moved to `amaranth.hdl.MemoryData.Init`. It is used for the `init` property of `MemoryData`. |
| 73 | + |
| 74 | +The `Memory` constructor is changed to: |
| 75 | + |
| 76 | +- `amaranth.lib.memory.Memory(data: MemoryData = None, *, shape=None, depth=None, init=None, name=None)` |
| 77 | + |
| 78 | + - either `data`, or all three of `shape`, `depth`, `init` need to be provided, but not both |
| 79 | + - if `data` is provided, it is used directly, and stored |
| 80 | + - if `shape`, `depth`, `init` (and possibly `name`) are provided, they are used to create a `MemoryData`, which is then stored |
| 81 | + |
| 82 | +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`. |
| 83 | + |
| 84 | +`MemoryInstance` constructor is likewise changed to: |
| 85 | + |
| 86 | +- `amaranth.hdl.MemoryInstance(data: MemoryData, *, attrs={})` |
| 87 | + |
| 88 | +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. |
| 89 | + |
| 90 | +`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. |
| 91 | + |
| 92 | +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. |
| 93 | + |
| 94 | +`sim.edge` remains restricted to plain `Signal` and single-bit slices thereof. `MemoryData._Row` is not supported. |
| 95 | + |
| 96 | +## Drawbacks |
| 97 | +[drawbacks]: #drawbacks |
| 98 | + |
| 99 | +None. |
| 100 | + |
| 101 | +## Rationale and alternatives |
| 102 | +[rationale-and-alternatives]: #rationale-and-alternatives |
| 103 | + |
| 104 | +`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. |
| 105 | + |
| 106 | +## Prior art |
| 107 | +[prior-art]: #prior-art |
| 108 | + |
| 109 | +`MemoryData` is conceptually equivalent to a 2D `Signal`, for simulation purposes. It thus follows similar rules. |
| 110 | + |
| 111 | +## Unresolved questions |
| 112 | +[unresolved-questions]: #unresolved-questions |
| 113 | + |
| 114 | +None. |
| 115 | + |
| 116 | +## Future possibilities |
| 117 | +[future-possibilities]: #future-possibilities |
| 118 | + |
| 119 | +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`. |
| 120 | + |
| 121 | +Support for `__getitem__(Value)` could be added (currently it would be blocked on CXXRTL capabilities). |
0 commit comments