3. Gate-level Modeling

A Hardware Description Language (HDL) allows us to model the structure and behavior of hardware modules. Different levels of abstraction exist for such models. Computer-Aided Design (CAD) tools which understand an HDL perform the mapping of abstract module descriptions to logic gates. This process process is called synthesis. Further, we are able to test our HDL-based hardware designs through simulation. Here, we apply inputs to our hardware modules and check the correctness of the outputs.

In this class we harness SystemVerilog as our HDL. SystemVerilog supports hardware modeling and simulation. The following tools will allow us to compile and test our designs:

Note

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

SystemVerilog may be used at different levels of abstractions. 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 since they allow us to transfer our 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

OR gate

xnor

2 or more

1

NOR 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( outputs, inputs );

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 m_my_gate, we’d use the and primitive in the following way:

and m_my_gate( o_c, i_a, i_b );

3.1. My First Module

Our first module using gate-level primitives implements the following function:

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

where \(A\) and \(B\) are one-bit inputs. \(C\) represents the one-bit output. Table 3.1.1 gives the truth table of this simple function.

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 the module my_module through gate-level primitives. Use the filename my_module.sv.

  2. Test your implementation in the testbench my_module_tb. Use the filename my_module_tb.sv. Verify that your module produces the correct outputs for all inputs (see Table 3.1.1).

  3. Visualize the waveforms generated by your module.

Hint

Use the arguments -Wall -Winfloop -g2012 when invoking iverilog. Details on using GTKWave with Icarus Verilog are available from the Icarus Verilog Wiki. You may increase the GTKWave’s font size, e.g., by using the arguments -A --rcvar 'fontname_signals Monospace 12' --rcvar 'fontname_waves Monospace 12' when invoking the GTKWave binary.

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, i.e., test/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  logic i_a,
11                                i_b,
12                                i_carry_in,
13                   output logic 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 m_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 task 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 in the lab. 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 \(F\) and \(G\) of Eq. (3.2.1) to our input:

(3.3.1)\[\begin{split} S_0 = F(A_0,B_0,C_\text{in}), \\ C_0 = G(A_0,B_0,C_\text{in}), \\ S_1 = F(A_1,B_1,C_0), \\ C_1 = G(A_1,B_1,C_0), \\ S_2 = F(A_2,B_2,C_1), \\ C_2 = G(A_2,B_2,C_1), \\ S_3 = F(A_3,B_3,C_2), \\ C_\text{out} = 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 to think about reasonable unit tests for our testbench. 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 gives the required tests for a successful completion of this task. 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 here
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 m_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 through 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 larger inputs. Here, the propagation delay is bound 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 number of involved full adders (or input bits).

The carry-lookahead adder is one option for a better design at the cost of additional required gates for the lookahead logic. This exercise implements a 64-bit carry-lookahead adder using 4-bit carry-lookahead blocks. The respective schematics are given 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 the module cla_pre_4 shown in Fig. 3.4.1. Use testbench cla_pre_4_tb for your tests.

  2. Implement the module cla_logic_4 shown in Fig. 3.4.2. Use the testbench cla_logic_4_tb for your tests.

  3. Implement 4-bit carry-lookahead block shown in Fig. 3.4.3 using the module name cla_block_4. Use the two modules cla_pre_4 and cla_logic_4 in your design. Use the testbench cla_block_tb_4 for your tests.

  4. Implement a 64-bit carry-lookahead adder by chaining sixteen 4-bit carry lookahead blocks as shown in Fig. 3.4.4. Use the testbench cla_adder_tb_64 for your tests.

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 shown in Fig. 3.5.1 realizes the Boolean equation \(D = \bar{C} A + CB\). This circuit was also our running example when discussing timing analyses in class. In this exercise we’ll use SystemVerilog’s support for delays to study the delayed circuit’s behavior in simulation. We may add delays to SystemVerilog’s gate-level primitives by using the following slightly extended syntax:

gate_type delay instance_name( outputs, inputs );

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 m_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 3ns for all gates in this exercise.

Tasks

  1. Implement the circuit shown in Fig. 3.5.1 in SystemVerilog using gate-level primitives. Use the file name module_with_delays.sv for your implementation. Add delays to your circuit.

  2. Illustrate the behavior of your circuit by implementing a testbench in the file module_with_delays_tb.sv. Show that we are able to simulate a glitch when changing the inputs from \((A, B, C) = (1, 1, 1)\) to \((A, B, C) = (1, 1, 0)\). Visualize the obtained waveforms.