(Last Mod: 17 March 2012 08:26:37 )
This is intended to be a quick start guide to writing Verilog code with the following primary objective in mind: Cover topics that are typically left out of other guides -- or even entire books -- but that are extremely valuable for writing test benches and performing simulations. There are several things that are left out of this guide; in general, when there are two ways of doing the same thing, only one is presented here and the other is ignored entirely.
The material here is presented in an evolutionary manner, meaning that we start with a very simple Verilog module, explain its contents, and then add additional features along the way. The intent is to increase your knowledge of the language incrementally and place each new piece of information in a context that will make it easier to comprehend and remember.
Most introductions to Verilog start with developing simple modules and then show how to connect them together. At some point, hopefully, the question of actually performing simulations and developing test benches is covered, though many books exist that, throughout the entire book, barely mention this critical aspect of design development. A "test bench" is nothing more than a Verilog program that provides input signals to a DUT (design under test) and permits the resulting output of the DUT to be evaluated for correctness. One reason that this is so scantily covered is probably that every simulation tool out there is different and has different features and different ways of accomplishing the same thing. Since most developers tend to use their tool's simulation and verification capabilities, the discussion would become very tool-specific very quickly. However, what these books seem to ignore or forget is that Verilog contains a set of system tasks that can used to create a test bench that will perform the same on any simulator that correctly implements the Verilog language. This is not to say that the intrinsic Verilog constructs are elegant, because they aren't; some of the techniques presented here are rather clumsy. But they work and you don't have to know anything about a particular simulator beyond how to load a design consisting of Verilog source code files, how to tell it what the top level module is, and how to launch the simulation.
This goal motivates us to introduce Verilog from the opposite direction. We are going to first introduce you to the aspects of the language that will allow you to build test benches and then, as we build our design, we will incorporate those elements from the beginning.
In the tradition of so many language introductions before it, let's begin with the ubiquitous Hello, World! program. Save the following text in a file named "hello.v".
module hello;
initial
$display("Hello, World!");
endmodule
The next step is to simulate this design (a.k.a., run the program) and the mechanics of doing that are very dependent on the simulator being used. You will need to refer to your simulator's documentation and/or tutorials for information on how to do that. In many simulators, you have to create a "project" or a "workspace" (or both) and add this file to it. You then usually need to indicate which module is the "top" module for the simulation. Finally, you need to start the simulation.
Assuming you were able to run the simulation, you may or may not have seen the phrase "Hello, World!" appear anywhere. If you are running from a command prompt, then you almost certainly saw it amongst the other messages produced by the simulator. However, if you are running in a GUI environment, then you may or may not be able to find it in one of the windows. Look for a "Console" or an "Output" window, but don't be disheartened if you can't locate it; because using $display() to print directly to the screen is iffy, we will not use it beyond this first example.
Before we do that, however, let's examine the design that we have. This is as good a point to make the major distinction (and one that we will harp on at every opportunity) between an HDL and a traditional program written in a language such as C or Java. An HDL is, as the name says, a "hardware description language". We are not writing a program but rather describing a hardware design. The distinction is both subtle and profound. In a typical computer program, lines are executed sequentially (one after another), unless some control structure (like an "if" statement) transfers control to a different line. But an HDL design essentially describes a circuit design in which the entire circuit is "running" all at the same time. To further stress this distinction, in a sequential program, such as one written in C, you think in terms of functions calling other functions. This is a reasonable, useful, and accurate way to thinking about how functions relate and interact with each other. But people tend to carry this same mindset over to HDL code and talk about one module "calling" an other. This is not reasonable, useful, or accurate; in fact, left uncorrected, it is pretty much guaranteed to get you into trouble sooner or later. Assuming that you have some familiarity with schematics at some level, imagine a schematic for a simple flashlight consisting of a battery, a switch, and a bulb that are connected by wires. Would you ever describe the battery as "calling" the switch? Of course not; it is a meaningless way of describing the relationship or interaction between a battery and a switch. Instead, you would talk about these parts being "connected" by some wires and you might describe the function of the switch as being to either allow or prevent a signal (electrical current) from flowing from the battery to the bulb. Taking a more complex example, consider a simplified block diagram of a computer, which might show the the power supply, the CPU, memory, the hard disk, the keyboard, the video monitor. You wouldn't talk about the power supply calling (or being called by) some other component. It would be understood that all of these components exist and do things simultaneously and continuously, but that the nature of what they do at any given moment is highly dependent on what one (or many) of the other components are doing. You would again talk about how these components are connected; but the focus of most of your conversations would be about how and what they communicate to each other and how and what the components do in response to the signals it received from other components. The same is true in an HDL, where a module describes a component (which, like a hard drive, might be described internally as a set of smaller components that are connected together) and components are connected together to form larger components and, eventually, a complete system (at whatever level of interest is appropriate, be it a video card or a complete computer network).
The consequence of this is that statements in an HDL have to be treated as being evaluated concurrently and constantly and, for the most part, the order in which they are written is unimportant. Having said this, we must now point out that it is not strictly true; some statements are executed sequentially. But, in general and as we shall see, this is so that the correct fully-concurrent logic can be easily described. Learning which statements ones are and which ones aren't execute in the more-or-less familiar sequential fashion is actually one of the more challenging aspects of using an HDL. We will address these issues as they come up.
The building blocks in Verilog are called "modules". Each module begins with
the keyword module
and ends with the keyword
endmodule
. When a module is defined, it is given a name (in this
case, "hello") and usually has one or more ports through which signals are
passed. Our hello module doesn't happen to have any ports, so we will wait to
describe them, as well as other things that can also appear in the
module
statement. Note, however, that the module
statement always must end with a semicolon. It does not, however, have to be all
on one line. For the most part, Verilog, like C, ignores line breaks.
At the other end of the module, the endmodule keyword
marks the end of a module's definition and it must not be ended with a
semicolon. This is one of the little quirks of Verilog that can make it
difficult to keep all of the syntax rules straight.
By convention, each module is kept in a separate file with the name of the file matching the name of the module. This is not required, but merely common practice.
The sole contents of our module is a single initial
block, which in turn contains a single $display
statement.
An initial
block is a control structure that executes a
statements exactly once, beginning at time 0 (the start of the simulation). Like
C, control structures in Verilog generally control the next statement. If you
want to control multiple statements, then they must be combined into a block. We
will see how to do this shortly. Similarly, indenting means nothing to the
simulator, but making the indenting match the logical structure of the code
greatly improves the readability for humans. The $display
statement outputs text to the standard output device (usually the screen
display). This particular instance simply outputs a "string literal" to the
screen, but we will soon see how to output other information, as well. Commands
that start with a dollar sign are known as "system tasks" and will be central to
letting us develop testbenches.
We mentioned previously that some simulators may make seeing the output from
a $display
statement difficult or impossible to see. A
simple way around this is to write the output to a file, instead. Adding this
feature gives us:
module hello;
integer FileID;
initial begin
FileID = $fopen("hello.txt", "w");
if (FileID == 0)
$display("Failed to open file for writing!");
else
$fdisplay(FileID, "Hello, World!");
$fclose(FileID);
end
endmodule
We have added a few new things to our code, namely a data object, a couple of new system tasks, an if-else control structure that includes a relational operator, and a begin-end block. The first thing in our module that now happens is that we create (instantiate) a data object of type "integer" named "FileID". This data type is generally a 32-bit signed integer (two's complement) data type, but is technically machine dependent. Verilog, like C, is a "weakly-typed" language, meaning that the language will automatically perform many of the conversions between data types for you. However, it is best not to rely on them because you may not always get what you expect and the results can be disastrous. Like C, Verilog is case sensitive, so the identifier FileID is completely different from fileID. Many people (in many languages), choose to never use capital letters in identifiers and, instead, to use underscore characters to separate words. Those people might have chosen "file_id" as their variable name. Others prefer to separate words by capitalizing the first character of each word (known as "camel-case" because of the humpy appearance that results. There are numerous other conventions, as well. Choosing one (or devising one of your own) is particularly useful in Verilog, as we will see once we start using wires.
Our initial
statement now controls a set of statements
that are grouped together into a single block defined by the begin
and end
keywords. Statements in a begin
/end
block are executed sequentially (one after another), but there is a really big
caveat involved that we are not ready to delve into yet.
The FileID
is used to hold the handle returned by the
$fopen()
system task. This task opens the file specified by
the first argument using the mode indicated by the second. The modes are "w"
(write), "r" (read), and "a" (append). If the operation fails, it will return a
value of 0. Otherwise it will return an integer that can be passed to the file
I/O system tasks, such as $fdisplay()
used above, to
identify which file is to be accessed. As is the case in C, it is always prudent
to verify that the file opened successfully before attempting to actually use
it. The results of failing to do this may not be pleasant. This check is done
using a relational expression such as the one shown above. The relational
operators are, for the most part, the same ones used in C; namely
{==,!=,>,<=,<,>=}, for "equal", "not equal", "greater-than",
"less-than-or-equal-to", "less-than", and "greater-than-or-equal-to",
respectively. There are a couple of additional relational operators that we will
discuss when we get to four-valued logic a bit later.
The if-else
control structure should be familiar to
anyone that has any programming experience. If the expression following the
if
keyword evaluates as "true", then the next statement is
executed and the statement following the (optional) else
clause is skipped. Conversely, if the expression evaluates as "false", the next
statement is skipped and the statement following the (optional) else
clause is executed instead. As before, multiple statements can be combined into
a block so that the control structure can exercise control over them as a group.
Finally, we use two new system tasks, namely $fdisplay()
and $fclose()
. The $fdisplay()
system
task works just like the $display() task, except that it takes a file handle as
its first argument and writes the its output to that file instead of the
standard output device. The file must be opened either for writing or for
appending. The $fclose()
system task simply closes the file
associated with the handle passed to it.
Our next evolution is going to represent a significant jump, namely creating a module that does something useful and is synthesizable and then instantiating it in a testbench intended to establish that the module behaves correctly. We will take this in several steps: first, we'll create the basic module to be tested and then we'll create the module to do the testing. After that, we'll make a number of enhancements to both.
For the first of these, we will model a simple two input NAND gate calling it NAND_2.
module NAND_2(
output Y, // Output
input A, B // Inputs
(symmetric)
);
assign Y = ~(A&B);
endmodule
We now have signals that we need to pass into and out of the module. This is
accomplished by including a parenthesized port list in the module
statement after the module name. The style shown above is known as the ANSI
style of port declaration and was incorporated with the 2001 Verilog standard.
Most mainstream simulators should recognize them by now. If not, consult a
Verilog text for the older style of port declaration. The port directions are
input, output, and inout (for bidirectional ports). In addition to the
direction, which must always be specified, there is also an object type. If left
unspecified, it defaults to a 1-bit wire, as the ports in our example do. We
will see other object types soon.
We have also introduced end-of-line comments, which are indicated by a double forward slash. Everything from the first slash to the end of the current line is a comment and is ignored by the simulator. Verilog also supports block comments. These will be introduced shortly.
The body of our module is the single assign
statement.
This is reevaluated whenever any of the objects on the right hand side change.
As we shall see a bit later, it is a shorthand notation for a special case of
the always
statement. The two operators in the expression
are the complement (~
) and logical-AND (&
)
operators, both of which are bitwise operators. The remaining bitwise operators
are the logical-OR (|
) and the logical-XOR (^
).
Next we will implement our testbench, beginning with a first attempt that will not work well at all.
// An example of how not to write a test bench
module tb_NAND_2;
wire Z;
reg C, D;
NAND_2 DUT (.Y(Z), .A(C), .B(D));
integer FileID;
initial begin
FileID = $fopen("tb_nand_2.txt", "w");
if (FileID == 0)
$display("Failed to open file for writing!");
else begin
// Test #1
C = 0;
D = 0;
if (Z != 1)
$fdisplay(FileID, "Failed case CD=00");
// Test #2
C = 0;
D = 1;
if (Z != 1)
$fdisplay(FileID, "Failed case CD=01");
// Test #3
C = 1;
D = 0;
if (Z != 1)
$fdisplay(FileID, "Failed case CD=10");
// Test #4
C = 1;
D = 1;
if (Z != 0)
$fdisplay(FileID, "Failed case CD=11");
// Final Housekeeping
$fclose(FileID);
end
$finish;
end
endmodule
There are a couple of common conventions when it comes to naming testbench modules. Probably the two most common are to use the name of the module being tested prefixed with "tb_" or the same name suffixed by "_tb". The first has the advantage that an alphabetical directory listing will but all of the testbenches together while the advantage of the second is that modules and their testbenches will be listed together. Take your pick.
Before we see what this actually does, let's walk through and see what we want it to do. In order to connect to our design under test "DUT", we have to declare the identifiers that we are going to connect to it with. Verilog offers a few options here, At the structural level, were we are primarily connecting different modules together, we use "net" objects, or more commonly called "wires". These are sufficient for carrying a signal from a driving source (such as the output of a module) to other input nodes. But we cannot use a net object to store values. For that, we need "reg" objects. Reg objects are not registers, per se, but are much more closely akin to variables in a traditional programming language. The easiest, though not precise, way to think of them is that a reg allows you to store a value from a behavioral assignment (as opposed to a structural connection). The output connect to our DUT has to be a wire (because the module is determining the value on it, not some behavioral assignment) but we need the two signals connected to the inputs to be regs because we do want to assign their values behaviorally.
Next we have the instantiation of the module itself. A module instantiation statement starts with the name of the module (remember, case sensitive!) followed by a label for the module. The label is required and must be unique within each module. Finally, we have the port list; there are a couple of ways of assigning signals to ports. The method we have chosen to use is "by name". To assign a port by name, you prefix the port name with a period and then following it with a pair of parentheses containing the name of the signal that is to connect to that port.
Once we have made the connections to the module, we have an initial
block that opens a file for writing and, after verifying that it opened
successfully, assigns values a pair of values to the two inputs and then prints
an error message to the file if the output isn't what was expected. It
progresses through all four possible combinations of the inputs and then closes
the file.
Finally, we explicitly end the simulation by invoking the $finish
system task.
As a teaser to get you to start thinking about the inherent concurrency of Verilog, the order of the module instantiation and the initial block doesn't matter and we could have put them in the opposite order. They are independent "things" that exist within completely in parallel.
If we now run this testbench everything looks fine. It compiles, it runs, the
file is written, and the file is empty, meaning that it didn't detect any
errors. Life would appear to be good. But just for grins, go back and change all
of the conditional expressions so that they should (erroneously) print out error
statements by changing the inequality operator (!=
) to the
equality operator (==
). If the test failed before, it has to
pass now, right? But if you run the file, you will probably see that it still
doesn't writing anything to the file. What is going on? To get more information,
place a copy of the following statement after each if()
statement (not as part of it, we want it to run each time regardless of the
outcome of the test).
$fdisplay(FileID, "C=%H, D=%H, Z=%H", C, D, Z);
The string in this statement now contains "format specifiers" which start
with a percent sign and tell the system how to print out the values of the
arguments that follow it. Since we have three arguments we need three specifiers
(make a strong effort to ensure that there is a one-to-one correspondence
between the two). Our three specifiers are all the same, namely "%H
",
which tells the task to print the value in hexadecimal.
When this is run, the following is output to the file:
C=0, D=0, Z=x
C=0, D=1, Z=x
C=1, D=0, Z=x
C=1, D=1, Z=x
Do not worry if your output is different. Different simulators can produce different outputs when given poor input such as we have done here. But why is our input so poor?
The first thing you are probably wondering is why Z has a value of "x" and what does that mean. Instead of just two values for a binary signal, Verilog actually has four. In addition to "0" and "1", we have "z", which indicated a node that is not being driven by anything and is in a high-impedance state. We also have an "x" state which is simply "unknown", meaning that the simulator can't determine which one of the other three states it is in. The is actually compounded further by also associating a drive string with each state, but for our immediate purposes it is enough to know that, for some reason, the simulator can't determine what the state of Z is. Your first thought is probably that we have done something wrong with our NAND_2 instantiation, since this module takes C and D (which appear to be okay) and produces Z, which is not happening. But rest assured that everything on that end is fine.
First let's address the reason that it managed to fail the test regardless of
whether you used the equality or inequality operator. In both cases, the
question came down to "is an unknown value equal to a given known value) and the
answer is... unknown. Maybe it is, and maybe it isn't. Verilog deals with this
by having tests involving unknown values yield a false result regardless of the
relational operator used. But Verilog also gives us another pair of
equality/inequality operators that ask for literal (exact) equality. These are
the literal equality (===
) and the literal inequality (!==
)
operators. So go back to the testbench code and use a literal inequality
operator in place of the original simple inequality operator and you should find
that now it fails each of the four tests.
Now that we have gotten our file to correctly fail all the tests, let's see
if we can figure out why it is failing at all. The key to understanding this is
to understand the mechanics of what is going on underneath the hood of the
simulator (or at least some feel for it). When a Verilog design is simulated,
the simulator works with time steps and determines what the values of all of the
nodes in the design are at each time step before continuing on. In the
initial
block of our testbench, we have not given it any indication
of a sense of time, and hence the entire block is assumed to occur at the first
time step (Time Step 0). Similarly, the NAND_2 block is evaluated at each time
step and, in this case, the value of Y (and hence Z) that it drives is
determined by the state of the inputs at Time Step 0. But we assign four
different sets of values to the variables that feed those inputs during the same
time step, so the simulator is hopelessly confused as to what the value of the
inputs at time zero are.
A basic rule of thumb that will keep you from making most of the common mistakes associated with timing is to remember that Verilog (and other HDLs) are describing hardware and that all of the pieces of the hardware, in general, are always running at the same time in parallel, which is very unlike a normal computer program. Thus the simulator must reflect this. As a result, HDL simulators have to largely ignore the order in which statements appear and evaluate them in a manner consistent with them all operating simultaneously. A common method of doing this is to break up simulation time into "time steps" and then evaluate all of the blocks in the entire program in more-or-less random order and to continue doing so until the design settles into a stable state before moving on to the next time step and repeating the process. This description is definitely too simplistic, but the general idea is there.
, which can span multiple lines (or be placed in the middle of a line leaving The port list is simply a parenthesized list of port names in parentheses. defining a module that
/***************************************
* NAND_2.v
***************************************
* Two-input positive-logic NAND gate
* William L. Bahn
* Dynasys Technical Services
***************************************
*/
module NAND_2(Y, A, B);
output Y;
input A, B;
assign Y = #1 ~(A&B);
endmodule
defining a module that