Basic Modules and Testbenches

Module Declaration and Definition

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.

Verilog Simulation Timescale

First notice the timescale directive:

`timescale 1ns/1ps

Every Verilog file should begin with a `timescale line, usually 1ns/1ps, which defines both the time unit (1ns in this case) and the time precision (1ps). Verilog syntax allows for time-delay statements of the form #N where N is a real number. If the timescale is 1ns/1ps then #1 means “delay for 1ns”, #0.001 means “delay for 1ps”, and all delays are rounded off to the nearest picosecond.

In most of our Verilog module designs we will not actually use delay statements, so the timescale line can seem unnecessary. There are some ways to avoid using it, but for small projects it’s simpler to just paste the same timescale directive at the top of every source file.

Module Header and Implementation

Next comes the module declaration which declares the module’s name and it’s interface ports. If you have experience using C++ or Java, you can think of a module as something similar to a class – it is a type of object that has a public interface but may also have private internal variables and behavior.

The Verilog module syntax is summarized as follows:

module simple_module 
(
   /* I/O Port List Goes Here */
);
 
   // Module Implementation Here

endmodule

As with traditional programming languages, a declaration or header specifies how the module is accessed “from the outside,” i.e.  how to interface with the module. An implementation or definition provides the actual structure or behavior of the module. The implementation of simple_module is shown below.

   always @(posedge clk) begin
      if (en)
        q <= d;   // NON-BLOCKING ASSIGNMENT specifies D Flip Flop
      else        // The flip-flop creates a one-cycle delay before
        q <= 0;   // q is changed
   end

This example shows a behavioral implementation. In general, a module can have many different implementations that behave identically at the ports. For instance, the simple_module can be implemented by the logic circuits shown below.

Two equivalent structural implementations using logic circuits.

The schematic diagrams represent structural implementations. They should function exactly the same as the behavioral implementation (exercise: verify the equivalency for all values of q and en.) The purpose of a synthesis tool is to translate a behavioral model into a functionally-equivalent structural implementation.

Instances

When a module is used in a circuit, we call it an instance of the module. There can be many instances of a module. Every instance is a distinct object and is given a unique name. For example, the following snippet places two instances of simple_module:

   wire q1, q2, d1, d2, en;
   
   simple_module mod1 (.q(q1),.d(d1),.en(en));
   simple_module mod2 (.q(q2),.d(d2),.en(en));

A module instance is considered structural syntax. We will often mix behavioral and structural syntax to define different parts of a complex design. Some examples of mixed syntax are used in the testbench examples that follow.

Testbenches

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.

Signal Declarations

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

Instantiate DUT

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)
             );

Initialize Signals and Variables

Following the instantiation are initializations which occur in an initial block:

   // INITIAL SIGNAL CONFIGURATION:
   initial begin  // BLOCKING ASSIGNMENTS!
      clk = 0;      
      en  = 0;
      d   = 0;      
   end

About Blocking Assignments (=)

The initial block uses blocking assignments (=), which behave exactly like assignments in a traditional programming language. So, if the initial block contains these lines:

   en = 0;
   d  = ~en;

then d should equal 1, because the en assignment was evalued prior to the d assignment.

Generate a Clock

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.

Define Stopping Conditions

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 <= clk_count + 1;
      if (clk_count == 8)
         $finish;
      
   end

Generate Input Patterns (Stimuli)

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.

About Non-Blocking Assignments

To better understand non-blocking assignments, consider this code snippet:

initial begin
   en = 0;
   d  = 0;
end

always @(posedge clk) begin
   en <= d;
   d  <= ~en;
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

Writing to a Text File

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

Assigned Tasks

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:

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.