Design for Simulations

This page is an introduction of the Simulation feature on the WebIDE, with some introductory examples of using and designing simulation testbench files.

Why need Simulations?

Simulation in the context of digital systems and Verilog design is an important and highly useful process. It involves creating a virtual model of a digital circuit to test and analyze its behavior and responses before implementing it on an actual hardware.

Simulation is an useful technique because it allows hardware (digital) designers to verify the functionality and performance of their designs under different conditions without the expense of physically building the circuit first. By simulating, designers can identify and rectify errors, analyze internal modular behaviors, optimize overall design performance, and ensure reliability, thereby reducing development time, cost, and the likelihood of failure in the actual hardware.

How to write a Testbench?

STEP 1: Understand the Module

To write a testbench, we need to understand the inputs, outputs, functionality and timing requirements of the module under test, or MUT.

For example, in Chapter 3 we designed a one-bit full-adder, module named as full_adder which has an internal structure as given below:

This is the module we want to test & simulate, which means that it requires 3 excitation inputs to observe the output response. Since this is a combinational circuit, there is no specific timing requirements.

STEP 2: Testbench Setup

To start off testbench design, we will create a module called fulladder_tb ( ), with three inputs and two outputs as given:

Module definition of the excitation testbench module
`timescale 1ns / 1ps
module fulladder_tb();
    // Inputs
    reg a;
    reg b;
    reg cin;

    // Outputs
    wire sum;
    wire co;

Note that the inputs are set as reg and outputs are wire.

You also noticed we have this line at the top:

`timescale 1ns / 1ps

In Verilog, the timescale directive is used to specify the time unit and time precision for the simulation of the design. This directive is important for modeling delays and specifying how the simulator interprets time literals in the Verilog code.

For this above timescale 1ns / 1ps directive, it means:

  • 1ns: This is the time unit. It tells the simulator that each time unit in the simulation will be interpreted as 1ns. So if you write timescale 5ns / 1ps instead, it means a delay of 5ns in the simulation.

  • 1ps: This is the time precision. It sets the smallest time increment that the simulator can resolve. In this case, 1ps is the smallest time increment for the simulation. Obviously the precision should be the same or smaller than the unit.

  • For both time unit and time precision, only use numbers in power of 10, such as 1, 10, 100; avoid using other integers or decimal numbers. This is because simulation time in Verilog is handled in decimal units only.

STEP 3: Instantiate and Connect

Inside the testbench, instantiate the MUT and connect the testbench vectors (reg for inputs, wire for outputs) to the inputs and outputs of the MUT.

`timescale 1ns / 1ps
module fulladder_tb();
    // Inputs
    reg a;
    reg b;
    reg cin;

    // Outputs
    wire sum;
    wire co;

// Instantiate the MUT
full_adder u1 (
    .a(a), 
    .b(b), 
    .cin(cin), 
    .sum(sum), 
    .co(co)
);

STEP 4: Stimulus Initialization

Write an initial block to apply test vectors to the MUT's inputs and simulate its behavior.

`timescale 1ns / 1ps
module fulladder_tb();
    // Inputs
    reg a;
    reg b;
    reg cin;

    // Outputs
    wire sum;
    wire co;

// Instantiate the MUT
full_adder u1 (
    .a(a), 
    .b(b), 
    .cin(cin), 
    .sum(sum), 
    .co(co)
);

initial begin
    // Initialize Inputs
    a = 0;
    b = 0;
    cin = 0;

    // Wait 10 ns for global reset to finish
    #10;
    
    // Add stimulus here
    a = 0; b = 0; cin = 0; #20;
    a = 0; b = 0; cin = 1; #20;
    a = 0; b = 1; cin = 0; #20;
    a = 0; b = 1; cin = 1; #20;
    a = 1; b = 0; cin = 0; #20;
    a = 1; b = 0; cin = 1; #20;
    a = 1; b = 1; cin = 0; #20;
    a = 1; b = 1; cin = 1; #20;
    // Finish simulation
    $finish;
end
endmodule

Other Commands

Verilog provides an array of tasks and functions specifically tailored to enhance the verification process, enabling thorough testing and efficient debugging within simulation frameworks. You will see the senarios of some of these functions in later examples.

  • $display: Used for displaying information in the simulation. It automatically moves to a new line after the output.

  • $write: Similar to $display but does not automatically append a newline at the end. It is used for displaying information in the simulation without moving to a new line.

  • $strobe: Similar to $display, this function is used for displaying information, but it outputs the values at the end of the simulation time step, which can be useful for debugging.

  • $monitor: This task continuously displays information whenever a change occurs in the variables specified in its argument list. It's typically used for ongoing observation of variables.

  • $stop: Temporarily stops the simulation and allows for interactive debugging.

  • $finish: Terminates the simulation run completely.

  • $time: Returns the current simulation time.

  • $random: Returns a random number each time it is called. This can be useful for generating random test vectors.

  • $readmemb: Reads binary data from a file into a memory array. It's often used to initialize memory contents in a simulation.

Running Simulation on WebIDE

STEP 1: Complete MUT file

Ensure the Module Under Test is completed, this means you need to properly design the module first and save it.

STEP 2: Creat testbench file

In general, the testbench file is named xxx_tb to indicate it is a testbench. For example, the MUT is named full_adder, therefore we call this testbench as: fulladder_tb. But you can name whatever way you preferred. Make sure you set this file as "simulation file".

STEP 3: Design testbench

Here we need to write the testbench. Since we have done this in STEP 4: Stimulus Initialization, so we will copy & paste the code here. Click Save. Note that you must complete STEP 2 before Save the simulation file.

STEP 4: Run Simulation

Simulation is implemented on IDE computations therefore you DO NOT need logic synthesis, pin assignment and FPGA mapping. Click Simulation, choose the testbench file fulladder_tb, and input simulation time.

If you testbench is incorrect, you may expect longer running time, and the run.log will indicate the line of code that results the error. For example, this log says "the design unit was not found", this is because I intentionally instantiate a wrong name "full_adderWrong".

Examples

Frequency Divider

In code 3.7, we wrote a divider_integer module to divider the base frequency by 12,000,000 times to generate 1Hz clock.

Code 3.7
module divider_integer # (           
    parameter   N     = 12000000,	// the divisor
    parameter   WIDTH = 24 		// the minimum bit-width to hold this divisor
)  
(
    input clk,
    output reg clkout 
);
reg [WIDTH-1:0] cnt; 
always @ (posedge clk) begin
    if(cnt>=(N-1))
        cnt <= 1'b0;
    else
        cnt <= cnt + 1'b1;
    clkout <= (cnt<N/2)?1'b1:1'b0;
end
endmodule

So we design a testbench as given below. Note that while instantiate the MUT, we reduce the divisor to N = 10 to save some computational power of the IDE simulation tool.

`timescale 1ns/100ps   
 
module divider_integer_tb();         
 
    parameter   N     = 12_000_000;	
    parameter   WIDTH = 24; 	
 
    reg    clk;         
    wire   clkout;
 
    initial
    begin
    	clk = 0;
    	#25;
    end
    
    // flip logic level every 10ns, generating a 20ns period clock, or 50MHz
    always #10 clk = ~clk;       
 
// instantiate the MUT
divider_integer #(.N(10), .WIDTH(4))  u1 (
	.clk	(clk), 
	.clkout	(clkout)   
);
endmodule

To run the simulation, we got the result as shown below. However, the parameters cnt and clkout have no signals shown.

This is because we need to initialize the register for cnt. To do so, we add an initialization for cnt to have it starting from 0.

Code 3.7 added with initialization

module divider_integer # (           
    parameter   N     = 100,	// the divisor
    parameter   WIDTH = 24 		// the minimum bit-width to hold this divisor
)  
(
    input clk,
    output reg clkout 
);
reg [WIDTH-1:0] cnt;

// initialize cnt = 0 for simulation
initial 
begin
    cnt = 0;
end

always @ (posedge clk) begin
    if(cnt>=(N-1))
        cnt <= 1'b0;
    else
        cnt <= cnt + 1'b1;
    clkout <= (cnt<N/2)?1'b1:1'b0;
end
endmodule

Now we save the modified MUT, and run the simulation again, this is the result we have:

LED Chaser

Here is the testbench we designed:

`timescale 1ns / 1ps

module LEDchaser_tb();
    reg clk = 0;
    wire [7:0] LEDs;

    // Instantiate the Unit Under Test (UUT)
    LEDchaser uut (
        .clk(clk),
        .LEDs(LEDs)
    );

    // Generate clock with a period of 20ns (50MHz)
    always #10 clk = ~clk;

    // Testbench Logic
    initial begin
        // Monitor changes in state of LEDs
        $monitor("Time: %0t | LEDs: %b", $time, LEDs);
        
        #10000; // 10000ns should be long enough to see the pattern multiple times
        $finish;
    end
endmodule

Notice that this time we used:

$monitor("Time: %0t | LEDs: %b", $time, LEDs);

to instruct the simulation tracking the LEDs registers and plot them against clock.

Do not forget that we also need to initialize other registers such as cnt and state. We have also changed CNT_NUM to 10 for quicker simulation purposes. Here is the modified MUT code.

module LEDchaser (
    input clk,
    output reg [7:0] LEDs
);
parameter   S0 = 3'b000,   S1 = 3'b001,   S2 = 3'b010,  S3 = 3'b011,               
            S4 = 3'b100,   S5 = 3'b101,   S6 = 3'b110,  S7 = 3'b111; 
       
reg [2:0] state;  

// initialize state register
initial
begin
    state = 0;
end

always @ (posedge clk) begin     
    case (state)  
        S0: LEDs = 8'b11111110;  
        S1: LEDs = 8'b11111101;  
        S2: LEDs = 8'b11111011;  
        S3: LEDs = 8'b11110111;  
        S4: LEDs = 8'b11101111;  
        S5: LEDs = 8'b11011111;  
        S6: LEDs = 8'b10111111;  
        S7: LEDs = 8'b01111111;  
        default: LEDs = 8'b11111111; // Default case to turn off all LEDs if state is unknown
    endcase
end

reg [23:0] cnt; 
parameter CNT_NUM = 10;     

// initialize cnt register
initial
begin
    cnt = 0;
end

always @ (posedge clk) begin
    if (cnt == CNT_NUM-1)  
        cnt <= 20'b0; 
    else
        cnt <= cnt + 1'b1;           
end
always @ (posedge clk) begin
    if (cnt == CNT_NUM-1)  
        state <= state + 1'b1; 
    if (state > S7)         // Reset state to S0 after reaching S7
        state <= S0;
end
endmodule

Here is the output simulation result. You can scroll in to examine the detailed signal changing in all variables (registers)

Last updated