Design
module gray_ctr
# (parameter N = 4)
( input clk,
input rstn,
output reg [N-1:0] out);
reg [N-1:0] q;
always @ (posedge clk) begin
if (!rstn) begin
q <= 0;
out <= 0;
end else begin
q <= q + 1;
`ifdef FOR_LOOP
for (int i = 0; i < N-1; i= i+1) begin
out[i] <= q[i+1] ^ q[i];
end
out[N-1] <= q[N-1];
`else
out <= {q[N-1], q[N-1:1] ^ q[N-2:0]};
`endif
end
end
endmodule
Testbench
module tb;
parameter N = 4;
reg clk;
reg rstn;
wire [N-1:0] out;
gray_ctr u0 ( .clk(clk),
.rstn(rstn),
.out(out));
always #10 clk = ~clk;
initial begin
{clk, rstn} <= 0;
$monitor ("T=%0t rstn=%0b out=0x%0h", $time, rstn, out);
repeat(2) @ (posedge clk);
rstn <= 1;
repeat(20) @ (posedge clk);
$finish;
end
endmodule
ncsim> run T=0 rstn=0 out=0xx T=10 rstn=0 out=0x0 T=30 rstn=1 out=0x0 T=50 rstn=1 out=0x1 T=70 rstn=1 out=0x3 T=90 rstn=1 out=0x2 T=110 rstn=1 out=0x6 T=130 rstn=1 out=0x7 T=150 rstn=1 out=0x5 T=170 rstn=1 out=0x4 T=190 rstn=1 out=0xc T=210 rstn=1 out=0xd T=230 rstn=1 out=0xf T=250 rstn=1 out=0xe T=270 rstn=1 out=0xa T=290 rstn=1 out=0xb T=310 rstn=1 out=0x9 T=330 rstn=1 out=0x8 T=350 rstn=1 out=0x0 T=370 rstn=1 out=0x1 T=390 rstn=1 out=0x3 T=410 rstn=1 out=0x2 Simulation complete via $finish(1) at time 430 NS + 0
Verilog design and testbench typically have many lines of code comprising of always
or initial
blocks, continuous assignments and other procedural statements which become active at different times in the course of a simulation.
Every change in value of a signal in the Verilog model is considered an update event. And processes such as always
and assign
blocks that are sensitive to these update events are evaluated in an arbitrary order and is called an evaluation event. Since these events can happen at different times, they are better managed and ensured of their correct order of execution by scheduling them into event queues that are arranged by simulation time.
module tb;
reg a, b, c;
wire d;
// 'always' is a process that gets evaluated when either 'a' or 'b' is updated.
// When 'a' or 'b' changes in value it is called an 'update event'. When 'always'
// block is triggered because of a change in 'a' or 'b' it is called an evaluation
// event
always @ (a or b) begin
c = a & b;
end
// Here 'assign' is a process which is evaluated when either 'a' or 'b' or 'c'
// gets updated
assign d = a | b ^ c;
endmodule
Event Queue
A simulation step can be segmented into four different regions. An active event queue is just a set of processes that need to execute at the current time which can result in more processes to be scheduled into active or other event queues. Events can be added to any of the regions, but always removed from the active region.
- Active events occur at the current simulation time and can be processed in any order.
- Inactive events occur at the current simulation time, but is processed after all active events are processed
- Nonblocking assign events that were evaluated previously will be assigned after all active and inactive events are processed.
- Monitor events are processed after all active, inactive and nonblocking assignments are done.
When all events in the active queue for the current time step has been executed, the simulator advances time to the next time step and executes its active queue.
module tb;
reg x, y, z
initial begin
#1 x = 1;
y = 1;
#1 z = 0;
end
endmodule
Simulation starts at time 0, and the first statement is scheduled to be executed when simulation time reaches 1 time unit at which it assigns x and y to 1. This is the active queue for the current time which is 1 time unit. Simulator then schedules the next statement after 1 more time unit at which z is assigned 0.
What makes simulation nondeterministic ?
There can be race conditions during simulation that end up giving different outputs for the same design and testbench. One of the reasons for nondeterministic behavior is because active events can be removed from the queue and processed in any order.
Verilog simulation depends on how time is defined because the simulator needs to know what a #1 means in terms of time. The `timescale
compiler directive specifies the time unit and precision for the modules that follow it.
Syntax
`timescale <time_unit>/<time_precision>
// Example
`timescale 1ns/1ps
`timescale 10us/100ns
`timescale 10ns/1ns
The time_unit is the measurement of delays and simulation time while the time_precision specifies how delay values are rounded before being used in simulation.
Use the following timescale constructs to use different time units in the same design. Remember that delay specifications in the design are not synthesizable and cannot be converted to hardware logic.
`timescale
for base unit of measurement and precision of time- $printtimescale system task to display time unit and precision
$time
and$realtime
system functions return the current time and the default reporting format can be changed with another system task$timeformat
.
Character | Unit |
---|---|
s | seconds |
ms | milliseconds |
us | microseconds |
ns | nanoseconds |
ps | picoseconds |
fs | femtoseconds |
The integers in these specifications can be either 1, 10 or 100 and the character string that specifies the unit can take any value mentioned in the table above.
Example #1: 1ns/1ns
// Declare the timescale where time_unit is 1ns
// and time_precision is also 1ns
`timescale 1ns/1ns
module tb;
// To understand the effect of timescale, let us
// drive a signal with some values after some delay
reg val;
initial begin
// Initialize the signal to 0 at time 0 units
val <= 0;
// Advance by 1 time unit, display a message and toggle val
#1 $display ("T=%0t At time #1", $realtime);
val <= 1;
// Advance by 0.49 time unit and toggle val
#0.49 $display ("T=%0t At time #0.49", $realtime);
val <= 0;
// Advance by 0.50 time unit and toggle val
#0.50 $display ("T=%0t At time #0.50", $realtime);
val <= 1;
// Advance by 0.51 time unit and toggle val
#0.51 $display ("T=%0t At time #0.51", $realtime);
val <= 0;
// Let simulation run for another 5 time units and exit
#5 $display ("T=%0t End of simulation", $realtime);
end
endmodule
The first delay statement uses #1 which makes the simulator wait for exactly 1 time unit which is specified to be 1ns with `timescale
directive. The esecond delay statement uses 0.49 which is less than half a time unit. However the time precision is specified to be 1ns and hence the simulator cannot go smaller than 1 ns which makes it to round the given delay statement and yields 0ns. So the second delay fails to advance the simulation time.
The third delay statement uses exactly half the time unit [hl]#0.5[/lh] and again the simulator will round the value to get #1 which represents one whole time unit. So this gets printed at T=2ns.
The fourth delay statement uses a value more than half the time unit and gets rounded as well making the display statement to be printed at T=3ns.
ncsim> run T=1 At time #1 T=1 At time #0.49 T=2 At time #0.50 T=3 At time #0.51 T=8 End of simulation ncsim: *W,RNQUIE: Simulation is complete.
The simulation runs for 8ns as expected, but notice that the waveform does not have smaller divisions between each nanosecond. This is because the precision of time is the same as the time unit.

Example #2: 10ns/1ns
The only change made in this example compared to the previous one is that the timescale has been changed from 1ns/1ns to 10ns/1ns. So the time unit is 10ns and precision is at 1ns.
// Declare the timescale where time_unit is 10ns
// and time_precision is 1ns
`timescale 10ns/1ns
// NOTE: Testbench is the same as in previous example
module tb;
// To understand the effect of timescale, let us
// drive a signal with some values after some delay
reg val;
initial begin
// Initialize the signal to 0 at time 0 units
val <= 0;
// Advance by 1 time unit, display a message and toggle val
#1 $display ("T=%0t At time #1", $realtime);
val <= 1;
// Advance by 0.49 time unit and toggle val
#0.49 $display ("T=%0t At time #0.49", $realtime);
val <= 0;
// Advance by 0.50 time unit and toggle val
#0.50 $display ("T=%0t At time #0.50", $realtime);
val <= 1;
// Advance by 0.51 time unit and toggle val
#0.51 $display ("T=%0t At time #0.51", $realtime);
val <= 0;
// Let simulation run for another 5 time units and exit
#5 $display ("T=%0t End of simulation", $realtime);
end
endmodule
Actual simulation time is obtained by multiplying the delay specified using #
with the time unit and then it is rounded off based on precision. The first delay statement will then yield 10ns and the second one gives 14.9 which gets rounded to become 15ns.
The third statement similarly adds 5ns (0.5 * 10ns) and the total time becomes 20ns. The fourth one adds another 5ns (0.51 * 10) to advance total time to 25ns.
ncsim> run T=10 At time #1 T=15 At time #0.49 T=20 At time #0.50 T=25 At time #0.51 T=75 End of simulation ncsim: *W,RNQUIE: Simulation is complete.
Note that the base unit in waveform is in tens of nanoseconds with a precision of 1ns.

Example #3: 1ns/1ps
The only change made in this example compared to the previous one is that the timescale has been changed from 1ns/1ns to 1ns/1ps. So the time unit is 1ns and precision is at 1ps.
// Declare the timescale where time_unit is 1ns
// and time_precision is 1ps
`timescale 1ns/1ps
// NOTE: Testbench is the same as in previous example
module tb;
// To understand the effect of timescale, let us
// drive a signal with some values after some delay
reg val;
initial begin
// Initialize the signal to 0 at time 0 units
val <= 0;
// Advance by 1 time unit, display a message and toggle val
#1 $display ("T=%0t At time #1", $realtime);
val <= 1;
// Advance by 0.49 time unit and toggle val
#0.49 $display ("T=%0t At time #0.49", $realtime);
val <= 0;
// Advance by 0.50 time unit and toggle val
#0.50 $display ("T=%0t At time #0.50", $realtime);
val <= 1;
// Advance by 0.51 time unit and toggle val
#0.51 $display ("T=%0t At time #0.51", $realtime);
val <= 0;
// Let simulation run for another 5 time units and exit
#5 $display ("T=%0t End of simulation", $realtime);
end
endmodule
See that the time units scaled to match the new precision value of 1ps. Also note that time is represented in the smallest resolution which in this case is picoseconds.
ncsim> run T=1000 At time #1 T=1490 At time #0.49 T=1990 At time #0.50 T=2500 At time #0.51 T=7500 End of simulation ncsim: *W,RNQUIE: Simulation is complete.

Components in a testbench often need to communicate with each other to exchange data and check output values of the design. A few mechanisms that allow components or threads to affect the control flow of data are shown in the table below.
Events | Different threads synchronize with each other via event handles in a testbench |
Semaphores | Different threads might need to access the same resource; they take turns by using a semaphore |
Mailbox | Threads/Components need to exchange data with each other; data is put in a mailbox and sent |
What are Events ?
An event is a way to synchronize two or more different processes. One process waits for the event to happen while another process triggers the event. When the event
is triggered, the process waiting for the event will resume execution.
event
event eventA; // Creates an event called "eventA"
2. Trigger an event using ->
operator
->eventA; // Any process that has access to "eventA" can trigger the event
3. Wait for event to happen
@eventA; // Use "@" operator to wait for an event
wait (eventA.triggered); // Or use the wait statement with "eventA.triggered"
4. Pass events as arguments to functions
module tb_top;
event eventA; // Declare an event handle called "eventA"
initial begin
fork
waitForTrigger (eventA); // Task waits for eventA to happen
#5 ->eventA; // Triggers eventA
join
end
// The event is passed as an argument to this task. It simply waits for the event
// to be triggered
task waitForTrigger (event eventA);
$display ("[%0t] Waiting for EventA to be triggered", $time);
wait (eventA.triggered);
$display ("[%0t] EventA has triggered", $time);
endtask
endmodule
[0] Waiting for EventA to be triggered [5] EventA has triggered ncsim: *W,RNQUIE: Simulation is complete.
Click here to read more about a SystemVerilog event !
What's a semaphore ?
Let's say you wanted to rent a room in the library for a few hours. The admin desk will give you a key to use the room for the time you have requested access. After you are done with your work, you will return the key to the admin, which will then be given to someone else who wants to use the same room. This way two people will not be allowed to use the room at the same time. The key is a semaphore
in this context.
A semaphore is used to control access to a resource and is known as a mutex (mutually exclusive) because only one entity can have the semaphore at a time.
module tb_top;
semaphore key; // Create a semaphore handle called "key"
initial begin
key = new (1); // Create only a single key; multiple keys are also possible
fork
personA (); // personA tries to get the room and puts it back after work
personB (); // personB also tries to get the room and puts it back after work
#25 personA (); // personA tries to get the room a second time
join_none
end
task getRoom (bit [1:0] id);
$display ("[%0t] Trying to get a room for id[%0d] ...", $time, id);
key.get (1);
$display ("[%0t] Room Key retrieved for id[%0d]", $time, id);
endtask
task putRoom (bit [1:0] id);
$display ("[%0t] Leaving room id[%0d] ...", $time, id);
key.put (1);
$display ("[%0t] Room Key put back id[%0d]", $time, id);
endtask
// This person tries to get the room immediately and puts
// it back 20 time units later
task personA ();
getRoom (1);
#20 putRoom (1);
endtask
// This person tries to get the room after 5 time units and puts it back after
// 10 time units
task personB ();
#5 getRoom (2);
#10 putRoom (2);
endtask
endmodule
[0] Trying to get a room for id[1] ... [0] Room Key retrieved for id[1] [5] Trying to get a room for id[2] ... [20] Leaving room id[1] ... [20] Room Key put back id[1] [20] Room Key retrieved for id[2] [25] Trying to get a room for id[1] ... [30] Leaving room id[2] ... [30] Room Key put back id[2] [30] Room Key retrieved for id[1] [50] Leaving room id[1] ... [50] Room Key put back id[1]
Note the following about semaphores.
- A semaphore object key is declared and created using
new ()
function. Argument tonew ()
defines the number of keys. - You get the key by using the
get ()
keyword which will wait until a key is available (blocking) - You put the key back using the
put ()
keyword
Click here to read more about a SystemVerilog semaphore !
What's a mailbox ?
A mailbox is like a dedicated channel established to send data between two components.
For example, a mailbox
can be created and the handles be passed to a data generator and a driver. The generator can push the data object into the mailbox and the driver will be able to retrieve the packet and drive the signals onto the bus.
// Data packet in this environment
class transaction;
rand bit [7:0] data;
function display ();
$display ("[%0t] Data = 0x%0h", $time, data);
endfunction
endclass
// Generator class - Generate a transaction object and put into mailbox
class generator;
mailbox mbx;
function new (mailbox mbx);
this.mbx = mbx;
endfunction
task genData ();
transaction trns = new ();
trns.randomize ();
trns.display ();
$display ("[%0t] [Generator] Going to put data packet into mailbox", $time);
mbx.put (trns);
$display ("[%0t] [Generator] Data put into mailbox", $time);
endtask
endclass
// Driver class - Get the transaction object from Generator
class driver;
mailbox mbx;
function new (mailbox mbx);
this.mbx = mbx;
endfunction
task drvData ();
transaction drvTrns = new ();
$display ("[%0t] [Driver] Waiting for available data", $time);
mbx.get (drvTrns);
$display ("[%0t] [Driver] Data received from Mailbox", $time);
drvTrns.display ();
endtask
endclass
// Top Level environment that will connect Gen and Drv with a mailbox
module tb_top;
mailbox mbx;
generator Gen;
driver Drv;
initial begin
mbx = new ();
Gen = new (mbx);
Drv = new (mbx);
fork
#10 Gen.genData ();
Drv.drvData ();
join_none
end
endmodule
[0] [Driver] Waiting for available data [10] Data = 0x9d [10] [Generator] Put data packet into mailbox [10] [Generator] Data put into mailbox [10] [Driver] Data received from Mailbox [10] Data = 0x9d ncsim: *W,RNQUIE: Simulation is complete.
Click here to read more about a SystemVerilog mailbox !
What is functional coverage ?
Functional coverage is a measure of what functionalities/features of the design have been exercised by the tests. This can be useful in constrained random verification (CRV) to know what features have been covered by a set of tests in a regression.