lib.memory: improve and finish documentation.
This commit is contained in:
parent
8faa6facfb
commit
8d44ec513d
6 changed files with 416 additions and 150 deletions
|
|
@ -5,76 +5,100 @@ from collections.abc import MutableSequence
|
|||
from ..hdl import MemoryIdentity, MemoryInstance, Shape, ShapeCastable, Const
|
||||
from ..hdl._mem import MemorySimRead
|
||||
from ..utils import ceil_log2
|
||||
from .data import ArrayLayout
|
||||
from . import wiring
|
||||
from .. import tracer
|
||||
from . import wiring, data
|
||||
|
||||
|
||||
__all__ = ["Memory", "ReadPort", "WritePort"]
|
||||
|
||||
|
||||
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
|
||||
----------
|
||||
shape : :ref:`shape-like <lang-shapelike>` object
|
||||
The shape of a single element of the storage.
|
||||
depth : int
|
||||
Word count. This memory contains ``depth`` storage elements.
|
||||
init : iterable of int or of any objects accepted by ``shape.const()``
|
||||
Initial values. At power on, each storage element in this memory is initialized to
|
||||
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.
|
||||
Shape of each memory row.
|
||||
depth : :class:`int`
|
||||
Number of memory rows.
|
||||
init : iterable of initial values
|
||||
Initial values for memory rows.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
shape : :ref:`shape-like <lang-shapelike>`
|
||||
depth : int
|
||||
init : :class:`Memory.Init`
|
||||
attrs : dict
|
||||
r_ports : tuple of :class:`ReadPort`
|
||||
w_ports : tuple of :class:`WritePort`
|
||||
Platform overrides
|
||||
------------------
|
||||
Define the :py:`get_memory()` platform method to override the implementation of
|
||||
:class:`Memory`, e.g. to instantiate library cells directly.
|
||||
"""
|
||||
|
||||
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 ``shape`` is a :class:`ShapeCastable`, each element can be cast to ``shape`` via :py:`shape.const()`
|
||||
- otherwise, each element is an :py:`int`
|
||||
If :py:`shape` is a :ref:`custom shape-castable object <lang-shapecustom>`, then:
|
||||
|
||||
* 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)
|
||||
if not isinstance(depth, int) or depth < 0:
|
||||
raise TypeError("Memory depth must be a non-negative integer, not {!r}"
|
||||
.format(depth))
|
||||
self._shape = shape
|
||||
self._depth = depth
|
||||
|
||||
if isinstance(shape, ShapeCastable):
|
||||
self._items = [None] * depth
|
||||
default = Const.cast(shape.const(None)).value
|
||||
self._raw = [default] * depth
|
||||
self._elems = [None] * depth
|
||||
self._raw = [Const.cast(shape.const(None)).value] * depth
|
||||
else:
|
||||
self._raw = self._items = [0] * depth
|
||||
self._elems = [0] * depth
|
||||
self._raw = self._elems # intentionally mutably aliased
|
||||
try:
|
||||
for idx, item in enumerate(items):
|
||||
self[idx] = item
|
||||
for index, item in enumerate(elems):
|
||||
self[index] = item
|
||||
except (TypeError, ValueError) as e:
|
||||
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):
|
||||
return self._items[index]
|
||||
return self._elems[index]
|
||||
|
||||
def __setitem__(self, index, value):
|
||||
if isinstance(index, slice):
|
||||
start, stop, step = index.indices(len(self._items))
|
||||
indices = range(start, stop, step)
|
||||
indices = range(*index.indices(len(self._elems)))
|
||||
if len(value) != len(indices):
|
||||
raise ValueError("Changing length of Memory.init is not allowed")
|
||||
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
|
||||
else:
|
||||
value = operator.index(value)
|
||||
self._items[index] = value
|
||||
# self._raw[index] assigned by the following line
|
||||
self._elems[index] = value
|
||||
|
||||
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):
|
||||
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):
|
||||
return self._depth
|
||||
|
||||
@property
|
||||
def depth(self):
|
||||
return self._depth
|
||||
|
||||
@property
|
||||
def shape(self):
|
||||
return self._shape
|
||||
|
||||
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.
|
||||
self._depth = depth
|
||||
|
||||
def __init__(self, *, shape, depth, init, attrs=None, src_loc_at=0):
|
||||
# shape and depth validation is performed in Memory.Init()
|
||||
self._shape = shape
|
||||
self._depth = depth
|
||||
self._init = Memory.Init(init, shape=shape, depth=depth)
|
||||
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._identity = MemoryIdentity()
|
||||
self._r_ports: "list[ReadPort]" = []
|
||||
self._w_ports: "list[WritePort]" = []
|
||||
super().__init__(wiring.Signature({}))
|
||||
self.src_loc = tracer.get_src_loc(src_loc_at=src_loc_at)
|
||||
|
||||
@property
|
||||
def depth(self):
|
||||
return self._depth
|
||||
self._identity = MemoryIdentity()
|
||||
self._read_ports: "list[ReadPort]" = []
|
||||
self._write_ports: "list[WritePort]" = []
|
||||
|
||||
super().__init__(wiring.Signature({}))
|
||||
|
||||
@property
|
||||
def shape(self):
|
||||
return self._shape
|
||||
|
||||
@property
|
||||
def depth(self):
|
||||
return self._depth
|
||||
|
||||
@property
|
||||
def init(self):
|
||||
return self._init
|
||||
|
|
@ -139,43 +160,90 @@ class Memory(wiring.Component):
|
|||
return self._attrs
|
||||
|
||||
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)
|
||||
|
||||
@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):
|
||||
"""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)
|
||||
|
||||
# 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
|
||||
def w_ports(self):
|
||||
"""Returns a tuple of all write ports defined so far."""
|
||||
return tuple(self._w_ports)
|
||||
"""All write ports defined so far.
|
||||
|
||||
This property is provided for the :py:`platform.get_memory()` override.
|
||||
"""
|
||||
return tuple(self._write_ports)
|
||||
|
||||
def elaborate(self, platform):
|
||||
if hasattr(platform, "get_memory"):
|
||||
return platform.get_memory(self)
|
||||
|
||||
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)
|
||||
w_ports = {}
|
||||
for port in self._w_ports:
|
||||
idx = instance.write_port(domain=port.domain, addr=port.addr, data=port.data, en=port.en)
|
||||
w_ports[port] = idx
|
||||
for port in self._r_ports:
|
||||
transparent_for = [w_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)
|
||||
instance = MemoryInstance(identity=self._identity, width=shape.width, depth=self.depth,
|
||||
init=self.init._raw, attrs=self.attrs, src_loc=self.src_loc)
|
||||
write_ports = {}
|
||||
for port in self._write_ports:
|
||||
write_ports[port] = instance.write_port(
|
||||
domain=port.domain, addr=port.addr, data=port.data, en=port.en)
|
||||
for port in self._read_ports:
|
||||
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
|
||||
|
||||
def __getitem__(self, index):
|
||||
|
|
@ -184,48 +252,44 @@ class Memory(wiring.Component):
|
|||
|
||||
|
||||
class ReadPort:
|
||||
"""A memory read port.
|
||||
"""A read memory port.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
signature : :class:`ReadPort.Signature`
|
||||
The signature of the port.
|
||||
memory : :class:`Memory`
|
||||
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.
|
||||
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>`.
|
||||
|
||||
An asynchronous (:py:`"comb"` domain) memory read port is always enabled. The :py:`en` input of
|
||||
such a port is tied to :py:`Const(1)`.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
signature : :class:`ReadPort.Signature`
|
||||
memory : :class:`Memory`
|
||||
domain : str
|
||||
transparent_for : tuple of :class:`WritePort`
|
||||
Signature of this memory port.
|
||||
memory : :class:`Memory` or :py:`None`
|
||||
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):
|
||||
"""A signature of a read port.
|
||||
"""Signature of a memory read port.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
addr_width : int
|
||||
Address width in bits. If the port is associated with a :class:`Memory`,
|
||||
it must be equal to :py:`ceil_log2(memory.depth)`.
|
||||
addr_width : :class:`int`
|
||||
Width of the address port.
|
||||
shape : :ref:`shape-like <lang-shapelike>` object
|
||||
The shape of the port data. If the port is associated with a :class:`Memory`,
|
||||
it must be equal to its element shape.
|
||||
Shape of the data port.
|
||||
|
||||
Members
|
||||
-------
|
||||
addr: :py:`unsigned(data_width)`
|
||||
data: ``shape``
|
||||
en: :py:`unsigned(1)`
|
||||
The enable signal. If ``domain == "comb"``, this is tied to ``Const(1)``.
|
||||
Otherwise it is a signal with ``init=1``.
|
||||
en: :py:`In(1, init=1)`
|
||||
Enable input.
|
||||
addr: :py:`In(addr_width)`
|
||||
Address input.
|
||||
data: :py:`Out(shape)`
|
||||
Data output.
|
||||
"""
|
||||
|
||||
def __init__(self, *, addr_width, shape):
|
||||
|
|
@ -234,12 +298,13 @@ class ReadPort:
|
|||
self._addr_width = addr_width
|
||||
self._shape = shape
|
||||
super().__init__({
|
||||
"en": wiring.In(1, init=1),
|
||||
"addr": wiring.In(addr_width),
|
||||
"data": wiring.Out(shape),
|
||||
"en": wiring.In(1, init=1),
|
||||
})
|
||||
|
||||
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)
|
||||
|
||||
@property
|
||||
|
|
@ -262,7 +327,7 @@ class ReadPort:
|
|||
def __init__(self, signature, *, memory, domain, transparent_for=(), path=None, src_loc_at=0):
|
||||
if not isinstance(signature, ReadPort.Signature):
|
||||
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):
|
||||
raise TypeError(f"Expected `Memory` or `None`, not {memory!r}")
|
||||
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:
|
||||
if not isinstance(port, WritePort):
|
||||
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")
|
||||
if port.domain != domain:
|
||||
raise ValueError("Transparent write ports must belong to the same domain")
|
||||
|
|
@ -287,7 +352,7 @@ class ReadPort:
|
|||
if domain == "comb":
|
||||
self.en = Const(1)
|
||||
if memory is not None:
|
||||
memory._r_ports.append(self)
|
||||
memory._read_ports.append(self)
|
||||
|
||||
@property
|
||||
def signature(self):
|
||||
|
|
@ -307,49 +372,50 @@ class ReadPort:
|
|||
|
||||
|
||||
class WritePort:
|
||||
"""A memory write port.
|
||||
"""A write memory port.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
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.
|
||||
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>`.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
signature : :class:`WritePort.Signature`
|
||||
memory : :class:`Memory`
|
||||
domain : str
|
||||
Signature of this memory port.
|
||||
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):
|
||||
"""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
|
||||
----------
|
||||
addr_width : int
|
||||
Address width in bits. If the port is associated with a :class:`Memory`,
|
||||
it must be equal to :py:`ceil_log2(memory.depth)`.
|
||||
addr_width : :class:`int`
|
||||
Width of the address port.
|
||||
shape : :ref:`shape-like <lang-shapelike>` object
|
||||
The shape of the port data. If the port is associated with a :class:`Memory`,
|
||||
it must be equal to its element shape.
|
||||
granularity : int or ``None``
|
||||
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``)
|
||||
Shape of the data port.
|
||||
granularity : :class:`int` or :py:`None`
|
||||
Granularity of memory access.
|
||||
|
||||
Members
|
||||
-------
|
||||
addr: :py:`unsigned(data_width)`
|
||||
data: ``shape``
|
||||
en: :py:`unsigned(en_width)`
|
||||
en: :py:`In(en_width)`
|
||||
Enable input.
|
||||
addr: :py:`In(addr_width)`
|
||||
Address input.
|
||||
data: :py:`In(shape)`
|
||||
Data input.
|
||||
"""
|
||||
|
||||
def __init__(self, *, addr_width, shape, granularity=None):
|
||||
|
|
@ -374,7 +440,7 @@ class WritePort:
|
|||
raise ValueError("Granularity must divide data width")
|
||||
else:
|
||||
en_width = actual_shape.width // granularity
|
||||
elif isinstance(shape, ArrayLayout):
|
||||
elif isinstance(shape, data.ArrayLayout):
|
||||
if shape.length == 0:
|
||||
en_width = 0
|
||||
elif granularity == 0:
|
||||
|
|
@ -386,12 +452,13 @@ class WritePort:
|
|||
else:
|
||||
raise TypeError("Granularity can only be specified for plain unsigned `Shape` or `ArrayLayout`")
|
||||
super().__init__({
|
||||
"en": wiring.In(en_width),
|
||||
"addr": wiring.In(addr_width),
|
||||
"data": wiring.In(shape),
|
||||
"en": wiring.In(en_width),
|
||||
})
|
||||
|
||||
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)
|
||||
|
||||
@property
|
||||
|
|
@ -420,7 +487,7 @@ class WritePort:
|
|||
def __init__(self, signature, *, memory, domain, path=None, src_loc_at=0):
|
||||
if not isinstance(signature, WritePort.Signature):
|
||||
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):
|
||||
raise TypeError(f"Expected `Memory` or `None`, not {memory!r}")
|
||||
if signature.shape != memory.shape or Shape.cast(signature.shape) != Shape.cast(memory.shape):
|
||||
|
|
@ -436,7 +503,7 @@ class WritePort:
|
|||
self._domain = domain
|
||||
self.__dict__.update(signature.members.create(path=path, src_loc_at=1 + src_loc_at))
|
||||
if memory is not None:
|
||||
memory._w_ports.append(self)
|
||||
memory._write_ports.append(self)
|
||||
|
||||
@property
|
||||
def signature(self):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue