3. Gate-level Modeling

A hardware description language (HDL) models the structure and behavior of hardware modules. Hardware can be modeled at different levels of abstraction. Computer-aided design (CAD) tools that understand an HDL map abstract module descriptions to logic gates. This process process is called synthesis. We can test HDL-based designs by simulation. In simulation, we apply inputs to a module and verify the outputs.

In this course, we use SystemVerilog as our HDL. SystemVerilog supports both modeling and simulation. We will use the following tools to compile and test our designs:

Note

We rely on open-source or free software where possible. This means that you may install and run the tools on your own machines if desired. However, for the labs, use the provided installations. If you’d like to explore further and use a custom installation, we can talk in the open labs.

SystemVerilog can be used at multiple levels of abstraction. More abstract models are typically easier to comprehend but further away from the actual hardware. In this exercise we’ll make our start in SystemVerilog by designing gate-level modules. Gate-level models represent a low-level abstraction and are also called structural-level models.

Note

Engineers typically design their circuits at higher levels. As we’ll see in Section 4.1 these higher-level formulations are then automatically lowered to the gate level. For the time being, SystemVerilog’s gate-level primitives are a good first choice because they let us to transfer schematics directly into the HDL.

Table 3.1 Subset of SystemVerilog’s gate-level primitives.

Primitive

#Inputs

#Outputs

Description

and

2 or more

1

AND gate

nand

2 or more

1

NAND gate

or

2 or more

1

OR gate

nor

2 or more

1

NOR gate

xor

2 or more

1

XOR gate

xnor

2 or more

1

XNOR gate

buf

1

1 or more

Buffer gate

not

1

1 or more

Inverter gate

Table 3.1 shows a selection of the available primitives. The syntax of the gate-level primitives is given as:

gate_type instance_name( output(s), input(s) );

For example, assume that we have two input signals i_a and i_b and would like to use them as inputs for a 2-input AND gate. If the output of the gate is assigned to o_c and the gate named my_gate, we’d use the and primitive in the following way:

and my_gate( o_c, i_a, i_b );

3.1. My First Module

Our first gate-level module implements the following function:

(3.1.1)\[C = \mathcal{F}(A,B) = \bar{A} + B,\]

where \(A\) and \(B\) are one-bit inputs, and \(C\) is the one-bit output. Table 3.1.1 shows the respective truth table.

Table 3.1.1 Truth table of the simple function in Eq. (3.1.1).

\(A\)

\(B\)

\(C\)

0

0

1

0

1

1

1

0

0

1

1

1

Tasks

  1. Implement Eq. (3.1.1) in a module named my_module using gate-level primitives. Save the code as my_module.sv.

  2. Test your implementation using the testbench my_module_tb. Save the code as my_module_tb.sv. Verify the outputs for all input combinations (see Table 3.1.1).

  3. Visualize the resulting waveforms.

Hint

Invoke iverilog with -Wall -Winfloop -g2012. Details on using Surfer are available from the user documentation.

3.2. Full Adder

This section designs a full adder in SystemVerilog. The full adder consists of the three one-bit inputs \(A\), \(B\) and \(C_\text{in}\) and has the two one-bit outputs \(S\) and \(C_\text{out}\):

(3.2.1)\[\begin{split} &S = F(A,B,C_\text{in}) = A \oplus B \oplus C_\text{in}, \\ &C_\text{out} = G(A,B,C_\text{in}) = AB + AC_\text{in} + BC_\text{in}.\end{split}\]
Table 3.2.1 Truth table of the full adder.

\(A\)

\(B\)

\(C_\text{in}\)

\(S\)

\(C_\text{out}\)

0

0

0

0

0

1

0

0

1

0

0

1

0

1

0

0

0

1

1

0

1

1

0

0

1

1

0

1

0

1

0

1

1

0

1

1

1

1

1

1

Table 3.2.1 represents the truth table of the full adder. In your tests and waveform visualization cover all possible inputs and outputs. Additionally, follow the order of the table, that is test and visualize the first row before the second one.

Listing 3.2.1 Template for the module full_adder.
 1/**
 2 * Gate-level implementation of a full adder.
 3 *
 4 * @param i_a input A.
 5 * @param i_b input B.
 6 * @param i_carry_in carry in.
 7 * @param o_s sum.
 8 * @param o_carry_out carry out.
 9 **/
10module full_adder( input  wire i_a,
11                               i_b,
12                               i_carry_in,
13                   output wire o_s,
14                               o_carry_out );
15  // TODO: implement full adder through gate-level primitives
16endmodule
Listing 3.2.2 Template for the testbench full_adder_tb.
 1/**
 2 * Testbench of the gate-level full adder.
 3 */
 4module full_adder_tb;
 5  logic l_a, l_b, l_s;
 6  logic l_carry_in, l_carry_out;
 7
 8  full_adder dut( .i_a(         l_a         ),
 9                  .i_b(         l_b         ),
10                  .i_carry_in(  l_carry_in  ),
11                  .o_s(         l_s         ),
12                  .o_carry_out( l_carry_out ) );
13
14  initial begin
15    $dumpfile("dump_full_adder.vcd");
16    $dumpvars;
17
18    l_a = 1'b0;
19    l_b = 1'b0;
20    l_carry_in = 1'b0;
21    #10;
22    assert( l_s === 1'b0 );
23    assert( l_carry_out === 1'b0 );
24
25    l_a = 1'b1;
26    l_b = 1'b0;
27    l_carry_in = 1'b0;
28    #10;
29    assert( l_s === 1'b1 );
30    assert( l_carry_out === 1'b0 );
31
32    // TODO: add additional tests
33
34    $finish;
35  end
36endmodule

Use the template in Listing 3.2.1 for your implementation of the full adder in the module full_adder using the filename full_adder.sv. Use the template in Listing 3.2.2 for your implementation of the testbench full_adder_tb in the file full_adder_tb.sv.

Tasks

  1. Implement the module full_adder. Exclusively use gate-level primitives in your design.

  2. Test your implementation in the testbench full_adder_tb. Your testbench should check the outputs for all possible inputs (see Table 3.2.1) through assert() statements.

  3. Visualize the waveform generated by your module. Only show i_a, i_b, i_carry_in, o_s and o_carry_out of the full_adder module in that order.

3.3. Four-bit Ripple-Carry Adder

This exercise covers basic modularity in SystemVerilog. In detail, we use simple(r) SystemVerilog modules as building blocks for more complex ones. Nothing else is done in processor design. This means, that one would combine a “simple” module, e.g., an Arithmetic and Logic Unit (ALU), with other modules such that the combination executes desired instructions. We’ll cover details on ALUs and processors later. For now, we’ll stick to our adders.

Specifically, instead of adding three one-bit inputs as done in Section 3.2, we’ll add two four-bit inputs \(A_{[3:0]}\) and \(B_{[3:0]}\), and the carry in \(C_\text{in}\). We’ll use the so-called ripple-carry adder to perform this task. A ripple-carry adder chains a series of full adders together and is one possible implementation of a carry-propagate adder.

We obtain a respective formula for the ripple-carry adder by repeatedly applying the two functions \(\mathcal{F}\) and \(\mathcal{G}\) of Eq. (3.2.1) to our input:

(3.3.1)\[\begin{split} S_0 = \mathcal{F}(A_0,B_0,C_\text{in}), \\ C_0 = \mathcal{G}(A_0,B_0,C_\text{in}), \\ S_1 = \mathcal{F}(A_1,B_1,C_0), \\ C_1 = \mathcal{G}(A_1,B_1,C_0), \\ S_2 = \mathcal{F}(A_2,B_2,C_1), \\ C_2 = \mathcal{G}(A_2,B_2,C_1), \\ S_3 = \mathcal{F}(A_3,B_3,C_2), \\ C_\text{out} = \mathcal{G}(A_3,B_3,C_2). \\\end{split}\]

Combined, Equations (3.3.1) compute the four-bit output \(S_{[3:0]}\) and the one-bit carry out \(C_\text{out}\) from the inputs. \(C_{[2:0]}\) are intermediate carries through which we connect our full adders: The carry ripples through the obtained multi-bit adder.

Table 3.3.1 Truth table of the four-bit ripple carry adder.

\(A_{[3:0]}\)

\(B_{[3:0]}\)

\(C_\text{in}\)

\(S_{[3:0]}\)

\(C_\text{out}\)

0000

0000

0

0000

0

1010

0001

0

1011

0

1100

0000

1

0101

1010

1

0111

1100

0

1111

1111

1

Once again, we have to think about sensible unit tests. Now, we have to deal with an excessive amount of potential inputs due to the increased size of the inputs. Thus, we limit ourselves to the small subset given in Table 3.3.1.

Note

Table 3.3.1 only lists the required tests for a successful completion of this exercise. Feel free to try additional inputs and implement further test if you see a need for this.

Listing 3.3.1 Template for the module ripple_carry_adder_4.
 1/**
 2 * 4-bit ripple-carry adder.
 3 *
 4 * @param i_a input A.
 5 * @param i_b input B.
 6 * @param i_carry_in carry in.
 7 * @param o_s sum.
 8 * @param o_carry_out carry out.
 9 **/
10module ripple_carry_adder_4( input  logic [3:0] i_a,
11                                                i_b,
12                             input  logic       i_carry_in,
13                             output logic [3:0] o_s,
14                             output logic       o_carry_out );
15  // TODO: implement 4-bit ripple-carry adder
16endmodule
Listing 3.3.2 Template for the testbench ripple_carry_adder_4_tb.
 1/**
 2 * Testbench of the 4-bit ripple-carry adder.
 3 */
 4module ripple_carry_adder_4_tb;
 5  logic [3:0] l_a, l_b, l_s;
 6  logic l_carry_in, l_carry_out;
 7
 8  ripple_carry_adder_4 dut( .i_a(         l_a         ),
 9                            .i_b(         l_b         ),
10                            .i_carry_in(  l_carry_in  ),
11                            .o_s(         l_s         ),
12                            .o_carry_out( l_carry_out ) );
13
14  initial begin
15    $dumpfile("dump_ripple_carry_adder_4.vcd");
16    $dumpvars;
17
18    l_a = 4'b0000;
19    l_b = 4'b0000;
20    l_carry_in = 1'b0;
21    #10;
22    assert( l_s === 4'b0000 );
23    assert( l_carry_out === 1'b0 );
24
25    // TODO: add additional tests
26
27    $finish;
28  end
29endmodule

Tasks

  1. Draw the schematic of the four-bit ripple-carry adder as a sequence of full adders.

  2. Derive the total number of possible inputs for the four-bit ripple-carry adder! How would this change if we were to implement a 32-bit ripple-carry adder?

  3. Complete Table 3.3.1!

  4. Implement the module ripple_carry_adder_4 by completing the module in Listing 3.3.1.

  5. Test your implementation in the testbench ripple_carry_adder_4_tb by completing the module in Listing 3.3.2. Your testbench should check the outputs for all inputs in Table 3.3.1 using assert() statements.

  6. Visualize the waveform generated by your module for Table 3.3.1’s inputs. Only show the four-bit inputs i_a, i_b, the carry in i_carry_in, the four-bit output o_s and the carry out o_carry_out of the ripple_carry_adder_4 module in that order.

3.4. Carry-Lookahead Adder

As discussed in the lectures, the ripple-carry adder is inefficient for wide inputs. Here, the propagation delay is bounded by the critical path from the carry in to the last full adder producing the final carry out. Accordingly, the propagation delay grows linearly with the bit width, which dictates the number of full adders.

A carry-lookahead adder reduces delay at the cost of extra lookahead logic. This exercise implements a 64-bit carry-lookahead adder using 4-bit carry-lookahead blocks. The schematics are shown in Fig. 3.4.1, Fig. 3.4.2, Fig. 3.4.3 and Fig. 3.4.4.

../_images/cla_pre_4.svg

Fig. 3.4.1 Schematic of the preprocessing logic which derives the signals \(G_0\) to \(G_3\) and \(P_0\) to \(P_3\). We use the name cla_pre_4 for the respective SystemVerilog module.

../_images/cla_logic_4.svg

Fig. 3.4.2 Schematic which realizes the core of the carry-lookahead logic. We use the name cla_logic_4 for the respective SystemVerilog module.

../_images/cla_block_4.svg

Fig. 3.4.3 Schematic of the logic which implements a four-bit carry lookahead block. We use the name cla_block_4 for the respective SystemVerilog module.

../_images/cla_adder_64.svg

Fig. 3.4.4 Schematic of the 64-bit carry-lookahead adder chaining sixteen four-bit carry-lookahead blocks. We use the name cla_adder_64 for the respective SystemVerilog module.

Tasks

  1. Implement cla_pre_4 (Fig. 3.4.1) and test with cla_pre_4_tb.

  2. Implement cla_logic_4 (Fig. 3.4.2) and test with cla_logic_4_tb.

  3. Implement the 4-bit carry-lookahead block cla_block_4 (Fig. 3.4.3). Use the two modules cla_pre_4 and cla_logic_4 in your design. Use the testbench cla_block_4_tb for your tests.

  4. Implement a 64-bit carry-lookahead adder by chaining sixteen 4-bit carry-lookahead blocks (Fig. 3.4.4). Use the testbench cla_adder_64_tb.

3.5. Delays

../_images/timing.svg

Fig. 3.5.1 Circuit implementing the Boolean equation \(D = \bar{C} A + CB\).

As discussed in class, the circuit in Fig. 3.5.1 implements \(D = \bar{C} A + CB\). This circuit was our running example when discussing timing analysis in class. In this exercise we’ll use SystemVerilog’s delays to study the circuit’s behavior under delay in simulation. Add delays to gate-level primitives using:

gate_type delay instance_name( output(s), input(s) );

The specified delays are assumed during simulation but ignored when lowering SystemVerilog modules to actual hardware. For example, if we’d like to add a delay of three time units to an AND gate, we could do the following:

and #3 my_gate( o_c, i_a, i_b );

Additionally to providing our desired delays we should inform the compiler of the time unit and scale of our modules. We use 1ns for both by adding the following line to the beginning of the simulated modules:

timeunit 1ns; timeprecision 1ns;

We model the combinational logic using inertial delays when adding delays as described above. Simulated inertial delays are described in the document Correct Methods For Adding Delays To Verilog Behavioral Models, especially in chapters 2.0 and 5.0.

Assume a delay of 3 ns for all gates in this exercise.

Tasks

  1. Implement the circuit shown in Fig. 3.5.1 in SystemVerilog using gate-level primitives. Save as module_with_delays.sv. Add delays to your circuit.

  2. Demonstrate the behavior with a testbench module_with_delays_tb.sv. Show that a glitch appears when changing the inputs from \((A, B, C) = (1, 1, 1)\) to \((A, B, C) = (1, 1, 0)\). Visualize the obtained waveforms.