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

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

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

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

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

1. Create an event using 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

  
Simulation Log
[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

  
Simulation Log
[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 to new () 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

  
Simulation Log
[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.