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:
Simulator: Icarus Verilog
Waveform viewer: Surfer
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.
Primitive |
#Inputs |
#Outputs |
Description |
|---|---|---|---|
|
2 or more |
1 |
AND gate |
|
2 or more |
1 |
NAND gate |
|
2 or more |
1 |
OR gate |
|
2 or more |
1 |
NOR gate |
|
2 or more |
1 |
XOR gate |
|
2 or more |
1 |
XNOR gate |
|
1 |
1 or more |
Buffer gate |
|
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:
where \(A\) and \(B\) are one-bit inputs, and \(C\) is the one-bit output. Table 3.1.1 shows the respective truth table.
\(A\) |
\(B\) |
\(C\) |
|---|---|---|
0 |
0 |
1 |
0 |
1 |
1 |
1 |
0 |
0 |
1 |
1 |
1 |
Tasks
Implement Eq. (3.1.1) in a module named
my_moduleusing gate-level primitives. Save the code asmy_module.sv.Test your implementation using the testbench
my_module_tb. Save the code asmy_module_tb.sv. Verify the outputs for all input combinations (see Table 3.1.1).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}\):
\(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.
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
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
Implement the module
full_adder. Exclusively use gate-level primitives in your design.Test your implementation in the testbench
full_adder_tb. Your testbench should check the outputs for all possible inputs (see Table 3.2.1) throughassert()statements.Visualize the waveform generated by your module. Only show
i_a,i_b,i_carry_in,o_sando_carry_outof thefull_addermodule 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:
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.
\(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.
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
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
Draw the schematic of the four-bit ripple-carry adder as a sequence of full adders.
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?
Complete Table 3.3.1!
Implement the module
ripple_carry_adder_4by completing the module in Listing 3.3.1.Test your implementation in the testbench
ripple_carry_adder_4_tbby completing the module in Listing 3.3.2. Your testbench should check the outputs for all inputs in Table 3.3.1 usingassert()statements.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 ini_carry_in, the four-bit outputo_sand the carry outo_carry_outof theripple_carry_adder_4module 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.
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.
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.
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.
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
Implement
cla_pre_4(Fig. 3.4.1) and test withcla_pre_4_tb.Implement
cla_logic_4(Fig. 3.4.2) and test withcla_logic_4_tb.Implement the 4-bit carry-lookahead block
cla_block_4(Fig. 3.4.3). Use the two modulescla_pre_4andcla_logic_4in your design. Use the testbenchcla_block_4_tbfor your tests.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
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
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.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.