In [1]:
import magma as m
Now let's build a 2-bit adder using full_adder
.
We'll use a simple ripple carry adder design by connecting the carry out of one full adder
to the carry in of the next full adder.
The resulting adder will accept as input a carry in,
and generate a final carry out. Here's a logisim
diagram of the circuit we will construct:
In [2]:
import ast_tools
from ast_tools.transformers.loop_unroller import unroll_for_loops
from ast_tools.passes import begin_rewrite, end_rewrite, loop_unroll
@m.circuit.combinational
def full_adder(A: m.Bit, B: m.Bit, C: m.Bit) -> (m.Bit, m.Bit):
return A ^ B ^ C, A & B | B & C | C & A # sum, carry
@m.circuit.combinational
@end_rewrite()
@loop_unroll()
@begin_rewrite()
def _add(I0: m.Bits[2], I1: m.Bits[2], CIN: m.Bit) -> (m.Bits[2], m.Bit):
O = []
COUT = io.CIN
for i in ast_tools.macros.unroll(range(2)):
Oi, COUT = full_adder(io.I0[i], io.I1[i], COUT)
O.append(Oi)
return m.uint(O), COUT
print(repr(_add.circuit_definition))
Although we are making an 2-bit adder,
we do this using a for loop that can be generalized to construct an n-bit adder.
To use a for loop inside combinational, we use the ast_tools
package's macro support.
These loop_unroll
macro will expand the for loop before passing the function to m.circuit.combinational
.
Each time through the for loop we call full adder.
Calling an circuit instance has the effect of wiring up the arguments to the inputs of the circuit. That is,
O, COUT = full_adder(I0, I1, CIN)
is equivalent to
m.wire(IO, full_adder.I0)
m.wire(I1, full_adder.I1)
m.wire(CIN, full_adder.CIN)
O = full_adder.O
COUT = full_adder.COUT
The outputs of the circuit are returned.
Inside this loop we append single bit outputs from the full adders
to the Python list O
.
We also set the CIN
of the next full adder to the COUT
of the previous instance.
Finally, we then convert the list O
to a UInt[n]
.
In addition to Bits[n]
,
magma
also has built in types UInt[n]
and SInt[n]
to represent unsigned and signed ints.
magma
also has type conversion functions bits
, uint
, and sint
to convert
between different types.
In this example, m.uint(C)
converts the list of bits to a UInt[len(C)]
.
One question you may be asking yourself, is how can this code be generalized to produce an n-bit adder. We do this by creating an add Generator.
A Generator
is a Python class that defines a static generate
method which takes parameters and returns a circuit class.
Calling the generator with different parameter values will create and instantiate different circuits.
The power of magma
results from being to use all the features of Python
to create powerful hardware generators.
Here is the code:
In [3]:
class Add(m.Generator):
@staticmethod
def generate(width: int):
T = m.UInt[width]
@m.circuit.combinational
@end_rewrite()
@loop_unroll()
@begin_rewrite()
def _add(I0: T, I1: T, CIN: m.Bit) -> (T, m.Bit):
O = []
COUT = io.CIN
for i in ast_tools.macros.unroll(range(width)):
Oi, COUT = full_adder(io.I0[i], io.I1[i], COUT)
O.append(Oi)
return m.uint(O), COUT
return _add
def add(i0, i1, cin):
"""
We define a convenience function that instantiates the
add generator for us based on the width of the inputs.
"""
if len(i0) != len(i1):
raise TypeError("add arguments must have same length")
if not isinstance(cin, m.Bit):
raise TypeError("add cin must be a Bit")
if (not isinstance(i0, m.UInt) and
not isinstance(i1, m.UInt)):
raise TypeError("add expects UInt inputs")
return Add(len(i0))(i0, i1, cin)
To generate a Circuit
from a Generator
, we can directly call the generate
static method.
In [4]:
from fault import PythonTester
Add2 = Add.generate(2)
add2 = PythonTester(Add2)
print(add2(1,2,0)[0] == 3)
assert add2(1, 2, 0) == (3, 0), "Failed"
print("Success!")
Let's inspected the generated code
In [5]:
m.compile("build/Add2", Add2, inline=True)
%cat build/Add2.v
In [6]:
!coreir -i build/Add2.json -p instancecount
We can instantiate a Generator
using the standard object syntax, which will implicitly call the generate
method based on teh parameters, and return an instance of the generated Circuit
. By default, this logic will cache definitions based on the generator parameters.
In [7]:
class Main(m.Circuit):
io = m.IO(I0=m.In(m.UInt[3]), I1=m.In(m.UInt[3]), CIN=m.In(m.Bit),
O=m.Out(m.UInt[3]), COUT=m.Out(m.Bit))
O, COUT = Add(3)(io.I0, io.I1, io.CIN)
io.O @= O
io.COUT @= COUT
print(repr(Main))
Here's an example of using the convenience add
function which handles the Generator
instantiation for us
In [8]:
class Main(m.Circuit):
io = m.IO(I0=m.In(m.UInt[3]), I1=m.In(m.UInt[3]), CIN=m.In(m.Bit),
O=m.Out(m.UInt[3]), COUT=m.Out(m.Bit))
O, COUT = add(io.I0, io.I1, io.CIN)
io.O @= O
io.COUT @= COUT
print(repr(Main))
In [ ]: