Source code for pyrtl.memory

"""
Defines PyRTL memories.
These blocks of memories can be read (potentially async) and written (sync)

MemBlocks supports any number of the following operations:

* read: `d = mem[address]`
* write: `mem[address] <<= d`
* write with an enable: `mem[address] <<= MemBlock.EnabledWrite(d, enable=we)`

Based on the number of reads and writes a memory will be inferred
with the correct number of ports to support that
"""

import collections

from .pyrtlexceptions import PyrtlError
from .core import working_block, LogicNet, _NameIndexer
from .wire import WireVector, Const, next_tempvar_name
from .corecircuits import as_wires
# ------------------------------------------------------------------------
#
#         ___        __   __          __        __   __
#   |\/| |__   |\/| /  \ |__) \ /    |__) |    /  \ /  ` |__/
#   |  | |___  |  | \__/ |  \  |     |__) |___ \__/ \__, |  \
#


_memIndex = _NameIndexer()

_MemAssignment = collections.namedtuple('_MemAssignment', 'rhs, is_conditional')
"""_MemAssignment is the type returned from assignment by |= or <<="""


def _reset_memory_indexer():
    global _memIndex
    _memIndex = _NameIndexer()


class _MemIndexed(WireVector):
    """ Object used internally to route memory assigns correctly.

    The normal PyRTL user should never need to be aware that this class exists,
    hence the underscore in the name.  It presents a very similar interface to
    WireVectors (all of the normal wirevector operations should still work),
    but if you try to *set* the value with <<= or |= then it will generate a
    _MemAssignment object rather than the normal wire assignment.
    """

    def __init__(self, mem, index):
        self.mem = mem
        self.index = index
        self.wire = None

    def __ilshift__(self, other):
        return _MemAssignment(rhs=other, is_conditional=False)

    def __ior__(self, other):
        return _MemAssignment(rhs=other, is_conditional=True)

    def _two_var_op(self, other, op):
        return as_wires(self)._two_var_op(other, op)

    def __invert__(self):
        return as_wires(self).__invert__()

    def __getitem__(self, item):
        return as_wires(self).__getitem__(item)

    def __len__(self):
        return self.mem.bitwidth

    def sign_extended(self, bitwidth):
        return as_wires(self).sign_extended(bitwidth)

    def zero_extended(self, bitwidth):
        return as_wires(self).zero_extended(bitwidth)

    @property
    def name(self):
        return as_wires(self).name
        # raise PyrtlError("MemIndexed is a temporary object and therefore doesn't have a name")

    @name.setter
    def name(self, n):
        as_wires(self).name = n


[docs] class MemBlock(object): """ MemBlock is the object for specifying block memories. It can be indexed like an array for both reading and writing. Writes under a conditional are automatically converted to enabled writes. For example, consider the following examples where ``addr``, ``data``, and ``we`` are all WireVectors:: data = memory[addr] # infer read port memory[addr] <<= data # infer write port mem[address] <<= MemBlock.EnabledWrite(data, enable=we) When the address of a memory is assigned to using an :class:`~.MemBlock.EnabledWrite` object items will only be written to the memory when the enable WireVector is set to high (1). """ # FIXME: write ports assume that only one port is under control of the conditional EnabledWrite = collections.namedtuple('EnabledWrite', 'data, enable') """ Allows for an enable bit for each write port, where data (the first field in the tuple) is the normal data address, and enable (the second field) is a one bit signal specifying that the write should happen (i.e. active high)."""
[docs] def __init__(self, bitwidth, addrwidth, name='', max_read_ports=2, max_write_ports=1, asynchronous=False, block=None): """ Create a PyRTL read-write memory. :param int bitwidth: Defines the bitwidth of each element in the memory :param int addrwidth: The number of bits used to address an element of the memory. This also defines the size of the memory :param str name: The identifier for the memory :param max_read_ports: limits the number of read ports each block can create; passing `None` indicates there is no limit :param max_write_ports: limits the number of write ports each block can create; passing `None` indicates there is no limit :param bool asynchronous: If false make sure that memory reads are only done using values straight from a register. (aka make sure that the read is synchronous) :param basestring name: Name of the memory. Defaults to an autogenerated name :param block: The block to add it to, defaults to the working block It is best practice to make sure your block memory/fifos read/write operations start on a clock edge if you want them to synthesize into efficient hardware. MemBlocks will enforce this by making sure that you only address them with a register or input, unless you explicitly declare the memory as asynchronous with ``asynchronous=True`` flag. Note that asynchronous mems are, while sometimes very convenient and tempting, rarely a good idea. They can't be mapped to block RAMs in FPGAs and will be converted to registers by most design tools even though PyRTL can handle them with no problem. For any memory beyond a few hundred entries it is not a realistic option. Each read or write to the memory will create a new `port` (either a read port or write port respectively). By default memories are limited to 2-read and 1-write port, but to keep designs efficient by default, but those values can be set as options. Note that memories with high numbers of ports may not be possible to map to physical memories such as block RAMs or existing memory hardware macros. """ self.max_read_ports = max_read_ports self.num_read_ports = 0 self.block = working_block(block) name = next_tempvar_name(name) if bitwidth <= 0: raise PyrtlError('bitwidth must be >= 1') if addrwidth <= 0: raise PyrtlError('addrwidth must be >= 1') self.bitwidth = bitwidth self.name = name self.addrwidth = addrwidth self.readport_nets = [] self.id = _memIndex.next_index() self.asynchronous = asynchronous self.block._add_memblock(self) self.max_write_ports = max_write_ports self.num_write_ports = 0 self.writeport_nets = []
@property def read_ports(self): raise PyrtlError('read_ports now called num_read_ports for clarity') def __getitem__(self, item): """ Builds circuitry to retrieve an item from the memory """ item = as_wires(item, bitwidth=self.addrwidth, truncating=False) if len(item) > self.addrwidth: raise PyrtlError('memory index bitwidth > addrwidth') return _MemIndexed(mem=self, index=item) def __setitem__(self, item, assignment): """ Builds circuitry to set an item in the memory """ if isinstance(assignment, _MemAssignment): self._assignment(item, assignment.rhs, is_conditional=assignment.is_conditional) else: raise PyrtlError('error, assigment to memories should use "<<=" not "=" operator') def _readaccess(self, addr): # FIXME: add conditional read ports return self._build_read_port(addr) def _build_read_port(self, addr): if self.max_read_ports is not None: self.num_read_ports += 1 if self.num_read_ports > self.max_read_ports: raise PyrtlError('maximum number of read ports (%d) exceeded' % self.max_read_ports) data = WireVector(bitwidth=self.bitwidth) readport_net = LogicNet( op='m', op_param=(self.id, self), args=(addr,), dests=(data,)) working_block().add_net(readport_net) self.readport_nets.append(readport_net) return data def _assignment(self, item, val, is_conditional): from .conditional import _build item = as_wires(item, bitwidth=self.addrwidth, truncating=False) if len(item) > self.addrwidth: raise PyrtlError('error, the wire indexing the memory bitwidth > addrwidth') addr = item if isinstance(val, MemBlock.EnabledWrite): data, enable = val.data, val.enable else: data, enable = val, Const(1, bitwidth=1) data = as_wires(data, bitwidth=self.bitwidth, truncating=False) enable = as_wires(enable, bitwidth=1, truncating=False) if len(data) != self.bitwidth: raise PyrtlError('error, write data larger than memory bitwidth') if len(enable) != 1: raise PyrtlError('error, enable signal not exactly 1 bit') if is_conditional: _build(self, (addr, data, enable)) else: self._build(addr, data, enable) def _build(self, addr, data, enable): """ Builds a write port. """ if self.max_write_ports is not None: self.num_write_ports += 1 if self.num_write_ports > self.max_write_ports: raise PyrtlError('maximum number of write ports (%d) exceeded' % self.max_write_ports) writeport_net = LogicNet( op='@', op_param=(self.id, self), args=(addr, data, enable), dests=tuple()) working_block().add_net(writeport_net) self.writeport_nets.append(writeport_net) def _make_copy(self, block=None): block = working_block(block) return MemBlock(bitwidth=self.bitwidth, addrwidth=self.addrwidth, name=self.name, max_read_ports=self.max_read_ports, max_write_ports=self.max_write_ports, asynchronous=self.asynchronous, block=block)
[docs] class RomBlock(MemBlock): """ PyRTL Read Only Memory. RomBlocks are the read only memory block for PyRTL. They support the same read interface and normal memories, but they are cannot be written to (i.e. there are no write ports). The ROM must be initialized with some values and construction through the use of the ``romdata`` which is the memory for the system. """
[docs] def __init__(self, bitwidth, addrwidth, romdata, name='', max_read_ports=2, build_new_roms=False, asynchronous=False, pad_with_zeros=False, block=None): """Create a Python Read Only Memory. :param int bitwidth: The bitwidth of each item stored in the ROM :param int addrwidth: The bitwidth of the address bus (determines number of addresses) :param romdata: This can either be a function or an array (iterable) that maps an address as an input to a result as an output :param str name: The identifier for the memory :param max_read_ports: limits the number of read ports each block can create; passing ``None`` indicates there is no limit :param bool build_new_roms: indicates whether to create and pass new RomBlocks during ``__getitem__`` to avoid exceeding `max_read_ports` :param bool asynchronous: If false make sure that memory reads are only done using values straight from a register. (aka make sure that reads are synchronous) :param bool pad_with_zeros: If true, extend any missing romdata with zeros out until the size of the romblock so that any access to the rom is well defined. Otherwise, the simulation should throw an error on access of unintialized data. If you are generating verilog from the rom, you will need to specify a value for every address (in which case setting this to True will help), however for testing and simulation it useful to know if you are off the end of explicitly specified values (which is why it is False by default) :param block: The block to add to, defaults to the working block """ super(RomBlock, self).__init__(bitwidth=bitwidth, addrwidth=addrwidth, name=name, max_read_ports=max_read_ports, max_write_ports=0, asynchronous=asynchronous, block=block) self.data = romdata self.build_new_roms = build_new_roms self.current_copy = self self.pad_with_zeros = pad_with_zeros
def __getitem__(self, item): import numbers if isinstance(item, numbers.Number): raise PyrtlError("There is no point in indexing into a RomBlock with an int. " "Instead, get the value from the source data for this Rom") # If you really know what you are doing, use a Const WireVector instead. return super(RomBlock, self).__getitem__(item) def __setitem__(self, item, assignment): raise PyrtlError('no writing to a read-only memory') def _get_read_data(self, address): import types try: if address < 0 or address > 2**self.addrwidth - 1: raise PyrtlError("Invalid address, " + str(address) + " specified") except TypeError: raise PyrtlError("Address: {} with invalid type specified".format(address)) if isinstance(self.data, types.FunctionType): try: value = self.data(address) except Exception: raise PyrtlError("Invalid data function for RomBlock") else: try: value = self.data[address] except KeyError: if self.pad_with_zeros: value = 0 else: raise PyrtlError( "RomBlock key is invalid, " "consider using pad_with_zeros=True for defaults" ) except IndexError: if self.pad_with_zeros: value = 0 else: raise PyrtlError( "RomBlock index is invalid, " "consider using pad_with_zeros=True for defaults" ) except Exception: raise PyrtlError("invalid type for RomBlock data object") try: if value < 0 or value >= 2**self.bitwidth: raise PyrtlError("invalid value for RomBlock data") except TypeError: raise PyrtlError("Value: {} from rom {} has an invalid type" .format(value, self)) return value def _build_read_port(self, addr): if self.build_new_roms and \ (self.current_copy.num_read_ports >= self.current_copy.max_read_ports): self.current_copy = self._make_copy() return super(RomBlock, self.current_copy)._build_read_port(addr) def _make_copy(self, block=None,): block = working_block(block) return RomBlock(bitwidth=self.bitwidth, addrwidth=self.addrwidth, romdata=self.data, name=self.name, max_read_ports=self.max_read_ports, asynchronous=self.asynchronous, pad_with_zeros=self.pad_with_zeros, block=block)