Verilog Quick Start

(Last Mod: 17 March 2012 08:26:37 )


Overview

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

 

 

Topic Index

  1. Digital Fundamentals
    1. Binary representations
    2. Combinational logic
      1. DeMorgan's Theorem
      2. Bubble Logic
    3. Sequential logic
  2. Bus Interfaces
    1. Bus signals
    2. Types of buses
    3. Bus contention
  3. I/O
    1. Disks
    2. Keyboard/Mice
    3. Displays
    4. Network Interfaces
  4. Processor Basics
    1. Von Neumann architecture
    2. Harvard architecture
    3. Performance metrics
      1. Execution time
      2. Cycles/instruction
      3. Instructions/second
      4. Benchmarking
    4. Register models
      1. Stack
      2. Register
      3. Memory
    5. Data Path
    6. Control Unit
    7. Building blocks
      1. Counters
      2. Multiplexers
      3. Arithmetic Logic Unit (ALU)
        1. Adders
        2. Bitwise operators
      4. Registers
      5. Memory
  5. Instruction Set Architecture
    1. Instruction types
    2. Addressing modes
    3. Op codes
  6. Single-cycle processor
  7. Multi-cycle processor
  8. Pipelined architecture
  9. Memory caches
    1. Direct mapped
    2. Set associative
    3. Fully associative
  10. Advanced Architectures
    1. Vector
    2. Superscalar
    3. Array