Implement RFC 45: Move hdl.Memory to lib.Memory.

This commit is contained in:
Wanda 2024-02-14 11:27:39 +01:00 committed by Catherine
parent 6d65dc1366
commit 890e099ec3
16 changed files with 983 additions and 141 deletions

View file

@ -4,7 +4,7 @@ from ._ast import Const, C, Mux, Cat, Array, Signal, ClockSignal, ResetSignal
from ._dsl import SyntaxError, SyntaxWarning, Module
from ._cd import DomainError, ClockDomain
from ._ir import UnusedElaboratable, Elaboratable, DriverConflict, Fragment, Instance
from ._mem import Memory, ReadPort, WritePort, DummyPort
from ._mem import MemoryIdentity, MemoryInstance, Memory, ReadPort, WritePort, DummyPort
from ._rec import Record
from ._xfrm import DomainRenamer, ResetInserter, EnableInserter
@ -21,7 +21,7 @@ __all__ = [
# _ir
"UnusedElaboratable", "Elaboratable", "DriverConflict", "Fragment", "Instance",
# _mem
"Memory", "ReadPort", "WritePort", "DummyPort",
"MemoryIdentity", "MemoryInstance", "Memory", "ReadPort", "WritePort", "DummyPort",
# _rec
"Record",
# _xfrm

View file

@ -1109,7 +1109,7 @@ class NetlistEmitter:
en=en,
clk=clk,
clk_edge=cd.clk_edge,
transparent_for=tuple(write_ports[idx] for idx in port._transparency),
transparent_for=tuple(write_ports[idx] for idx in port._transparent_for),
src_loc=port._data.src_loc,
)
data = self.netlist.add_value_cell(len(port._data), cell)

View file

@ -5,6 +5,7 @@ from .. import tracer
from ._ast import *
from ._ir import Elaboratable, Fragment
from ..utils import ceil_log2
from .._utils import deprecated
__all__ = ["Memory", "ReadPort", "WritePort", "DummyPort"]
@ -33,18 +34,19 @@ class MemorySimWrite:
class MemoryInstance(Fragment):
class _ReadPort:
def __init__(self, *, domain, addr, data, en, transparency):
def __init__(self, *, domain, addr, data, en, transparent_for):
assert isinstance(domain, str)
self._domain = domain
self._addr = Value.cast(addr)
self._data = Value.cast(data)
self._en = Value.cast(en)
self._transparency = tuple(transparency)
self._transparent_for = tuple(transparent_for)
assert len(self._en) == 1
if domain == "comb":
assert isinstance(self._en, Const)
assert self._en.width == 1
assert self._en.value == 1
assert not self._transparent_for
class _WritePort:
def __init__(self, *, domain, addr, data, en):
@ -70,22 +72,24 @@ class MemoryInstance(Fragment):
self._identity = identity
self._width = operator.index(width)
self._depth = operator.index(depth)
self._init = tuple(init) if init is not None else ()
mask = (1 << self._width) - 1
self._init = tuple(item & mask for item in init) if init is not None else ()
assert len(self._init) <= self._depth
self._init += (0,) * (self._depth - len(self._init))
for x in self._init:
assert isinstance(x, int)
self._attrs = attrs or {}
self._read_ports = []
self._write_ports = []
self._read_ports: "list[MemoryInstance._ReadPort]" = []
self._write_ports: "list[MemoryInstance._WritePort]" = []
def read_port(self, *, domain, addr, data, en, transparency):
port = self._ReadPort(domain=domain, addr=addr, data=data, en=en, transparency=transparency)
def read_port(self, *, domain, addr, data, en, transparent_for):
port = self._ReadPort(domain=domain, addr=addr, data=data, en=en, transparent_for=transparent_for)
assert len(port._data) == self._width
assert len(port._addr) == ceil_log2(self._depth)
for x in port._transparency:
assert isinstance(x, int)
assert x in range(len(self._write_ports))
for idx in port._transparent_for:
assert isinstance(idx, int)
assert idx in range(len(self._write_ports))
assert self._write_ports[idx]._domain == port._domain
for signal in port._data._rhs_signals():
self.add_driver(signal, port._domain)
self._read_ports.append(port)
@ -124,6 +128,8 @@ class Memory(Elaboratable):
init : list of int
attrs : dict
"""
# TODO(amaranth-0.6): remove
@deprecated("`amaranth.hdl.Memory` is deprecated, use `amaranth.lib.memory.Memory` instead")
def __init__(self, *, width, depth, init=None, name=None, attrs=None, simulate=True):
if not isinstance(width, int) or width < 0:
raise TypeError("Memory width must be a non-negative integer, not {!r}"
@ -132,8 +138,8 @@ class Memory(Elaboratable):
raise TypeError("Memory depth must be a non-negative integer, not {!r}"
.format(depth))
self.name = name or tracer.get_var_name(depth=2, default="$memory")
self.src_loc = tracer.get_src_loc()
self.name = name or tracer.get_var_name(depth=3, default="$memory")
self.src_loc = tracer.get_src_loc(src_loc_at=1)
self.width = width
self.depth = depth
@ -208,12 +214,12 @@ class Memory(Elaboratable):
for port in self._read_ports:
port._MustUse__used = True
if port.domain == "comb":
f.read_port(domain="comb", addr=port.addr, data=port.data, en=Const(1), transparency=())
f.read_port(domain="comb", addr=port.addr, data=port.data, en=Const(1), transparent_for=())
else:
transparency = []
transparent_for = []
if port.transparent:
transparency = write_ports.get(port.domain, [])
f.read_port(domain=port.domain, addr=port.addr, data=port.data, en=port.en, transparency=transparency)
transparent_for = write_ports.get(port.domain, [])
f.read_port(domain=port.domain, addr=port.addr, data=port.data, en=port.en, transparent_for=transparent_for)
return f
@ -346,13 +352,15 @@ class DummyPort:
It does not include any read/write port specific attributes, i.e. none besides ``"domain"``;
any such attributes may be set manually.
"""
# TODO(amaranth-0.6): remove
@deprecated("`DummyPort` is deprecated, use `amaranth.lib.memory.ReadPort` or `amaranth.lib.memory.WritePort` instead")
def __init__(self, *, data_width, addr_width, domain="sync", name=None, granularity=None):
self.domain = domain
if granularity is None:
granularity = data_width
if name is None:
name = tracer.get_var_name(depth=2, default="dummy")
name = tracer.get_var_name(depth=3, default="dummy")
self.addr = Signal(addr_width,
name=f"{name}_addr", src_loc_at=1)

View file

@ -263,7 +263,7 @@ class FragmentTransformer:
addr=port._addr,
data=port._data,
en=port._en,
transparency=port._transparency,
transparent_for=port._transparent_for,
)
for port in fragment._read_ports
]

View file

@ -1,12 +1,11 @@
"""First-in first-out queues."""
import warnings
from .. import *
from ..asserts import *
from ..utils import ceil_log2
from .coding import GrayEncoder, GrayDecoder
from .cdc import FFSynchronizer, AsyncFFSynchronizer
from .memory import Memory
__all__ = ["FIFOInterface", "SyncFIFO", "SyncFIFOBuffered", "AsyncFIFO", "AsyncFIFOBuffered"]
@ -130,7 +129,7 @@ class SyncFIFO(Elaboratable, FIFOInterface):
do_read = self.r_rdy & self.r_en
do_write = self.w_rdy & self.w_en
storage = m.submodules.storage = Memory(width=self.width, depth=self.depth)
storage = m.submodules.storage = Memory(shape=self.width, depth=self.depth, init=[])
w_port = storage.write_port()
r_port = storage.read_port(domain="comb")
produce = Signal(range(self.depth))
@ -257,9 +256,9 @@ class SyncFIFOBuffered(Elaboratable, FIFOInterface):
do_inner_read = inner_r_rdy & (~self.r_rdy | self.r_en)
storage = m.submodules.storage = Memory(width=self.width, depth=inner_depth)
storage = m.submodules.storage = Memory(shape=self.width, depth=inner_depth, init=[])
w_port = storage.write_port()
r_port = storage.read_port(domain="sync", transparent=False)
r_port = storage.read_port(domain="sync")
produce = Signal(range(inner_depth))
consume = Signal(range(inner_depth))
@ -438,9 +437,9 @@ class AsyncFIFO(Elaboratable, FIFOInterface):
m.d[self._w_domain] += self.w_level.eq(produce_w_bin - consume_w_bin)
m.d.comb += self.r_level.eq(produce_r_bin - consume_r_bin)
storage = m.submodules.storage = Memory(width=self.width, depth=self.depth)
storage = m.submodules.storage = Memory(shape=self.width, depth=self.depth, init=[])
w_port = storage.write_port(domain=self._w_domain)
r_port = storage.read_port (domain=self._r_domain, transparent=False)
r_port = storage.read_port (domain=self._r_domain)
m.d.comb += [
w_port.addr.eq(produce_w_bin[:-1]),
w_port.data.eq(self.w_data),

430
amaranth/lib/memory.py Normal file
View file

@ -0,0 +1,430 @@
import operator
from collections import OrderedDict
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
__all__ = ["WritePort", "ReadPort", "Memory"]
class WritePort:
"""A memory write 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.
Attributes
----------
signature : :class:`WritePort.Signature`
memory : :class:`Memory`
domain : str
"""
class Signature(wiring.Signature):
"""A signature of a write 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)`.
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``)
Members
-------
addr: :py:`unsigned(data_width)`
data: ``shape``
en: :py:`unsigned(en_width)`
"""
def __init__(self, *, addr_width, shape, granularity=None):
if not isinstance(addr_width, int) or addr_width < 0:
raise TypeError(f"`addr_width` must be a non-negative int, not {addr_width!r}")
self._addr_width = addr_width
self._shape = shape
self._granularity = granularity
if granularity is None:
en_width = 1
elif not isinstance(granularity, int) or granularity < 0:
raise TypeError(f"Granularity must be a non-negative int or None, not {granularity!r}")
elif not isinstance(shape, ShapeCastable):
actual_shape = Shape.cast(shape)
if actual_shape.signed:
raise ValueError("Granularity cannot be specified with signed shape")
elif actual_shape.width == 0:
en_width = 0
elif granularity == 0:
raise ValueError("Granularity must be positive")
elif actual_shape.width % granularity != 0:
raise ValueError("Granularity must divide data width")
else:
en_width = actual_shape.width // granularity
elif isinstance(shape, ArrayLayout):
if shape.length == 0:
en_width = 0
elif granularity == 0:
raise ValueError("Granularity must be positive")
elif shape.length % granularity != 0:
raise ValueError("Granularity must divide data array length")
else:
en_width = shape.length // granularity
else:
raise TypeError("Granularity can only be specified for plain unsigned `Shape` or `ArrayLayout`")
super().__init__({
"addr": wiring.In(addr_width),
"data": wiring.In(shape),
"en": wiring.In(en_width),
})
@property
def addr_width(self):
return self._addr_width
@property
def shape(self):
return self._shape
@property
def granularity(self):
return self._granularity
def __repr__(self):
granularity = f", granularity={self.granularity}" if self.granularity is not None else ""
return f"WritePort.Signature(addr_width={self.addr_width}, shape={self.shape}{granularity})"
def __init__(self, signature, *, memory, domain):
if not isinstance(signature, WritePort.Signature):
raise TypeError(f"Expected `WritePort.Signature`, not {signature!r}")
if memory is not None:
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):
raise ValueError(f"Memory shape {memory.shape!r} doesn't match port shape {signature.shape!r}")
if signature.addr_width != ceil_log2(memory.depth):
raise ValueError(f"Memory address width {ceil_log2(memory.depth)!r} doesn't match port address width {signature.addr_width!r}")
if not isinstance(domain, str):
raise TypeError(f"Domain has to be a string, not {domain!r}")
if domain == "comb":
raise ValueError("Write port domain cannot be \"comb\"")
self._signature = signature
self._memory = memory
self._domain = domain
self.__dict__.update(signature.members.create())
if memory is not None:
memory._w_ports.append(self)
@property
def signature(self):
return self._signature
@property
def memory(self):
return self._memory
@property
def domain(self):
return self._domain
class ReadPort:
"""A memory read 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.
Attributes
----------
signature : :class:`ReadPort.Signature`
memory : :class:`Memory`
domain : str
transparent_for : tuple of :class:`WritePort`
"""
class Signature(wiring.Signature):
"""A signature of a 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)`.
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.
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``.
"""
def __init__(self, *, addr_width, shape):
if not isinstance(addr_width, int) or addr_width < 0:
raise TypeError(f"`addr_width` must be a non-negative int, not {addr_width!r}")
self._addr_width = addr_width
self._shape = shape
super().__init__({
"addr": wiring.In(addr_width),
"data": wiring.Out(shape),
"en": wiring.In(1, init=1),
})
@property
def addr_width(self):
return self._addr_width
@property
def shape(self):
return self._shape
def __repr__(self):
return f"ReadPort.Signature(addr_width={self.addr_width}, shape={self.shape})"
def __init__(self, signature, *, memory, domain, transparent_for=()):
if not isinstance(signature, ReadPort.Signature):
raise TypeError(f"Expected `ReadPort.Signature`, not {signature!r}")
if memory is not None:
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):
raise ValueError(f"Memory shape {memory.shape!r} doesn't match port shape {signature.shape!r}")
if signature.addr_width != ceil_log2(memory.depth):
raise ValueError(f"Memory address width {ceil_log2(memory.depth)!r} doesn't match port address width {signature.addr_width!r}")
if not isinstance(domain, str):
raise TypeError(f"Domain has to be a string, not {domain!r}")
transparent_for = tuple(transparent_for)
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:
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")
self._signature = signature
self._memory = memory
self._domain = domain
self._transparent_for = transparent_for
self.__dict__.update(signature.members.create())
if domain == "comb":
self.en = Const(1)
if memory is not None:
memory._r_ports.append(self)
@property
def signature(self):
return self._signature
@property
def memory(self):
return self._memory
@property
def domain(self):
return self._domain
@property
def transparent_for(self):
return self._transparent_for
class Memory(wiring.Component):
"""A word addressable storage.
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.
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`
"""
class Init(MutableSequence):
"""Initial data of a :class:`Memory`.
This is a container implementing the ``MutableSequence`` protocol, enforcing two constraints:
- 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`
"""
def __init__(self, items, *, 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
else:
self._raw = self._items = [0] * depth
try:
for idx, item in enumerate(items):
self[idx] = item
except (TypeError, ValueError) as e:
raise type(e)("Memory initialization value at address {:x}: {}"
.format(idx, e)) from None
def __getitem__(self, index):
return self._items[index]
def __setitem__(self, index, value):
if isinstance(index, slice):
start, stop, step = index.indices(len(self._items))
indices = range(start, stop, step)
if len(value) != len(indices):
raise ValueError("Changing length of Memory.init is not allowed")
for actual_index, actual_value in zip(indices, value):
self[actual_index] = actual_value
else:
if isinstance(self._shape, ShapeCastable):
self._raw[index] = Const.cast(self._shape.const(value)).value
else:
value = operator.index(value)
self._items[index] = value
def __delitem__(self, index):
raise TypeError("Deleting items from Memory.init is not allowed")
def insert(self, index, value):
raise TypeError("Inserting items 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})"
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
self._shape = shape
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({}))
def read_port(self, *, domain="sync", transparent_for=()):
"""Adds a new read port and returns it.
Equivalent to creating a :class:`ReadPort` with a signature of :py:`ReadPort.Signature(addr_width=ceil_log2(self.depth), shape=self.shape)`
"""
signature = ReadPort.Signature(addr_width=ceil_log2(self.depth), shape=self.shape)
return ReadPort(signature, memory=self, domain=domain, transparent_for=transparent_for)
def write_port(self, *, domain="sync", granularity=None):
"""Adds a new write port and returns it.
Equivalent to creating a :class:`WritePort` with a signature of :py:`WritePort.Signature(addr_width=ceil_log2(self.depth), shape=self.shape, granularity=granularity)`
"""
signature = WritePort.Signature(addr_width=ceil_log2(self.depth), shape=self.shape, granularity=granularity)
return WritePort(signature, memory=self, domain=domain)
@property
def depth(self):
return self._depth
@property
def shape(self):
return self._shape
@property
def init(self):
return self._init
@property
def attrs(self):
return self._attrs
@property
def w_ports(self):
"""Returns a tuple of all write ports defined so far."""
return tuple(self._w_ports)
@property
def r_ports(self):
"""Returns a tuple of all read ports defined so far."""
return tuple(self._r_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)
return instance
def __getitem__(self, index):
"""Simulation only."""
return MemorySimRead(self._identity, index)

View file

@ -505,7 +505,7 @@ class _FragmentCompiler:
addr = emitter.def_var("read_addr", f"({(1 << len(port._addr)) - 1:#x} & {addr})")
data = emitter.def_var("read_data", f"slots[{memory_index}].read({addr})")
for idx in port._transparency:
for idx in port._transparent_for:
waddr, wdata, wen = write_vals[idx]
emitter.append(f"if {addr} == {waddr}:")
with emitter.indent():

View file

@ -85,7 +85,7 @@ class _VCDWriter:
trace_names[trace_signal] = {("bench", name)}
assigned_names.add(name)
self.traces.append(trace_signal)
elif isinstance(trace, (MemoryInstance, Memory)):
elif hasattr(trace, "_identity") and isinstance(trace._identity, MemoryIdentity):
if not trace._identity in memories:
raise ValueError(f"{trace!r} is a memory not part of the elaborated design")
self.traces.append(trace._identity)