Start by importing Magma
and Mantle
. Magma
is the core system which implements circuits and the methods to compose them, and Mantle
is a library of useful circuits.
In [1]:
import magma as m
import mantle
A full adder has three single bit inputs, and returns the sum and the carry. The sum is the exclusive or of the 3 bits, the carry is 1 if any two of the inputs bits are 1. Here is a schematic of a full adder circuit (from logisim
).
We start by defining a Python function that implements a full adder.
The full adder function takes three single bit inputs and returns two outputs as a tuple.
The first element of tuple is the sum, the second element is the carry.
We compute the sum and carry using standard Python bitwise operators &
, |
, and ^
.
In [2]:
def fulladder(A, B, C):
return A^B^C, A&B|B&C|C&A # sum, carry
We can test our Python function to verify that our implementation behaves as expected. We'll use the standard Python assert pattern.
In [3]:
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 that we have an implementation of fulladder
as a Python function,
we'll use it to construct a Magma
Circuit
.
A Circuit
in Magma
corresponds to a module
in verilog
.
In [4]:
class FullAdder(m.Circuit):
name = "FullAdderExample"
IO = ["I0", m.In(m.Bit), "I1", m.In(m.Bit), "CIN", m.In(m.Bit), "O", m.Out(m.Bit), "COUT", m.Out(m.Bit)]
@classmethod
def definition(io):
O, COUT = fulladder(io.I0, io.I1, io.CIN)
io.O <= O
io.COUT <= COUT
First, notice that the FullAdder
is a subclass of Circuit
. All Magma
circuits are classes in python.
Second, the attribute IO
defines the interface to the circuit.
IO
is a list of alternating keys and values.
The key is the name of the argument, and the value is the type.
In this circuit, all the inputs and outputs have Magma
type Bit
.
We also qualify each type as an input or an output using the functions In
and Out
.
Third, we provide a function definition
. definition
must be a class method and this is indicated with the decorator @classmethod
.
The purpose of the definition
function is to create the actual full adder circuit.
The arguments are passed to definition
as the object io
.
This object has fields for each argument in the interface.
The body of definition
calls our previously defined python function fulladder
.
Note that when we call the python function fulladder
inside definition
it is passed Magma
values not standard python values.
When we tested fulladder
sbove we called it with ints.
When we called it inside definition
the values passed to the Python fulladder
function
are Magma
values of type Bit
.
The Python bitwise operators are overloaded to compute logical functions of the Magma
values (this corresponds to constructing the circuits to compute logical functions and
, or
, and xor
, and wiring inputs to outputs).
fulladder
returns two values.
These values are assigned to the python variables O
and COUT
.
Remember that assigning to a Python variable
sets the variable to refer to the object.
Magma
values are Python objects,
so assigning an object to a variable creates a reference to that Magma
value.
In order to complete the definition of the circuit,
O
and COUT
need to be wired to the outputs in the interface.
The python <=
operator is overloaded to perform wiring.
Next we simulate the circuit and compare the results to the python function fulladder
.
In [5]:
from magma.simulator import PythonSimulator
fulladder_magma = PythonSimulator(FullAdder)
assert fulladder_magma(1, 0, 0) == fulladder(1, 0, 0), "Failed"
assert fulladder_magma(0, 1, 0) == fulladder(0, 1, 0), "Failed"
assert fulladder_magma(1, 1, 0) == fulladder(1, 1, 0), "Failed"
assert fulladder_magma(1, 0, 1) == fulladder(1, 0, 1), "Failed"
assert fulladder_magma(1, 1, 1) == fulladder(1, 1, 1), "Failed"
print("Success!")
Here is another way to test the circuit. We define a set of test vectors and plot them in python.
In [6]:
from magma.waveform import waveform
test_vectors_raw = [
[0, 0, 0, 0, 0],
[0, 0, 1, 1, 0],
[0, 1, 0, 1, 0],
[0, 1, 1, 0, 1],
[1, 0, 0, 1, 0],
[1, 0, 1, 0, 1],
[1, 1, 0, 0, 1],
[1, 1, 1, 1, 1]
]
waveform(test_vectors_raw, ["a", "b", "cin", "sum", "cout"])
We can use the simulator to also generate a set of test vectors.
In [7]:
from fault.test_vectors import generate_simulator_test_vectors
from bit_vector import BitVector
test_vectors = [
[BitVector(x) for x in test_vector]
for test_vector in test_vectors_raw
]
tests = generate_simulator_test_vectors(FullAdder, flatten=False)
Finally, compare the simulated test vectors to the expected values.
In [8]:
print( "Success" if tests == test_vectors else "Failure" )
The last step we will do is generate coreir
and verilog
for the full adder circuit.
In [9]:
m.compile("build/FullAdder", FullAdder, output="coreir")
%cat build/FullAdder.json
In [10]:
m.compile("build/FullAdder", FullAdder, output="coreir-verilog")
%cat build/FullAdder.v