magma Primitives

magma defines a core set of primitive generators that are parametrized over a type T as well as some useful functions for working with magma types.

Primitive Generators

Register

The Register(T) primitive creates a register circuit that stores a value of type T. Here's a simple example that delays an input byte by a cycle.

import magma as m
from magma.primitives import Register


class DelayByteByOneCycle(m.Circuit):
    io = m.IO(I=m.In(m.Bits[8]), O=m.Out(m.Bits[8])) 
    io += m.ClockIO(has_reset=True)
    io.O @= Register(m.Bits[8], m.Bits[8](0xDE), reset_type=m.Reset)()(io.I)

Register __init__ arguments: * T: m.Kind - The type of the value stored in the register (e.g. Bits[5]) * init: Union[m.Type, int] - (optional) A const value (i.e. init.const() == True) of type T or an int to be used as the initial value of the register. If no value is provided, the register will be initialized with 0. * has_enable: bool - (optional) whether the register has an enable signal * reset_type: m.AbstractReset - (optional) The type of the reset port (also specifies the semantic behavior of the reset signal) * reset_priority: bool - (optional) boolean flag choosing whether synchronous reset (RESET or RESETN) has priority over enable

Memory

The Memory(N, T) primitive creates a one read port and one write port memory storing N values of type T. Here's a simple example that generates a memory with 4 entries, each contain 5 bits:

class SimpleMem(m.Circuit):
    io = m.IO(
        raddr=m.In(m.Bits[2]),
        rdata=m.Out(m.Bits[5]),
        waddr=m.In(m.Bits[2]),
        wdata=m.In(m.Bits[5]),
        clk=m.In(m.Clock),
        wen=m.In(m.Enable)

    )
    Mem4x5 = m.Memory(4, m.Bits[5])()
    Mem4x5.RADDR @= io.raddr
    io.rdata @= Mem4x5.RDATA
    Mem4x5.WADDR @= io.waddr
    Mem4x5.WDATA @= io.wdata

Mem __init__ arguments: * height: int - number of entries in the memory * T: m.Kind - type of each entry * read_latency: int - (optional, default 0) the number of cycles it takes for a value to appear on the read port. * read_only: bool - (optional, default False) set to True to disable the generation of a write port * init: Optional[tuple] - (optional) initial contents of the memory as a tuple containing an initial value for each entry

The memory provides a convenience interface following the Python __getitem__/__setitem__ pattern, here is an example:

class MemoryGetItemSetItem(m.Circuit):
    io = m.IO(
        raddr=m.In(m.Bits[2]),
        rdata=m.Out(m.Bits[5]),
        waddr=m.In(m.Bits[2]),
        wdata=m.In(m.Bits[5]),
        clk=m.In(m.Clock),
        wen=m.In(m.Enable)

    )
    Mem4x5 = m.Memory(4, m.Bits[5])()
    io.rdata @= Mem4x5[io.raddr]
    Mem4x5[io.waddr] @= io.wdata

Reading from a memory using the __getitem__ syntax (e.g. Mem4x5[io.raddr]) is equivalent to wiring the RADDR port to the __getitem__ key/index and returning the RDATA port. Note that writing to a memory uses @= with __getitem__ on the left hand side (as opposed to the standard = and __setitem__ pattern used in Python). This is so that writing to a memory is consistent with standard wiring using the @= syntax. Writing to a memory (e.g. Mem4x5[io.waddr] @= io.wdata) is equivalent to wiring the key/index (io.waddr) to the WADDR port and the value (io.wdata) to the WDATA port.

Mux

The Mux(N, T) primitive creates a mux circuit that selects between N input values of type T. Here's a simple example that selects between two bits:

class SelectABit(m.Circuit):
    io = m.IO(I=m.In(m.Bits[2]), S=m.In(m.Bit), O=m.Out(m.Bit))
    io.O @= Mux(2, m.Bit)()(io.I[0], io.I[1], io.S)

Mux __init__ arguments: * height: int - number of inputs to select from * T: m.Kind - type of the input values

The generated mux circuit will have separate ports for each input (i.e. I0 for the 1st input, I1 for the second...). The select input will select the input value in ascending order (S == 0 will select I0, S == 1 will select I1, ...).

Dict and List Lookup

magma provides the helper functions dict_lookup and list_lookup to facilitate mux generation for looking up values stored in a dictionary or list.

Here are the interfaces to the functions:

def dict_lookup(dict_, select, default=0):
    """
    Use `select` as an index into `dict` (similar to a case statement)

    `default` is used when `select` does not match any of the keys and has a
    default value of 0
    """

def list_lookup(list_, select, default=0):
    """
    Use `select` as an index into `list` (similar to a case statement)

    `default` is used when `select` does not match any of the indices (e.g.
    when the select width is longer than the list) and has a default value of
    0.
    """

Here's examples of using them:

class DictLookup(m.Circuit):
    io = m.IO(S=m.In(m.Bits[2]), O=m.Out(m.Bits[5]))

    dict_ = {
        0: BitVector[5](0),
        2: BitVector[5](2),
        3: BitVector[5](3)
    }
    io.O @= m.dict_lookup(dict_, io.S, default=BitVector[5](1))


class ListLookup(m.Circuit):
    io = m.IO(S=m.In(m.Bits[2]), O=m.Out(m.Bits[5]))

    list_ = [BitVector[5](0), BitVector[5](1), BitVector[5](2)]
    io.O @= m.list_lookup(list_, io.S, default=BitVector[5](3))

LUT

The LUT(T, contents) primitive creates a lookup table circuit programmed with contents (a tuple of values of type T). Here's a simple example with 4 entries of 8-bit values:

contents = (
    m.Bits[8](0xDE),
    m.Bits[8](0xAD),
    m.Bits[8](0xBE),
    m.Bits[8](0xEF)
)

class SimpleLUT(m.Circuit):
    io = m.IO(I=m.In(m.Bits[2]), O=m.Out(m.Bits[8]))
    io.O @= LUT(m.Bits[8], contents)()(io.I)

LUT __init__ arguments: * T: m.Kind - type of the lookup table contents * contents - the contents of the LUT (a tuple containing elements of type T that are constant)

Functions

reduce (bitwise)

The m.reduce function allows you to apply a reduction operator on a value of type Bits. Here's a simple example:

import operator
import magma as m

class ReduceAnd(m.Circuit):
    io = m.IO(I=m.In(m.Bits[5]), O=m.Out(m.Bit))
    io.O @= m.reduce(operator.and_, io.I)

The m.reduce function will generate an instance of the coreir bitwise reduce primitive, which will in turn compile to a verilog bitwise reduction (e.g. assign O = &I).

For convenience, we also provide the following methods on the Bits type: * reduce_and() - shorthand for m.reduce(operator.and_, bits_value) * reduce_or() - shorthand for m.reduce(operator.or_, bits_value) * reduce_xor() - shorthand for m.reduce(operator.xor, bits_value)

For other reduction patterns (non-bitwise), you can simply use the standard functools.reduce.

Reduce interface: reduce(operator, bits: Bits)

get_slice and set_slice

These operators provide a functional-style syntax for working with dynamic slices of magma arrays. They allow you to use a dynamic slice start index with a constant width (e.g. get a slice of 8 bits starting at index x where x is an input to the circuit). We choose this explicit functional style syntax to constrain the support of dynamic slicing to constant widths. For non-dynamic slicing (i.e. the start and stop are known at compile time), you can simply use the built-in slice syntax [start:stop]. magma does not support dynamic width slicing.

Here's two simple slice examples:

class GetSlice(m.Circuit):
    io = m.IO(
        I=m.In(m.Bits[10]),
        x=m.In(m.Bits[2]),
        O=m.Out(m.Bits[6])
    )
    # O will output the 6 bits starting at index io.x
    io.O @= m.get_slice(io.I, start=io.x, width=6)

class SetSlice(m.Circuit):
    io = m.IO(
        I=m.In(m.Bits[6]),
        x=m.In(m.UInt[2]),
        O=m.Out(m.Bits[12])
    )

    # default value
    O = m.Bits[12](0xFFF)
    # O will output 0xFF except with the 6 bits start at index x replaced with
    # the value of io.I
    io.O @= m.set_slice(O, io.I, start=io.x, width=6)

Slice function interfaces: * get_slice(value: Bits, start: Bits, width: int) - Dynamic slice of value based off the dynamic value of start and the constant value of width. * set_slice(target: Bits, value: Bits, start: UInt, width: int) * target: the output with a dynamic slice range replaced by value * value: the output driving a slice of target * start: dynamic start index of the slice * width: constant slice width

set_bit

Similar to set_slice/get_slice, this function allows you to dynamically set the value of a bit in a Bits value. Here's an example:

class SetBit(m.Circuit):
    io = m.IO(I=m.In(m.Bits[4]),
              val=m.In(m.Bit),
              idx=m.In(m.Bits[2]),
              O=m.Out(m.Bits[4]))
    io.O @= m.set_bit(io.I, io.val, io.idx)

Interface:

def set_bit(target: Bits, value: Bit, idx: UInt):
    """
    Returns a new value where index `idx` of value `target` is set to `value`
    """