A generate block allows to multiply module instances or perform conditional instantiation of any module. It provides the ability for the design to be built based on Verilog parameters. These statements are particularly convenient when the same operation or module instance needs to be repeated multiple times or if certain code has to be conditionally included based on given Verilog parameters.

A generate block cannot contain port, parameter, specparam declarations or specify blocks. However, other module items and other generate blocks are allowed. All generate instantiations are coded within a module and between the keywords generate and endgenerate.

Generated instantiations can have either modules, continuous assignments, always or initial blocks and user defined primitives. There are two types of generate constructs - loops and conditionals.

Generate for loop

A half adder will be instantiated N times in another top level design module called my_design using a generate for loop construct. The loop variable has to be declared using the keyword genvar which tells the tool that this variable is to be specifically used during elaboration of the generate block.

  
  
// Design for a half-adder
module ha ( input   a, b,
            output  sum, cout);
 
  assign sum  = a ^ b;
  assign cout = a & b;
endmodule

// A top level design that contains N instances of half adder
module my_design 
	#(parameter N=4) 
		(	input [N-1:0] a, b,
			output [N-1:0] sum, cout);
			
	// Declare a temporary loop variable to be used during
	// generation and won't be available during simulation
	genvar i;
	
	// Generate for loop to instantiate N times
	generate 
		for (i = 0; i < N; i = i + 1) begin
          ha u0 (a[i], b[i], sum[i], cout[i]);
		end
	endgenerate
endmodule

  

Testbench

The testbench parameter is used to control the number of half adder instances in the design. When N is 2, my_design will have two instances of half adder.

  
  
module tb;
	parameter N = 2;
  reg  [N-1:0] a, b;
  wire [N-1:0] sum, cout;
  
  // Instantiate top level design with N=2 so that it will have 2
  // separate instances of half adders and both are given two separate
  // inputs
  my_design #(.N(N)) md( .a(a), .b(b), .sum(sum), .cout(cout));
  
  initial begin
    a <= 0;
    b <= 0;
    
    $monitor ("a=0x%0h b=0x%0h sum=0x%0h cout=0x%0h", a, b, sum, cout);
    
    #10 a <= 'h2;
    		b <= 'h3;
    #20 b <= 'h4;
    #10 a <= 'h5;
  end
endmodule

  

a[0] and b[0] gives the output sum[0] and cout[0] while a[1] and b[1] gives the output sum[1] and cout[1].

Simulation Log
ncsim> run
a=0x0 b=0x0 sum=0x0 cout=0x0
a=0x2 b=0x3 sum=0x1 cout=0x2
a=0x2 b=0x0 sum=0x2 cout=0x0
a=0x1 b=0x0 sum=0x1 cout=0x0
ncsim: *W,RNQUIE: Simulation is complete.
ncsim> exit

See that elaborated RTL does indeed have two half adder instances generated by the generate block.

Generate if

Shown below is an example using an if else inside a generate construct to select between two different multiplexer implementations. The first design uses an assign statement to implement a mux while the second design uses a case statement. A parameter called USE_CASE is defined in the top level design module to select between the two choices.

  
  
// Design #1: Multiplexer design uses an "assign" statement to assign 
// out signal 
module mux_assign ( input a, b, sel,
                   output out);
  assign out = sel ? a : b;
  
  // The initial display statement is used so that 
  // we know which design got instantiated from simulation
  // logs  
  initial
  	$display ("mux_assign is instantiated");
endmodule

// Design #2: Multiplexer design uses a "case" statement to drive
// out signal
module mux_case (input a, b, sel,
                 output reg out);
  always @ (a or b or sel) begin
  	case (sel)
    	0 : out = a;
   	 	1 : out = b;
  	endcase
  end
  
  // The initial display statement is used so that 
  // we know which design got instantiated from simulation
  // logs
  initial 
    $display ("mux_case is instantiated");
endmodule

// Top Level Design: Use a parameter to choose either one
module my_design (	input a, b, sel,
         			output out);
  parameter USE_CASE = 0;
  
  // Use a "generate" block to instantiate either mux_case
  // or mux_assign using an if else construct with generate
  generate
  	if (USE_CASE) 
      mux_case mc (.a(a), .b(b), .sel(sel), .out(out));
    else
      mux_assign ma (.a(a), .b(b), .sel(sel), .out(out));
  endgenerate
    
endmodule

  

Testbench

Testbench instantiates the top level module my_design and sets the parameter USE_CASE to 1 so that it instantiates the design using case statement.

  
  
module tb;
	// Declare testbench variables
  reg a, b, sel;
  wire out;
  integer i;
  
  // Instantiate top level design and set USE_CASE parameter to 1 so that
  // the design using case statement is instantiated
  my_design #(.USE_CASE(1)) u0 ( .a(a), .b(b), .sel(sel), .out(out));
  
  initial begin
  	// Initialize testbench variables
  	a <= 0;
    b <= 0;
    sel <= 0;
    
    // Assign random values to DUT inputs with some delay
    for (i = 0; i < 5; i = i + 1) begin
      #10 a <= $random;
      	  b <= $random;
          sel <= $random;
      $display ("i=%0d a=0x%0h b=0x%0h sel=0x%0h out=0x%0h", i, a, b, sel, out);
    end
  end
endmodule

  

When the parameter USE_CASE is 1, it can be seen from the simulation log that the multiplexer design using case statement is instantiated. And when USE_CASE is zero, the multiplexer design using assign statement is instantiated. This is visible from the display statement that gets printed in the simulation log.

Simulation Log
// When USE_CASE = 1
ncsim> run
mux_case is instantiated
i=0 a=0x0 b=0x0 sel=0x0 out=0x0
i=1 a=0x0 b=0x1 sel=0x1 out=0x1
i=2 a=0x1 b=0x1 sel=0x1 out=0x1
i=3 a=0x1 b=0x0 sel=0x1 out=0x0
i=4 a=0x1 b=0x0 sel=0x1 out=0x0
ncsim: *W,RNQUIE: Simulation is complete.

// When USE_CASE = 0
ncsim> run
mux_assign is instantiated
i=0 a=0x0 b=0x0 sel=0x0 out=0x0
i=1 a=0x0 b=0x1 sel=0x1 out=0x0
i=2 a=0x1 b=0x1 sel=0x1 out=0x1
i=3 a=0x1 b=0x0 sel=0x1 out=0x1
i=4 a=0x1 b=0x0 sel=0x1 out=0x1
ncsim: *W,RNQUIE: Simulation is complete.

Generate Case

A generate case allows modules, initial and always blocks to be instantiated in another module based on a case expression to select one of the many choices.

  
  
// Design #1: Half adder
module ha (input a, b,
           output reg sum, cout);
  always @ (a or b)
  {cout, sum} = a + b;
  
  initial
    $display ("Half adder instantiation");
endmodule

// Design #2: Full adder
module fa (input a, b, cin,
           output reg sum, cout);
  always @ (a or b or cin)
  {cout, sum} = a + b + cin;
  
    initial
      $display ("Full adder instantiation");
endmodule

// Top level design: Choose between half adder and full adder
module my_adder (input a, b, cin,
                 output sum, cout);
  parameter ADDER_TYPE = 1;
  
  generate
    case(ADDER_TYPE)
      0 : ha u0 (.a(a), .b(b), .sum(sum), .cout(cout));
      1 : fa u1 (.a(a), .b(b), .cin(cin), .sum(sum), .cout(cout));
    endcase
  endgenerate
endmodule

  

Testbench

  
  
module tb;
  reg a, b, cin;
  wire sum, cout;
  
  my_adder #(.ADDER_TYPE(0)) u0 (.a(a), .b(b), .cin(cin), .sum(sum), .cout(cout));
  
  initial begin
    a <= 0;
    b <= 0;
    cin <= 0;
    
    $monitor("a=0x%0h b=0x%0h cin=0x%0h cout=0%0h sum=0x%0h",
             a, b, cin, cout, sum);
    
    for (int i = 0; i < 5; i = i + 1) begin
      #10 a <= $random;
      b <= $random;
      cin <= $random;
    end
  end
endmodule

  

Note that because a half adder is instantiated, cin does not have any effect on the outputs sum and cout.

Simulation Log
ncsim> run
Half adder instantiation
a=0x0 b=0x0 cin=0x0 cout=00 sum=0x0
a=0x0 b=0x1 cin=0x1 cout=00 sum=0x1
a=0x1 b=0x1 cin=0x1 cout=01 sum=0x0
a=0x1 b=0x0 cin=0x1 cout=00 sum=0x1
ncsim: *W,RNQUIE: Simulation is complete.

The verilog always block can be used for both sequential and combinational logic. A few design examples were shown using an assign statement in a previous article. The same set of designs will be explored next using an always block.

Example #1 : Simple combinational logic

The code shown below implements a simple digital combinational logic which has an output signal called z of type reg that gets updated whenever one of the signals in the sensitivity list changes its value. The sensitivity list is declared within parentheses after the @ operator.

  
  
module combo ( 	input 	a, b, c, d, e,
								output 	reg z);

	always @ ( a or b or c or d or e) begin
		z = ((a & b) | (c ^ d) & ~e);
	end
	
endmodule

  

The module combo gets elaborated into the following hardware schematic using synthesis tools and can be seen that the combinational logic is implemented with digital gates.

simple combinational logic with assign

Use blocking assigments when modeling combinational logic with an always block

Testbench

The testbench is a platform for simulating the design to ensure that the design does behave as expected. All combinations of inputs are driven to the design module using a for loop with a delay statement of 10 time units so that the new value is applied to the inputs after some time.

  
  
module tb;
	// Declare testbench variables
  reg a, b, c, d, e;
  wire z;
  integer i;
  
  // Instantiate the design and connect design inputs/outputs with
  // testbench variables
  combo u0 ( .a(a), .b(b), .c(c), .d(d), .e(e), .z(z));
  
  initial begin
  	// At the beginning of time, initialize all inputs of the design
  	// to a known value, in this case we have chosen it to be 0.
    a <= 0;
    b <= 0;
    c <= 0;
    d <= 0;
    e <= 0;
    
    // Use a $monitor task to print any change in the signal to 
    // simulation console 
    $monitor ("a=%0b b=%0b c=%0b d=%0b e=%0b z=%0b", 
              a, b, c, d, e, z);
    
    // Because there are 5 inputs, there can be 32 different input combinations
    // So use an iterator "i" to increment from 0 to 32 and assign the value
    // to testbench variables so that it drives the design inputs
    for (i = 0; i < 32; i = i + 1) begin
      {a, b, c, d, e} = i;
      #10;
    end
  end
endmodule

  
Simulation Log
ncsim> run
a=0 b=0 c=0 d=0 e=0 z=0
a=0 b=0 c=0 d=0 e=1 z=0
a=0 b=0 c=0 d=1 e=0 z=1
a=0 b=0 c=0 d=1 e=1 z=0
a=0 b=0 c=1 d=0 e=0 z=1
a=0 b=0 c=1 d=0 e=1 z=0
a=0 b=0 c=1 d=1 e=0 z=0
a=0 b=0 c=1 d=1 e=1 z=0
a=0 b=1 c=0 d=0 e=0 z=0
a=0 b=1 c=0 d=0 e=1 z=0
a=0 b=1 c=0 d=1 e=0 z=1
a=0 b=1 c=0 d=1 e=1 z=0
a=0 b=1 c=1 d=0 e=0 z=1
a=0 b=1 c=1 d=0 e=1 z=0
a=0 b=1 c=1 d=1 e=0 z=0
a=0 b=1 c=1 d=1 e=1 z=0
a=1 b=0 c=0 d=0 e=0 z=0
a=1 b=0 c=0 d=0 e=1 z=0
a=1 b=0 c=0 d=1 e=0 z=1
a=1 b=0 c=0 d=1 e=1 z=0
a=1 b=0 c=1 d=0 e=0 z=1
a=1 b=0 c=1 d=0 e=1 z=0
a=1 b=0 c=1 d=1 e=0 z=0
a=1 b=0 c=1 d=1 e=1 z=0
a=1 b=1 c=0 d=0 e=0 z=1
a=1 b=1 c=0 d=0 e=1 z=1
a=1 b=1 c=0 d=1 e=0 z=1
a=1 b=1 c=0 d=1 e=1 z=1
a=1 b=1 c=1 d=0 e=0 z=1
a=1 b=1 c=1 d=0 e=1 z=1
a=1 b=1 c=1 d=1 e=0 z=1
a=1 b=1 c=1 d=1 e=1 z=1
ncsim: *W,RNQUIE: Simulation is complete.

Note that both methods, assign and always, get implemented into the same hardware logic.

Example #2: Half Adder

The half adder module accepts two scalar inputs a and b and uses combinational logic to assign the output signals sum and carry bit cout. The sum is driven by an XOR between a and b while the carry bit is obtained by an AND between the two inputs.

  
  
module ha ( input 	a, b,
						output	sum, cout);

	always @ (a or b) begin
		{cout, sum} = a + b;
	end

endmodule

  
half adder circuit with assign

Testbench

  
  
module tb;
	// Declare testbench variables
  reg a, b;
  wire sum, cout;
  integer i;

  // Instantiate the design and connect design inputs/outputs with
  // testbench variables  
  ha u0 ( .a(a), .b(b), .sum(sum), .cout(cout));
  
  initial begin
  	// At the beginning of time, initialize all inputs of the design
  	// to a known value, in this case we have chosen it to be 0.  
    a <= 0;
    b <= 0;
    
    // Use a $monitor task to print any change in the signal to 
    // simulation console     
    $monitor("a=%0b b=%0b sum=%0b cout=%0b", a, b, sum, cout);
    
    // Because there are only 2 inputs, there can be 4 different input combinations
    // So use an iterator "i" to increment from 0 to 4 and assign the value
    // to testbench variables so that it drives the design inputs    
    for (i = 0; i < 4; i = i + 1) begin
      {a, b} = i;
      #10;
    end
  end
endmodule

  
Simulation Log
ncsim> run
a=0 b=0 sum=0 cout=0
a=0 b=1 sum=1 cout=0
a=1 b=0 sum=1 cout=0
a=1 b=1 sum=0 cout=1
ncsim: *W,RNQUIE: Simulation is complete.

Example #3: Full Adder

An always block can be used to describe the behavior of a full adder to drive the outputs sum and cout.

  
  
module fa (	input 	a, b, cin,
			output reg	sum, cout);

  always @ (a or b or cin) begin
    {cout, sum} = a + b + cin;
  end

endmodule

  
full adder circuit with assign

Testbench

  
  
module tb;
  reg a, b, cin;
  wire sum, cout;
  integer i;
  
  fa u0 ( .a(a), .b(b), .cin(cin), .sum(sum), .cout(cout));
  
  initial begin
    a <= 0;
    b <= 0;
    
    $monitor("a=%0b b=%0b cin=%0b cout=%0b sum=%0b", a, b, cin, cout, sum);
    
    for (i = 0; i < 8; i = i + 1) begin
      {a, b, cin} = i;
      #10;
    end
  end
endmodule

  
Simulation Log
ncsim> run
a=0 b=0 cin=0 cout=0 sum=0
a=0 b=0 cin=1 cout=0 sum=1
a=0 b=1 cin=0 cout=0 sum=1
a=0 b=1 cin=1 cout=1 sum=0
a=1 b=0 cin=0 cout=0 sum=1
a=1 b=0 cin=1 cout=1 sum=0
a=1 b=1 cin=0 cout=1 sum=0
a=1 b=1 cin=1 cout=1 sum=1
ncsim: *W,RNQUIE: Simulation is complete.

Example #4: 2x1 Multiplexer

The simple 2x1 multiplexer uses a ternary operator to decide which input should be assigned to the output c. If sel is 1, output is driven by a and if sel is 0 output is driven by b.

  
  
module mux_2x1 (input 	a, b, sel,
				output 	reg c);
		
  
  always @ ( a or b or sel) begin
	c = sel ? a : b;
  end
endmodule

  
2x1 multiplexer

Testbench

  
  
module tb;
	// Declare testbench variables
  reg a, b, sel;
  wire c;
  integer i;
  
  // Instantiate the design and connect design inputs/outputs with
  // testbench variables  
  mux_2x1 u0 ( .a(a), .b(b), .sel(sel), .c(c));
  
  initial begin
  	// At the beginning of time, initialize all inputs of the design
  	// to a known value, in this case we have chosen it to be 0.    
    a <= 0;
    b <= 0;
    sel <= 0;
    
    $monitor("a=%0b b=%0b sel=%0b c=%0b", a, b, sel, c);

    for (i = 0; i < 3; i = i + 1) begin
      {a, b, sel} = i;
      #10;
    end
  end
endmodule

  
Simulation Log
ncsim> run
a=0 b=0 sel=0 c=0
a=0 b=0 sel=1 c=0
a=0 b=1 sel=0 c=1
ncsim: *W,RNQUIE: Simulation is complete.

Example #5: 1x4 Demultiplexer

The demultiplexer uses a combination of sel and f inputs to drive the different output signals. Each output signal is of type reg and used inside an always block that gets updated based on changes in the signals listed in the sensitivity list.

  
  
module demux_1x4 (	input 				f,
										input [1:0]	 	sel,
										output reg		a, b, c, d);

  always @ ( f or sel) begin
    a = f & ~sel[1] & ~sel[0];
    b = f &  sel[1] & ~sel[0];
    c = f & ~sel[1] &  sel[0];
    d = f &  sel[1] &  sel[0];
  end

endmodule

  
1x4 demultiplexer

Testbench

  
  
module tb;
	// Declare testbench variables
  reg f;
  reg [1:0] sel;
  wire a, b, c, d;
  integer i;
  
  // Instantiate the design and connect design inputs/outputs with
  // testbench variables  
  demux_1x4 u0 ( .f(f), .sel(sel), .a(a), .b(b), .c(c), .d(d));
  
  // At the beginning of time, initialize all inputs of the design
  // to a known value, in this case we have chosen it to be 0.  
  initial begin
    f <= 0;
    sel <= 0;
    
    $monitor("f=%0b sel=%0b a=%0b b=%0b c=%0b d=%0b", f, sel, a, b, c, d);
    
    // Because there are 3 inputs, there can be 8 different input combinations
    // So use an iterator "i" to increment from 0 to 8 and assign the value
    // to testbench variables so that it drives the design inputs    
    for (i = 0; i < 8; i = i + 1) begin
      {f, sel} = i;
      #10;
    end
  end
endmodule

  
Simulation Log
ncsim> run
f=0 sel=0 a=0 b=0 c=0 d=0
f=0 sel=1 a=0 b=0 c=0 d=0
f=0 sel=10 a=0 b=0 c=0 d=0
f=0 sel=11 a=0 b=0 c=0 d=0
f=1 sel=0 a=1 b=0 c=0 d=0
f=1 sel=1 a=0 b=0 c=1 d=0
f=1 sel=10 a=0 b=1 c=0 d=0
f=1 sel=11 a=0 b=0 c=0 d=1
ncsim: *W,RNQUIE: Simulation is complete.

Example #6: 4x16 Decoder

  
  
module dec_3x8 ( 	input 			en,
					input 	[3:0] 	in,
					output  reg [15:0] 	out);

  always @ (en or in) begin
    out = en ? 1 << in: 0;
  end
	
endmodule

  
4x16 decoder

Testbench

  
  
module tb;
  reg en;
  reg [3:0] in;
  wire [15:0] out;
  integer i;
  
  dec_3x8 u0 ( .en(en), .in(in), .out(out));
  
  initial begin
    en <= 0;
    in <= 0;
    
    $monitor("en=%0b in=0x%0h out=0x%0h", en, in, out);
    
    for (i = 0; i < 32; i = i + 1) begin
      {en, in} = i;
      #10;
    end
  end
endmodule

  
Simulation Log
ncsim> run
en=0 in=0x0 out=0x0
en=0 in=0x1 out=0x0
en=0 in=0x2 out=0x0
en=0 in=0x3 out=0x0
en=0 in=0x4 out=0x0
en=0 in=0x5 out=0x0
en=0 in=0x6 out=0x0
en=0 in=0x7 out=0x0
en=0 in=0x8 out=0x0
en=0 in=0x9 out=0x0
en=0 in=0xa out=0x0
en=0 in=0xb out=0x0
en=0 in=0xc out=0x0
en=0 in=0xd out=0x0
en=0 in=0xe out=0x0
en=0 in=0xf out=0x0
en=1 in=0x0 out=0x1
en=1 in=0x1 out=0x2
en=1 in=0x2 out=0x4
en=1 in=0x3 out=0x8
en=1 in=0x4 out=0x10
en=1 in=0x5 out=0x20
en=1 in=0x6 out=0x40
en=1 in=0x7 out=0x80
en=1 in=0x8 out=0x100
en=1 in=0x9 out=0x200
en=1 in=0xa out=0x400
en=1 in=0xb out=0x800
en=1 in=0xc out=0x1000
en=1 in=0xd out=0x2000
en=1 in=0xe out=0x4000
en=1 in=0xf out=0x8000
ncsim: *W,RNQUIE: Simulation is complete.

UVM accelerates the development process and facilitates re-use. Inorder to create a testbench from UVCs, you'll need to

  • Review the configuration parameters of each UVC
  • Instantiate and configure UVCs
  • Create re-usable sequences for interface components
  • Add a virtual sequencer
  • Add checking and functional coverage extensions
  • Create tests to cover all functionalities

Concurrent assertions describe behavior that spans over simulation time and are evaluated only at the occurence of a clock tick.

SystemVerilog concurrent assertion statements can be specified in a module, interface or program block running concurrently with other statements. Following are the properties of a concurrent assertion:

  • Test expression is evaluated at clock edges based on values in sampled variables
  • Sampling of variables is done in the preponed region and evaluation of the expression is done in the observed region of the simulation scheduler.
  • It can be placed in a procedural, module, interface, or program block
  • It can be used in both dynamic and formal verification techniques

Example #1

Two signals a and b are declared and driven at positive edges of a clock with some random value to illustrate how a concurrent assertion works. The assertion is written by the assert statement on an immediate property which defines a relation between the signals at a clocking event.

In this example, both signals a and b are expected to be high at the positive edge of clock for the entire simulation. The assertion is expected to fail for all instances where either a or b is found to be zero.

  
  
  module tb;
      bit a, b;
      bit clk;

      always #10 clk = ~clk;

      initial begin
          for (int i = 0; i < 10; i++) begin
              @(posedge clk);
              a <= $random;
              b <= $random;
              $display("[%0t] a=%0b b=%0b", $time, a, b);
          end
          #10 $finish;
      end
      
    // This assertion runs for entire duration of simulation
    // Ensure that both signals are high at posedge clk
    assert property (@(posedge clk) a & b);   

  endmodule

  

The assertion is executed on every positive edge of clk and evaluates the expression using values of variables in the preponed region, which is a delta cycle before given edge of clock. So, if a changes from 0 to 1 on the same edge as clock goes from 0 to 1, the value of a taken for assertion will be zero because it was zero just before the clock edge.

preponed-region systemverilog-concurrent-assertion

It can be seen that assertion fails for all cases where either a or b is found zero because the expression given within the assert statement is expected to be true for the entire duration of simulation.

Time (ns) a b Result
10 0 0 FAIL
30 0 1 FAIL
50 1 1 PASS
70 1 1 PASS
90 1 0 FAIL
110 1 1 PASS
130 0 1 FAIL
150 1 0 FAIL
170 1 0 FAIL
190 1 0 FAIL
Simulation Log
Compiler version P-2019.06-1; Runtime version P-2019.06-1;  Dec 11 14:46 2019
[10] a=0 b=0
testbench.sv", 24: tb.unnamed$$_4: started at 10ns failed at 10ns
	Offending '(a & b)'
[30] a=0 b=1
"testbench.sv", 24: tb.unnamed$$_4: started at 30ns failed at 30ns
	Offending '(a & b)'
[50] a=1 b=1
[70] a=1 b=1
[90] a=1 b=0
"testbench.sv", 24: tb.unnamed$$_4: started at 90ns failed at 90ns
	Offending '(a & b)'
[110] a=1 b=1
[130] a=0 b=1
"testbench.sv", 24: tb.unnamed$$_4: started at 130ns failed at 130ns
	Offending '(a & b)'
[150] a=1 b=0
"testbench.sv", 24: tb.unnamed$$_4: started at 150ns failed at 150ns
	Offending '(a & b)'
[170] a=1 b=0
"testbench.sv", 24: tb.unnamed$$_4: started at 170ns failed at 170ns
	Offending '(a & b)'
[190] a=1 b=0
"testbench.sv", 24: tb.unnamed$$_4: started at 190ns failed at 190ns
	Offending '(a & b)'
$finish called from file "testbench.sv", line 14.
$finish at simulation time                  200

Example #2

The expression defined as a property for the assert statement is modified from the above example to an OR condition.

  
  
  module tb;
      bit a, b;
      bit clk;

      always #10 clk = ~clk;

      initial begin
          for (int i = 0; i < 10; i++) begin
              @(posedge clk);
              a <= $random;
              b <= $random;
              $display("[%0t] a=%0b b=%0b", $time, a, b);
          end
          #10 $finish;
      end
      
    // This assertion runs for entire duration of simulation
    // Ensure that atleast 1 of the two signals is high on every clk
    assert property (@(posedge clk) a | b);   

  endmodule

  
concurrent-assertion-a-or-b
Time (ns) a b Result
10 0 0 FAIL
30 0 1 PASS
50 1 1 PASS
70 1 1 PASS
90 1 0 PASS
110 1 1 PASS
130 0 1 PASS
150 1 0 PASS
170 1 0 PASS
190 1 0 PASS
Simulation Log
Compiler version P-2019.06-1; Runtime version P-2019.06-1;  Dec 11 15:13 2019
[10] a=0 b=0
testbench.sv", 24: tb.unnamed$$_4: started at 10ns failed at 10ns
	Offending '(a | b)'
[30] a=0 b=1
[50] a=1 b=1
[70] a=1 b=1
[90] a=1 b=0
[110] a=1 b=1
[130] a=0 b=1
[150] a=1 b=0
[170] a=1 b=0
[190] a=1 b=0
$finish called from file "testbench.sv", line 14.

Example #3

The expression defined as a property for the assert statement is modified from the above example to an XNOR condition after negation of a.

  
  
  module tb;
      bit a, b;
      bit clk;

      always #10 clk = ~clk;

      initial begin
          for (int i = 0; i < 10; i++) begin
              @(posedge clk);
              a <= $random;
              b <= $random;
              $display("[%0t] a=%0b b=%0b", $time, a, b);
          end
          #10 $finish;
      end
      
    // This assertion runs for entire duration of simulation
    // Ensure that atleast 1 of the two signals is high on every clk
    assert property (@(posedge clk) !(!a ^ b));   

  endmodule

  
concurrent-assertion-not-a-xnor-b
Time (ns) a b Expression !( !a ^ b ) Result
10 0 0 0 FAIL
30 0 1 1 PASS
50 1 1 0 FAIL
70 1 1 0 FAIL
90 1 0 1 PASS
110 1 1 0 FAIL
130 0 1 1 PASS
150 1 0 1 PASS
170 1 0 1 PASS
190 1 0 1 PASS
Simulation Log
Compiler version P-2019.06-1; Runtime version P-2019.06-1;  Dec 11 15:26 2019
[10] a=0 b=0
"testbench.sv", 24: tb.unnamed$$_4: started at 10ns failed at 10ns
	Offending '(!((!a) ^ b))'
[30] a=0 b=1
[50] a=1 b=1
"testbench.sv", 24: tb.unnamed$$_4: started at 50ns failed at 50ns
	Offending '(!((!a) ^ b))'
[70] a=1 b=1
"testbench.sv", 24: tb.unnamed$$_4: started at 70ns failed at 70ns
	Offending '(!((!a) ^ b))'
[90] a=1 b=0
[110] a=1 b=1
"testbench.sv", 24: tb.unnamed$$_4: started at 110ns failed at 110ns
	Offending '(!((!a) ^ b))'
[130] a=0 b=1
[150] a=1 b=0
[170] a=1 b=0
[190] a=1 b=0
$finish called from file "testbench.sv", line 14.
$finish at simulation time                  200

What is the UVM register layer ?

The UVM register layer classes are used to create a high-level, object-oriented model for memory-mapped registers and memories in a design under verification (DUV). The register layer defines many base classes which can be extended appropriately to abstract read and write operations to the DUV. Before we go into the details of UVM register layer, let's first review how registers are organized and how they function in a digital design.

What are registers ?

Most digital design blocks have software controllable registers that can be accessed via a peripheral bus. These registers allow the hardware to behave in certain ways when programmed with certain values. For example, there could be a 32-bit register with several individual fields within it. Each field represents a particular feature that can be configured by software when required.


hardware-register-example

The figure above is an example of a single register within the design with five different functional fields. Field properties may be specified by the designer as below.