Skip to content

Section 2: Verilog Combinational Gate-Level Design

In this discussion section you will develop your first Verilog hardware design, build a simulator for your design, and then test the functionality of your design. Remember that Verilog is not a programming language; it is a hardware description language! Verilog has two distinct subsets: the synthesizable subset is the portion meant for modeling real hardware, while the non-synthesizable subset is meant for testing and not for modeling real hardware. You will gain experience with both subsets in this discussion section.

1. Logging Into ecelinux with VS Code

Follow the same process as last discussion section. Find a free workstation and log into the workstation using your NetID and standard NetID password. Then complete the following steps (described in more detail in the last discussion section):

  • Start VS Code
  • Install the Remote-SSH, Verilog, and Surfer extensions
  • Use View > Command Palette to execute Remote-SSH: Connect Current Window to Host...
  • Enter netid@ecelinux-XX.ece.cornell.edu where XX is an ecelinux server number
  • Install the Verilog extension on the server
  • Use View > Explorer to open your home directory on ecelinux
  • Use View > Terminal to open a terminal on ecelinux

You should have received an email with a link to join the course GitHub organization. If you have not joined the course GitHub organization then use this link to do so now:

Now fork the GitHub repo we will be using in this discussion section. As mentioned last week, we won't actually be forking repos for the lab assignments, but it is an easy way for you to grab some example code for this discussion section and also to see how GitHub actions work. Go to the example repo here:

Click on the "Fork" button. Wait a few seconds and then visit the new copy of this repo in your own person GitHub workspace:

  • https://github.com/githubid/ece2300-sec02-verilog-gl

where githubid is your username on the public version of GitHub. Enable GitHub Actions on this repo. Click on the Actions tab in your repository on GitHub and click I understand my workflows, go ahead and enable them. Now let's clone your new repo to the ecelinux machine.

% source setup-ece2300.sh
% mkdir -p ${HOME}/ece2300
% cd ${HOME}/ece2300
% git clone git@github.com:githubid/ece2300-sec02-verilog-gl sec02
% cd sec02
% tree

Where githubid is your username on the public version of GitHub. The repo includes the following files:

  • PairTripleDetector_GL.v : Verilog for simple hardware module
  • PairTripleDetector_GL-adhoc.v : adhoc test for hardware module
  • PairTripleDetector_GL-test.v : test cases for hardware module
  • ece2300-misc.v : ECE 2300 miscellaneous macros
  • ece2300-test.v : ECE 2300 unit testing library

2. Background on a Pair/Triple Detector

We will be implementing a 3-input pair/triple detector as the target hardware in this discussion section. A pair/triple detector has some number of input ports and one output port. The output is one if either two or three of the inputs are one, and the output should be zero otherwise. Here is a truth table for a three-bit pair/triple detector.

in0 in1 in2 out
0 0 0 0
0 0 1 0
0 1 0 0
0 1 1 1
1 0 0 0
1 0 1 1
1 1 0 1
1 1 1 1

Verify that this truth table matches the specification above. Here is a gate-level network that implements this truth table.

Here is an example of an incomplete timing diagram.

Activity 1: Simulation Tables and Waveforms

Complete the above timing diagram. Assume a zero delay model.

3. Implementing and Linting a Pair/Triple Detector in Verilog

In this part, we will first implement the pair/triple detector before linting the detector to check for errors.

3.1. Implementing a Verilog Design

In this part, we will be using the synthesizable subset of Verilog; recall that the synthesizable subset refers to the portion of the Verilog language that is used to actually model real hardware. Before implementing the pair/triple detector, you might want to review how to instantiate our primitive logic gates in Verilog.

We have provided you with the interface for the pair/triple detector in PairTripleDetector_GL.v.

`ifndef PAIR_TRIPLE_DETECTOR_GL_V
`define PAIR_TRIPLE_DETECTOR_GL_V

`include "ece2300-misc.v"

module PairTripleDetector_GL
(
  input  wire in0,
  input  wire in1,
  input  wire in2,
  output wire out
);

  //''' ACTIVITY '''''''''''''''''''''''''''''''''''''''''''''''''''''''''
  // Implement pair/triple detector using explicit gate-level modeling
  //>'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

  `ECE2300_UNUSED( in0 );
  `ECE2300_UNUSED( in1 );
  `ECE2300_UNUSED( in2 );
  `ECE2300_UNDRIVEN( out );

endmodule

`endif /* PAIR_TRIPLE_DETECTOR_GL_V */

There are several key points to note:

  • This file name (and module name) has a _GL suffix which standes for "gate-level". We will use various suffixes to clearly indicate what level of modeling is being used within the implementation of the corresponding Verilog hardware design.

  • The ifdef, define, and endif statements are for the Verilog preprocessor and act as include guards; then ensure if we include a Verilog module file multiple times the actual module definition is only included once.

  • We include the ece2300-misc.v file which has miscellaneous macros we use in the course. include is analogous to import in Python.

  • The module has three input ports (named in0, in1, and in2) and one output port (named out).

  • We have included a comment with instructions for your implementation. You should remove these kind of "instructional comments" before writing your own implementation (this is also true for all lab assignments).

  • We use the ECE2300_UNUSED macro to indicate that it is ok that the input ports are unused and we use the ECE2300_UNDRIVEN macro to indicate that it is ok that the output port is undriven. You will need to remove the ECE2300_UNUSED and ECE2300_UNDRIVEN macros before writing your own implementation (this is also true for all lab assignments).

Open the PairTripleDetector_GL.v Verilog file using VS Code.

% cd ${HOME}/ece2300/sec02
% code PairTripleDetector_GL.v

Activity 2: Implement a Pair/Triple Detector

Create a Verilog hardware design that implements a pair/triple detector by declaring wires, instantiating primitive logic gates, and connecting the wires and gates appropriately.

3.2. Linting a Verilog Design

Note that Verilog is a very relaxed language which allows all kinds of sketchy constructs many of which are only flagged with warnings. So we will be using the open-source verilator tool to lint our Verilog hardware designs. Linting means to check the design for static errors. It is critical be clear that verilator will only check for static errors (i.e., syntax errors) and does not actually test that your hardware design has the desired dynamic behavior. We will use testing in the next part to verify your hardawre design has the desired dynamic behavior. There is more information about verilator on its webpage:

Here is how to use verilator to lint our design.

% cd ${HOME}/ece2300/sec02
% verilator -Wall --lint-only PairTripleDetector_GL.v

If verilator does not report any warnings or errors then you are ready to compile a simulator. Let's purposefully make an error in our Verilog hardware design. For example, modify your hardware design to purposefully omit the semicolon at the end of the module interface.

Activity 3: Injecting a Static Bug

Try removing the semicolon after the closed parenthesis to create a bug. Rerun verilator to lint your design. Remeber you can just press the up arrow key to rerun a previous command! You should see an error. verilator is much, much better at catching syntax bugs and providing useful error messages. So we will always lint our designs first with verilator, and then (only once we are sure there are no static errors!) we will move on to testing our hardware design's functionality. Go ahead and put the semicolon back.

4. Ad-Hoc Testing a Pair/Triple Detector in Verilog

In this part, we will first implement a test bench for our pair/triple detector before simulating the detector to test its functionality.

4.1. Implementing a Verilog Test Bench

Now that we have successfully linted our design, we want to test its functionality (i.e., make sure it produces the correct output for various inputs).

In this part, we will be using the non-synthesizable subset of Verilog; recall that the non-synthesizable subset refers to the portion of the Verilog language that is used to test your design and does not model real hardware. More specifically, we will be implementing a Verilog test bench which will set inputs and check outputs. It is critical to keep the synthesizable and non-synthesizable uses of Verilog separate! When using Verilog for hardware design we are using Verilog to model hardware. We must be very careful, restrict ourselves to a very limited subset of the Verilog language, and ensure we always know what is the hardware we are modeling. It is easy to write Verilog which does not model any real hardware! When using Verilog for test benches, we can use any part of the Verilog language we want; our goal is not to model hardware but to test hardware.

We have provided you a simple ad-hoc test bench in PairTripleDetector_GL-adhoc.v. Take a look at this file in VS Code.

% cd ${HOME}/ece2300/sec02
% code PairTripleDetector_GL-adhoc.v
`include "PairTripleDetector_GL.v"

module Top();

  logic in0;
  logic in1;
  logic in2;
  logic out;

  PairTripleDetector_GL dut
  (
    .in0 (in0),
    .in1 (in1),
    .in2 (in2),
    .out (out)
  );

  initial begin
    $dumpfile("waves1.vcd");
    $dumpvars;

    in0 = 0;
    in1 = 0;
    in2 = 0;
    #10;
    $display( "%b %b %b > %b", in0, in1, in2, out );

    in0 = 0;
    in1 = 1;
    in2 = 1;
    #10;
    $display( "%b %b %b > %b", in0, in1, in2, out );

    in0 = 0;
    in1 = 1;
    in2 = 0;
    #10;
    $display( "%b %b %b > %b", in0, in1, in2, out );

    in0 = 1;
    in1 = 1;
    in2 = 1;
    #10;
    $display( "%b %b %b > %b", in0, in1, in2, out );

  end

endmodule

The include statement is used by the Verilog preprocessor and indicates that the adhoc test depends on the Verilog hardware design we implemented in the previous part. We start by declaring four wires that will be connected to the design-under-test. Note that we use logic not wire in test benches. A logic is a more abstract kind of signal than a wire in Verilog. We then instantiates the design-under-test (DUT) and hook up the ports to the logic signals we just declared.

An initial block is a special piece of code which starts running at the beginning of a simulation. Initial blocks are part of the non-synthesizable subset of Verilog. You should NEVER use an initial block when modeling hardware. However, it is perfectly fine to use an initial block in your test benches. We call two system tasks ($dumpfile,$dumpvars) to tell the simulator to output a VCD file which contains waveforms so we can visualize what our hardware design is doing. Then we set input values for all input ports. Then wait 10 units of time. Then we display all of the input and output values. We do this four times with four different sets of input values.

4.2. Ad-Hoc Testing a Verilog Design

Now that we have implemented and linted our Verilog hardware design and implemented a test bench, we want to simulate our hardware design to verify its functionality. We will be using the open-source iverilog (Icarus Verilog) simulator. One weakness of iverilog is that its error messages are not great; this is one of the primary reasons we always want to lint our designs first using verilator. There is more information about iverilog on its webpage:

When using iverilog there are always two steps. First, we create a simulator and then we need to explicitly run the simulator to test hardware design. Let's start by using iverilog to create a simulator based on our pair/triple hardware design and test bench.

% cd ${HOME}/ece2300/sec02
% iverilog -Wall -g2012 -o PairTripleDetector_GL-adhoc PairTripleDetector_GL-adhoc.v

If there are no errors you should now have a simulator named PairTripleDetector_GL-adhoc. Go ahead and execute the simulator.

% cd ${HOME}/ece2300/sec02
% ./PairTripleDetector_GL-adhoc

The ad-hoc test will print out a table which shows four sets of inputs and the corresponding output. Compare the rows in this table to the truth table you created by hand earlier in this discussion section. The ad-hoc test also dumps out a waveforms in VCD format. Then view the corresponding waveforms (i.e., a timing diagram) using the Surfer extension.

% cd ${HOME}/ece2300/sec02
% code waves1.vcd

Find the Scopes panel and click on the arrow next to Top. Click on dut and then click on the signals in the Variables panel to see the waveforms. Verify that the timing diagram matches the timing diagram you derived by hand in the previous part.

5. Systematic Testing for a Pair/Triple Detector in Verilog

So far we have been using "ad-hoc" testing. Our test bench will display outputs on the terminal. If it is not what we expected, we can debug our hardware design until it meets our expectations. Unfortunately, ad-hoc testing is error prone and not easily reproducible. If you later make a change to your implementation, you would have to take another look at the output to ensure your implementation still works. If another designer wants to understand your implementation and verify that it is working, he or she would also need to take a look at the output and think hard about what is the expected result. Ad-hoc testing is usually verbose, which makes it error prone, and does not use any kind of standard test output. While ad-hoc testing might be feasible for very simple implementations, it is obviously not a scalable approach when developing the more complicated implementations we will tackle in this course.

We will be using a more systematic way to do automated unit testing including standardized naming conventions, test benches, and test output. We have provided you an example of such systematic testing in PairTripleDetector_GL-test.v. Take a look at this file in VS Code.

% cd ${HOME}/ece2300/sec02
% code PairTripleDetector_GL-test.v

The systematic test bench still declares four wires that will be connected to the DUT, and also still instantiates the DUT and hooks up the ports. We then declare a check task. We will be using Verilog tasks in our test bench. Tasks are a Verilog feature used for creating test benches and should be avoided when modeling hardware. A task is similar to a function in a software programming language and are critical for creating clean test benches.

  task check
  (
    input logic in0_,
    input logic in1_,
    input logic in2_,
    input logic out_
  );
    if ( !t.failed ) begin

      in0 = in0_;
      in1 = in1_;
      in2 = in2_;

      #8;

      if ( t.n != 0 )
        $display( "%3d: %b %b %b > %b", t.cycles, in0, in1, in2, out );

      `ECE2300_CHECK_EQ( out, out_ );

      #2;

    end
  endtask

The check task takes as values we want to use as input to test our design, and the correct output values we want to check for. The task then sets the input, waits for some amount of time, displays the input and output values, and then checks that the output is as expected. We then declare one or more test case tasks. Each test case task has a sequence of checks. Here is the basic test case.

  task test_case_1_basic();
    t.test_case_begin( "test_case_1_basic" );

    //     in0 in1 in2 out
    check( 0, 0, 0, 0 );
    check( 0, 1, 1, 1 );
    check( 0, 1, 0, 0 );
    check( 1, 1, 1, 1 );

    t.test_case_end();
  endtask

Finally, our test bench uses an initial block to decide which test cases to run.

1
2
3
4
5
6
7
8
9
  initial begin
    t.test_bench_begin( `__FILE__ );

    if ((t.n <= 0) || (t.n == 1)) test_case_1_basic();
    if ((t.n <= 0) || (t.n == 2)) test_case_2_exhaustive();
    if ((t.n <= 0) || (t.n == 3)) test_case_3_xprop();

    t.test_bench_end();
  end

We can compile and run our systematic testing just like our ad-hoc testing.

% cd ${HOME}/ece2300/sec02
% iverilog -Wall -g2012 -o PairTripleDetector_GL-test PairTripleDetector_GL-test.v
% ./PairTripleDetector_GL-test

Remember, you can always use the up arrow key to retrieve a previously entered command. You can then quickly edit it as opposed to having to type a complete command from scratch.

The simple unit testing framework we provide you enables to specify a single test case to run (with +test-case=1 or +test-case=2), and generate a VCD file for viewing waveforms (with +dump-vcd=waves.vcd).

% cd ${HOME}/ece2300/sec02
% ./PairTripleDetector_GL-test +test-case=1
% ./PairTripleDetector_GL-test +test-case=1 +dump-vcd=waves2.vcd
% code waves2.vcd

Recall that in Verilog signals can have four possible values:

Value Verilog Literal Syntax
Logic 0 0, 1'b0
Logic 1 1, 1'b1
X (undefined, unknown) 'x, 1'bx
Z (floating) 'z, 1'bz

Notice the need for a leading tick mark (') when specifying an X or Z.

When writing test cases we always want a test case that makes sure our design propagates Xs as expected if one or more of the inputs are X. At a minimum we want to make sure that if all inputs are X, then the output should also be X. Take a look at the third "xprop" test case for an example.

% cd ${HOME}/ece2300/sec02
% ./PairTripleDetector_GL-test +test-case=3

Activity 4: Exhaustive Testing

Finish the second test case which should use exhaustive testing. Exhaustive testing simply means we test all possible input values. You can refer to the truth table from earlier in the discussion section and simply have one check for every row in the truth table. When you have finished rerun the the test using +test-case=2 to make sure your test bench is testing what you think it is.

Activity 5: Inject a Dynamic Bug

Try changing the OR gate to a NOR gate. Rerun iverilog to see if the test bench fails. Go ahead and put the OR gate back.

6. Automating the Development Process

The key to being a productive hardware designer is to automate as much of the process as possible. We will learn how to use a real build system in the next discussion section to help automate linting, compiling, simulating, and testing hardware designs. In this discussion section, we will briefly discuss two other ways to automate the development process.

6.1. Using Bash Shell Scripts for Testing

If you find yourself continually having to use the same complex commands over and over, consider creating a Bash shell script to automatically execute those commands. A Bash shell script is just a text file with a list of commands that you can run using the source command.

For example, let's create a Bash shell script to automatically lint, compile, and run the systematic test bench developed earlier in the discussion section. Use VS Code to open a new file called PairTripleDetector_GL-test.sh (note that by convention we usually use the .sh extension for Bash shell scripts).

% cd ${HOME}/ece2300/sec02
% code PairTripleDetector_GL-test.sh

Then enter the following commands into this new Bash shell script.

verilator -Wall --lint-only PairTripleDetector_GL.v
iverilog -Wall -g2012 -o PairTripleDetector_GL-test PairTripleDetector_GL-test.v
./PairTripleDetector_GL-test

Then save the Bash shell script and execute it using the source command.

% cd ${HOME}/ece2300/sec02
% source PairTripleDetector_GL-test.sh

The script will automatically lint your design, compile your design with the test bench into a simulator, and then run the simulator to test your design all in a single step.

6.2. Using GitHub Actions for Continuous Integration

We will be using GitHub Actions for continuous integration which means that GitHub Actions will run all of your tests in the cloud every time you push to GitHub. Go ahead and commit your work and push to GitHub.

% cd ${HOME}/ece2300/sec02
% git commit -a -m "finished implementation"
% git push

Then go to the Actions tab of your repo.

  • https://github.com/githubid/ece2300-sec02-verilog-gl/actions

where githubid is your username on the public version of GitHub. You should be able to see a workflow run. Click on the name of the workflow run, then click on sim, then if you click on Run sim_tests you should be able to see the output of running the tests in the cloud through GitHub Actions.

7. To-Do On Your Own

Let's say we started from just the original truth table for a pair/triple detector without the gate-level network. As in lecture, we can also directly transform this truth table into a (unoptimized) Verilog hardware design.

We first declare three wires for the complement of each input. The signal named in0_n is the NOT of the signal named in0. We instantiate four NOT gates and connect them to these new wires. We then declare four wires, one for each row in the truth table where the output is one. We use four AND gates, one for each row in the truth table where the output is one. Then we OR together the outputs of the AND gates to derive the final output.

module PairTripleDetector_GL
(
  input  wire in0,
  input  wire in1,
  input  wire in2,
  output wire out
);

  // NOT gates to create complement of each input

  wire in0_n, in1_n, in2_n;

  not( in0_n, in0 );
  not( in1_n, in1 );
  not( in2_n, in2 );

  // AND gates for each row in the truth table where output is one

  wire row3, row5, row6, row7;

  and( row3, in0_n, in1,   in2   );
  and( row5, in0,   in1_n, in2   );
  and( row6, in0,   in1,   in2_n );
  and( row7, in0,   in1,   in2   );

  // OR together the outputs of the AND gates

  or( out, row3, row5, row6, row7 );

endmodule

Try replacing your original implementation with this new (unoptimized) implementation. If you rerun the tests you should see it will pass the basic test case and exhausitive test case, but will fail the xprop test case. This is because our original xprop test case is an example of "white box testing"; it depends on the actual gate-level implementation of the module. For black box xprop testing, we can usually only check that if all of the inputs are X then the output should also be X. Go ahead and modify the xprop test as follows and verify that your new (unoptimized) design passes all three test cases.

  task test_case_3_xprop();
    t.test_case_begin( "test_case_3_xprop" );

    //     in0 in1 in2 out
    check( 'x, 'x, 'x, 'x );
    // check( 'x,  1,  0, 'x );
    // check(  1,  1, 'x,  1 );

    t.test_case_end();
  endtask