In this notebook we will discuss the combinational and sequential syntaxes in more detail. See https://magma.readthedocs.io/en/latest/circuit_definitions/ for the full documentation


In [1]:
import magma as m
import inspect
import fault
from hwtypes import BitVector

Combinational

The combinational syntax allows you to use if/else statements. These conditional statements are not executed in Python, instead they are lowered to hardware muxes.


In [2]:
@m.circuit.combinational
def basic_if(I: m.Bits[2], S: m.Bit) -> m.Bit:
    if S:
        x = I[0]
    else:
        x = I[1]
    return x
print(repr(basic_if.circuit_definition))


basic_if = DefineCircuit("basic_if", "I", In(Bits[2]), "S", In(Bit), "O", Out(Bit))
Mux2xOutBit_inst0 = Mux2xOutBit()
wire(basic_if.I[1], Mux2xOutBit_inst0.I0)
wire(basic_if.I[0], Mux2xOutBit_inst0.I1)
wire(basic_if.S, Mux2xOutBit_inst0.S)
wire(Mux2xOutBit_inst0.O, basic_if.O)
EndCircuit()

Magma implements this syntax by converting the function to SSA form, and using the mux circuit to implement the phi nodes. We can inspect the intermediate Python code used by magma.


In [3]:
m.compile("build/basic_if", basic_if)
with open('.magma/basic_if.py', 'r') as f:
    print(f.read())


import magma as m
from mantle import mux as phi


class basic_if(m.Circuit):
    io = m.IO(I=m.In(m.Bits[2]), S=m.In(m.Bit), O=m.Out(m.Bit))
    x_0 = io.I[0]
    x_1 = io.I[1]
    x_2 = phi([x_1, x_0], io.S)
    __magma_ssa_return_value_0 = x_2
    O = __magma_ssa_return_value_0
    m.wire(O, io.O)

Let's test our function using fault


In [4]:
tester = fault.PythonTester(basic_if)
assert tester(BitVector[2]([0, 1]), 0) == 1
assert tester(BitVector[2]([0, 1]), 1) == 0

In [5]:
tester = fault.Tester(basic_if)
tester(BitVector[2]([0, 1]), 0).expect(1)
tester(BitVector[2]([0, 1]), 1).expect(0)
tester.compile_and_run("verilator")

You can insert code to instance magma circuits inside combinational.


In [6]:
from mantle import Not

@m.circuit.combinational
def invert(a: m.Bit) -> m.Bit:
    return Not()(a)

print(repr(invert.circuit_definition))


invert = DefineCircuit("invert", "a", In(Bit), "O", Out(Bit))
not_inst0 = not()
wire(invert.a, not_inst0.in)
wire(not_inst0.out, invert.O)
EndCircuit()

In [7]:
tester = fault.PythonTester(invert)
assert tester(0) == 1
assert tester(1) == 0

In [8]:
tester = fault.Tester(invert)
tester(1).expect(0)
tester(0).expect(1)
# Need coreir commonlib since we are compiling multiple circuits so the namespace already has references to mux
tester.compile_and_run("verilator", magma_opts={"coreir_libs": {"commonlib"}})

We can return multiple values as Python tuples. These will create output ports named O{i} where i is the index in the tuple


In [9]:
@m.circuit.combinational
def return_py_tuple(I: m.Bits[2]) -> (m.Bit, m.Bit):
    return I[0], I[1]

print(repr(return_py_tuple.circuit_definition))


return_py_tuple = DefineCircuit("return_py_tuple", "I", In(Bits[2]), "O0", Out(Bit), "O1", Out(Bit))
wire(return_py_tuple.I[0], return_py_tuple.O0)
wire(return_py_tuple.I[1], return_py_tuple.O1)
EndCircuit()

You can also return a magma tuple (this will only create one output)


In [10]:
@m.circuit.combinational
def return_magma_tuple(I: m.Bits[2]) -> m.Tuple[m.Bit, m.Bit]:
    return m.tuple_([I[0], I[1]])

print(repr(return_magma_tuple.circuit_definition))


return_magma_tuple = DefineCircuit("return_magma_tuple", "I", In(Bits[2]), "O", Tuple[Bit[Out], Bit[Out]])
wire(return_magma_tuple.I[0], return_magma_tuple.O[0])
wire(return_magma_tuple.I[1], return_magma_tuple.O[1])
EndCircuit()

You can also return a magmas product (useful if you'd like to name the outputs)


In [11]:
@m.circuit.combinational
def return_magma_named_tuple(I: m.Bits[2]) -> m.Product.from_fields("anon", {"x": m.Bit, "y": m.Bit}):
    return m.product(x=I[0], y=I[1])

print(repr(return_magma_named_tuple.circuit_definition))


return_magma_named_tuple = DefineCircuit("return_magma_named_tuple", "I", In(Bits[2]), "O", Tuple(x=Out(Bit),y=Out(Bit)))
wire(return_magma_named_tuple.I[0], return_magma_named_tuple.O.x)
wire(return_magma_named_tuple.I[1], return_magma_named_tuple.O.y)
EndCircuit()

Statically elaborated for loops are supported using the ast_tools loop unrolling macro. Here's an example:


In [12]:
import ast_tools
from ast_tools.passes import begin_rewrite, loop_unroll, end_rewrite

n = 4
@m.circuit.combinational
@end_rewrite()
@loop_unroll()
@begin_rewrite()
def logic(a: m.Bits[n]) -> m.Bits[n]:
    O = []
    for i in ast_tools.macros.unroll(range(n)):
        O.append(a[n - 1 - i])
    return m.bits(O, n)

print(repr(logic.circuit_definition))


logic = DefineCircuit("logic", "a", In(Bits[4]), "O", Out(Bits[4]))
wire(logic.a[3], logic.O[0])
wire(logic.a[2], logic.O[1])
wire(logic.a[1], logic.O[2])
wire(logic.a[0], logic.O[3])
EndCircuit()

Sequential

The @m.circuit.sequential decorator extends the @m.circuit.combinational syntax with the ability to use Python's class system to describe stateful circuits.

The basic pattern uses the __init__ method to declare state, and a __call__ function that uses @m.circuit.combinational syntax to describe the transition function from the current state to the next state, as well as a function from the inputs to the outputs. State is referenced using the first argument self and is implicitly updated by writing to attributes of self (e.g. self.x = 3).

Here's an example of a Counter with an enable input inc.


In [13]:
@m.circuit.sequential(async_reset=True)
class Counter:
    def __init__(self):
        self.count : m.UInt[16] = 0

    def __call__(self, inc : m.Bit) -> m.UInt[16]:
        if inc:
            self.count = self.count + 1

        O = self.count
        return O


m.compile("Counter", Counter, inline=True)
!coreir -i Counter.json -p instancecount -l commonlib


An instance count of all the primitives
=======================================
invert | instances in current | instances in children | 
  corebit_not | 1 | 0

Mux2xOutUInt16 | instances in current | instances in children | 

Mux2xOutBit | instances in current | instances in children | 

basic_if | instances in current | instances in children | 

Counter_comb | instances in current | instances in children | 
  coreir_add__width16 | 1 | 0
  coreir_const__width16 | 1 | 0

Counter | instances in current | instances in children | 
  coreir_add__width16 | 0 | 1
  coreir_const__width16 | 0 | 1
  coreir_reg_arst__width16 | 1 | 0

=======================================
{"top":"global.Counter",
"namespaces":{
  "global":{
    "modules":{
      "Counter":{
        "type":["Record",[
          ["inc","BitIn"],
          ["CLK",["Named","coreir.clkIn"]],
          ["ASYNCRESET",["Named","coreir.arstIn"]],
          ["O",["Array",16,"Bit"]]
        ]],
        "instances":{
          "Counter_comb_inst0":{
            "modref":"global.Counter_comb"
          },
          "reg_PR_inst0":{
            "genref":"coreir.reg_arst",
            "genargs":{"width":["Int",16]},
            "modargs":{"arst_posedge":["Bool",true], "clk_posedge":["Bool",true], "init":[["BitVector",16],"16'h0000"]}
          }
        },
        "connections":[
          ["reg_PR_inst0.in","Counter_comb_inst0.O0"],
          ["self.O","Counter_comb_inst0.O1"],
          ["self.inc","Counter_comb_inst0.inc"],
          ["reg_PR_inst0.out","Counter_comb_inst0.self_count_O"],
          ["self.ASYNCRESET","reg_PR_inst0.arst"],
          ["self.CLK","reg_PR_inst0.clk"]
        ]
      },
      "Counter_comb":{
        "type":["Record",[
          ["inc","BitIn"],
          ["self_count_O",["Array",16,"BitIn"]],
          ["O0",["Array",16,"Bit"]],
          ["O1",["Array",16,"Bit"]]
        ]],
        "instances":{
          "Mux2xOutUInt16_inst0":{
            "modref":"global.Mux2xOutUInt16"
          },
          "const_1_16":{
            "genref":"coreir.const",
            "genargs":{"width":["Int",16]},
            "modargs":{"value":[["BitVector",16],"16'h0001"]}
          },
          "magma_Bits_16_add_inst0":{
            "genref":"coreir.add",
            "genargs":{"width":["Int",16]}
          }
        },
        "connections":[
          ["self.self_count_O","Mux2xOutUInt16_inst0.I0"],
          ["magma_Bits_16_add_inst0.out","Mux2xOutUInt16_inst0.I1"],
          ["self.O0","Mux2xOutUInt16_inst0.O"],
          ["self.O1","Mux2xOutUInt16_inst0.O"],
          ["self.inc","Mux2xOutUInt16_inst0.S"],
          ["magma_Bits_16_add_inst0.in1","const_1_16.out"],
          ["self.self_count_O","magma_Bits_16_add_inst0.in0"]
        ]
      },
      "Mux2xOutBit":{
        "type":["Record",[
          ["I0","BitIn"],
          ["I1","BitIn"],
          ["S","BitIn"],
          ["O","Bit"]
        ]],
        "instances":{
          "coreir_commonlib_mux2x1_inst0":{
            "genref":"commonlib.muxn",
            "genargs":{"N":["Int",2], "width":["Int",1]}
          }
        },
        "connections":[
          ["self.I0","coreir_commonlib_mux2x1_inst0.in.data.0.0"],
          ["self.I1","coreir_commonlib_mux2x1_inst0.in.data.1.0"],
          ["self.S","coreir_commonlib_mux2x1_inst0.in.sel.0"],
          ["self.O","coreir_commonlib_mux2x1_inst0.out.0"]
        ]
      },
      "Mux2xOutUInt16":{
        "type":["Record",[
          ["I0",["Array",16,"BitIn"]],
          ["I1",["Array",16,"BitIn"]],
          ["S","BitIn"],
          ["O",["Array",16,"Bit"]]
        ]],
        "instances":{
          "coreir_commonlib_mux2x16_inst0":{
            "genref":"commonlib.muxn",
            "genargs":{"N":["Int",2], "width":["Int",16]}
          }
        },
        "connections":[
          ["self.I0","coreir_commonlib_mux2x16_inst0.in.data.0"],
          ["self.I1","coreir_commonlib_mux2x16_inst0.in.data.1"],
          ["self.S","coreir_commonlib_mux2x16_inst0.in.sel.0"],
          ["self.O","coreir_commonlib_mux2x16_inst0.out"]
        ]
      },
      "basic_if":{
        "type":["Record",[
          ["I",["Array",2,"BitIn"]],
          ["S","BitIn"],
          ["O","Bit"]
        ]],
        "instances":{
          "Mux2xOutBit_inst0":{
            "modref":"global.Mux2xOutBit"
          }
        },
        "connections":[
          ["self.I.1","Mux2xOutBit_inst0.I0"],
          ["self.I.0","Mux2xOutBit_inst0.I1"],
          ["self.O","Mux2xOutBit_inst0.O"],
          ["self.S","Mux2xOutBit_inst0.S"]
        ]
      },
      "invert":{
        "type":["Record",[
          ["a","BitIn"],
          ["O","Bit"]
        ]],
        "instances":{
          "not_inst0":{
            "modref":"corebit.not"
          }
        },
        "connections":[
          ["self.a","not_inst0.in"],
          ["self.O","not_inst0.out"]
        ]
      }
    }
  }
}
}
/Users/travis/build/leonardt/pycoreir/coreir-cpp/src/binary/coreir.cpp:238 Modified?: No

In the __init__ method, the circuit declares a statement self.count with an annotated type m.UInt[16] and an initial value 0. The __call__ method accepts an input inc of type Bit which acts as an enable on the counter logic. The __call__ method updates the counter state if the enable is high, and returns the next value of the counter (so when enable is high, it will output the state value plus one). Writes to state elements use Python semantics (Verilog blocking). Notice that the input and output of the __call__ method have type annotations just like m.circuit.combinational functions. The __call__ method should be treated as a standard @m.circuit.combinational function, with the special parameter self that provides access to the state.


In [14]:
tester = fault.PythonTester(Counter, Counter.CLK)
tester.poke(Counter.inc, True)
tester.eval()
for i in range(4):
    print(tester.peek(Counter.O))
    assert tester.peek(Counter.O) == i + 1
    tester.step(2)
tester.poke(Counter.inc, False)
tester.eval()
for i in range(4):
    print(tester.peek(Counter.O))
    assert tester.peek(Counter.O) == 4
    tester.step(2)

tester.poke(Counter.ASYNCRESET, 1)
tester.eval()
print(tester.peek(Counter.O))
assert tester.peek(Counter.O) == 0


1
2
3
4
4
4
4
4
0

Sequential supports hierarchical composition


In [15]:
@m.circuit.sequential(async_reset=True)
class Register:
    def __init__(self):
        self.value: m.Bits[2] = m.bits(0, 2)

    def __call__(self, I: m.Bits[2]) -> m.Bits[2]:
        O = self.value
        self.value = I
        return O
    
@m.circuit.sequential(async_reset=True)
class TestShiftRegister:
    def __init__(self):
        self.x: Register = Register()
        self.y: Register = Register()

    def __call__(self, I: m.Bits[2]) -> m.Bits[2]:
        x_prev = self.x(I)
        y_prev = self.y(x_prev)
        return y_prev
    
print(repr(TestShiftRegister))

# Need coreir commonlib since we are compiling multiple circuits so the namespace already has references to mux
m.compile("build/TestShiftRegister", TestShiftRegister, inline=True, coreir_libs={"commonlib"})
!cat build/TestShiftRegister.v


TestShiftRegister = DefineCircuit("TestShiftRegister", "I", In(Bits[2]), "CLK", In(Clock), "ASYNCRESET", In(AsyncReset), "O", Out(Bits[2]))
Register_inst0 = Register()
Register_inst1 = Register()
TestShiftRegister_comb_inst0 = TestShiftRegister_comb()
wire(TestShiftRegister_comb_inst0.O0, Register_inst0.I)
wire(TestShiftRegister_comb_inst0.O1, Register_inst1.I)
wire(TestShiftRegister.I, TestShiftRegister_comb_inst0.I)
wire(Register_inst0.O, TestShiftRegister_comb_inst0.self_x_O)
wire(Register_inst1.O, TestShiftRegister_comb_inst0.self_y_O)
wire(TestShiftRegister_comb_inst0.O2, TestShiftRegister.O)
EndCircuit()
module coreir_reg_arst #(
    parameter width = 1,
    parameter arst_posedge = 1,
    parameter clk_posedge = 1,
    parameter init = 1
) (
    input clk,
    input arst,
    input [width-1:0] in,
    output [width-1:0] out
);
  reg [width-1:0] outReg;
  wire real_rst;
  assign real_rst = arst_posedge ? arst : ~arst;
  wire real_clk;
  assign real_clk = clk_posedge ? clk : ~clk;
  always @(posedge real_clk, posedge real_rst) begin
    if (real_rst) outReg <= init;
    else outReg <= in;
  end
  assign out = outReg;
endmodule

module TestShiftRegister_comb (
    input [1:0] I,
    input [1:0] self_x_O,
    input [1:0] self_y_O,
    output [1:0] O0,
    output [1:0] O1,
    output [1:0] O2
);
assign O0 = I;
assign O1 = self_x_O;
assign O2 = self_y_O;
endmodule

module Register_comb (
    input [1:0] I,
    input [1:0] self_value_O,
    output [1:0] O0,
    output [1:0] O1
);
assign O0 = I;
assign O1 = self_value_O;
endmodule

module Register (
    input [1:0] I,
    input CLK,
    input ASYNCRESET,
    output [1:0] O
);
wire [1:0] Register_comb_inst0_O0;
wire [1:0] reg_PR_inst0_out;
Register_comb Register_comb_inst0 (
    .I(I),
    .self_value_O(reg_PR_inst0_out),
    .O0(Register_comb_inst0_O0),
    .O1(O)
);
coreir_reg_arst #(
    .arst_posedge(1'b1),
    .clk_posedge(1'b1),
    .init(2'h0),
    .width(2)
) reg_PR_inst0 (
    .clk(CLK),
    .arst(ASYNCRESET),
    .in(Register_comb_inst0_O0),
    .out(reg_PR_inst0_out)
);
endmodule

module TestShiftRegister (
    input [1:0] I,
    input CLK,
    input ASYNCRESET,
    output [1:0] O
);
wire [1:0] Register_inst0_O;
wire [1:0] Register_inst1_O;
wire [1:0] TestShiftRegister_comb_inst0_O0;
wire [1:0] TestShiftRegister_comb_inst0_O1;
Register Register_inst0 (
    .I(TestShiftRegister_comb_inst0_O0),
    .CLK(CLK),
    .ASYNCRESET(ASYNCRESET),
    .O(Register_inst0_O)
);
Register Register_inst1 (
    .I(TestShiftRegister_comb_inst0_O1),
    .CLK(CLK),
    .ASYNCRESET(ASYNCRESET),
    .O(Register_inst1_O)
);
TestShiftRegister_comb TestShiftRegister_comb_inst0 (
    .I(I),
    .self_x_O(Register_inst0_O),
    .self_y_O(Register_inst1_O),
    .O0(TestShiftRegister_comb_inst0_O0),
    .O1(TestShiftRegister_comb_inst0_O1),
    .O2(O)
);
endmodule

NOTE Currently it is required that every sub sequential circuit element receive an explicit invocation in the __call__ method. For example, if you have a sub sequential circuit self.x that you would like to keep constant, you must still call it with self.x(...) to ensure that some input value is provided every cycle (the sub sequential circuit must similarly be designed in such a way that the logic expects inputs every cycle, so enable logic must be explicitly defined).


In [ ]: