amaranth/amaranth/hdl/_mem.py

371 lines
14 KiB
Python

import operator
from collections import OrderedDict
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"]
class MemoryIdentity: pass
class MemorySimRead:
def __init__(self, identity, addr):
assert isinstance(identity, MemoryIdentity)
self._identity = identity
self._addr = Value.cast(addr)
def eq(self, value):
return MemorySimWrite(self._identity, self._addr, value)
class MemorySimWrite:
def __init__(self, identity, addr, data):
assert isinstance(identity, MemoryIdentity)
self._identity = identity
self._addr = Value.cast(addr)
self._data = Value.cast(data)
class MemoryInstance(Fragment):
class _ReadPort:
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._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):
assert isinstance(domain, str)
assert domain != "comb"
self._domain = domain
self._addr = Value.cast(addr)
self._data = Value.cast(data)
self._en = Value.cast(en)
if len(self._data):
assert len(self._data) % len(self._en) == 0
@property
def _granularity(self):
if not len(self._data):
return 1
return len(self._data) // len(self._en)
def __init__(self, *, identity, width, depth, init=None, attrs=None, src_loc=None):
super().__init__(src_loc=src_loc)
assert isinstance(identity, MemoryIdentity)
self._identity = identity
self._width = operator.index(width)
self._depth = operator.index(depth)
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: "list[MemoryInstance._ReadPort]" = []
self._write_ports: "list[MemoryInstance._WritePort]" = []
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 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)
def write_port(self, *, domain, addr, data, en):
port = self._WritePort(domain=domain, addr=addr, data=data, en=en)
assert len(port._data) == self._width
assert len(port._addr) == ceil_log2(self._depth)
self._write_ports.append(port)
return len(self._write_ports) - 1
class Memory(Elaboratable):
"""A word addressable storage.
Parameters
----------
width : int
Access granularity. Each storage element of this memory is ``width`` bits in size.
depth : int
Word count. This memory contains ``depth`` storage elements.
init : list of int
Initial values. At power on, each storage element in this memory is initialized to
the corresponding element of ``init``, if any, or to zero otherwise.
Uninitialized memories are not currently supported.
name : str
Name hint for this memory. If ``None`` (default) the name is inferred from the variable
name this ``Signal`` is assigned to.
attrs : dict
Dictionary of synthesis attributes.
Attributes
----------
width : int
depth : int
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}"
.format(width))
if not isinstance(depth, int) or depth < 0:
raise TypeError("Memory depth must be a non-negative integer, not {!r}"
.format(depth))
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
self.attrs = OrderedDict(() if attrs is None else attrs)
self.init = init
self._read_ports = []
self._write_ports = []
self._identity = MemoryIdentity()
@property
def init(self):
return self._init
@init.setter
def init(self, new_init):
self._init = [] if new_init is None else list(new_init)
if len(self.init) > self.depth:
raise ValueError("Memory initialization value count exceed memory depth ({} > {})"
.format(len(self.init), self.depth))
try:
for addr, val in enumerate(self._init):
operator.index(val)
except TypeError as e:
raise TypeError("Memory initialization value at address {:x}: {}"
.format(addr, e)) from None
def read_port(self, *, src_loc_at=0, **kwargs):
"""Get a read port.
See :class:`ReadPort` for details.
Arguments
---------
domain : str
transparent : bool
Returns
-------
An instance of :class:`ReadPort` associated with this memory.
"""
return ReadPort(self, src_loc_at=1 + src_loc_at, **kwargs)
def write_port(self, *, src_loc_at=0, **kwargs):
"""Get a write port.
See :class:`WritePort` for details.
Arguments
---------
domain : str
granularity : int
Returns
-------
An instance of :class:`WritePort` associated with this memory.
"""
return WritePort(self, src_loc_at=1 + src_loc_at, **kwargs)
def __getitem__(self, index):
"""Simulation only."""
return MemorySimRead(self._identity, index)
def elaborate(self, platform):
f = MemoryInstance(identity=self._identity, width=self.width, depth=self.depth, init=self.init, attrs=self.attrs, src_loc=self.src_loc)
write_ports = {}
for port in self._write_ports:
port._MustUse__used = True
iport = f.write_port(domain=port.domain, addr=port.addr, data=port.data, en=port.en)
write_ports.setdefault(port.domain, []).append(iport)
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), transparent_for=())
else:
transparent_for = []
if port.transparent:
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
class ReadPort(Elaboratable):
"""A memory read port.
Parameters
----------
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 : bool
Port transparency. If set (default), a read at an address that is also being written to in
the same clock cycle will output the new value. Otherwise, the old value will be output
first. This behavior only applies to ports in the same domain.
Attributes
----------
memory : :class:`Memory`
domain : str
transparent : bool
addr : Signal(range(memory.depth)), in
Read address.
data : Signal(memory.width), out
Read data.
en : Signal or Const, in
Read enable. If asserted, ``data`` is updated with the word stored at ``addr``.
Exceptions
----------
Raises :exn:`ValueError` if the read port is simultaneously asynchronous and non-transparent.
"""
def __init__(self, memory, *, domain="sync", transparent=True, src_loc_at=0):
if domain == "comb" and not transparent:
raise ValueError("Read port cannot be simultaneously asynchronous and non-transparent")
self.memory = memory
self.domain = domain
self.transparent = transparent
self.addr = Signal(range(memory.depth),
name=f"{memory.name}_r_addr", src_loc_at=1 + src_loc_at)
self.data = Signal(memory.width,
name=f"{memory.name}_r_data", src_loc_at=1 + src_loc_at)
if self.domain != "comb":
self.en = Signal(name=f"{memory.name}_r_en", init=1,
src_loc_at=1 + src_loc_at)
else:
self.en = Const(1)
memory._read_ports.append(self)
def elaborate(self, platform):
if self is self.memory._read_ports[0]:
return self.memory
else:
return Fragment()
class WritePort(Elaboratable):
"""A memory write port.
Parameters
----------
memory : :class:`Memory`
Memory associated with the port.
domain : str
Clock domain. Defaults to ``"sync"``. Writes have a latency of 1 clock cycle.
granularity : int
Port granularity. Defaults to ``memory.width``. Write data is split evenly in
``memory.width // granularity`` chunks, which can be updated independently.
Attributes
----------
memory : :class:`Memory`
domain : str
granularity : int
addr : Signal(range(memory.depth)), in
Write address.
data : Signal(memory.width), in
Write data.
en : Signal(memory.width // granularity), in
Write enable. Each bit selects a non-overlapping chunk of ``granularity`` bits on the
``data`` signal, which is written to memory at ``addr``. Unselected chunks are ignored.
Exceptions
----------
Raises :exn:`ValueError` if the write port granularity is greater than memory width, or does not
divide memory width evenly.
"""
def __init__(self, memory, *, domain="sync", granularity=None, src_loc_at=0):
if granularity is None:
granularity = memory.width
if not isinstance(granularity, int) or granularity < 0:
raise TypeError("Write port granularity must be a non-negative integer, not {!r}"
.format(granularity))
if granularity > memory.width:
raise ValueError("Write port granularity must not be greater than memory width "
"({} > {})"
.format(granularity, memory.width))
if memory.width // granularity * granularity != memory.width:
raise ValueError("Write port granularity must divide memory width evenly")
self.memory = memory
self.domain = domain
self.granularity = granularity
self.addr = Signal(range(memory.depth),
name=f"{memory.name}_w_addr", src_loc_at=1 + src_loc_at)
self.data = Signal(memory.width,
name=f"{memory.name}_w_data", src_loc_at=1 + src_loc_at)
self.en = Signal(memory.width // granularity,
name=f"{memory.name}_w_en", src_loc_at=1 + src_loc_at)
memory._write_ports.append(self)
def elaborate(self, platform):
if not self.memory._read_ports and self is self.memory._write_ports[0]:
return self.memory
else:
return Fragment()
class DummyPort:
"""Dummy memory port.
This port can be used in place of either a read or a write port for testing and verification.
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=3, default="dummy")
self.addr = Signal(addr_width,
name=f"{name}_addr", src_loc_at=1)
self.data = Signal(data_width,
name=f"{name}_data", src_loc_at=1)
self.en = Signal(data_width // granularity,
name=f"{name}_en", src_loc_at=1)