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.
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.
Your repo contains the following files which are part of the automated build system:
Makefile.in
: Makefile for the build systemconfigure
: Configure script for the build systemconfigure.ac
: Used to generate the configure scriptscripts
: 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 adderAdderRippleCarry_8b_GL.v
: 8-bit ripple-carry adderAdderRippleCarry_16b_GL.v
: 16-bit ripple-carry adderMux2_1b_GL.v
: 1-bit 2-to-1 multiplexorMux2_8b_GL.v
: 8-bit 2-to-1 multiplexorMux2_16b_GL.v
: 16-bit 2-to-1 multiplexorAdderCarrySelect_16b_GL.v
: 16-bit carry-select adderAdder_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 adderAdderRippleCarry_8b_GL-test.v
: Tests for 8-bit ripple-carry adderAdder-test-cases.v
: Shared test cases for 16-bit addersAdderRippleCarry_16b_GL-test.v
: Tests for 16-bit ripple-carry adderMux2_1b_GL-test.v
: Tests for 1-bit 2-to-1 multiplexorMux2_8b_GL-test.v
: Tests for 8-bit 2-to-1 multiplexorMux2_16b_GL-test.v
: Tests for 16-bit 2-to-1 multiplexorAdderCarrySelect_16b_GL-test.v
: Tests for 16-bit carry-select adderAdder_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.
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:
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.
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
.
You can also build and run a single test simulator.
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.