lib.memory: improve and finish documentation.

This commit is contained in:
Catherine 2024-03-22 08:27:10 +00:00
parent 8faa6facfb
commit 8d44ec513d
6 changed files with 416 additions and 150 deletions

View file

@ -5,76 +5,100 @@ from collections.abc import MutableSequence
from ..hdl import MemoryIdentity, MemoryInstance, Shape, ShapeCastable, Const from ..hdl import MemoryIdentity, MemoryInstance, Shape, ShapeCastable, Const
from ..hdl._mem import MemorySimRead from ..hdl._mem import MemorySimRead
from ..utils import ceil_log2 from ..utils import ceil_log2
from .data import ArrayLayout
from . import wiring
from .. import tracer from .. import tracer
from . import wiring, data
__all__ = ["Memory", "ReadPort", "WritePort"] __all__ = ["Memory", "ReadPort", "WritePort"]
class Memory(wiring.Component): class Memory(wiring.Component):
"""A word addressable storage. """Addressable array of rows.
This :ref:`component <wiring>` is used to construct a memory array by first specifying its
dimensions and initial contents using the :py:`shape`, :py:`depth`, and :py:`init` parameters,
and then adding memory ports using the :meth:`read_port` and :meth:`write_port` methods.
Because it is mutable, it should be created and used locally within
the :ref:`elaborate <lang-elaboration>` method.
The :py:`init` parameter and assignment to the :py:`init` attribute have the same effect, with
:class:`Memory.Init` converting elements of the iterable to match :py:`shape` and using
a default value for rows that are not explicitly initialized.
.. warning::
Uninitialized memories (including ASIC memories and some FPGA memories) are
`not yet supported <https://github.com/amaranth-lang/amaranth/issues/270>`_, and
the :py:`init` parameter must be always provided, if only as :py:`init=[]`.
Parameters Parameters
---------- ----------
shape : :ref:`shape-like <lang-shapelike>` object shape : :ref:`shape-like <lang-shapelike>` object
The shape of a single element of the storage. Shape of each memory row.
depth : int depth : :class:`int`
Word count. This memory contains ``depth`` storage elements. Number of memory rows.
init : iterable of int or of any objects accepted by ``shape.const()`` init : iterable of initial values
Initial values. At power on, each storage element in this memory is initialized to Initial values for memory rows.
the corresponding element of ``init``, if any, or to the default value of ``shape`` otherwise.
Uninitialized memories are not currently supported.
attrs : dict
Dictionary of synthesis attributes.
Attributes Platform overrides
---------- ------------------
shape : :ref:`shape-like <lang-shapelike>` Define the :py:`get_memory()` platform method to override the implementation of
depth : int :class:`Memory`, e.g. to instantiate library cells directly.
init : :class:`Memory.Init`
attrs : dict
r_ports : tuple of :class:`ReadPort`
w_ports : tuple of :class:`WritePort`
""" """
class Init(MutableSequence): class Init(MutableSequence):
"""Initial data of a :class:`Memory`. """Memory initialization data.
This is a container implementing the ``MutableSequence`` protocol, enforcing two constraints: This is a special container used only for the :attr:`Memory.init` attribute. It is similar
to :class:`list`, but does not support inserting or deleting elements; its length is always
the same as the depth of the memory it belongs to.
- the length is immutable and must equal ``depth`` If :py:`shape` is a :ref:`custom shape-castable object <lang-shapecustom>`, then:
- if ``shape`` is a :class:`ShapeCastable`, each element can be cast to ``shape`` via :py:`shape.const()`
- otherwise, each element is an :py:`int` * Each element must be convertible to :py:`shape` via :meth:`.ShapeCastable.const`, and
* Elements that are not explicitly initialized default to :py:`shape.const(None)`.
Otherwise (when :py:`shape` is a :class:`.Shape`):
* Each element must be an :class:`int`, and
* Elements that are not explicitly initialized default to :py:`0`.
""" """
def __init__(self, items, *, shape, depth): def __init__(self, elems, *, shape, depth):
Shape.cast(shape) Shape.cast(shape)
if not isinstance(depth, int) or depth < 0: if not isinstance(depth, int) or depth < 0:
raise TypeError("Memory depth must be a non-negative integer, not {!r}" raise TypeError("Memory depth must be a non-negative integer, not {!r}"
.format(depth)) .format(depth))
self._shape = shape self._shape = shape
self._depth = depth self._depth = depth
if isinstance(shape, ShapeCastable): if isinstance(shape, ShapeCastable):
self._items = [None] * depth self._elems = [None] * depth
default = Const.cast(shape.const(None)).value self._raw = [Const.cast(shape.const(None)).value] * depth
self._raw = [default] * depth
else: else:
self._raw = self._items = [0] * depth self._elems = [0] * depth
self._raw = self._elems # intentionally mutably aliased
try: try:
for idx, item in enumerate(items): for index, item in enumerate(elems):
self[idx] = item self[index] = item
except (TypeError, ValueError) as e: except (TypeError, ValueError) as e:
raise type(e)("Memory initialization value at address {:x}: {}" raise type(e)("Memory initialization value at address {:x}: {}"
.format(idx, e)) from None .format(index, e)) from None
@property
def shape(self):
return self._shape
# TODO: redundant with __len__
@property
def depth(self):
return self._depth
def __getitem__(self, index): def __getitem__(self, index):
return self._items[index] return self._elems[index]
def __setitem__(self, index, value): def __setitem__(self, index, value):
if isinstance(index, slice): if isinstance(index, slice):
start, stop, step = index.indices(len(self._items)) indices = range(*index.indices(len(self._elems)))
indices = range(start, stop, step)
if len(value) != len(indices): if len(value) != len(indices):
raise ValueError("Changing length of Memory.init is not allowed") raise ValueError("Changing length of Memory.init is not allowed")
for actual_index, actual_value in zip(indices, value): for actual_index, actual_value in zip(indices, value):
@ -84,48 +108,45 @@ class Memory(wiring.Component):
self._raw[index] = Const.cast(self._shape.const(value)).value self._raw[index] = Const.cast(self._shape.const(value)).value
else: else:
value = operator.index(value) value = operator.index(value)
self._items[index] = value # self._raw[index] assigned by the following line
self._elems[index] = value
def __delitem__(self, index): def __delitem__(self, index):
raise TypeError("Deleting items from Memory.init is not allowed") raise TypeError("Deleting elements from Memory.init is not allowed")
def insert(self, index, value): def insert(self, index, value):
raise TypeError("Inserting items into Memory.init is not allowed") """:meta private:"""
raise TypeError("Inserting elements into Memory.init is not allowed")
def __len__(self): def __len__(self):
return self._depth return self._depth
@property
def depth(self):
return self._depth
@property
def shape(self):
return self._shape
def __repr__(self): def __repr__(self):
return f"Memory.Init({self._items!r})" return f"Memory.Init({self._elems!r})"
def __init__(self, *, depth, shape, init, attrs=None, src_loc_at=0, src_loc=None):
# shape and depth validation performed in Memory.Init constructor. def __init__(self, *, shape, depth, init, attrs=None, src_loc_at=0):
self._depth = depth # shape and depth validation is performed in Memory.Init()
self._shape = shape self._shape = shape
self._depth = depth
self._init = Memory.Init(init, shape=shape, depth=depth) self._init = Memory.Init(init, shape=shape, depth=depth)
self._attrs = {} if attrs is None else dict(attrs) self._attrs = {} if attrs is None else dict(attrs)
self.src_loc = src_loc or tracer.get_src_loc(src_loc_at=src_loc_at) self.src_loc = tracer.get_src_loc(src_loc_at=src_loc_at)
self._identity = MemoryIdentity()
self._r_ports: "list[ReadPort]" = []
self._w_ports: "list[WritePort]" = []
super().__init__(wiring.Signature({}))
@property self._identity = MemoryIdentity()
def depth(self): self._read_ports: "list[ReadPort]" = []
return self._depth self._write_ports: "list[WritePort]" = []
super().__init__(wiring.Signature({}))
@property @property
def shape(self): def shape(self):
return self._shape return self._shape
@property
def depth(self):
return self._depth
@property @property
def init(self): def init(self):
return self._init return self._init
@ -139,43 +160,90 @@ class Memory(wiring.Component):
return self._attrs return self._attrs
def read_port(self, *, domain="sync", transparent_for=()): def read_port(self, *, domain="sync", transparent_for=()):
"""Adds a new read port and returns it. """Request a read port.
Equivalent to creating a :class:`ReadPort` with a signature of :py:`ReadPort.Signature(addr_width=ceil_log2(self.depth), shape=self.shape)` If :py:`domain` is :py:`"comb"`, the created read port is asynchronous and always enabled
(with its enable input is tied to :py:`Const(1)`), and its data output always reflects
the contents of the selected row. Otherwise, the created read port is synchronous,
and its data output is updated with the contents of the selected row at each
:ref:`active edge <lang-sync>` of :py:`domain` where the enable input is asserted.
The :py:`transparent_for` parameter specifies the *transparency set* of this port: zero or
more :class:`WritePort`\\ s, all of which must belong to the same memory and clock domain.
If another port writes to a memory row at the same time as this port reads from the same
memory row, and that write port is a part of the transparency set, then this port retrieves
the new contents of the row; otherwise, this port retrieves the old contents of the row.
If another write port belonging to a different clock domain updates a memory row that this
port is reading at the same time, the behavior is undefined.
The signature of the returned port is
:py:`ReadPort.Signature(shape=self.shape, addr_width=ceil_log2(self.depth))`.
Returns
-------
:class:`ReadPort`
""" """
signature = ReadPort.Signature(addr_width=ceil_log2(self.depth), shape=self.shape) signature = ReadPort.Signature(shape=self.shape, addr_width=ceil_log2(self.depth))
return ReadPort(signature, memory=self, domain=domain, transparent_for=transparent_for) return ReadPort(signature, memory=self, domain=domain, transparent_for=transparent_for)
@property
def r_ports(self):
"""Returns a tuple of all read ports defined so far."""
return tuple(self._r_ports)
def write_port(self, *, domain="sync", granularity=None): def write_port(self, *, domain="sync", granularity=None):
"""Adds a new write port and returns it. """Request a write port.
Equivalent to creating a :class:`WritePort` with a signature of :py:`WritePort.Signature(addr_width=ceil_log2(self.depth), shape=self.shape, granularity=granularity)` The created write port is synchronous, updating the contents of the selected row at each
:ref:`active edge <lang-sync>` of :py:`domain` where the enable input is asserted.
Specifying a *granularity* when :py:`shape` is :func:`unsigned(width) <.unsigned>` or
:class:`data.ArrayLayout(_, width) <.data.ArrayLayout>` makes it possible to partially
update a memory row. In this case, :py:`granularity` must be an integer that evenly divides
:py:`width`, and the memory row is split into :py:`width // granularity` equally sized
parts, each of which is updated if the corresponding bit of the enable input is asserted.
The signature of the new port is
:py:`WritePort.Signature(shape=self.shape, addr_width=ceil_log2(self.depth), granularity=granularity)`.
Returns
-------
:class:`WritePort`
""" """
signature = WritePort.Signature(addr_width=ceil_log2(self.depth), shape=self.shape, granularity=granularity) signature = WritePort.Signature(
shape=self.shape, addr_width=ceil_log2(self.depth), granularity=granularity)
return WritePort(signature, memory=self, domain=domain) return WritePort(signature, memory=self, domain=domain)
# TODO: rename to read_ports
@property
def r_ports(self):
"""All read ports defined so far.
This property is provided for the :py:`platform.get_memory()` override.
"""
return tuple(self._read_ports)
# TODO: rename to write_ports
@property @property
def w_ports(self): def w_ports(self):
"""Returns a tuple of all write ports defined so far.""" """All write ports defined so far.
return tuple(self._w_ports)
This property is provided for the :py:`platform.get_memory()` override.
"""
return tuple(self._write_ports)
def elaborate(self, platform): def elaborate(self, platform):
if hasattr(platform, "get_memory"): if hasattr(platform, "get_memory"):
return platform.get_memory(self) return platform.get_memory(self)
shape = Shape.cast(self.shape) shape = Shape.cast(self.shape)
instance = MemoryInstance(identity=self._identity, width=shape.width, depth=self.depth, init=self.init._raw, attrs=self.attrs, src_loc=self.src_loc) instance = MemoryInstance(identity=self._identity, width=shape.width, depth=self.depth,
w_ports = {} init=self.init._raw, attrs=self.attrs, src_loc=self.src_loc)
for port in self._w_ports: write_ports = {}
idx = instance.write_port(domain=port.domain, addr=port.addr, data=port.data, en=port.en) for port in self._write_ports:
w_ports[port] = idx write_ports[port] = instance.write_port(
for port in self._r_ports: domain=port.domain, addr=port.addr, data=port.data, en=port.en)
transparent_for = [w_ports[write_port] for write_port in port.transparent_for] for port in self._read_ports:
instance.read_port(domain=port.domain, data=port.data, addr=port.addr, en=port.en, transparent_for=transparent_for) transparent_for = tuple(write_ports[write_port] for write_port in port.transparent_for)
instance.read_port(
domain=port.domain, data=port.data, addr=port.addr, en=port.en,
transparent_for=transparent_for)
return instance return instance
def __getitem__(self, index): def __getitem__(self, index):
@ -184,48 +252,44 @@ class Memory(wiring.Component):
class ReadPort: class ReadPort:
"""A memory read port. """A read memory port.
Parameters Memory read ports, which are :ref:`interface objects <wiring>`, can be constructed by calling
---------- :meth:`Memory.read_port` or via :meth:`ReadPort.Signature.create() <.Signature.create>`.
signature : :class:`ReadPort.Signature`
The signature of the port. An asynchronous (:py:`"comb"` domain) memory read port is always enabled. The :py:`en` input of
memory : :class:`Memory` such a port is tied to :py:`Const(1)`.
Memory associated with the port.
domain : str
Clock domain. Defaults to ``"sync"``. If set to ``"comb"``, the port is asynchronous.
Otherwise, the read data becomes available on the next clock cycle.
transparent_for : iterable of :class:`WritePort`
The set of write ports that this read port should be transparent with. All ports
must belong to the same memory and the same clock domain.
Attributes Attributes
---------- ----------
signature : :class:`ReadPort.Signature` signature : :class:`ReadPort.Signature`
memory : :class:`Memory` Signature of this memory port.
domain : str memory : :class:`Memory` or :py:`None`
transparent_for : tuple of :class:`WritePort` Memory associated with this memory port.
domain : :class:`str`
Name of this memory port's clock domain. For asynchronous ports, :py:`"comb"`.
transparent_for : :class:`tuple` of :class:`WritePort`
Transparency set of this memory port.
""" """
class Signature(wiring.Signature): class Signature(wiring.Signature):
"""A signature of a read port. """Signature of a memory read port.
Parameters Parameters
---------- ----------
addr_width : int addr_width : :class:`int`
Address width in bits. If the port is associated with a :class:`Memory`, Width of the address port.
it must be equal to :py:`ceil_log2(memory.depth)`.
shape : :ref:`shape-like <lang-shapelike>` object shape : :ref:`shape-like <lang-shapelike>` object
The shape of the port data. If the port is associated with a :class:`Memory`, Shape of the data port.
it must be equal to its element shape.
Members Members
------- -------
addr: :py:`unsigned(data_width)` en: :py:`In(1, init=1)`
data: ``shape`` Enable input.
en: :py:`unsigned(1)` addr: :py:`In(addr_width)`
The enable signal. If ``domain == "comb"``, this is tied to ``Const(1)``. Address input.
Otherwise it is a signal with ``init=1``. data: :py:`Out(shape)`
Data output.
""" """
def __init__(self, *, addr_width, shape): def __init__(self, *, addr_width, shape):
@ -234,12 +298,13 @@ class ReadPort:
self._addr_width = addr_width self._addr_width = addr_width
self._shape = shape self._shape = shape
super().__init__({ super().__init__({
"en": wiring.In(1, init=1),
"addr": wiring.In(addr_width), "addr": wiring.In(addr_width),
"data": wiring.Out(shape), "data": wiring.Out(shape),
"en": wiring.In(1, init=1),
}) })
def create(self, *, path=None, src_loc_at=0): def create(self, *, path=None, src_loc_at=0):
""":meta private:""" # work around Sphinx bug
return ReadPort(self, memory=None, domain="sync", path=path, src_loc_at=1 + src_loc_at) return ReadPort(self, memory=None, domain="sync", path=path, src_loc_at=1 + src_loc_at)
@property @property
@ -262,7 +327,7 @@ class ReadPort:
def __init__(self, signature, *, memory, domain, transparent_for=(), path=None, src_loc_at=0): def __init__(self, signature, *, memory, domain, transparent_for=(), path=None, src_loc_at=0):
if not isinstance(signature, ReadPort.Signature): if not isinstance(signature, ReadPort.Signature):
raise TypeError(f"Expected `ReadPort.Signature`, not {signature!r}") raise TypeError(f"Expected `ReadPort.Signature`, not {signature!r}")
if memory is not None: if memory is not None: # may be None if created via `Signature.create()`
if not isinstance(memory, Memory): if not isinstance(memory, Memory):
raise TypeError(f"Expected `Memory` or `None`, not {memory!r}") raise TypeError(f"Expected `Memory` or `None`, not {memory!r}")
if signature.shape != memory.shape or Shape.cast(signature.shape) != Shape.cast(memory.shape): if signature.shape != memory.shape or Shape.cast(signature.shape) != Shape.cast(memory.shape):
@ -275,7 +340,7 @@ class ReadPort:
for port in transparent_for: for port in transparent_for:
if not isinstance(port, WritePort): if not isinstance(port, WritePort):
raise TypeError("`transparent_for` must contain only `WritePort` instances") raise TypeError("`transparent_for` must contain only `WritePort` instances")
if memory is not None and port not in memory._w_ports: if memory is not None and port not in memory._write_ports:
raise ValueError("Transparent write ports must belong to the same memory") raise ValueError("Transparent write ports must belong to the same memory")
if port.domain != domain: if port.domain != domain:
raise ValueError("Transparent write ports must belong to the same domain") raise ValueError("Transparent write ports must belong to the same domain")
@ -287,7 +352,7 @@ class ReadPort:
if domain == "comb": if domain == "comb":
self.en = Const(1) self.en = Const(1)
if memory is not None: if memory is not None:
memory._r_ports.append(self) memory._read_ports.append(self)
@property @property
def signature(self): def signature(self):
@ -307,49 +372,50 @@ class ReadPort:
class WritePort: class WritePort:
"""A memory write port. """A write memory port.
Parameters Memory write ports, which are :ref:`interface objects <wiring>`, can be constructed by calling
---------- :meth:`Memory.write_port` or via :meth:`WritePort.Signature.create() <.Signature.create>`.
signature : :class:`WritePort.Signature`
The signature of the port.
memory : :class:`Memory` or ``None``
Memory associated with the port.
domain : str
Clock domain. Defaults to ``"sync"``. Writes have a latency of 1 clock cycle.
Attributes Attributes
---------- ----------
signature : :class:`WritePort.Signature` signature : :class:`WritePort.Signature`
memory : :class:`Memory` Signature of this memory port.
domain : str memory : :class:`Memory` or :py:`None`
Memory associated with this memory port.
domain : :class:`str`
Name of this memory port's clock domain. Never :py:`"comb"`.
""" """
class Signature(wiring.Signature): class Signature(wiring.Signature):
"""A signature of a write port. """Signature of a memory write port.
Width of the enable input is determined as follows:
* If :py:`granularity` is :py:`None`,
then :py:`en_width == 1`.
* If :py:`shape` is :func:`unsigned(data_width) <.unsigned>`,
then :py:`en_width == data_width // granularity`.
* If :py:`shape` is :class:`data.ArrayLayout(_, elem_count) <.data.ArrayLayout>`,
then :py:`en_width == elem_count // granularity`.
Parameters Parameters
---------- ----------
addr_width : int addr_width : :class:`int`
Address width in bits. If the port is associated with a :class:`Memory`, Width of the address port.
it must be equal to :py:`ceil_log2(memory.depth)`.
shape : :ref:`shape-like <lang-shapelike>` object shape : :ref:`shape-like <lang-shapelike>` object
The shape of the port data. If the port is associated with a :class:`Memory`, Shape of the data port.
it must be equal to its element shape. granularity : :class:`int` or :py:`None`
granularity : int or ``None`` Granularity of memory access.
Port granularity. If ``None``, the entire storage element is written at once.
Otherwise, determines the size of access covered by a single bit of ``en``.
One of the following must hold:
- ``granularity is None``, in which case ``en_width == 1``, or
- ``shape == unsigned(data_width)`` and ``data_width == 0 or data_width % granularity == 0`` in which case ``en_width == data_width // granularity`` (or 0 if ``data_width == 0``)
- ``shape == amaranth.lib.data.ArrayLayout(_, elem_count)`` and ``elem_count == 0 or elem_count % granularity == 0`` in which case ``en_width == elem_count // granularity`` (or 0 if ``elem_count == 0``)
Members Members
------- -------
addr: :py:`unsigned(data_width)` en: :py:`In(en_width)`
data: ``shape`` Enable input.
en: :py:`unsigned(en_width)` addr: :py:`In(addr_width)`
Address input.
data: :py:`In(shape)`
Data input.
""" """
def __init__(self, *, addr_width, shape, granularity=None): def __init__(self, *, addr_width, shape, granularity=None):
@ -374,7 +440,7 @@ class WritePort:
raise ValueError("Granularity must divide data width") raise ValueError("Granularity must divide data width")
else: else:
en_width = actual_shape.width // granularity en_width = actual_shape.width // granularity
elif isinstance(shape, ArrayLayout): elif isinstance(shape, data.ArrayLayout):
if shape.length == 0: if shape.length == 0:
en_width = 0 en_width = 0
elif granularity == 0: elif granularity == 0:
@ -386,12 +452,13 @@ class WritePort:
else: else:
raise TypeError("Granularity can only be specified for plain unsigned `Shape` or `ArrayLayout`") raise TypeError("Granularity can only be specified for plain unsigned `Shape` or `ArrayLayout`")
super().__init__({ super().__init__({
"en": wiring.In(en_width),
"addr": wiring.In(addr_width), "addr": wiring.In(addr_width),
"data": wiring.In(shape), "data": wiring.In(shape),
"en": wiring.In(en_width),
}) })
def create(self, *, path=None, src_loc_at=0): def create(self, *, path=None, src_loc_at=0):
""":meta private:""" # work around Sphinx bug
return WritePort(self, memory=None, domain="sync", path=path, src_loc_at=1 + src_loc_at) return WritePort(self, memory=None, domain="sync", path=path, src_loc_at=1 + src_loc_at)
@property @property
@ -420,7 +487,7 @@ class WritePort:
def __init__(self, signature, *, memory, domain, path=None, src_loc_at=0): def __init__(self, signature, *, memory, domain, path=None, src_loc_at=0):
if not isinstance(signature, WritePort.Signature): if not isinstance(signature, WritePort.Signature):
raise TypeError(f"Expected `WritePort.Signature`, not {signature!r}") raise TypeError(f"Expected `WritePort.Signature`, not {signature!r}")
if memory is not None: if memory is not None: # may be None if created via `Signature.create()`
if not isinstance(memory, Memory): if not isinstance(memory, Memory):
raise TypeError(f"Expected `Memory` or `None`, not {memory!r}") raise TypeError(f"Expected `Memory` or `None`, not {memory!r}")
if signature.shape != memory.shape or Shape.cast(signature.shape) != Shape.cast(memory.shape): if signature.shape != memory.shape or Shape.cast(signature.shape) != Shape.cast(memory.shape):
@ -436,7 +503,7 @@ class WritePort:
self._domain = domain self._domain = domain
self.__dict__.update(signature.members.create(path=path, src_loc_at=1 + src_loc_at)) self.__dict__.update(signature.members.create(path=path, src_loc_at=1 + src_loc_at))
if memory is not None: if memory is not None:
memory._w_ports.append(self) memory._write_ports.append(self)
@property @property
def signature(self): def signature(self):

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 48 KiB

View file

@ -1,10 +1,144 @@
Memories Memory arrays
-------- -------------
.. py:module:: amaranth.lib.memory .. py:module:: amaranth.lib.memory
The :mod:`amaranth.lib.memory` module provides a way to efficiently store data organized as an array of identically shaped rows, which may be addressed (read and/or written) one at a time. The :mod:`amaranth.lib.memory` module provides a way to efficiently store data organized as an array of identically shaped rows, which may be addressed (read and/or written) one at a time. This organization is well suited for an efficient implementation in hardware.
.. todo::
Write the rest of this document. Introduction
============
A memory :ref:`component <wiring>` is accessed through read and write *memory ports*, which are :ref:`interface objects <wiring>` with address, data, and enable ports. The address input selects the memory row to be accessed, the enable input determines whether an access will be made, and the data output (for read ports) or data input (for write ports) transfers data between the memory row and the rest of the design. Read ports can be synchronous (where the memory row access is triggered by the :ref:`active edge <lang-sync>` of a clock) or asynchronous (where the memory row is accessed continuously); write ports are always synchronous.
.. note::
Unfortunately, the terminology related to memories has an ambiguity: a "port" could refer to either an *interface port* (:class:`.Signal` objects created by the :mod:`amaranth.lib.wiring` module) or to a *memory port* (:class:`ReadPort` or :class:`WritePort` object created by :class:`amaranth.lib.memory.Memory`).
Amaranth documentation always uses the term "memory port" when referring to the latter.
To use a memory, first create a :class:`Memory` object, providing a shape, depth (the number of rows), and initial contents. Then, request as many memory ports as the number of concurrent accesses you need to perform by using the :meth:`Memory.read_port` and :meth:`Memory.write_port` methods.
.. warning::
While :class:`Memory` will provide virtually any memory configuration on request and all will simulate correctly, only a subset of configurations can implemented in hardware efficiently or `at all`. Exactly what any given hardware platform supports is specific to both the device and the toolchain used.
However, the following two configurations are well-supported on most platforms:
1. Zero or one write ports and any amount of read ports. Almost all devices include one or two read ports in a hardware memory block, but the toolchain will replicate memory blocks as needed to meet the requested amount of read ports, using more resources.
2. Two write ports and any amount of read ports whose address input always matches that of the either write port. Most devices include two combined read/write ports in a hardware memory block (known as "true dual-port", or "TDP", block RAM), and the toolchain will replicate memory blocks to meet the requested amount of read ports. However, some devices include one read-only and one write-only port in a hardware memory block (known as "simple dual-port", or "SDP", block RAM), making this configuration unavailable. Also, the combined (synchronous) read/write port of a TDP block RAM usually does not have independent read enable and write enable inputs; in this configuration, the read enable input should usually be left in the (default if not driven) asserted state.
Most devices include hardware primitives that can efficiently implement memories with asynchronous read ports (known as "LUT RAM", "distributed RAM", or "DRAM"; not to be confused with "dynamic RAM", also abbreviated as "DRAM"). On devices without these hardware primitives, memories with asynchronous read ports are implemented using logic resources, which are consumed at an extremely high rate. Synchronous read ports should be always preferred over asynchronous ones.
Additionally, some memory configurations (that seem like they should be supported by the device and the toolchain) may fail to be recognized, or may use much more resources than they should. This can happen due to device and/or toolchain errata (including defects in Amaranth). Unfortunately, such issues can only be handled on a case-by-case basis; in general, simpler memory configurations are better and more widely supported.
Examples
========
.. testsetup::
from amaranth import *
First, import the :class:`Memory` class.
.. testcode::
from amaranth.lib.memory import Memory
Read-only memory
++++++++++++++++
In the following example, a read-only memory is used to output a fixed message in a loop:
.. testcode::
:hide:
m = Module()
.. testcode::
message = b"Hello world\n"
m.submodules.memory = memory = \
Memory(shape=unsigned(8), depth=len(message), init=message)
rd_port = memory.read_port(domain="comb")
with m.If(rd_port.addr == memory.depth - 1):
m.d.sync += rd_port.addr.eq(0)
with m.Else():
m.d.sync += rd_port.addr.eq(rd_port.addr + 1)
character = Signal(8)
m.d.comb += character.eq(rd_port.data)
In this example, the memory read port is asynchronous, and a change of the address input (labelled `a` on the diagram below) results in an immediate change of the data output (labelled `d`).
.. image:: _images/memory/example_hello.svg
First-in, first-out queue
+++++++++++++++++++++++++
In a more complex example, a power-of-2 sized writable memory is used to implement a first-in, first-out queue:
.. testcode::
:hide:
m = Module()
.. testcode::
push = Signal()
pop = Signal()
m.submodules.memory = memory = \
Memory(shape=unsigned(8), depth=16, init=[])
wr_port = memory.write_port()
m.d.comb += wr_port.en.eq(push)
with m.If(push):
m.d.sync += wr_port.addr.eq(wr_port.addr + 1)
rd_port = memory.read_port(transparent_for=(wr_port,))
m.d.comb += rd_port.en.eq(pop)
with m.If(pop):
m.d.sync += rd_port.addr.eq(rd_port.addr + 1)
# Data can be shifted in via `wr_port.data` and out via `rd_port.data`.
# This example assumes that empty queue will be never popped from.
In this example, the memory read and write ports are synchronous. A write operation (labelled `x`, `w`) updates the addressed row 0 on the next clock cycle, and a read operation (labelled `y`, `r`) retrieves the contents of the same addressed row 0 on the next clock cycle as well.
However, the memory read port is also configured to be *transparent* relative to the memory write port. This means that if a write and a read operation (labelled `t`, `u` respectively) access the same row with address 3, the new contents will be read out, reducing the minimum push-to-pop latency to one cycle, down from two cycles that would be required without the use of transparency.
.. image:: _images/memory/example_fifo.svg
Memories
========
..
attributes are not documented because they can be easily used to break soundness and we don't
document them for signals either; they are rarely necessary for interoperability
.. autoclass:: Memory(*, depth, shape, init, src_loc_at=0)
:no-members:
.. autoclass:: amaranth.lib.memory::Memory.Init(...)
.. automethod:: read_port
.. automethod:: write_port
.. autoproperty:: r_ports
.. autoproperty:: w_ports
Memory ports
============
.. autoclass:: ReadPort(...)
.. autoclass:: WritePort(...)

View file

@ -1,3 +1,5 @@
.. _wiring:
Interfaces and connections Interfaces and connections
########################## ##########################

View file

@ -353,10 +353,10 @@ class MemoryTestCase(FHDLTestCase):
r"^Changing length of Memory.init is not allowed$"): r"^Changing length of Memory.init is not allowed$"):
m.init[1:] = [1, 2] m.init[1:] = [1, 2]
with self.assertRaisesRegex(TypeError, with self.assertRaisesRegex(TypeError,
r"^Deleting items from Memory.init is not allowed$"): r"^Deleting elements from Memory.init is not allowed$"):
del m.init[1:2] del m.init[1:2]
with self.assertRaisesRegex(TypeError, with self.assertRaisesRegex(TypeError,
r"^Inserting items into Memory.init is not allowed$"): r"^Inserting elements into Memory.init is not allowed$"):
m.init.insert(1, 3) m.init.insert(1, 3)
def test_port(self): def test_port(self):