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 anecelinux
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 modulePairTripleDetector_GL-adhoc.v
: adhoc test for hardware modulePairTripleDetector_GL-test.v
: test cases for hardware moduleece2300-misc.v
: ECE 2300 miscellaneous macrosece2300-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
.
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
, andendif
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 toimport
in Python. -
The module has three input ports (named
in0
,in1
, andin2
) and one output port (namedout
). -
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 theECE2300_UNDRIVEN
macro to indicate that it is ok that the output port is undriven. You will need to remove theECE2300_UNUSED
andECE2300_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.
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.
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.
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.
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.
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.
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.
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.
Finally, our test bench uses an initial block to decide which test cases to run.
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.
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).
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.
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.
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.
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.