Skip to content

RFC 62: The MemoryData class. #62

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 25, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions text/0062-memory-data.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
- Start Date: 2024-03-25
- RFC PR: [amaranth-lang/rfcs#62](https://github.com/amaranth-lang/rfcs/pull/62)
- Amaranth Issue: [amaranth-lang/amaranth#1241](https://github.com/amaranth-lang/amaranth/issues/1241)

# The `MemoryData` class

## Summary
[summary]: #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
[motivation]: #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 to `elaborate`, this results in elaboratables that break when elaborated more than once
- `amaranth.sim` currently requires a gross hack to recognize `Memory` objects in `traces`
- 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)
- `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

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 in `elaborate`

## Guide-level explanation
[guide-level-explanation]: #guide-level-explanation

If the memory is to be accessible in simulation, the code to create a memory changes from:

```py
m.submodules.memory = memory = Memory(shape=..., depth=..., init=...)
port = memory.read_port(...)
```

to:

```py
# 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:

```py
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
[reference-level-explanation]: #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 the `MemoryData`, like it does for `Signal`.
- `__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__`
- `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

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 of `shape`, `depth`, `init` need to be provided, but not both
- if `data` is provided, it is used directly, and stored
- if `shape`, `depth`, `init` (and possibly `name`) are provided, they are used to create a `MemoryData`, which is then stored

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
[drawbacks]: #drawbacks

None.

## Rationale and alternatives
[rationale-and-alternatives]: #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
[prior-art]: #prior-art

`MemoryData` is conceptually equivalent to a 2D `Signal`, for simulation purposes. It thus follows similar rules.

## Unresolved questions
[unresolved-questions]: #unresolved-questions

None.

## Future possibilities
[future-possibilities]: #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).