In this tutorial, we will construct a n-bit adder from n full adders.
Magma has built in support for addition using the + operator,
so please don't think Magma is so low-level that you need to create
logical and arithmetic functions in order to use it!
We use this example to show how circuits are composed to form new circuits.
Since we are using the ICE40, we need to set the target of Mantle to "ice40".
In [1]:
import magma as m
m.set_mantle_target("ice40")
In [2]:
from mantle import FullAdder
We can print out the interface of the FullAdder.
In [3]:
print(FullAdder)
This tells us that the full adder has three inputs I0, I1, and CIN.
Note that the type of these arguments are In(Bit).
There are also two outputs O and COUT, both with type Out(Bit).
In Magma arguments in the circuit interface are normally qualified to be inputs or outputs.
In [4]:
fulladder = FullAdder()
print(fulladder.I0, type(fulladder.I0))
print(fulladder.I1, type(fulladder.I1))
print(fulladder.CIN, type(fulladder.CIN))
print(fulladder.O, type(fulladder.O))
print(fulladder.COUT, type(fulladder.O))
Before testing the full adder on the IceStick board,
let's test it using the Python simulator.
In [5]:
from magma.simulator import PythonSimulator
fulladder = PythonSimulator(FullAdder)
assert fulladder(1, 0, 0) == (1, 0), "Failed"
assert fulladder(0, 1, 0) == (1, 0), "Failed"
assert fulladder(1, 1, 0) == (0, 1), "Failed"
assert fulladder(1, 0, 1) == (0, 1), "Failed"
assert fulladder(1, 1, 1) == (1, 1), "Failed"
print("Success!")
Now let's build a 2-bit adder using FullAdder.
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:
Here is a Python class that implements a 2-bit adder.
In [6]:
class Add2(m.Circuit):
IO = ['I0', m.In(m.UInt[2]), 'I1', m.In(m.UInt[2]), 'CIN', m.In(m.Bit),
'O', m.Out(m.UInt[2]), 'COUT', m.Out(m.Bit) ]
@classmethod
def definition(io):
n = len(io.I0)
O = []
COUT = io.CIN
for i in range(n):
fulladder = FullAdder()
Oi, COUT = fulladder(io.I0[i], io.I1[i], COUT)
O.append(Oi)
io.O <= m.uint(O)
io.COUT <= COUT
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.
Each time through the for loop we create an instance of a full adder
by calling FullAdder().
Recall that circuits are python classes,
so that calling a class returns an instance of that class.
Note how we wire up the full adders. Calling an circuit instance has the effect of wiring up the arguments to the inputs of the circuit. That is,
O, COUT = fulladder(I0, I1, CIN)
is equivalent to
m.wire(IO, fulladder.I0)
m.wire(I1, fulladder.I1)
m.wire(CIN, fulladder.CIN)
O = fulladder.O
COUT = fulladder.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 function that takes parameters and returns a circuit class.
Calling the generator with different parameter values will create 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 [7]:
def DefineAdd(n):
class _Add(m.Circuit):
name = f'Add{n}'
IO = ['I0', m.In(m.UInt[n]), 'I1', m.In(m.UInt[n]), 'CIN', m.In(m.Bit),
'O', m.Out(m.UInt[n]), 'COUT', m.Out(m.Bit) ]
@classmethod
def definition(io):
O = []
COUT = io.CIN
for i in range(n):
fulladder = FullAdder()
Oi, COUT = fulladder(io.I0[i], io.I1[i], COUT)
O.append(Oi)
io.O <= m.uint(O)
io.COUT <= COUT
return _Add
def Add(n):
return DefineAdd(n)()
def add(i0, i1, cin):
assert len(i0) == len(i1)
return Add(len(i0))(i0, i1, cin)
First, notice that a circuit generator by convention begins with the prefix Define.
In this example,
DefineAdd has a parameter n which is the width of the adder.
A circuit generator returns a subclass of Circuit.
A standard way to write this is to construct a new Circuit class
within the body of the generator.
The code within the body of the generator can refer to the arguments
to the generator.
Like Verilog modules, Magma circuits must have unique names.
Because Python does not provide the facilities
to dynamically generate the class name,
dynamically constructed Magma circuits are named using the name class variable.
Python generators need to create unique names for each generated circuit
because Magma will cache circuit definitions based on the name.
Note how the name of the circuit is set using the format string f'Add{n}'.
For example, if n is 2, the name of the circuit will be Add2.
Magma allows you to use Python string manipulation functions to create mnemonic names.
As we will see, the resulting verilog module will have the same name.
This is very useful for debugging.
We also can create the parameterized types within the generator.
In this example, we use the type UInt(n) which depends on n.
The loop within definition can also refer to the parameter n'
Finally, notice we defined three interrelated functions:
DefineAdd(n), Add(n), and add(i0, i1, cin).
Why are there three functions?
Because there are three stages in using Magma to create hardware.
The first stage is to generate or define circuits.
The second stage is to create instances of these circuits.
And the third stage is to wire up the circuits.
Functions named DefineX are generators. Generators are functions that return Circuits.
Functions named X return circuit instances. This is done by calling DefineX and then instancing the circuit. This may seem very inefficient. Fortunately, circuits classes are cached and only defined once.
Finally, functions named lowercase x do one more thing. They wire the arguments of to x to the circuit. They can also construct the appropriate circuit class depending on the types of the arguments.
In this example, add constructs an n-bit adder, where n is the width of the inputs.
We strongly recommend that you follow this naming convention.
In [8]:
N = 2
from loam.boards.icestick import IceStick
icestick = IceStick()
for i in range(N):
icestick.J1[i].input().on()
icestick.J1[i+N].input().on()
for i in range(N+1):
icestick.J3[i].output().on()
We define a main function that instances our 2-bit adder and wires it up to J1 and J3. Notice the use of Python's slicing syntax using our width variable N.
In [9]:
main = icestick.DefineMain()
O, COUT = add( main.J1[0:N], main.J1[N:2*N], 0 )
main.J3[0:N] <= O
main.J3[N] <= COUT
m.EndDefine()
As before, we compile.
In [10]:
m.compile('build/add', main)
And use our yosys, arcachne-pnr, and icestorm tool flow.
In [11]:
%%bash
cd build
yosys -q -p 'synth_ice40 -top main -blif add.blif' add.v
arachne-pnr -q -d 1k -o add.txt -p add.pcf add.blif
icepack add.txt add.bin
#iceprog add.bin
You can test the program by connecting up some switches and LEDs to the headers. You should see the sum of the inputs displayed on the LEDs. First, we need to find out what pins J1 and J3 are wired up to. (Note: you can use % to execute shell commands inline in Jupyter notebooks)
In [12]:
%cat build/add.pcf
In this example, we have J1 wire up to the four switch/LED circuits on the left, and J3 wired up to the three LED (no switch) circuits on the right
Again, it can be useful to examine the compiled Verilog.
Notice that it includes a Verilog definition of the mantle FullAdder implemented using the SB_LUT4 and SB_CARRY primtives. The Add2 module instances two FullAdders and wires them up.
In [13]:
%cat build/add.v
You can also display the circuit using graphviz.
In [14]:
#DefineAdd(4)