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.
Click here for: How to use WebIDE
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:
`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.
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.
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
Now let us try another example. This time we will design a testbench for the LEDchaser module Verilog Codeimplemented in Chapter 5.
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
Was this helpful?