Skip to content

Section 4: Lab 2 Head Start

Today you will get started on Lab 2. By the end of this section, you will have implemented a full adder, tested the full adder using exhaustive testing, implemented a four-bit ripple-carry adder, and tested the ripple-carry adder using directed and random testing. Feel free to copy any code you would like from your work in this discussion section into your lab group repo, but keep in mind for Lab 2 you will be implementing an eight-bit ripple-carry adder as opposed to a four-bit ripple-carry adder.

1. Logging Into ecelinux with VS Code

We have (hopefully) drastically simplified the process of logging into ecelinux with VS Code using the workstations. Find a free workstation and log into the workstation using your NetID and standard NetID password. Then complete the following steps:

  • Open the ECE 2300 folder on the desktop to locate two shortcuts
    • ECE 2300 Handouts: opens the handouts page
    • ECE 2300 VS Code: starts VS Code (enter server number and password)

That is it! Our custom ECE 2300 VS Code script will take care of:

  • installing Remote SSH, Verilog HDL, Surfer extensions
  • configuring SSH for accessing ecelinux without extra prompts
  • starting VS Code connected to ecelinux without extra prompts
  • opening the file explorer in your ${HOME}/ece2300 directory
  • installing the Verilog HDL extension on the server
  • opening a terminal

There is no need to fork the repo for today's discussion section. Clone the repo as follows.

% source setup-ece2300.sh
% mkdir -p ${HOME}/ece2300
% cd ${HOME}/ece2300
% git clone git@github.com:cornell-ece2300/ece2300-sec04-lab2-head-start sec04
% cd sec04
% tree

The repo includes the following files:

  • Makefile.in: Makefile for the build system
  • configure: Configure script for the build system
  • configure.ac: Used to generate the configure script
  • scripts: Scripts used by the build system
  • ece2300: ECE 2300 unit testing library and miscellaneous macros
  • lab2/lab2.mk: Tells build system about files in this subproject
  • lab2/FullAdder_GL.v: Verilog for full adder
  • lab2/AdderRippleCarry_4b_GL.v: Verilog for four-bit ripple-carry adder
  • lab2/test/FullAdder_GL-test.v: test cases for full adder
  • lab2/test/AdderRippleCarry_4b_GL-test.v: test cases for four-bit ripple-carry adder

Create a build directory, run configure to generate a Makefile, and run the tests.

% cd ${HOME}/ece2300/sec04
% mkdir -p build
% cd build
% ../configure
% make check

To make it easier to cut-and-paste commands from this handout onto the command line, you can tell Bash to ignore the % character using the following command:

% alias %=""

Now you can cut-and-paste a sequence of commands from this tutorial document and Bash will not get confused by the % character which begins each line.

2. Implementing a Full Adder

A full adder adds three one bit binary values and produces a two-bit binary output. This is the interface for the module:

Review your notes from lecture and fill out the following truth table for a full adder. Make sure you completely understand this truth table before continuing.

in0 in1 cin cout sum
0 0 0
0 0 1
0 1 0
0 1 1
1 0 0
1 0 1
1 1 0
1 1 1

We showed in lecture how to implement this truth table as a gate-level network.

Spend minute confirming that this gate-level network does indeed implement the truth table above. Now go ahead and implement this gate-level netlist in a Verilog hardware module. We have provided you with the interface for a full adder in FullAdder_GL.v.

module FullAdder_GL
(
  (* keep=1 *) input  wire in0,
  (* keep=1 *) input  wire in1,
  (* keep=1 *) input  wire cin,
  (* keep=1 *) output wire cout,
  (* keep=1 *) output wire sum
);

The module has three input ports and two output ports. Notice we are using the special (* keep=1 *) Verilog attribute with every port. Without this attribute, the FPGA tools will "flatten" your entire design (i.e., eliminate all module boundries) during the synthesis process. While this can enable improved optimizations, it also complicates analyzing the area and timing of your design. The (* keep=1 *) attribute tells the FPGA tools to preserve the module hierarchy. We will use (* keep=1 *) on all ports for the remaining labs. Open the FullAdder_GL.v Verilog file using VS Code.

% cd ${HOME}/ece2300/sec04/build
% code ../lab2/FullAdder_GL.v

Activity 1: Implement a Full Adder

Complete the truth table for a full adder on the paper worksheet. Create a Verilog hardware design that implements a full adder using explicit gate-level modeling.

3. Exhaustive Testing for a Full Adder

After implementing any Verilog hardware module, your next task must always be to test that Verilog hardware module. Never attempt to use a Verilog hardware module before thoroughly testing its functionality! Since our full adder only has eight possible input values, we can use exhaustive testing to verify its functionality.

We have provided you a Verilog test bench in FullAdder_GL-test.v with a basic test case named test_case_1_basic and an exhaustive test case named test_case_2_exhaustive.

% cd ${HOME}/ece2300/sec04/build
% code ../lab2/test/FullAdder_GL-test.v

Go ahead and build and run the test simulator like this:

% cd ${HOME}/ece2300/sec04/build
% make FullAdder_GL-test
% ./FullAdder_GL-test
% ./FullAdder_GL-test +test-case=2

Verify the output from running the exhaustive test case exactly matches the truth table you created above.

4. Implementing a Ripple-Carry Adder

A four-bit adder adds two four-bit binary values and produces a four-bit binary output. This is the interface for the module:

Note that our four-bit adder also includes a carry in and a carry out. Review your notes from lecture and fill out the following truth table for the four-bit adder. carry0 is the carry out of bit0, carry1 is the carry out of bit1, and so on. Make sure you completely understand this truth table before continuing.

in0 in1 carry2 carry1 carry0 cin cout sum
0000 0000 0
0001 0001 0
0001 0001 1
0011 0011 0

A ripple-carry adder chains together full adders to implement a multi-bit binary adder. For example, we can chain four full adders together to implement a four-bit binary adder.

We now want to implement this ripple-carry adder in a Verilog hardware module. We have provided you with the interface for the adder in AdderRippleCarry_4b_GL.v.

module AdderRippleCarry_4b_GL
(
  (* keep=1 *) input  wire [3:0] in0,
  (* keep=1 *) input  wire [3:0] in1,
  (* keep=1 *) input  wire       cin,
  (* keep=1 *) output wire       cout,
  (* keep=1 *) output wire [3:0] sum
);

Notice that we are using multi-bit input and output ports. The block diagram illustrates how we can use "wiring slicing" (also called bus slicing); we slice wires out of the multi-bit bus to connect each bit of the multi-input ports to a different instances of the full adder module. The carry chain is implemented using temporary wires to connect the carry out of one full adder to the carry in of the next full adder.

Take a minute to reflect on our use of the three abstraction principles: modularity: our ripple carry adder has a well-defined port-based interface; hierarchy: our ripple carry adder instantiates a full adder module; and regularity: we have reused the same full adder four times to implement the ripple carry adder.

Open the AdderRippleCarry_4b_GL.v Verilog file using VS Code.

% cd ${HOME}/ece2300/sec04/build
% code ../lab2/AdderRippleCarry_4b_GL.v

Activity 2: Implement a Ripple Carry Adder

Complete the partial truth table for the ripple carry adder. Implement the ripple carry adder by instantiating four full adders and connecting them appropriately.

5. Directed Testing for a Ripple-Carry Adder

Since we have already thoroughly unit tested our full adder, we can now focus on integration testing for our four-bit ripple-carry adder. Our four-bit ripple carry adder has a total of 2^9 or 512 possible input values which means exhaustive testing is not really feasible. We will need to use directed and random testing to verify the functionality of the ripple-carry adder.

We have provided you a Verilog test bench in AdderRippleCarry_4b_GL-test.v with a basic test case named test_case_1_basic.

% cd ${HOME}/ece2300/sec04/build
% code ../lab2/test/AdderRippleCarry_4b_GL-test.v

The basic test case includes four checks that correspond to the exact same four rows in that you completed in the partial truth table above. Go ahead and run this test case and confirm the output matches your partial truth table.

% cd ${HOME}/ece2300/sec04/build
% make AdderRippleCarry_4b_GL-test
% ./AdderRippleCarry_4b_GL-test +test-case=1

Activity 3: Test Ripple-Carry Adder with Directed Tests

Add multiple directed test cases. Do not just add more checks to the basic test case! You must create new test cases. Each directed test case should be designed to test a specific behavior. For example, you might want to write the following test cases:

  • Test inputs where cin is zero and cout is zero
  • Test inputs where cin is one and cout is zero
  • Test inputs where cin is zero and cout is one
  • Test inputs where cin is one and cout is one
  • Test inputs where every carry bit is one
  • Test inputs that are all X

After writing each directed test case, build and run the test simulator like this:

% cd ${HOME}/ece2300/sec04/build
% make AdderRippleCarry_4b_GL-test && ./AdderRippleCarry_4b_GL-test

Once you implemented your test cases, use Surfer to look at a timing diagram for the test case of your choice. For example, we can look at the timing diagram for test case 2 like this.

% cd ${HOME}/ece2300/sec04/build
% make AdderRippleCarry_4b_GL-test && ./AdderRippleCarry_4b_GL-test +test-case=2 +dump-vcd=waves.vcd
% code waves.vcd

Activity 4: Understand Simulation Timing Diagrams

Carefully look at the timing diagram in Surfer. Display all of the ports for all four full adders. This means you should have at least 20 signals displayed in Surfer. What delay model is our simulation using?

6. Random Testing for a Ripple-Carry Adder

Once you have finished writing and running your directed test cases, move on to random testing. Review what we learned in the previous discussion section about how to write random tests. There are a couple of hints that might be useful as you write the Verilog code to determine the correct answer given random inputs.

You can use the + operator to perform addition to check the answers in your random test case. However, Verilator will complain if you attempt to add signals of different bitwidths; you can zero-extend signals to ensure they are all the same bitwidth using the Verilog concatenation operator ({}). For example, we can zero extend a one-bit signal named y into a four bit signal named y_zext by concatentating three zeros with y as follows.

  wire y;
  wire [3:0] y_zext;
  assign y_zext = { 3'b0, y };

If we just add two four-bit values we will get a four-bit result, but we want a five-bit result so we know the correct carry out for the four-bit adder. So we need to zero-extend all signals to five bits to produce a five-bit result; we can then use wire slicing to get the correct carry out and the correct sum bits. Here is a partial template you can use for your random test case.

  logic [3:0] rand_in0;
  logic [3:0] rand_in1;
  logic       rand_cin;
  logic [4:0] rand_result; // this must be 5-bit not 4-bit!
  logic       rand_cout;
  logic [3:0] rand_sum;

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

    ...

    // Adding two 4-bit values can produce up to a 4-bit result, so it is
    // important that we make sure rand_result is five bits. Then the
    // most significant bit (i.e., rand_result[4]) is the correct carry
    // out and the remaining four bits are the correct sum.

    rand_result = rand_in0 + rand_in1 + rand_cin;
    rand_cout = rand_result[4];
    rand_sum = rand_result[3:0];

    ...

    t.test_case_end();
  endtask

After writing a random test case, you build and run the test simulator like this:

% cd ${HOME}/ece2300/sec04/build
% make AdderRippleCarry_4b_GL-test && ./AdderRippleCarry_4b_GL-test

Then zoom in and look at the trace output for just the random simulaton.

% cd ${HOME}/ece2300/sec04/build
% make AdderRippleCarry_4b_GL-test && ./AdderRippleCarry_4b_GL-test +test-case=3

Activity 5: Test Ripple-Carry Adder with Random Testing

Use the above template to write a random test case for your four-bit ripple-carry adder. Chooosing one set of random inputs is not compelling. Choosing 50 sets of random inputs seems more compelling. You must use the $urandom system call with t.seed to ensure your random values are pseudo-random and reproducible. Make sure to include the actual checks with ECE2300_CHECK to ensure the outputs from your ripple-carry adder match the calculated correct answer.

7. Discussion Section Check-Off

As a final step, do a clean build to verify everything is working correctly.

% cd ${HOME}/ece2300/sec04
% trash build
% mkdir -p build
% cd build
% ../configure
% make check

How to submit your discussion section check-off

We are trying something different for this discussion section. Please run your random test case:

% cd ${HOME}/ece2300/sec04/build
% make AdderRippleCarry_4b_GL-test
% ./AdderRippleCarry_4b_GL-test +test-case=3

Then take a screeshot by pressing Windows + Shift + S and save it to the workstation. You only need to capture the last 20 or so random checks. Then go to canvas and find the corresponding assignment for this discussion section. Upload the screenshot to complete the discussion section check-off. Once you have finished the discusssion section check-off you are free to leave or continue to the next section.

8. Further Exploration

There are many different ways to implement a full adder. Replace your original full adder implementation with the following alternative implementation.

Spend a few minutes confirming this gate-level network does indeed implement a full adder by: (1) deriving the truth table for this gate-level network; and (2) comparing it our original truth table. Then rerun all of your tests. How would your alternative implementation compare to the original implementaion in terms of delay and area?

Take a minute to reflect on our use of the three abstraction principles: modularity: our full adder has a well-defined port-based interface which makes it easy to change the implementation as long as we adhere to the same interface; hierarchy: our ripple carry adder instantiates a full adder module so it will work without modification even if we change the full adder implementation; and regularity: we have reused the same full adder four times to implement the ripple carry adder, so we only need to change the implementation in one place.