1. A module is a box with ports
Verilog is a hardware description language: you write text, and the text describes a digital circuit. It looks a lot like C, but a line of Verilog is not a command to execute. Most lines declare how wires and gates are connected.
The unit of a Verilog design is the module: a name, the ports that connect it to the outside, and
the behavior inside. One module describes one block of circuitry, and its ports are the terminals
through which it connects to other blocks. Ports have a direction. input brings a signal in;
output sends one out. The behavior inside the module sees the outside only through the port names.
Form
module module_name (port list);
// signal declarations
// circuit description
endmoduleHere is about the smallest useful module: it outputs the AND of two inputs.
module and_gate (input a, input b, output y);
assign y = a & b;
endmodule| Code | Meaning |
|---|---|
module and_gate (...) | Starts a module named and_gate and declares its ports |
input a, input b | Two 1-bit input ports |
output y | One 1-bit output port |
assign y = a & b; | Declares that y is always the AND of a and b |
endmodule | Closes the module |
Note. A module body is not a program that runs once from top to bottom. assign y = a & b;
does not mean "put a value in y now"; it means "y equals a & b, always". The simulator
recomputes y every time a or b changes.
A port is one bit unless it says otherwise. Ports that bundle several bits are vectors, the subject of Vectors, bit selects and operators.
1.1 Lexical rules
| Item | Rule | Example |
|---|---|---|
| Case | Significant | Data and data are different names |
| Line comment | From // to the end of the line | // output port |
| Block comment | From /* to */ | /* description */ |
| End of statement | Simple statements end with ;; a begin/end block ends with end | assign y = a; |
| Grouping | Where the syntax expects one statement, begin and end group several | initial begin ... end |
2. Ports: direction, kind and width
2.1 Declaring ports in the port list
In the ANSI style, each port's direction, kind, and width are declared right in the port list. Every example in this course uses this style.
Form
module module_name (
direction [kind] [width] port_name,
direction [kind] [width] port_name
);
// module body
endmodulemodule counter (
input clock,
input [7:0] limit,
output reg [7:0] count
);| Part | Meaning |
|---|---|
input, output | The port's direction |
reg | The port's kind. Without it, the port is a wire |
[7:0] | The port's width. Without it, the port is one bit wide |
clock, limit, count | Port names. The module body reads and writes the signals by these names |
There is no comma after the last port. The body follows the ); that closes the port list.
2.2 Port directions and kinds
| Direction | Inside the module | What the parent may connect |
|---|---|---|
input | A net the module only reads | Any expression |
output | A net by default; a variable if declared output reg | A net |
inout | A bidirectional net | A net |
If an output is driven by assign, leave it a wire. If it is assigned inside always or
initial, declare it output reg. This is the net-versus-variable rule that
Nets, variables and the combinational
always spells out; until then, every output in this chapter is driven by assign and stays a
wire.
Tip. Older code often lists only the port names in the header and declares direction and width again in the body. Both styles describe the same module.
module and_old (a, b, y);
input a, b;
output y;
assign y = a & b;
endmodule2.3 The top module
The module that no other module instantiates is the top of the hierarchy. In this course the top is
always a testbench called tb, which has no ports. Icarus finds the module nobody uses and makes it
the top automatically.
3. assign is a wire
assign left = right; connects the value of the right-hand expression to the left-hand signal, all
the time. When an input on the right changes, the left follows at once. It is not a statement that
runs once, like an assignment in software; it is a wire laid down in the circuit for good. In the
expression, ^ is XOR and & is AND.
3.1 From one AND gate to a half adder
The and_gate of section 1 is a single assign. A half adder needs only one more. Adding two 1-bit
numbers gives 0, 1 or 2, and 2 needs two bits: carry, worth 2, and sum, worth 1. Copy the table
and fill in the last two columns before you read on.
a | b | a + b | carry | sum |
|---|---|---|---|---|
| 0 | 0 | 0 | ? | ? |
| 0 | 1 | 1 | ? | ? |
| 1 | 0 | 1 | ? | ? |
| 1 | 1 | 2 | ? | ? |
Now read each column you wrote as the truth table of a gate.
Check
Which two-input gate has exactly the truth table of your carry column?
Check
Which two-input gate has exactly the truth table of your sum column?
So the half adder is the and_gate with its output renamed carry, plus one XOR line for sum.
HalfAdder.v is that half adder, written in Verilog: two 1-bit inputs and two 1-bit outputs. sum
is the XOR of the inputs and carry is their AND. Four ports and two assign lines are the whole
design. Keep the declaration order in mind: a, b, sum, carry.
// Sum and carry of two bits, with no stored state.
module half_adder (
input a,
input b,
output sum,
output carry
);
assign sum = a ^ b;
assign carry = a & b;
endmodule4. The testbench drives the inputs and reads the outputs
A design does nothing on its own. The second file, Tb.v, is the testbench: a module without ports
that feeds inputs into a design and looks at its outputs. It makes an instance of the half adder
named dut, connects its own signals by port name (.a(a)), then applies the four combinations to
a and b and prints sum and carry once for each. To print values, or to make things happen in
a particular order during simulation, a testbench uses a procedural block.
4.1 The initial block and $display
An initial block starts at the beginning of simulation, time 0, and executes the statements inside
it once, in order.
Form
initial begin
statement;
statement;
endmodule hello;
initial begin
$display("Hello, Verilog");
$finish;
end
endmodule| Construct | Role |
|---|---|
initial begin ... end | Starts at time 0 and runs the enclosed statements once, in order |
$display(...) | Prints text and values as one line, much like C's printf |
$finish; | Ends the simulation |
Names that start with $ identify system tasks or system functions. Tasks such as $display
perform an action; functions such as $clog2 return a value. Many of these services have no
hardware meaning and synthesis tools ignore or reject them, though a few ($clog2, $readmemh) are
accepted by synthesis. The always block, which repeats instead of running once, is covered in
Testbench techniques; its clocked form is the subject
of Registers and nonblocking assignment.
Every example runs on Icarus Verilog. iverilog compiles the source files into a file called
sim.vvp, and vvp simulates that file. Pressing Run performs both steps in order.
4.2 Reading Tb.v
You do not need every line of Tb.v to use it. Three lines matter in this chapter:
Line in Tb.v | What it does for you |
|---|---|
half_adder dut (.a(a), .b(b), .carry(carry), .sum(sum)); | Places the half adder and ties each port to the testbench signal of the same name |
{a, b} = i; | Sets the inputs to the next row of your table: 00, 01, 10, 11. The low two bits of i land in a and b, left to right |
#1 $display("HA a=%b b=%b -> carry=%b sum=%b", ...); | Waits one time unit so the assign outputs have followed the new inputs, then prints one row |
The rest, the reg and wire declarations, the integer i loop and the #1 delay, is the machinery
that drives a design through time. You can use it without writing it yet;
Testbench techniques explains each piece. One more
detail: the connection list names .carry before .sum, the opposite of the declaration order.
module tb;
reg a, b;
wire sum, carry;
integer i;
// Named connections: .port(signal), so order in the list does not matter.
half_adder dut (.a(a), .b(b), .carry(carry), .sum(sum));
initial begin
for (i = 0; i < 4; i = i + 1) begin
// Unpack the loop count into the two input bits.
{a, b} = i;
#1 $display("HA a=%b b=%b -> carry=%b sum=%b", a, b, carry, sum);
end
$finish;
end
endmoduleWhere to look. Each printed line is one row of your table. a= and b= are the inputs the
testbench chose, so they only tell you which row you are on. The two columns to check are carry=
and sum=: compare them with the last two columns you filled in for
From one AND gate to a half adder.
Predict before you run. There will be four lines. For a, b equal to 0 0, 0 1, 1 0 and
1 1, write down carry and sum from the half-adder truth table in the fundamentals.
- How many of the four lines show
carry=1, and what issumon that line? - Does listing
.carryfirst change any printed column?
Check
On the line where a = 1 and b = 1, what are carry and sum?
Press Run: the lab opens, compiles the two files and simulates them. Compare the four printed lines
with your prediction one by one. They come in the same order as the rows of the truth table.
Check the result.
| Output line | What it shows |
|---|---|
a=0 b=0 | Both outputs are 0 |
a=0 b=1, a=1 b=0 | Only sum is 1: with exactly one input high, the XOR is 1 |
a=1 b=1 | carry=1, sum=0, which is 1 + 1 = 2 written as the two bits 10 |
The four lines are the half-adder truth table, and the order of .carry and .sum made no
difference, because named connections match by name.
Caution. Output appearing means the simulation ran, not that the design is correct. Compare the
values with the truth table to check its behavior. Whether the circuit can be
synthesized into a chip is a separate question; a combinational circuit written with assign alone
is the typical shape synthesis tools accept. The testbench's $display and #1 exist only in
simulation.
5. Change one wire
In the lab's editor, change assign sum = a ^ b; in HalfAdder.v to assign sum = a | b; and run
again. Before you do, write down which line will change.
Check
If sum becomes a | b, on which of the four lines does sum change?
Now the other output. Restore ^ in sum, change assign carry = a & b; to
assign carry = a | b;, and predict again before you run.
Check
With carry = a | b and sum = a ^ b, which printed lines show carry=1?
The edited code stays in the lab. To return to the original, change | back to &, or press the
page's Run to load the example afresh.
A second change touches the testbench instead of the design. Replace the connection list with the
positional form half_adder dut (a, b, carry, sum);. Positional connections follow the definition's
order a, b, sum, carry, so the testbench's carry net is tied to the child's sum and its sum
net to the child's carry. It still compiles, but the two columns trade places: the middle two lines
now show carry=1, and the last line shows sum=1. The rules for positional connections, and why
this course connects by name, are in Instances,
hierarchy and parameters.
6. Summary
| Element | Meaning |
|---|---|
module … endmodule | the design unit: name, ports, behavior. Its body is a set of connections, not a program that runs in order |
direction [kind] [width] name | a port declaration. Without a kind the port is a wire; without a width it is one bit |
input, output, inout | port direction |
assign y = expr; | a wire that keeps y equal to the expression |
initial begin … end, $display, $finish | a procedural block that runs once at time 0; system tasks for simulation |
instance name dut (.port(signal)); | a module placed inside another, connected by port name |
7. Check your understanding
-
The half-adder instance was switched to the positional list
(a, b, carry, sum). Why did it still compile?Show answer
A positional list just connects signals in the definition's order
a, b, sum, carry; it never compares names.carryandsumare both 1-bit nets, so swapping them still fits, and nothing gives the compiler a reason to complain. -
assign y = a & b;appears once in a module. How many times does the simulator evaluate it?Show answer
As often as
aorbchanges. A continuous assignment is a connection, not a statement that runs once:yis recomputed whenever an input of the expression changes, for as long as the simulation runs.
This chapter has no Practice problem; its checkpoint is the run above. The other port declaration style and positional connection are not needed until you place one module inside another.