A very simple module is defined in the file named src/simple_module.v
.
Use a text editor to open the file and examine its structure. First
notice the timescale directive:
`timescale 1ns/1ps
Every Verilog file should begin with a `timescale
line, usually 1ns/1ps
.
Next comes the module declaration. A module
is
declared by the module
keyword, followed by a list of Input/Output Ports, then
the module definition:
module simple_module
(
/* I/O Port List Goes Here */
);
// Module Definition Here
endmodule
As with traditional programming languages, a declaration specifies how the module is accessed “from the outside,” i.e. how to interface with the module. A definition provides the actual structure or behavior of the module.
We will come back to the details of simple_module
after taking a detour to
examine the testbench template.
Best practice dictates that designers should verify
every module in a design as exhaustively as possible. Therefore, for any
module you design, it’s a good idea to specify a testbench just for that
module. An example testbench is provided in the file named src/testbench.v
.
The testbench contains several key sections, as itemized below.
The testbench is a module with no I/O ports. In order to test the
simple_module
, its I/O signals
need to be declared inside the testbench. We can start by copying the
port declarations from simple_module
, and edit the lines:
input clk, // <-- "input" becomes "reg"
input en, // <-- ',' becomes ';'
input d,
output reg q // <-- "output reg" becomes "wire"
The purpose of a testbench
is
to generate input patterns for simple_module
and evaluate output
patterns. To achieve this, all the inputs to simple_module
are declared as reg
signals
within the testbench
, so they
can be given assignments in always
blocks.
The outputs from simple_module
are not assigned values
by testbench
, so the should be
declared wire
type
within testbench
.
With these changes, the testbench
module
and
signal declarations are as follows:
module testbench ();
// DECLARE SIGNALS
reg clk; // "reg" type signals are controlled
reg en; // by the testbench
reg d;
wire q; // "wire" type signals are controlled
// by the module being tested
Following the signal declarations, we see the Device Under Test (DUT). This is an instance of the module we are testing.
// INSTANTIATE the DEVICE UNDER TEST (DUT)
(
simple_module DUT(clk),
.clk(en),
.en(d),
.d(q)
.q);
Following the instantiation are initializations
which occur in an initial
block:
// INITIAL SIGNAL CONFIGURATION:
initial begin // BLOCKING ASSIGNMENTS!
= 0;
clk = 0;
en = 0;
d end
The initial block uses blocking assignments (=), which behave exactly like assignments in a traditional programming language. So, if the initial block contains these lines:
= 0;
en = ~en; d
then d
should equal 1,
because the en
assignment was
evalued prior to the d
assignment.
Next the testbench generates a main clock for the simulation:
// GENERATE CLOCK:
initial forever #10 clk = ~clk;
In this line, initial
means
“evaluate prior to starting the simulation,” forever
means
“keep evaluating indefinitely,” and #10
means
“delay for 10 time units.” The “time units” are set by the `timescale
directive, usually the time
unit is 1ns, so #10
means
10ns.
Next, we need to create a clock counter and define a stopping condition to tell the simulator when to finish. The code below instructs the simulator to count each clock cycle, and stop when the count reaches 8:
// DEFINE WHEN TO TERMINATE SIMULATION:
integer clk_count = 0;
always @(posedge clk) begin
<= clk_count + 1;
clk_count if (clk_count == 8)
$finish;
end
Next, perhaps most importantly, the testbench
creates
stimuli for the DUT. An exhaustive
test creates all possible input patterns in order to verify
every state of the DUT. In this example, the DUT has two inputs, en
and d
. That makes four possible patterns.
The testbench uses if/else if
statements to cycle through them:
// CREATE STIMULI:
always @(posedge clk) begin
// A simple state machine that cycles through
// all input values:
if ({en,d} == 2'b00) // {en,d} CONCATENATES en,d into
{en,d} <= 2'b01; // a 2-bit vector
else if ({en,d} == 2'b01)
{en,d} <= 2'b10;
else if ({en,d} == 2'b10)
{en,d} <= 2'b11;
else if ({en,d} == 2'b11)
{en,d} <= 2'b00;
end
Let’s break down the syntax. This section of code starts with an
always
declaration:
always @(posedge clk) begin
Here the sensitivity list is @(posedge clk)
,
which directs the simulator to evaluate the block upon every
rising edge of clk
. This is the usual method for
defining synchronous clocked logic.
Next, we have the first if
condition:
if ({en,d} == 2'b00) // {en,d} CONCATENATES en,d into
{en,d} <= 2'b01; // a 2-bit vector
There are two important things happening here:
1. The concatenation operator {en,d}
joins en
and d
into a single two-bit vector. 2.
Non-blocking assignments (<=) are used to indicate
register assignments.
Within an always
block,
all non-blocking assignments are evaluated concurrently
– they happen in parallel. The results of non-blocking assignments take
effect at the next clock edge, in the future. So there is a
one-cycle delay.
To better understand non-blocking assignments, consider this code snippet:
initial begin
= 0;
en = 0;
d end
always @(posedge clk) begin
<= d;
en <= ~en;
d end
In this code, en
and d
are both 0 at the very start of
simulation. At the time of the first clock edge, their values
are not yet changed. The statement en <= d
takes effect just prior to the second clock edge. Therefore in
the last statement, d <= ~en
,
the value of en
is still 0 at
the time of evaluation. So d
is
assigned to be 1, which takes effect just before the next clock
edge.
Over time, the pattern will be
en d
initial: 0 0
clock 1: 0 0
clock 2: 0 1
clock 3: 1 1
clock 4: 1 0
clock 5: 0 0
Notice that this logic cycles through all four input patterns, and is
more compact than the for/else
conditions.
In order to verify the simulation results, you need to print
information to the console or to a file. In this example, we use the
$write
system task, which is nearly identical to C’s printf
statement:
// WRITE OUTPUT TO CONSOLE:
always @(posedge clk) begin
$write("clk: %d",clk_count);
$write("\ten: %b", en);
$write("\td: %b", d);
$write("\tq: %b", q);
$write("\n");
end
The $write
task
prints text to the simulation console. To write to a text file, a
similar task called $fwrite
is
used like this:
integer fid; // declare file reference
initial fid = $fopen("filename","w"); // Open the file for writing
always @(posedge clk) begin
//...//
$fwrite(fid,"clk: %d", clk_count);
//...and so on...//
end
In the terminal, run a simulation by typing “make” and press return. Verify the outputs:
clk: 0 en: 0 d: 0 q: x
clk: 1 en: 0 d: 1 q: 0
clk: 2 en: 1 d: 0 q: 0
clk: 3 en: 1 d: 1 q: 0
clk: 4 en: 0 d: 0 q: 1
clk: 5 en: 0 d: 1 q: 0
clk: 6 en: 1 d: 0 q: 0
clk: 7 en: 1 d: 1 q: 0
Make the following changes:
q
an initial value of
zeroq
is assigned to be the XOR of q
with d
$write
statements to make $fwrite
s, with
filename “test_result.txt”if/else
conditions and replace them with the logic en <= d; d <= ~en;
Run the simulation again. Open “test_result.txt” and verify that the
output is correct. Your testbench should cover all four input patterns,
and q
should toggle each time
d
is 1.
To turn in your work, run these commands:
git add test_result.txt
git commit . -m "Complete."
git push origin main
Then indicate on Canvas that your work is done.