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. 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.

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.