Lesson 3 - Creating a structural n bit Full Adder¶

The idea here is to create a slightly more complex structural circuit. We will start by reusing the 1-bit full adder implemented in lesson 2

In [1]:
import sys
import platform
if (platform.uname().node == 'TPY14'):
    print('Dev machine')
    sys.path.append('..\\..\\..\\py4hw')
Dev machine
In [2]:
import py4hw
In [3]:
class FullAdder(py4hw.Logic):
    
    def __init__(self, parent, name, x, y, ci, s, co):
        super().__init__(parent, name)
        
        self.x = self.addIn('x', x)
        self.y = self.addIn('y', y)
        self.ci = self.addIn('ci', ci)
        self.s = self.addOut('s', s)
        self.co = self.addOut('co', co)

    def propagate(self):
        self.s.put((self.x.get() ^ self.y.get()) ^ self.ci.get())
        self.co.put((self.x.get() & self.y.get()) | ( (self.x.get() ^ self.y.get())  & self.ci.get()))

Creating a little more complex Structural Circuit¶

When someone wants to create a repetivie structure in Verilog it can mix behavioural code with some specific statements to generate structures. This mix of design styles in the same description can cause some confusion on the language novices. In py4hw we try to be very clear on the design level you are using, to try to avoid confusion.

Structural circuits have all their code in the constructor.

A feature that can blow the mind of the typical Hardware designer is that signal (wire) widths are never specified in circuits. By default we assume that every circuit has parametric wire widths.

If the designer wants to limit wire widths to an specific value, he/she can still do it by adding assertions or exceptions checking wire widths.

An other aspect that can anoy the typical Hardware designer is the limitations to directly address bits from a wire. We need circuits to extract bits from wires and to concatenate bits into wires. The reason for this is that it greatly simplifies the simulation infrastructure, but at the same time, it forces the designer to carefully thing about which elements are driving the wires, avoiding errors.

In [4]:
class FullAdderN(py4hw.Logic):
    
    def __init__(self, parent, name, x, y, ci, s, co):
        super().__init__(parent, name)
        
        self.addIn('x', x)
        self.addIn('y', y)
        self.addIn('ci', ci)
        
        self.addOut('s', s)
        self.addOut('co', co)
        
        assert(ci.getWidth() == 1)
        assert(co.getWidth() == 1)
        assert(x.getWidth() == y.getWidth())
        assert(x.getWidth() == s.getWidth())
        
        xb = self.wires('xb', x.getWidth(), 1)
        yb = self.wires('yb', y.getWidth(), 1)
        sb = self.wires('sb', s.getWidth(), 1)
        
        py4hw.BitsLSBF(self, 'xb', x, xb)
        py4hw.BitsLSBF(self, 'yb', y, yb)
        py4hw.ConcatenateLSBF(self, 'sb', sb, s)
        
        pci = ci # partial carry in
        
        for i in range(x.getWidth()):
            pco = self.wire('co{}'.format(i))
            FullAdder(self, 'FA{}'.format(i), xb[i], yb[i], pci, sb[i], pco)
            pci = pco
            
        py4hw.Buf(self, 'co', pco, co)

Visualizaing the circuit¶

With the visualization we can easily see that carrys are daisy-chained, as we would expect

In [11]:
sys = py4hw.HWSystem()

x = sys.wire('x', 4)
y = sys.wire('y', 4)
ci = sys.wire('ci')
s = sys.wire('s', 4)
co = sys.wire('co')
fan = FullAdderN(sys, 'fa', x, y, ci, s, co)

sch = py4hw.Schematic(fan)
sch.draw()

Simulating the System¶

To verify the correct behaviour of the system we can simulate it.

In [12]:
sys = py4hw.HWSystem()

x = sys.wire('x', 4)
y = sys.wire('y', 4)
ci = sys.wire('ci')
s = sys.wire('s', 4)
co = sys.wire('co')
fan = FullAdderN(sys, 'fa', x, y, ci, s, co)

py4hw.Sequence(sys, 'x', [1,3,5,7,9], x)
py4hw.Sequence(sys, 'y', [7,11], y)
py4hw.Sequence(sys, 'ci', [0, 0, 0, 0, 0, 1], ci)

wvf = py4hw.Waveform(sys, 'wvf', [x,y,ci,s,co])

sys.getSimulator().clk(20)
wvf.draw_wavedrom()
Out[12]:

Generating Verilog for synthesis¶

To generate verilog for the circuit, all the structural elements are translated into verllog. By now all instances of the circuit will be generated as a separate verilog module. This will create a rather large output. In future versions of py4hw this will be simplified.

In [14]:
rtl = py4hw.VerilogGenerator(fan)
print(rtl.getVerilogForHierarchy())
transpiling /HWSystem[HWSystem]/FullAdderN[fa]/FullAdder[FA0]
transpiling /HWSystem[HWSystem]/FullAdderN[fa]/FullAdder[FA1]
transpiling /HWSystem[HWSystem]/FullAdderN[fa]/FullAdder[FA2]
transpiling /HWSystem[HWSystem]/FullAdderN[fa]/FullAdder[FA3]
// This file was automatically created by py4hw RTL generator
module FullAdderN (
	input [3:0] x,
	input [3:0] y,
	input  ci,
	output [3:0] s,
	output  co);
wire w_xb_2;
wire w_yb_2;
wire w_sb_2;
wire w_co0;
wire w_yb_1;
wire w_sb_1;
wire w_xb_3;
wire w_yb_3;
wire w_sb_3;
wire w_co3;
wire w_yb_0;
wire w_xb_0;
wire w_sb_0;
wire w_co2;
wire w_co1;
wire w_xb_1;

assign w_xb_0 = x[0];
assign w_xb_1 = x[1];
assign w_xb_2 = x[2];
assign w_xb_3 = x[3];
assign w_yb_0 = y[0];
assign w_yb_1 = y[1];
assign w_yb_2 = y[2];
assign w_yb_3 = y[3];
assign s ={w_sb_3,w_sb_2,w_sb_1,w_sb_0};
FullAdder_26e837e1908 i_FA0(.x(w_xb_0),.y(w_yb_0),.ci(ci),.s(w_sb_0),.co(w_co0));
FullAdder_26e837e1c88 i_FA1(.x(w_xb_1),.y(w_yb_1),.ci(w_co0),.s(w_sb_1),.co(w_co1));
FullAdder_26e837ec048 i_FA2(.x(w_xb_2),.y(w_yb_2),.ci(w_co1),.s(w_sb_2),.co(w_co2));
FullAdder_26e837ec3c8 i_FA3(.x(w_xb_3),.y(w_yb_3),.ci(w_co2),.s(w_sb_3),.co(w_co3));
assign co = w_co3;
endmodule

// This file was automatically created by py4hw RTL generator
module FullAdder_26e837e1908 (
	input  x,
	input  y,
	input  ci,
	output  reg  s,
	output  reg  co);
// Code generated from propagate method
// variable declaration 
reg i0;
reg i1;
reg i2;
reg i3;
// process 
always @(*)
begin
s<=i0^ci;
co<=i1|i2;
i0<=x^y;
i1<=x&y;
i2<=i3&ci;
i3<=x^y;
end
endmodule

// This file was automatically created by py4hw RTL generator
module FullAdder_26e837e1c88 (
	input  x,
	input  y,
	input  ci,
	output  reg  s,
	output  reg  co);
// Code generated from propagate method
// variable declaration 
reg i0;
reg i1;
reg i2;
reg i3;
// process 
always @(*)
begin
s<=i0^ci;
co<=i1|i2;
i0<=x^y;
i1<=x&y;
i2<=i3&ci;
i3<=x^y;
end
endmodule

// This file was automatically created by py4hw RTL generator
module FullAdder_26e837ec048 (
	input  x,
	input  y,
	input  ci,
	output  reg  s,
	output  reg  co);
// Code generated from propagate method
// variable declaration 
reg i0;
reg i1;
reg i2;
reg i3;
// process 
always @(*)
begin
s<=i0^ci;
co<=i1|i2;
i0<=x^y;
i1<=x&y;
i2<=i3&ci;
i3<=x^y;
end
endmodule

// This file was automatically created by py4hw RTL generator
module FullAdder_26e837ec3c8 (
	input  x,
	input  y,
	input  ci,
	output  reg  s,
	output  reg  co);
// Code generated from propagate method
// variable declaration 
reg i0;
reg i1;
reg i2;
reg i3;
// process 
always @(*)
begin
s<=i0^ci;
co<=i1|i2;
i0<=x^y;
i1<=x&y;
i2<=i3&ci;
i3<=x^y;
end
endmodule

Final Comments¶

This lesson was used to describe how to use [what other HDLs call] generative structural code. If you come from a software background you will easily understand that instatiating an object from a class is equivalent to create a circuit, and if you put that instantiation inside a loop in will create a collection of objects.

On the other hand, in py4hw we try to promote the reuse of existing circuits. So we would encourage to use the Add circuit from the library to implement the same circuit.

In [7]:
sys = py4hw.HWSystem()

x = sys.wire('x', 4)
y = sys.wire('y', 4)
ci = sys.wire('ci')
s = sys.wire('s', 4)
co = sys.wire('co')
fan = py4hw.Add(sys, 'fa', x, y, s, ci, co)

py4hw.Sequence(sys, 'x', [1,3,5,7,9], x)
py4hw.Sequence(sys, 'y', [7,11], y)
py4hw.Sequence(sys, 'ci', [0, 0, 0, 0, 0, 1], ci)

wvf = py4hw.Waveform(sys, 'wvf', [x,y,ci,s,co])

sys.getSimulator().clk(20)
wvf.draw_wavedrom()
Out[7]:
In [ ]: