VerilogTutorial
VerilogTutorial
Introduction
The following tutorial is intended to get you going quickly in circuit design in Verilog. It
isn’t a comprehensive guide to System Verilog, but should contain everything you need
to design circuits for your class.
If you have questions, or want to learn more about the language, I’d recommend Vahid
and Lysecky’s Verilog for Digital Design.
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 performs a computation on the inputs to generate an
output. However, a Verilog module really is a collection of logic gates, and each time
you call a module you are creating that set of gates.
An example of a simple module:
AND_OR
A
andOut
TheAndGate
orOut
B
TheOrGate
Basic Gates
Simple modules can be built from several different types of gates:
buf <name> (OUT1, IN1); // Sets output equal to input
not <name> (OUT1, IN1); // Sets output to opposite of input
The <name> can be whatever you want, but start with a letter, and consist of letters,
numbers, and the underscore “_”. Avoid keywords from Verilog (i.e. “module”,
“output”, etc.).
There are multi-input gates as well, which can each take two or more inputs:
and <name> (OUT, IN1, IN2); // Sets output to AND of inputs
or <name> (OUT, IN1, IN2); // Sets output to OR of inputs
nand <name> (OUT, IN1, IN2); // Sets to NAND of inputs
nor <name> (OUT, IN1, IN2); // Sets output to NOR of inputs
xor <name> (OUT, IN1, IN2); // Sets output to XOR of inputs
xnor <name> (OUT, IN1, IN2); // Sets to XNOR of inputs
If you want to have more than two inputs to a multi-input gate, simply add more. For
example, this is a five-input and gate:
and <name> (OUT, IN1, IN2, IN3, IN4, IN5); // 5-input AND
Hierarchy
Just like we build up a complex software program by having procedures call
subprocedures, Verilog builds up complex circuits from modules that call submodules.
For example, we can take our previous AND_OR module, and use it to build a
NAND_NOR:
NAND_NOR
AND_OR
X A
andOut andVal nandOut
n1
TheAndGate
Defining constants
Sometimes you want to have named constants - variables whose value you set in one
place and use throughout a piece of code. For example, setting the delay of all units in a
module can be useful. We do that as follows:
// Compute the logical AND and OR of inputs A and B.
module AND_OR(andOut, orOut, A, B);
output logic andOut, orOut;
input logic A, B;
parameter delay = 5;
Parameterized Design
Parameters can also be inputs to designs, that allow 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:
module adder #(parameter WIDTH=5) (out, a, b);
output logic [WIDTH-1:0] out;
input logic [WIDTH-1:0] a, b;
assign out = a + b;
endmodule
This defines a parameter “WIDTH” 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:
// A 16-bit adder
adder #(.WIDTH(16)) add1 (.out(o1), .a(a1), .b(b1));
// A default-width adder, so 5-bit
adder add2 (.out(o2), .a(a2), .b(b2));
Test benches
Once a circuit is designed, you need some way to test it. For example, we’d like to see
how the NAND_NOR circuit we designed earlier behaves. To do this, we create a test
bench. A test bench is a module that calls your device under test (DUT) with the desired
input patterns, and collects the results. For example consider the following:
TEST
NAND_NOR
$monitor($time, ,a,o,R,S);
AND_OR
X A
initial andOut andVal nandOut
begin
R=1; S=1; n1
#10 R=0; TheAndGate
#10 S=0;
#10 R=1;
orOut orVal norOut
end Y B
n2
TheOrGate
endmodule
The code to notice is that of the module “NAND_NOR_testbench”. It instantiates one
copy of the NAND_NOR gate, called “dut” (device under test), and hooks up “logic”
signals to all of the I/Os.
In order to provide test data to the dut, we have a stimulus block:
initial begin // Stimulus
X = 1; Y = 1; #10;
X = 0; #10;
Y = 0; #10;
X = 1; #10;
end
The code inside the “initial” statement is only executed once. It first sets X and Y to true.
Then, due to the “#10” the system waits 10 time units, keeping X and Y at the assigned
values. We then set X to false. Since Y wasn’t changed, it remains at true. Again we
wait 10 time units, and then we change Y to false (X remains at false). If we consider the
entire block, the inputs XY go through the pattern 11 -> 01 -> 00 -> 10, which tests all
input combinations for this circuit. Other orders are also possible. For example we could
have done:
initial begin // Stimulus
X = 0; Y = 0; #10;
Y = 1; #10;
X = 1; Y = 0; #10;
Y = 1; #10;
end
This goes through the pattern 00 -> 01 -> 10 -> 11. In fact, there’s a shorthand for doing
this format:
integer i;
initial begin // Stimulus
for(i=0; i<4; i++) begin
{X,Y} = i; #10;
end
end
We use the fact that integers are encoded in binary, and the binary values go through the
pattern 000, 001, 010, 011, 100, 101, … If you want to put N binary signals through all
combinations of inputs, then use the same code, but replace the upper limit of 4 in the
loop condition with the integer whose value is 2N.
Multi-bit Constants
In test benches and other places, you may want to assign a value to a multi-bit signal.
You can do this in several ways, shown in the following code:
logic [15:0] test;
initial begin // stimulus
test = 12;
#(10) test = 16'h1f;
#(10) test = 16'b01101;
end
The 16-bit variable test is assigned three different values. The first is in decimal, and
represents twelve. The second is a hexadecimal number (specified by the 'h) 1f, or 16+15
= 31. The last is a binary number (specified by the 'b) 01101 = 1+4+8 = 13. In each case
the value is assigned, in the equivalent binary, to the variable test. Unspecified bits are
padded to 0. So, the line:
test = 12;
is equivalent to:
test = ‘b0000000000001100;
It sets test[2] and test[3] to 1, and all other bits to 0.
always_comb begin
for (i=0; i<8; i=i+1)
LEDG[7-i] = GPIO_0[28+i];
for (i=0; i<10; i=i+1)
LEDR[9-i] = GPIO_0[18+i];
end
In this code we set LEDG[7] = GPIO_0[28], LEDG[6] = GPIO_0[29], etc.
Multi-Dimensional Buses
Sometimes it can be useful to have structures with more than one dimension – for
example, we might want to hold 16 8-bit values. Verilog allows you to define multiple
sets of indexes for a variable:
logic [15:0][7:0] string;
To index a value, you move left-to-right through the indices. For example, the following
code sets all the bits of a 4-dimensions bus to 0:
logic [15:0][9:0][7:0][3:0] vals;
integer i, j, k, l;
always_comb begin
for(i=0; i <= 15; i++)
for(j=0; j<=9; j++)
for(k=0; k<=7; k++)
for(l=0; l<=3; l++)
vals[i][j][k][l] = 1'b0;
end
Subsets
Sometimes you want to break apart multi-bit values. We can do that by selecting a subset
of a value. For example, if we have
logic [31:0] foo;
initial foo[3:1] = 3'b101;
This would set foo[3] = 1, foo[2] = 0, and foo[1] = 1. All other bits of foo will not be
touched. We could also use the same form to take a subset of a multi-bit wire and pass it
as an input to another module.
Note that this subdividing can be done to save you work in creating large, repetitive
structures. For example, consider the definition of a simple 16-bit register built from a
base D_FF unit:
module D_FF16(q, d, clk);
output logic [15:0] q;
input logic [15:0] d;
input logic clk;
Concatenations
Sometimes instead of breaking apart a bus into pieces, you instead want to group things
together. Anything inside {}’s gets grouped together. For example, if we want to swap
the low and high 8 bits of an input to a D_FF16 we could do:
logic [15:0] data, result;
Sequential Logic
In combinational logic we start an always block with “always_comb”, which means the
logic output is recomputed every time any of the inputs changes. For sequential logic, we
need to introduce a clock, which will require a somewhat different always statement:
endmodule
Most of this should be familiar. The new part is the “always_ff @(posedge clk)”. We
capture the input with the “always_ff @(posedge clk)”, which says to only execute the
following statements at the instant you see a positive edge of the clk. That means we
have a positive edge-triggered flip-flop. We can build a negative edge-triggered flip-flop
via “always_ff @(negedge clk)”.
Clocks
A sequential circuit will need a clock. We can make the test bench provide it with the
following code:
logic clk;
parameter PERIOD = 100; // period = length of clock
// Make the clock LONG to test
initial begin
clk <= 0;
forever #(PERIOD/2) clk = ~clk;
end
This code would be put into the testbench code for your system, and all modules that are
sequential will take the clock as an input.
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 easier to use. Sometimes we may
use PARAMETER 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 an FSM:
enum { RED, BLUE, GREEN} ps, ns;
This defines two variables, ns and ps, and requires that their values be either RED,
GREEN, or BLUE. We can test the value of variables, and assign new values, using
those names:
always_comb begin
if (ps == RED)
ns = BLUE;
else
…
Verilog will then assign specific values to each of these variables. If you want to set
specific values, you can use:
enum { RED=0, BLUE=1, GREEN=2 } ps, ns;
Make sure to have one of the values be equal to 0, but the other values can be whatever
you want. One last tip – if you want to print the value of an enum variable ps, you can
call ps.name to return the string for that value (i.e. if ps is 0, ps.name will return RED).
// Parity example
always_comb begin
ns = ps ^ in;
end
module parity_testbench;
logic in, reset, clk;
logic out;
parameter period = 100;
initial begin
clk <= 1;
forever #(period/2) clk <= ~clk;
end
initial begin
reset <= 1; in <= 0; @(posedge clk);
reset <= 0; @(posedge clk);
in <= 1; @(posedge clk);
@(posedge clk);
in <= 0; @(posedge clk);
in <= 1; @(posedge clk);
in <= 0; @(posedge clk);
@(posedge clk);
$stop();
end
endmodule
If we require that at least one input to a unit must always be true, we can test it with an
always-running assertion:
initial assert(WIDTH>0);
genvar i;
generate
for(i=0; i<WIDTH; i++) begin : eachDff
D_FF dff (.q(q[i]), .d(d[i]), .clk);
end
endgenerate
endmodule
Note that for the case of the register, one could just use a parameter and the FSM format
to do the same thing:
initial assert(WIDTH>0);
But, the register is a simple way to show the power of generate statements, which can be
useful in some situations that often cannot be handled in any other way. Below are a few
other examples to help you see how generate statements can be used.
First is a version of a 99 position tug-of-war game, with edge conditions. Note that you
could also do the left and right edge positions outside of the generate loop and adjust the
loop variable accordingly.
module tugOfWar99 (clk, rst, L, R, leds);
genvar i;
generate
for(i = 0; i < 99; i++) begin : eachLight
end
endgenerate
endmodule
Here is an example for setting up a control FSM for each position of a 16x16 LED array.
module controller2D(clk, rst, row_sel, col_sel, leds);
genvar x,y;
generate
for(x = 0; x < 16; x++) begin : eachRow
for(y = 0; y < 16; y++) begin: eachCol
light pixel (.clk, .rst, .row(row_sel[x]),
.col(col_sel[y]), .lightOn(leds[x][y]));
end
end
endgenerate
endmodule
endmodule