Skip to content

Lab 2: Two-Function Calculator
Part A: Adders and Muxes

Lab 2 will give you experience designing, implementing, testing, and prototyping more complicated combinational logic using the Verilog hardware description language. This lab will primarily leverage concepts from Topic 2: Combinational Logic, Topic 3: Boolean Algebra, and Topic 4: Combinational Building Blocks including experience with adders, multiplexors, and multipliers. This lab will also reinforce three key abstraction principles: modularity, hierarchy, and regularity.

You will be implementing a two-function calculator that takes as input two binary values and then calculates either the sum or the product of these two values. The input values and the result will be displayed on seven-segment displays using your Verilog hardware design from Lab 1. Your implementation will mostly use gate-level modeling, but you will also start to explore very simple register-transfer-level modeling. Parts of the calculator will be used in future labs. The lab includes four parts:

  • Part A: Adders and Muxes

    • Due 9/25 @ 11:59pm via GitHub
    • Students should work on Part A before, during, and after your assigned lab section during the week of 9/22
    • Pre-lab survey on Canvas is (roughly) due by end of lab section during the week of 9/22
  • Part B: Multipliers and Calculator

    • Due 10/2 @ 11:59pm via GitHub
    • Plan to start on Part B during the week of 9/22
    • Even though Part B is due on 10/2 you still need the code ready to go before your lab section the week of 9/29!
  • Part C: FPGA Prototype

    • Due week of 9/29 during assigned lab section
    • Even though completed with a partner, every student must turn in their own paper check-off sheet in their lab section!
  • Part D: Report

    • Due week of 9/29, three days after lab section @ 11:59pm via Canvas
    • Post-lab survey on Canvas is due at the same time as the report

All parts of Lab 2 must be done with a partner. You can confirm your partner on Canvas (Click on People, then Groups, then search for your name to find your lab group).

Both students must contribute to all parts!

It is not acceptable for one student to exclusively work on the code while the other student exclusively works on the report. It is not acceptable for one student to exclusively work on hardware design while the other student exclusively works on testing. Both students must contribute to all parts. Student understanding of Verilog design and testing will be assessed on the prelim exams, final exam, and Verilog coding exam. The instructors will also survey the Git commit log on GitHub to confirm that both students are contributing equally. If you are using pair programming, then both students must take turns using their own account so both students have representative Git commits. Students should create commits after finishing each step of the lab, so their contribution is clear in the Git commit log. A student's whose contribution is limited as represented by the Git commit log will receive a significant deduction to their lab score.

This handout assumes that you have read and understand the course tutorials and that you have attended the discussion sections. To get started, use VS Code to log into a specific ecelinux server, source the setup script, and clone your remote repository from GitHub:

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

where XX should be replaced with your group number. You can both pull and push to your remote repository. If you have already cloned your remote repository, then use git pull to ensure you have any recent updates before working on your lab assignment.

% cd ${HOME}/ece2300/groupXX
% git pull
% tree

where XX should be replaced with your group number. Go ahead and create a build directory, run configure to generate a Makefile, and run all of the tests.

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

Your repo contains the following files which are part of the automated build system:

  • 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

Your repo includes the following files in the lab2 subdirectory for Part A. These files are for modeling real hardware using the synthesizable subset of Verilog.

  • FullAdder_GL.v: Full adder
  • AdderRippleCarry_8b_GL.v: 8-bit ripple-carry adder
  • AdderRippleCarry_16b_GL.v: 16-bit ripple-carry adder
  • Mux2_1b_GL.v: 1-bit 2-to-1 multiplexor
  • Mux2_8b_GL.v: 8-bit 2-to-1 multiplexor
  • Mux2_16b_GL.v: 16-bit 2-to-1 multiplexor
  • AdderCarrySelect_16b_GL.v: 16-bit carry-select adder
  • Adder_16b_RTL.v: 16-bit register-transfer-level adder

The lab2/test subdirectory includes the following test libraries and test benches for Part A.

  • FullAdder_GL-test.v: Tests for full adder
  • AdderRippleCarry_8b_GL-test.v: Tests for 8-bit ripple-carry adder
  • Adder-test-cases.v: Shared test cases for 16-bit adders
  • AdderRippleCarry_16b_GL-test.v: Tests for 16-bit ripple-carry adder
  • Mux2_1b_GL-test.v: Tests for 1-bit 2-to-1 multiplexor
  • Mux2_8b_GL-test.v: Tests for 8-bit 2-to-1 multiplexor
  • Mux2_16b_GL-test.v: Tests for 16-bit 2-to-1 multiplexor
  • AdderCarrySelect_16b_GL-test.v: Tests for 16-bit carry-select adder
  • Adder_16b_RTL-test.v: Tests for 16-bit register-transfer-level adder

The _GL suffix indicates that these hardware designs must be implemented using explicit gate-level modeling. This means students are only allowed to use these Verilog constructs in their Verilog hardware designs:

  • wire (single bit and multiple bit)
  • not, and, nand, or, nor, xor, xnor
  • literals (e.g., 1'b0, 1'b1)
  • wire slicing (e.g., x[0], x[1:0])
  • assign for connecting wires (e.g., assign x = y;);
  • assign for setting a wire to a constant value (e.g., assign x = 1'b0;)
  • module instantiation

The _RTL suffix indicates which hardware designs should be implemented using register-transfer-level (RTL) modeling. For RTL designs, students can additionally use the following Verilog constructs.

  • + *

Using any other Verilog constructs in your Verilog hardware designs will result in significant penalties for code functionality and code quality. If you have any questions on what Verilog constructs can and cannot be used, please ask an instructor. There are no restrictions on Verilog constructs in test benches or interactive simulators.

Part A is divided into eight steps. Steps 1-3 can be done in parallel with steps 4-6 so consider splitting up the work between partners. Alternatively, consider having one partner work on the hardware implementation while the other partner works on the test cases in parallel for one module; then switch roles for the next module.

  • Step 0. Copy your lab 1 design and testing code into lab1 subdirectory
  • Step 1. Implement and test FullAdder_GL
  • Step 2. Implement and test AdderRippleCarry_8b_GL
  • Step 3. Implement and test AdderRippleCarry_16b_GL
  • Step 4. Implement and test Mux2_1b_GL
  • Step 5. Implement and test Mux2_8b_GL
  • Step 6. Implement and test Mux2_16b_GL
  • Step 7. Implement and test AdderCarrySelect_16b_GL
  • Step 8. Implement and test Adder_16b_RTL

Students will almost certainly need to spend significant time outside of their lab session to complete Part A. Students with a lab session early in the week can use their lab session to get started with the help of the course staff and then finish on their own before the deadline. Students with a lab session late in the week can get started on their own and use their lab session to finish their lab with the help of the course staff.

1. Pre-Lab Survey

Take some time to meet with your partner to discuss the pre-lab survey which is on Canvas. The pre-lab survey includes questions on your learning outcomes, workload distribution, workload roadmap, communication, and collaboration. The survey is due (roughly) by the end of your assigned lab section the week of 9/22. Students with an early lab section on Monday might want to complete the pre-lab survey right after their lab section, while students with a late lab section on Wednesday should meet earlier in the week and complete the pre-lab survey then. A student will not receive a grade for the lab unless the pre-lab survey is completed.

2. Five-Digit Display

Before starting work on Lab 2, you must copy your work from Lab 1 into your new group repo. Choose whichever student's work you like best. Copy the implementations from these files:

  • BinaryToSevenSegUnopt_GL.v
  • BinaryToBinCodedDec_GL.v
  • DisplayUnopt_GL.v
  • BinaryToSevenSegOpt_GL.v
  • DisplayOpt_GL.v

Copy just the test cases you wrote (not the entire file) from these files:

  • BinaryToSevenSeg-test-cases.v
  • BinaryToBinCodedDec_GL-test.v
  • Display-test-cases.v

Do not copy the entire lab1 subdirectory since we have made some changes. Do not copy the entire test files; copy just the test cases since we have made some changes! Make sure lab 1 passes all of the tests.

% cd ${HOME}/ece2300/groupXX/build
% make check-lab1

Do not continue unless Lab 1 is passing all of your tests!

3. Interface and Implementation Specification

In Part A, you will be implementing and composing a variety of combinational building blocks including adders and muxes. This section describe the required interface (i.e., the ports for the module and the module's functional behavior) before describing the required implementation (i.e., what goes inside the module) for each combinational building block.

3.1. Full Adder

A full adder adds three one-bit input values to produce a single two-bit output.

Review the lecture notes to derive the truth table for a full adder and implement this truth table in FullAdder_GL.v. Use explicit gate-level modeling. Students are free to use any explicit gate-level modeling approach they prefer including:

  • canonical sum-of-products (non-minimal version using an OR of all minterms)
  • minimal sum-of-products (use Karnaugh map to find minimal version)
  • further Boolean optimizations (use multi-level logic and/or XOR/XNOR gates)

As we learned in Lab 1 that the FPGA tools will thoroughly optimize your logic regardless of which approach you take. Having said this, your implementation must be clean, well commented, and readable.

3.2. Eight-Bit Ripple-Carry Adder

An eight-bit adder performs eight-bit binary addition (i.e., adds two eight-bit input values to determine an eight-bit sum output). An eight-bit ripple-carry adder chains together eight full-adders to enable adding two eight-bit values producing an eight-bit sum.

Review the lecture notes to understand how an eight-bit ripple-carry adder implements binary addition. This specific ripple-carry adder includes a dedicated carry input port (cin) since we are going to want to chain multiple instances of the ripple carry adder together to create even larger adders.

To implement the ripple-carry adder, we need to take an eight-bit input port and use wire slicing to connect each bit of the input port to a different full adder module. We also need to take eight one-bit outputs from the eight full adders and use wire slicing to connect them to each bit of the eight-bit output port. In the above block-level diagram, we indicate wire slicing with a small open triangle and which bit is being sliced in brackets.

Implement an eight-bit ripple-carry adder in AdderRippleCarry_8b_GL.v by instantiating eight FullAdder_GL modules and correctly connecting all of the ports. You will need seven internal wires to implement the carry chain.

3.3. 16-Bit Ripple-Carry Adder

We can implement a 16-bit ripply-carry adder by simply chaining together two eight-bit ripple carry adders.

We again need to use bit slicing to connect eight-bit slices of the input and output ports to the eight-bit ripple carry adders.

Implement a 16-bit ripple-carry adder in AdderRippleCarry_16b_GL.v by instantiating AdderRippleCarry_8b_GL modules and correctly connecting all of the ports. You will need an internal wire to implement the carry chain.

3.4. One-Bit Two-to-One Multiplexor

A one-bit two-to-one multiplexor has two input ports and a select input port which chooses which input port should be assigned to the output port.

Review the lecture notes to derive the truth table for a one-bit two-to-one multiplexor and implement this truth table in Mux2_1b_GL.v. Use explicit gate-level network modeling. Students are free to use any explicit gate-level modeling approach they prefer including:

  • canonical sum-of-products (non-minimal version using an OR of all minterms)
  • minimal sum-of-products (use Karnaugh map to find minimal version)
  • further Boolean optimizations (use multi-level logic and/or XOR/XNOR gates)

As we learned in Lab 1 that the FPGA tools will thoroughly optimize your logic regardless of which approach you take. Having said this, your implementation must be clean, well commented, and readable.

3.5. Eight-Bit Two-to-One Multiplexor

An eight-bit two-to-one multiplexor has two eight-bit input ports and a select input which chooses which input port should be assigned to the eight-bit output port.

We can implement an eight-bit two-to-one multiplexor by using eight one-bit two-to-one multiplexors in parallel. The top-level select input is connected to every child mux's select input. In the diagram above, we are using "fly over" connections where the select signal "flys over" each mux; the arrow heads indicate where the fly over connects to each mux's select input. We use bit slicing to connect each bit of the eight-bit input and output ports to the appropropriate one-bit input and output ports of the child muxes.

Implement an eight-bit two-to-one multiplexor in Mux2_8b_GL.v by instantiating eight Mux2_1b_GL modules and correctly connecting all of the ports.

3.6. 16-Bit Two-to-One Multiplexor

We can implement a 16-bit two-to-one multiplexor by simply instantiating two eight-bit two-to-one multiplexors.

We again need to use bit slicing to connect eight-bit slices of the input and output ports to the eight-bit muxes.

Implement a 16-bit two-to-one multiplexor in Mux2_16b_GL.v by instantiating Mux2_8b_GL modules and correctly connecting all of the ports.

3.7. 16-Bit Carry-Select Adder

A 16-bit carry-select adder has the same interface as a 16-bit ripple-carry adder but a very different implementation. A 16-bit carry-select adder breaks the addition operation into two parts: an eight-bit lower ripple-carry adder is used to calculate the lower eight bits of the sum output. Two eight-bit upper ripple-carry adders are used to redundantly calculate the sum of the upper eight bits; one assumes the carry out from the lower adder is zero and the other assumes the carry out from the lower adder is one. In this way all three eight-bit ripple-carry adders can operate in parallel. Once we know the carry output from the lower adder we can use an eight-bit two-to-one mux to quickly choose the correct sum for the upper eight bits.

Review the lectures notes on carry-select adders and implement an 16-bit carry-select adder in AdderCarrySelect_16b_GL. You will need to use the include Verilog preprocessor macro to include the appropriate child modules. You will likely need some internal wires.

3.8. 16-Bit Register-Transfer-Level Adder

All of work in the lab assignments so far has involved using explicit gate-level modeling. We will gradually start to experiment with using register-transfer-level (RTL) modeling throughout the rest of the lab assignments. RTL modeling involves working at a higher-level of abstraction. This can drastically increase designer productivity but only if the designer always keeps in mind the hardware we are actually modeling! It is possible to use Verilog RTL which does not model any kind of real hardware.

The simplest form of RTL modeling enables using a very limited number of Verilog operators (~, &, |, ^) to implement Boolean equations. RTL modeling can also enable using more sophisticated operators. For example, instead of implementing a 16-bit ripple-carry adder or a 16-bit carry-select adder, we can use RTL modeling through the + operator to implement a 16-bit adder in a single line of Verilog.

For example, here is a 16-bit RTL adder without a carry input or carry output:

module Adder_16b_RTL
(
  input  logic [15:0] in0,
  input  logic [15:0] in1,
  output logic [15:0] sum
);

  assign sum = in0 + in1;

endmodule

Notice how we use logic instead of wire in RTL modeling; the logic datatype is meant for modeling at higher levels of abstraction. Clearly this is much more productive than implementing a ripple-carry or carry-select adder using explicit gate-level modeling. However, we have also given up control over the exact adder implementation. When using RTL modeling, we usually give the FPGA tools more freedom to choose the detailed implementation of some combinational building blocks like adders.

In this lab assignment, we want our adders to have carry input and carry output ports. We can use the following RTL to achieve implement this kind of adder.

module Adder_16b_RTL
(
  input  logic [15:0] in0,
  input  logic [15:0] in1,
  input  logic        cin,
  output logic        cout,
  output logic [15:0] sum
);

  logic [16:0] result;
  assign result = in0 + in1 + { 15'b0, cin };
  assign cout = result[16];
  assign sum = result[15:0];

endmodule

We must zero extend cin since Verilator will not allow us to add signals of different bitwidths. We assign the output to a 17-bit wire so we can retrieve the carry output.

Implement a 16-bit RTL adder in Adder_16b_RTL.

4. Testing Strategy

You are responsible for developing an effective testing strategy to ensure all implementations are correct. Writing tests is one of the most important and challenging aspects of designing hardware. Hardware engineers often spend far more time implementing tests than they do implementing the actual hardware design.

4.1. Basic Testing

We will be using the same lightweight testing framework from Lab 1. For each hardware module, we provide a test bench for you to use along with one basic test case. You can run the basic tests for all hardware modules using the generated Makefile.

% cd ${HOME}/ece2300/groupXX/build
% make check

You can also build and run a single test simulator.

% cd ${HOME}/ece2300/groupXX/build
% make FullAdder_GL-test
% ./FullAdder_GL-test

You can specify which specific test case to run on the command line and also dump waveforms that can be viewed using Surfer.

% cd ${HOME}/ece2300/groupXX/build
% make FullAdder_GL-test
% ./FullAdder_GL-test +test-case=1
% ./FullAdder_GL-test +test-case=1 +dump-vcd=waves.vcd

4.2. Exhaustive Testing

The following test benches are for hardware modules with a limited number of inputs, and thus you can (and should) use exhaustive testing:

  • FullAdder_GL-test.v
  • Mux2_1b_GL-test.v

4.3. Directed Testing

The remaining hardware modules all have many more inputs and thus would required hundreds or even thousands of checks to implement exhaustive testing. So for the remaining hardware modules you must use directed testing to check for specific corner cases. You should implement a few directed test cases for each hardware module where exhaustive testing is not applicable. Each directed test case should focus on testing a very specific kind of functionality and they should contain 2-10 checks. Be sure to add your tests cases to the list in the initial block and to check the output of the test simulator to confirm that your directed test cases are running and testing what you expect them to. Consider purposefully inserting a bug in your designs to see if your directed test cases will catch the bug.

4.4. Random Testing

Directed testing is useful for testing the known unknowns, but what about the unknown unknowns? How should we test for corner cases we have not even thought of yet? Random testing can help increase our testing coverage and increase our confident that our hardware design is functionally correct. You should implement one random test case for each hardware module where exhaustive testing is not applicable. Random test cases should include a for loop with approximately 50 iterations. Each iteration should: (1) generate random input values; (2) use Verilog test code to programmatically determine the correct output values; and (3) use the check task to ensure the design-under-test produces the correct outputs give the corresponding random inputs.

4.5. X-Propagation Testing

You must also add one X-propagation test case for each module. If nothing else, check that if all of the inputs are X then all of the outputs are X.

5. Lab Code Submission

To submit your code you simply push your code to GitHub. You can push your code as many times as you like before the deadline. Students are responsible for going to the GitHub website for your repository, browsing the source code, and confirming the code on GitHub is the code they want to submit is on GitHub Be sure to verify your code is passing your tests both on ecelinux and on GitHub Actions. Your design code will be assessed both in terms of code quality, verification quality, and functionality.

5.1. Code Quality

Your code quality score will be based on how well you follow the course coding conventions posted here:

Unlike in Lab 1, code quality for Part A will be assessed after the Part A deadline.

5.2. Verification Quality

Verification quality is based on how well your testing enables making a compelling case for correctness. You will need to write compelling directed test cases, use reasonable randomg testing, and include a simple X-propgation test case. Use comments appropriately to describe your test cases. Code quality for Part A will be assessed after the Part A deadline.

5.3. Functionality

Your functionality score will be determined by running your code against a series of tests developed by the instructors to test its correctness. Note that we will be using the automated build system to test your final code submission as shown below.

% mkdir -p ${HOME}/ece2300
% cd ${HOME}/ece2300
% git clone git@github.com:cornell-ece2300/groupXX
% cd groupXX

% mkdir -p build
% cd build
% ../configure
% make check-lab2-partA