In [1]:
import magma as m

Add2 Circuit

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))


_add = DefineCircuit("_add", "I0", In(Bits[2]), "I1", In(Bits[2]), "CIN", In(Bit), "O0", Out(Bits[2]), "O1", Out(Bit))
full_adder_inst0 = full_adder()
full_adder_inst1 = full_adder()
wire(_add.I0[0], full_adder_inst0.A)
wire(_add.I1[0], full_adder_inst0.B)
wire(_add.CIN, full_adder_inst0.C)
wire(_add.I0[1], full_adder_inst1.A)
wire(_add.I1[1], full_adder_inst1.B)
wire(full_adder_inst0.O1, full_adder_inst1.C)
wire(full_adder_inst0.O0, _add.O0[0])
wire(full_adder_inst1.O0, _add.O0[1])
wire(full_adder_inst1.O1, _add.O1)
EndCircuit()

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)].

Add Generator

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!")


Bit(True)
Success!

Let's inspected the generated code


In [5]:
m.compile("build/Add2", Add2, inline=True)
%cat build/Add2.v


module full_adder (
    input A,
    input B,
    input C,
    output O0,
    output O1
);
assign O0 = (A ^ B) ^ C;
assign O1 = ((A & B) | (B & C)) | (C & A);
endmodule

module _add (
    input [1:0] I0,
    input [1:0] I1,
    input CIN,
    output [1:0] O0,
    output O1
);
wire full_adder_inst0_O0;
wire full_adder_inst0_O1;
wire full_adder_inst1_O0;
full_adder full_adder_inst0 (
    .A(I0[0]),
    .B(I1[0]),
    .C(CIN),
    .O0(full_adder_inst0_O0),
    .O1(full_adder_inst0_O1)
);
full_adder full_adder_inst1 (
    .A(I0[1]),
    .B(I1[1]),
    .C(full_adder_inst0_O1),
    .O0(full_adder_inst1_O0),
    .O1(O1)
);
assign O0 = {full_adder_inst1_O0,full_adder_inst0_O0};
endmodule


In [6]:
!coreir -i build/Add2.json -p instancecount


An instance count of all the primitives
=======================================
full_adder | instances in current | instances in children | 
  corebit_and | 3 | 0
  corebit_or | 2 | 0
  corebit_xor | 2 | 0

_add | instances in current | instances in children | 
  corebit_and | 0 | 6
  corebit_or | 0 | 4
  corebit_xor | 0 | 4

=======================================
{"top":"global._add",
"namespaces":{
  "global":{
    "modules":{
      "_add":{
        "type":["Record",[
          ["I0",["Array",2,"BitIn"]],
          ["I1",["Array",2,"BitIn"]],
          ["CIN","BitIn"],
          ["O0",["Array",2,"Bit"]],
          ["O1","Bit"]
        ]],
        "instances":{
          "full_adder_inst0":{
            "modref":"global.full_adder"
          },
          "full_adder_inst1":{
            "modref":"global.full_adder"
          }
        },
        "connections":[
          ["self.I0.0","full_adder_inst0.A"],
          ["self.I1.0","full_adder_inst0.B"],
          ["self.CIN","full_adder_inst0.C"],
          ["self.O0.0","full_adder_inst0.O0"],
          ["full_adder_inst1.C","full_adder_inst0.O1"],
          ["self.I0.1","full_adder_inst1.A"],
          ["self.I1.1","full_adder_inst1.B"],
          ["self.O0.1","full_adder_inst1.O0"],
          ["self.O1","full_adder_inst1.O1"]
        ]
      },
      "full_adder":{
        "type":["Record",[
          ["A","BitIn"],
          ["B","BitIn"],
          ["C","BitIn"],
          ["O0","Bit"],
          ["O1","Bit"]
        ]],
        "instances":{
          "magma_Bit_and_inst0":{
            "modref":"corebit.and"
          },
          "magma_Bit_and_inst1":{
            "modref":"corebit.and"
          },
          "magma_Bit_and_inst2":{
            "modref":"corebit.and"
          },
          "magma_Bit_or_inst0":{
            "modref":"corebit.or"
          },
          "magma_Bit_or_inst1":{
            "modref":"corebit.or"
          },
          "magma_Bit_xor_inst0":{
            "modref":"corebit.xor"
          },
          "magma_Bit_xor_inst1":{
            "modref":"corebit.xor"
          }
        },
        "connections":[
          ["self.A","magma_Bit_and_inst0.in0"],
          ["self.B","magma_Bit_and_inst0.in1"],
          ["magma_Bit_or_inst0.in0","magma_Bit_and_inst0.out"],
          ["self.B","magma_Bit_and_inst1.in0"],
          ["self.C","magma_Bit_and_inst1.in1"],
          ["magma_Bit_or_inst0.in1","magma_Bit_and_inst1.out"],
          ["self.C","magma_Bit_and_inst2.in0"],
          ["self.A","magma_Bit_and_inst2.in1"],
          ["magma_Bit_or_inst1.in1","magma_Bit_and_inst2.out"],
          ["magma_Bit_or_inst1.in0","magma_Bit_or_inst0.out"],
          ["self.O1","magma_Bit_or_inst1.out"],
          ["self.A","magma_Bit_xor_inst0.in0"],
          ["self.B","magma_Bit_xor_inst0.in1"],
          ["magma_Bit_xor_inst1.in0","magma_Bit_xor_inst0.out"],
          ["self.C","magma_Bit_xor_inst1.in1"],
          ["self.O0","magma_Bit_xor_inst1.out"]
        ]
      }
    }
  }
}
}
/Users/travis/build/leonardt/pycoreir/coreir-cpp/src/binary/coreir.cpp:238 Modified?: No

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))


Main = DefineCircuit("Main", "I0", In(UInt[3]), "I1", In(UInt[3]), "CIN", In(Bit), "O", Out(UInt[3]), "COUT", Out(Bit))
_add_inst0 = _add()
wire(Main.I0, _add_inst0.I0)
wire(Main.I1, _add_inst0.I1)
wire(Main.CIN, _add_inst0.CIN)
wire(_add_inst0.O0, Main.O)
wire(_add_inst0.O1, Main.COUT)
EndCircuit()

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))


Main = DefineCircuit("Main", "I0", In(UInt[3]), "I1", In(UInt[3]), "CIN", In(Bit), "O", Out(UInt[3]), "COUT", Out(Bit))
_add_inst0 = _add()
wire(Main.I0, _add_inst0.I0)
wire(Main.I1, _add_inst0.I1)
wire(Main.CIN, _add_inst0.CIN)
wire(_add_inst0.O0, Main.O)
wire(_add_inst0.O1, Main.COUT)
EndCircuit()

In [ ]: