Verilog Tutorial
Verilog Tutorial
Scott Hauck, Justin Hsia, Max Arnold, Matthew Cinnamon (last updated Oct. 2020)
Introduction
The following tutorial is intended to get you going quickly in circuit design in SystemVerilog. It is not a
comprehensive guide but should contain everything you need to design circuits in this class. For a more
thorough reference, Prof. Hauck recommends Vahid and Lysecky’s Verilog for Digital Design.
Table of Contents
Modules ..................................................................................................................................................................... 2
Basic Gates................................................................................................................................................................. 3
Hierarchy ................................................................................................................................................................... 3
Boolean Equations and “Assign” ............................................................................................................................... 5
Multi-bit Signals ......................................................................................................................................................... 5
Delays ........................................................................................................................................................................ 7
Defining Named Constants ........................................................................................................................................ 8
Parameterized Design ............................................................................................................................................... 8
Enumerations ............................................................................................................................................................ 8
Register Transfer Level (RTL) Code – Behavioral Verilog .......................................................................................... 9
Test Benches ............................................................................................................................................................ 12
Printing Values to the Console ................................................................................................................................ 14
Advanced Features – Multi-Dimensional Buses ...................................................................................................... 15
Advanced Features – Assert Statements ................................................................................................................. 16
Advanced Features – Generate Statements............................................................................................................ 16
1
Modules
The basic building block of Verilog is a module. This is similar to a function or procedure in C/C++/Java in that it
takes input values, performs a computation, and generates outputs. However, modules compile into collections
of logic gates and each time you “call” a module you are creating separate instances of hardware.
Line-by-Line Analysis
Lines 1-2 are single-line comments, designated by the ‘ ’ (green syntax highlighting in Quartus) and ignored
during compilation. Comments can be placed at the end of lines of code or on separate lines by themselves.
Lines 3-5 define the module name and port list, which is the list of inputs and outputs signals. Line 3 gives the
port names while lines 4-5 define the port types and directions. Here, all 4 port signals are of type ,
and are outputs, and and are inputs. The name ( ) is user-defined but must start with
a letter and can only consist of letters, numbers, and underscores. Avoid using keywords ( syntax
highlighting in Quartus) as names.
Line 7-8 each instantiate a gate following the standard module instantiation syntax of:
Line 7 creates an AND gate called and Line 8 creates an OR gate called . The port lists
are explained below in the section
Basic Gates.
The keyword on line 9 closes the module definition started with the keyword on line 3.
2
names are not repeated. The following example is equivalent to the previous module:
Basic Gates
Verilog comes with a number of predefined modules for basic gates that follow the standard module
instantiation syntax of:
The port lists for these gates are defined such that the first connection is always the output. The following
examples show a one-input gate and a multi-input gate:
The other 1-input gate is and the other multi-input gates are , , , , and .
If you want to have more than two inputs to a multi-input gate, simply add more connections to the port list. For
example, the following is a five-input AND gate:
Hierarchy
Just like we build up a complex software program by having procedures call subprocedures, Verilog builds up
complex circuits from modules that instantiate submodules. For example, we can take our previous
module and use it to build a module:
3
Notice that we now instantiate the module just like the standard Verilog gates. We also happen to have
multiple NOT gates in this module. You can instantiate the same type of module (basic or user-defined) more
than once as long as the instance names are different (here, and for the gates).
Local Signals
Line 7 creates local signals and , which are essentially local variables in the module. In
this case, these are wires that carry the signals from the output of the gate to the inverters.
Port Connections
Like C/C++/Java arguments and parameters, Verilog will, by default, connect the ports in order of the port list of
the module definition when you instantiate a module. However, we can also explicitly name the ports in Verilog.
That is, when we use in the port list, we are connecting the wire in the
module to the input port of the module instance. This explicit connection tends to avoid
mistakes, especially when someone adds or deletes ports in a module definition.
Note that every signal name in a module must be distinct. However, the same name can be used in different
modules independently. You can connect a module signal to a submodule port of the same name using an
implicit connection. For example, if we had renamed the and input ports as and , then:
While the and ports are still explicitly connected, the and ports will be implicitly connected
to the and signals of the module (which just happen to also be input ports).
4
There is another style of implicit connection which will implicitly connect as many ports as it can ( ). This saves
a lot of space when connecting many signals. The following is again equivalent to the two examples above.
Note that the value of will automatically update (though it may not change) after any of its inputs ( , , , )
change. should not be assigned anywhere else in its module or there will be a conflict.
Multi-bit Signals
So far, we have created/declared single-bit signals (i.e., they can only have the value or ) using
statements. However, Verilog also supports multi-bit signals. The following multi-bit declaration example
creates a 3-bit variable named :
This syntax, called a packed array, creates a set of individual signals that can also be treated as a group/bus. The
numbers inside the brackets ( ) define the signal indices with being the most-significant bit ( in this
case) and being the least significant bit ( ). By convention, for an 𝑛-bit signal we use 𝑛-1 as and as .
The individual signals can be used just like any other single-bit signal in Verilog. For example:
This computes AND the right-most bit of and ties the result to the left-most bit of . Multi-bit signals can also
be passed as a group to a module through multi-bit ports:
Note that the bus widths of multi-bit ports and connected signals must match.
5
Multi-bit Signal Declaration Issue
Be aware that all signals declared in the same statement take on the same properties. A common error with
signal declarations looks something like:
What this line does is declare all three signals as 32-bit variables, whereas and are generally one-bit
signals. You may be tipped off to this error if you see a compiler warning about signal or port size mismatches.
What you actually want is:
Array Slices
Sometimes you want to work with a subsection of a multi-bit value, called a slice. We can create a slice by
specifying the start and end indices of the slice separated by a colon. For example, the following code sets
and to and to , leaving all other bits unchanged:
A common use case for slices is breaking apart large packed arrays to pass to submodules. For example, a single
byte can be represented as two hex digits. If we have a module named which converts a 4-bit
value to the segments of a 7-segment display, the following code displays the hex code for a byte:
Multi-bit Literals
You can assign a value to a multi-bit signal using a multi-bit literal. The general format contains 5 parts in the
following order, most of which can be omitted and will assume a default value:
1) the width of the literal in bits (default: 32)
2) a single quote ( )
3) an ‘ ’ character if the number is signed (default: unsigned)
4) the radix indicator (‘ ’ for binary, ‘ ’ for octal, ‘ ’ for decimal, or ‘ ’ for hex; default: decimal)
5) the value expressed in the specified radix
For example, the following statements produce equivalent behavior for a 16-bit signal :
On line 1, the value will be automatically truncated to 16 bits by the compiler (you will get a warning for this).
6
On line 3, the literal is specified as a signed constant, but has the same binary representation as its unsigned
equivalent.
Unspecified bits default to (i.e., can be treated as leading zeros). So, the following is equivalent to Line 3:
Concatenations
You can concatenate multiple signals together into a new grouping by separating them with commas inside curly
braces ( ). For example, if we have two 8-bit signals called and , then creates a 16-bit signal.
Note that the ordering of bits of the new grouping is determined by the ordering of the signals within the
braces.
All types of signals can be used in a concatenation – constants, subsets, buses, single wires, expressions, etc. For
example, the following is a concatenation of a constant with a multi-bit signal:
Bit Replication
Sometimes you would like to replicate a bit or set of bits multiple times. The syntax uses two sets of nested curly
braces with the value to be replicated on the inside and a constant for the number of replications between the
opening braces. The following example replicates the 4-bit signal three times:
Like all other signal expressions, the replication operator can be used inside of a concatenation and vice versa.
Delays
Normally, Verilog statements are assumed to execute instantaneously. However, Verilog does support some
notion of execution delay for basic gates via the # operator (simulation only!!!). For example:
This says that the AND gate takes 5 arbitrary time units to compute, while the OR gate takes 10 units. Note that
the units of time can be whatever you want as long as you are consistent relative to other delays. This can be
handy when testing and simulating designs to mimic combination delays through actual gates in the real world.
7
Defining Named Constants
Sometimes you want to have named constants – values you associate with a meaningful name that you can
reuse throughout a piece of code. For example, you could set the same delay for all units in a module:
This sets the delay of both gates to the value of , which in this case is 5 time units. If we wanted to speed
up both gates, we would only have to reduce the constant in the parameter line.
You may also see parameters used to define names for states in a state machine. However, this particular use
case can be accomplished via an , which is detailed later.
Parameterized Design
Parameters can also be inputs to designs, allowing the caller of the module to set the size of features of that
specific instance of the module. So, if we have a module such as:
This defines a parameter with a default value of 5. Any instantiation of the adder module that does not
specify a width will have all of the internal variable widths set to 5. However, we can also instantiate other
widths as well:
Note that this allows us to instantiate modules of varying size from a single Verilog definition, similar to
templates in C++. You cannot instantiate “on the fly” so parameter values must be known at compile time.
Enumerations
For FSMs and the like, we want to have variables that can take on one of multiple named values – while we
could just use numbers, names are generally clearer. Sometimes we may use statements to set up
names for variables, but for FSM state variables enumerations work better. For example, the following code
defines the allowable states for a hypothetical FSM:
8
This defines two variables, and , and restricts their values to be either , , or . We can test
the value of variables and assign new values using those names:
The compiler will automatically assign values to each of these variables or you can also specify one or more
specific values:
Make sure to have one of the values be equal to , but the other values can be whatever you want. One last tip:
if you want to print the value of an enum variable , you can call to return the string for that value
(e.g., if the current value of is , will return “ ”).
In behavioral code, it is easy to forget how the hardware actually works and pretend that it is just C or
Java (i.e., software). This is a great way to design AWFUL hardware. Because of this, we will give you
stylized ways of using the constructs which will guide you towards better designs – think about the
hardware that your system actually requires!
RTL code is divided into two types, combinational logic and sequential logic. In each of these cases, RTL code is
written in block variants, which allow for the use of powerful constructs like - - and
statements. This helps greatly when creating large and complex designs.
Begin-end
and statements denote a block of code, much like the “ ” braces in C and Java, and group multiple
statements together in determining computation “flow.” These can be used with procedural blocks (e.g.,
, ) and conditional statements (e.g., - - , ). Like C, you do not have to use
them for single statements, but they generally help to create more readable and maintainable code.
Combinational Logic
The output of combinational logic is determined purely based on the current value of the inputs. Simple
computations can be written as statements, but complex designs generally go in blocks,
which will execute/trigger whenever any of the input signals change.
9
Sequential Logic
Sequential logic only updates its output when a specific event, typically a clock trigger, occurs. This allows the
design to hold/store state information and behave differently based on previous inputs. The event (i.e.,
expression that evaluates to true) is specified after the keyword and usually triggers from low to
high, , or high to low, . For example, the following block triggers on a rising edge of :
If-else if-else
The - - constructs look similar to software, but with the important distinction that each signal
must be defined in all cases. This is because the statement is equivalent to defining a logic function. is true
only when = , or when = and = . This is equivalent to the function = + . Similarly, = .
10
Failing to define a signal in all cases will result in one of two outcomes:
1) If the conditional block is inside an block, it will fail with the message:
always_comb construct does not infer purely combinational logic.
2) If not in an block, the code may compile with the warning:
inferring latch(es) for variable "var", which holds its previous value in one or more paths through the
always construct.
Although it may compile successfully, the design will likely exhibit unexpected behavior that is not intended to
be used in this class and therefore should be avoided.
Case
As we move to multi-bit signals that can take on values more than just and , the statement becomes
quite useful. The variable to be considered is placed in the parenthesis of the statement, and then
different values are listed with the associated action. For example in the code below, is set to when the
variable is or and is set to if is , , or . There must also always be a case to catch
values that don’t match any other case. Any variable set in any part of the statement should be set in all
cases. That is, dropping the assignment from any of the cases would be incorrect. Here, we also use the
value to indicate that we do not care if the value is or , allowing the compiler to optimize the design.
Repeat
A loop is a simple statement that repeats a block of code a specified number of times. If you don’t need
to use the value of the loop variable, this is a much simpler syntax than writing out a -loop. For example, the
following code adds to ten times with an 8 arbitrary time unit delay between each addition.
For-Loops
A -loop in SystemVerilog is a much more limited structure compared to other programming languages. It
should only be used as a way of simplifying repetitive statements, not as a way of performing any form of logic.
A good rule to follow is:
11
If you cannot manually expand a -loop in your code, do not use it!
The main scenario where -loops are useful in SystemVerilog is working with multi-bit signals. Sometimes
array slices are insufficient, for example if you want to assign bits in reverse order:
-loops can also be useful in test benches, as they often involve repetitive inputs which can be simplified.
Test Benches
Once a circuit is designed, you need some way to test it. For example, we would like to see how the
circuit we designed earlier behaves. To do this, we create a test bench. A test bench is a module that calls your
original module, often called the Device Under Test or DUT, with the desired input patterns and collects the
results. For example, consider the following setup:
12
The new section of code in this example is the module . It instantiates one copy of the
module, called , and connects input signals that we control and output signals that we can read.
In order to provide test data to the device under test, we create a stimulus block (lines 25-30). The code inside
the statement is executed once at the beginning of the simulation. It sets and to immediately
and then waits 10 arbitrary time units, keeping and at the assigned values. It then set to , keeping
unchanged, and again waits 10 time units. If we consider the entire block, the inputs go through the bit
patterns → → → , which tests all possible input combinations for this circuit. Other
orderings are possible and valid as long as they contain all desired input combinations. For example:
A -loop can be used to simplify sweeping through inputs. The following code is identical in function to the
previous example.
We use the fact that integers are encoded in binary and therefore can be assigned to other variables. This code
easily scales to an arbitrary number of signals of total width – the only changes required are to replace the
upper limit (i.e., ) with 2 (written as in Verilog) and to change the concatenation to include all the input
signals being tested.
13
Testbench Clock Simulator
A sequential circuit will need a clock. We can make a test bench simulate this with the following code:
Many other code variants exist that can produce a valid simulated clock signal. The simulated clock would be put
into the testbench for your system and all sequential modules would take as an input.
takes an arbitrary number of arguments which are concatenated and printed to the console.
is a special function which returns the simulated time when it is called. The empty value between the call
and the string simply adds extra space to make the output more readable.
prints once at the time specified. If you would prefer constant monitoring, use :
14
prints once at the time specified and then any time afterward when any of the signals being
monitored changes.
To access a multi-dimensional packed variable, successive indices refer to the packed dimensions from left-to-
right. For example, the following code sets all the bits of a 3-dimensional bus to :
Unpacked Arrays
It is also possible to declare an array with the size after the variable name. This is called an unpacked array,
because successive elements of an unpacked dimension cannot be treated as one unit. A packed array of 8 bits
can be used as a byte, but an unpacked array of 8 bits can only be accessed one bit at a time:
A mismatched array type and will give an error message about the unpacked array type, often something like:
“unpacked array type cannot be assigned to integer vector type - types do not match”
It is possible to have a variable with both packed and unpacked dimensions. In this case, the unpacked
dimensions are access first from left-to-right, followed by the packed dimensions from left-to-right.
15
Advanced Features – Assert Statements
As you design larger systems, you will often have assumptions you would like to verify are true. For example,
you may have a parameterized module with a limited number of legal parameter values. Or, you may have a
module that assumes the inputs obey certain requirements. You could check this via simulation, but as the
design gets larger you are more and more likely to miss things.
The solution to this is the statement. During simulation, it will raise an error whenever the value inside
the assertion is false. So, if we have a parameter with only a few legal values, we can test it with an assertion
inside the module:
If we require that at least one input to a unit must always be true, we can test it with an always-running
assertion:
16