Thursday, June 23, 2022

Utilizing Enumerations in Functional Coverage for Parameterized Buses & Address Spaces

I am a fan of utilizing enumerations in functional coverage. One such use was presented in a previous post, lets take a look at another use that can simplify both implementation and maintenance for parameterized bus widths. 

The example design for this post supports a parameterized address bus, each instance has the potential of a different address width, a maximum of up to thirty two bits is never exceeded. Lets also define the coverage bins for the bus as bit based where the highest set MSB is collected per sampled value. 

What would be the most basic functional coverage implementation? Coding coverpoint bins to the maximum bus width of thirty two bits guarantees support for all bus widths, instances utilizing narrow widths will require coverage exclusions.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class block_cov;
  covergroup block_cg with function sample(bit[31:0] address);
    address_ranges_c : coverpoint address {
      bins BIT0  = {[('h0)   :( ('h1<<1)-1 )]};
      bins BIT1  = {[('h1<<1):( ('h1<<2)-1 )]};
      bins BIT2  = {[('h1<<2):( ('h1<<3)-1 )]};
      bins BIT3  = {[('h1<<3):( ('h1<<4)-1 )]};
      bins BIT4  = {[('h1<<4):( ('h1<<5)-1 )]};
      bins BIT5  = {[('h1<<5):( ('h1<<6)-1 )]};
      //..
      //..
      bins BIT31 = {['h1<<31:$]};

    } 
  endgroup: block_cg
  //-----------------
  function new();
    block_cg = new;
  endfunction
endclass: block_cov

Since this post is about enumerations and functional coverage, the first step toward the sought solution is to covert the coverpoint from a bit based variable to an enumeration type. This conversion will require populating an enumeration variable before calling the covergroup sample method, a wrapper method to the sample call provides this ability. While this change may initially appear cumbersome the full picture will be clear shortly.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
typedef enum {
  RANGE_BIT_0 = 0,
  RANGE_BIT_1,
  RANGE_BIT_2,
  RANGE_BIT_3,
  RANGE_BIT_4,
  
  //...
  //...
  
  RANGE_BIT_30,
  RANGE_BIT_31
} address_range_e;

//----------------------------------------------------------------------
class block_cov;
  covergroup block_cg with function sample(address_range_e address_range);
    address_ranges_c : coverpoint address_range;  
  endgroup: block_cg
  //-----------------
  function new();
    block_cg = new;
  endfunction
  //-----------------
  function void do_sample(bit[31:0] address);
    address_range_e range;
    
    case(address) inside 
      [('h0)    :( ('h1<<1) -1 )]: range = RANGE_BIT_0; 
      [('h1<<1) :( ('h1<<2) -1 )]: range = RANGE_BIT_1; 
      [('h1<<2) :( ('h1<<3) -1 )]: range = RANGE_BIT_2; 
      [('h1<<3) :( ('h1<<4) -1 )]: range = RANGE_BIT_3; 
      [('h1<<4) :( ('h1<<5) -1 )]: range = RANGE_BIT_4; 
      
      //...
      //...

      [('h1<<30):( ('h1<<31)-1 )]: range = RANGE_BIT_30; 
      [('h1<<31):( $           )]: range = RANGE_BIT_31; 
    endcase

    block_cg.sample( .address_range(range) );

  endfunction: do_sample
endclass: block_cov

Now that some groundwork has been laid, lets begin optimizing this code one section at a time. First up would be to recode the enumeration definition be taking advantage of enumeration type ranges as described in the LRM 1800-2017 section 6.19.2.

1
2
3
4
5
`define ADR_WIDTH 10 
//----------------------------------------------------------------------
typedef enum {
  RANGE_BIT_[`ADR_WIDTH] = 0
} address_range_e;

Now lets take another look at the population of the enumeration variable. It doesn't require more that a quick glance to recognize that this code section is both bug and maintenance prone. The same result can be produced by searching for the highest set MSB with a loop. 

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
  function void do_sample(bit[31:0] address);
    address_range_e range;
    
    range = RANGE_BIT_0; // will default to this if address is zero
    for(int bit_num=`ADR_WIDTH; bit_num; bit_num--) begin
      if (address[bit_num-1] == 1) begin
        range = address_range_e'(bit_num-1);
        break;
      end
    end

    block_cg.sample( .address_range(range) );

  endfunction: do_sample

By taking advantage of the bit based range definition, the enumeration population can go through another optimization step by utilizing system tasks that further simplify this code section.

1
2
3
4
5
6
7
8
9
  function void do_sample(bit[31:0] address);
    address_range_e range;
    
    if(address == 0) range  = RANGE_BIT_0;
    else             range  = address_range_e'($onehot(address)? $clog2(address) : $clog2(address)-1);
    
    block_cg.sample( .address_range(range) );

  endfunction: do_sample

Now that all the building blocks are ready lets put them all together creating elegant, clear and easily maintainable code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//----------------------------------------------------------------------
`define ADR_WIDTH 10 
//----------------------------------------------------------------------
typedef enum {
  RANGE_BIT_[`ADR_WIDTH] = 0
} address_range_e;
//----------------------------------------------------------------------
class block_cov;
  covergroup block_cg with function sample(address_range_e address_range);
    address_ranges_c : coverpoint address_range;  
  endgroup: block_cg
  //-----------------
  function new();
    block_cg = new;
  endfunction
  //-----------------
  function void do_sample(bit[31:0] address);
    address_range_e range;
    
    if(address == 0) range  = RANGE_BIT_0;
    else             range  = address_range_e'($onehot(address)? $clog2(address) : $clog2(address)-1);
    
    block_cg.sample( .address_range(range) );

  endfunction: do_sample
endclass: block_cov

Lets take this implementation another step by adapting it to a different design construct, one that could easily support variable address spaces. 

In this new design, the address bus is always defined as thirty two bits however the actual supported address space can be lower than the maximum range. Functional coverage is still defined as bit based. With a few minor tweaks the solution presented above is viable for these requirements and has an added bonus of identifying out or range requests. 

First off, lets expand the enumeration definition to have both valid values and a single out of range value, the preprocessing define of ADR_WIDTH now represents the valid address range.

1
2
3
4
typedef enum {
  RANGE_BIT_[`ADR_WIDTH] = 0
  RANGE_OUT_OF_RANGE
} address_range_e;

The sample wrapper method recognizes invalid ranges by dynamically casting the received MSB value on to the enumeration, a failed casting indicates the request is indeed out of range. 

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
  function void do_sample(bit[31:0] address);
    logic [31:0]    bit_num = '0;
    address_range_e range   = RANGE_OUT_OF_RANGE;

    // address of zero get special attention, all other values are evaluated with the same line
    case(1) 
      (address == 0): bit_num = 0;
      default:        bit_num = $onehot(address)? $clog2(address) : $clog2(address)-1;
    endcase

    if(!$cast(range, bit_num)) range = RANGE_OUT_OF_RANGE; // default to OUT_OF_RANGE if casting fails 
    
    block_cg.sample( .address_range(range) );

  endfunction: do_sample

Now lets aggregate the previous solution with these updates into a full code snippet.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//----------------------------------------------------------------------
`define ADR_WIDTH 8 
//----------------------------------------------------------------------
typedef enum {
  RANGE_BIT_[`ADR_WIDTH] = 0,
  RANGE_OUT_OF_RANGE
} address_range_e;
//----------------------------------------------------------------------
class block_cov;
  covergroup block_cg with function sample(address_range_e address_range);
    address_ranges_c : coverpoint address_range;  
  endgroup: block_cg
  //-----------------
  function new();
    block_cg = new;
  endfunction
  //-----------------
  function void do_sample(bit[31:0] address);
    logic [31:0]    bit_num = '0;
    address_range_e range   = RANGE_OUT_OF_RANGE;

    // address of zero get special attention, all other values are evaluated with the same line
    case(1) 
      (address == 0): bit_num = 0;
      default:        bit_num = $onehot(address)? $clog2(address) : $clog2(address)-1;
    endcase

    if(!$cast(range, bit_num)) range = RANGE_OUT_OF_RANGE; // default to OUT_OF_RANGE if casting fails 
    
    block_cg.sample( .address_range(range) );

  endfunction: do_sample
endclass: block_cov


Tuesday, June 4, 2019

Using Enumeration Types for Testbench Definitions & Connectivity

It is very popular for environments to have multiple agents of the same type. One common method to maintain a list of multiple agents is to define an enumeration for each agent type. Using this method has a number of positive features such as unique naming of agents, simplicity in maintaining lists of agents from project to project and readability. This post will focus on the use of enums for testbench needs such as interface definition and wire-up, environment implementations will have a separate post so stay tuned.
Lets dive right into it and start off with what would seem to be an intuitive and simple implementation and then dissect issues that crop up.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package my_pkg;
  typedef enum {
    ENUM1,
    ENUM2,
    ENUM3
  } my_enum_e;
  
  // `include files ....
endpackage: my_pkg
//-----------------------------
interface my_if();
  logic [31:0] sig1;
endinterface: my_if
//-----------------------------
module tb();
  import my_pkg::*;
  my_enum_e e;
  
  generate for( genvar i=0; i<e.num();i++) begin : my_genvar
    my_if my_if_i();

    assign my_if_i.sig1 = i+4;// different per interface
  end endgenerate
  
endmodule: tb

When trying to simulate this code some vendors allow this implementation while others reported a compile/elaboration failure on line 19. In theory there shouldn't be a limitation for this implementation, the enum size value is constant and can't be dynamically updated effectively making the "num()" method return value a constant. I can theorize why some vendors are creating this failure but don't want to speculate on that here.
So if the vendor you're using allows this syntax and you have no need to support multiple vendors, everything is great. For those of you with failures, lets present a different solution.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package my_pkg;
  typedef enum {
    ENUM1,
    ENUM2,
    ENUM3,
    MY_ENUM_MAX
  } my_enum_e;
  
  // `include files ....
endpackage: my_pkg
//-----------------------------
interface my_if();
  logic [31:0] sig1;
endinterface: my_if
//-----------------------------
module tb();
  import my_pkg::*;
  my_enum_e e;
  
  generate for( genvar i=0; i<my_pkg::MY_ENUM_MAX;i++) begin : my_genvar
    my_if my_if_i();

    assign my_if_i.sig1 = i+4;// different per interface
  end endgenerate
  
endmodule: tb

In the implementation above a value was added to the enum definition on line 6. How does this addition help solve our compilation/elaboration issue? The answer is in understanding how enumerations are  implementation as stated in the LRM section 6.19. You are welcome to read this section in depth, here are a couple of guidelines taken directly from the LRM which will help explain the solution above.

1. "In the absence of a data type declaration, the default data type shall be int."
2. "A name without a value is automatically assigned an increment of the value of the previous name. ... If the first name is not assigned a value, it is given the initial value of 0."

In the current solution, values assigned to the enum definitions start at zero and increment per definition. Appending the end of the enum with an additional value inexplicitly makes the last value hold the number of defined enumerations.
Great! A solution for all is in sight. Well... there is one possible pitfall that should be addressed:
when and if the enum needs to have values added to it there is a risk that the new values will be added after the MY_ENUM_MAX. Now if you are sure that you or your team can never make such a mistake, all the more power to you. And even though such an issue will eventually wash out, debugging this sort of failure can be frustrating and time consuming. So how can we have our cake and eat it too? Lets present yet another solution which reports erroneous coding when incorrect modifications is done to the enum.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package my_pkg;
  typedef enum {
    ENUM1,
    ENUM2,
    ENUM3
  } my_enum_e;
  
  parameter MY_ENUM_E_SIZE = ENUM3+1; // update to last value when enum is modified

  // `include files ....
endpackage: my_pkg
//-----------------------------
interface my_if();
  logic [31:0] sig1;
endinterface: my_if
//-----------------------------
module tb();
  import my_pkg::*;
  my_enum_e e;
  
  generate for( genvar i=0; i<my_pkg::MY_ENUM_E_SIZE;i++) begin : my_genvar
    my_if my_if_i();

    assign my_if_i.sig1 = i+4;// different per interface
  end endgenerate
  // ------------------
  // in an UVM env, this check can be done in one of the build phases (i.e. env or base test)
  initial begin 
    e = e.last();

    if(e.num() !== my_pkg::MY_ENUM_E_SIZE) 
      $fatal("enum size %0d which differs from parameter MY_ENUM_E_SIZE value of %0d, last value is %s ", e.num(), 
                                                                                                          my_pkg::MY_ENUM_E_SIZE, 
                                                                                                          e.name() );
  end
endmodule: tb

In the last example the enum definition no longer has the max enum size where on line 8 a parameter was added to the package holding the current enum size. This parameter is used in a run time checker shown on lines 28 to 35 verifying the enum size is as expected. If your environment is UVM based, such a checker can be coded into the build phase of the env or base test.

Thursday, March 15, 2018

Using Object Members in Functional Coverage Coverbins

While implementing a functional coverage model one of the requirements was to create coverbins whose values are a function of the sampled objects members. Below is a simplified example of the initial implementation with a sampled class object and a coverpoint meeting these requirements.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class my_item;
  rand bit [7:0] data_size;
  rand bit [7:0] inside_data_size;

  constraint my_c { 
    inside_data_size <= data_size;
    inside_data_size >= 0;
  }
endclass
//-----------------------------------------//
class my_cov;
my_item itm_p;
//---------//
covergroup my_grp;
  data_size_cp:        coverpoint itm_p.data_size;
  
  inside_data_size_cp: coverpoint itm_p.inside_data_size {
    bins  zero        = {0};
    bins  lower_half  = {[1:(itm_p.data_size/2)-1]};
    bins  half        = {itm_p.data_size/2};
    bins  upper_half  = {[(itm_p.data_size/2)+1:itm_p.data_size-1]};
    bins  max         = {itm_p.data_size};
  }
endgroup
//----------//
function new();
  my_grp = new;
endfunction: new
//----------//
task do_sample();
  my_grp.sample();
endtask: do_sample
endclass
//-----------------------------------------//
module tb();
initial begin
  my_item itm_p;
  my_cov  cov_p;
  
  cov_p = new();
  itm_p = new();

  for(int i = 0; i < 10; i++) begin
    itm_p.randomize();
    cov_p.itm_p = itm_p;
    cov_p.do_sample();
  end
end
endmodule: tb

Needless to say, this coverpoint implementation would not work. If you don't see the problem here, I will point it out; When the covergroup is created, there is an attempt to access the object member "itm_p.data_size" for the coverbin "inside_data_size_cp". Since an object has not been created yet and the current object pointer is null, a null pointer access is triggered terminating the simulation. So such an implementation is obviously not the way to go however the dependency for our coverage model still exists. Lets go for a different coverbin implementation, one that will work. 
What is needed is a variable which is a function of the data size but not a member of the sampled object. This new variable can be a member is the coverage class wrapper and updated right before the covergroup sampling. 

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class my_cov;

typedef enum {
  ZERO = 0,
  LOWER_HALF,
  HALF,
  UPPER_HALF,
  MAX
} e_inside_data_size;

my_item             itm_p;
e_inside_data_size  inside_data_size_e;
//---------//
covergroup my_grp;
  data_size_cp:        coverpoint itm_p.data_size;
  inside_data_size_cp: coverpoint inside_data_size_e;
endgroup
//----------//
function new();
  my_grp = new;
endfunction: new
//----------//
task do_sample();
  case(1)
    (itm_p.inside_data_size == 0):                  inside_data_size_e = ZERO;
    (itm_p.inside_data_size > 0 && 
     itm_p.inside_data_size < itm_p.data_size/2):   inside_data_size_e = LOWER_HALF;
    (itm_p.inside_data_size == itm_p.data_size/2):  inside_data_size_e = HALF;
    (itm_p.inside_data_size > itm_p.data_size/2 && 
     itm_p.inside_data_size < itm_p.data_size ):    inside_data_size_e = UPPER_HALF;
    (itm_p.inside_data_size == itm_p.data_size):    inside_data_size_e = MAX;
    default: $error("inside_data_size=%x is not within data_size of %x", itm_p.inside_data_size, itm_p.data_size);
  endcase

  my_grp.sample();
endtask: do_sample
endclass

In the last example an enum type was created defining different value ranges for our coverbin. A class member is defined in the coverage wrapper class using this enum as the type. Right before the covergroup is sampled, this member is updated as a function of the received item using the case(1) statement. So now the coverbin no longer creates a null pointer access and the required dependency for the functional coverage is fulfilled.

Sunday, January 8, 2017

Implementing Reusable UVM Sequences

In many designs, it is not uncommon for blocks and/or registers to be reused multiple times. Implementation of Register Abstraction Layer (RAL) sequences for replicated design blocks and/or registers greatly reduces time spent coding and automatically permeates environment bug fixes. Let's take a look how such flows can be implemented.

Example RAL structure 

Before getting into the heart of things, an example RAL structure referenced throughout this post is shown in the code snippet below:

class ral_my_reg extends uvm_reg;
  rand uvm_reg_field FIELD1;
  rand uvm_reg_field FIELD2;
.
.
endclass : ral_my_reg
//-----------------------------------------//
class ral_my_block extends uvm_reg_block;
  rand ral_my_reg REG1;
  rand ral_my_reg REG2;
.
.
endclass : ral_my_block
//-----------------------------------------//
class ral_my_sys extends uvm_reg_block;
  rand ral_my_block BLOCK1;
  rand ral_my_block BLOCK2;
.
.
endclass : ral_my_sys

Different Sequence Implementations for Each Block

Let's start at the beginning: how would sequences be implemented without any code reuse in mind? Each sequence would hold the same functional code using different RAL pointers to differentiate different block/register access.

class block1_sequence extends uvm_sequence #(sequence_item);

task body();
  ral.BLOCK1.REG1.write( .status(status), .value(data) );
  ral.BLOCK1.REG2.read(  .status(status), .value(data) );
endtask: body

endclass: block1_sequence
//-----------------------------------------//
class block2_sequence extends uvm_sequence #(sequence_item);

task body();
  ral.BLOCK2.REG1.write( .status(status), .value(data) );
  ral.BLOCK2.REG2.read(  .status(status), .value(data) );
endtask: body

endclass: block2_sequence
//------------------------------------------------------------------//
class block_access_test extends uvm_test;
  
  // boilerplate code excluded

task main_phase(uvm_phase phase);
  block1_sequence block1_seq;
  block2_sequence block2_seq;
  
  super.main_phase(phase);
  phase.raise_objection(this);
  
  block1_seq = block1_sequence::type_id::create("block1_seq");
  block1_seq.start(null);
  
  block2_seq = block2_sequence::type_id::create("block2_seq");
  block2_seq.start(null);
  
  // add more blocks as needed
  
  phase.drop_objection(this);
  
endtask: main_phase

endclass: block_access_test

This example shows only 2 blocks but can easily represent N blocks.

Sequence Reuse for reused Design Blocks

When coding a sequence for reuse between replicated blocks, a RAL block pointer is used for the functional code where the decision of which block is under test is decided by the sequence caller. In the example below, the test creates and holds the sequence and decides which block to verify by passing the appropriate pointer before execution.

class my_block_sequence extends uvm_sequence #(sequence_item);
  ral_my_block myblk;

task body();  
  if(myblk == null) `uvm_fatal(get_type_name(), $sformatf("pointer myblk is null!") )
  
  myblk.REG1.write( .status(status), .value(data) );
  myblk.REG2.read(  .status(status), .value(data) );
endtask: body

endclass: my_block_sequence
//------------------------------------------------------------------//
class block_access_test extends uvm_test;

task main_phase(uvm_phase phase);
  my_block_sequence my_block_seq;
  
  super.main_phase(phase);
  phase.raise_objection(this);
  
  my_block_seq = my_block_sequence::type_id::create("my_block_seq");
  
  my_block_seq.myblk = ral.BLOCK1;
  my_block_seq.start(null);
  
  my_block_seq.myblk = ral.BLOCK2;
  my_block_seq.start(null);

  phase.drop_objection(this);
  
endtask: main_phase

endclass: block_access_test


The test code could be further simplified by creating a queue of the block pointers and executing the code using a foreach statement.

task main_phase(uvm_phase phase);
  ral_my_block my_blocks[$] = '{ral.BLOCK1, ral.BLOCK2};
  my_block_sequence my_block_seq;
  
  super.main_phase(phase);
  phase.raise_objection(this);
  
  my_block_seq = my_block_sequence::type_id::create("my_block_seq");
  
  foreach(my_blocks[i]) begin
    my_block_seq.myblk = my_blocks[i];
    my_block_seq.start(null);
  end
  
  phase.drop_objection(this);
  
endtask: main_phase

Sequence Reuse for Different Design Blocks

It is possible that multiple non-replicated blocks in a design have the same functionality implemented. Since the block types differ, a specific block type pointer can no longer be used and a parent pointer will take it's place. One needs to remember that members defined in the derived class cannot be directly accessed using the parent pointer. For this purpose, UVM RAL classes have built-in methods allowing retrieval of member pointers in derived classes using strings.

class my_block_sequence extends uvm_sequence #(sequence_item);
  uvm_reg_block myblk;

task body();  
  if(myblk == null) `uvm_fatal(get_type_name(), $sformatf("pointer myblk is null!") )
  
  myblk.get_reg_by_name("REG1").write( .status(status), .value(data) );
  myblk.get_reg_by_name("REG2").read(  .status(status), .value(data) );
endtask: body

endclass: my_block_sequence

Register Reuse for Identical Functionality

Just like sequences, registers can and are replicated with identical functionality in design blocks. What coding reuse options are available for identical registers/fields? As with the sequences let's look at a number of examples showing how one would code without reuse and then with reuse in mind.

class my_block_sequence extends uvm_sequence #(sequence_item);
  ral_my_block myblk;

task body();  
  if(myblk == null) `uvm_fatal(get_type_name(), $sformatf("pointer myblk is null!") )
  
  myblk.REG1.write( .status(status), .value(data) );
  myblk.REG1.mirror( .status(status) );
  
  if( myblk.REG1.FIELD1.get() !== 1 ) `uvm_error(get_type_name(), $sformatf("value of FIELD1 expected 1, received value %0h", myblk.REG1.FIELD1.get()) )
  else                                `uvm_info( get_type_name(), $sformatf("value of FIELD1 expected 1, received value %0h", myblk.REG1.FIELD1.get()), UVM_NONE )

  myblk.REG2.write( .status(status), .value(data) );
  myblk.REG2.mirror( .status(status) );
  
  if( myblk.REG2.FIELD1.get() !== 1 ) `uvm_error(get_type_name(), $sformatf("value of FIELD1 expected 1, received value %0h", myblk.REG2.FIELD1.get()) )
  else                                `uvm_info( get_type_name(), $sformatf("value of FIELD1 expected 1, received value %0h", myblk.REG2.FIELD1.get()), UVM_NONE )
  
endtask: body
endclass: my_block_sequence


So what would be the first step to making the code more generic? By referencing the previous section of this post on block reuse, register accesses can be executed using a parent pointer of uvm_reg. Derived class members are then retrieved via the built in UVM string search methods.

class my_block_sequence extends uvm_sequence #(sequence_item);
  ral_my_block myblk;
//---------//
task check_ral_my_reg_functionality(uvm_reg current_reg);
  if(current_reg == null) `uvm_fatal(get_type_name(), $sformatf("pointer current_reg is null!") )
  
  current_reg.write( .status(status), .value(data) );
  current_reg.mirror( .status(status) );
  
  if( current_reg.get_field_by_name("FIELD1").get() !== 1 ) `uvm_error(get_type_name(), $sformatf("value of FIELD1 expected 1, received value %0h", current_reg.get_field_by_name("FIELD1").get()) )
  else                                                      `uvm_info( get_type_name(), $sformatf("value of FIELD1 expected 1, received value %0h", current_reg.get_field_by_name("FIELD1").get()), UVM_NONE )
  
endtask: check_ral_my_reg_functionality
//---------//
task body();  
  if(myblk == null) `uvm_fatal(get_type_name(), $sformatf("pointer myblk is null!") )
  
  check_ral_my_reg_functionality( .current_reg(myblk.REG1) );
  check_ral_my_reg_functionality( .current_reg(myblk.REG2) );
  
endtask: body
endclass: my_block_sequence

The previous code snippet is already looking pretty good for reuse. But, with a few more tweaks, a single functional implementation is feasible without any prior knowledge of either the register or field types/names.

class my_block_sequence extends uvm_sequence #(sequence_item);
  ral_my_block myblk;
//---------//
task check_ral_my_reg_functionality(uvm_reg current_reg, string field_to_verify);
  if(current_reg == null) `uvm_fatal(get_type_name(), $sformatf("pointer current_reg is null!") )
  
  current_reg.write( .status(status), .value(data) );
  current_reg.mirror( .status(status) );
  
  if( current_reg.get_field_by_name(field_to_verify).get() !== 1 ) `uvm_error(get_type_name(), $sformatf("value of %s expected 1, received value %0h", field_to_verify, current_reg.get_field_by_name(field_to_verify).get()) )
  else                                                             `uvm_info( get_type_name(), $sformatf("value of %s expected 1, received value %0h", field_to_verify, current_reg.get_field_by_name(field_to_verify).get()), UVM_NONE )
  
endtask: check_ral_my_reg_functionality
//---------//
task body();  
  if(myblk == null) `uvm_fatal(get_type_name(), $sformatf("pointer myblk is null!") )
  
  check_ral_my_reg_functionality( .current_reg(myblk.REG1), .field_to_verify("FIELD1") );
  check_ral_my_reg_functionality( .current_reg(myblk.REG2), .field_to_verify("FIELD1") );
  
endtask: body
endclass: my_block_sequence


Wednesday, July 20, 2016

Correct Definition of Time Components in RTL Simulations

Over a number of projects I've run into many different needs and issues regarding time component definitions. I've concentrated my findings into a paper which gives a general background to the reader on time components, describes many issues which can be encountered and how to avoid pitfalls.

I am sharing this document for the greater good in hope that it may help others avoid many issues on this subject. Enjoy!

Link to paper

Wednesday, May 4, 2016

Using uvm_object Hook Methods to Implement User Specific Functionality

It is sometimes necessary to modify or add functionality to existing built-in methods in uvm objects. Numerous methods in the uvm object class have build-in hooks called do_<method name> allowing user to extend existing implementations of the <method name>. These hooks are implemented as empty methods in the uvm_object class type and can be overloaded in a derived class with user specific functionality. Each hook is called automatically by its respective default method call.

function bit  uvm_object::compare (uvm_object rhs, uvm_comparer comparer=null);
.
// UVM implementation here
.
dc = do_compare(rhs, comparer); // call to hook
.
.

But what happens when the default implementation cannot be used due to a corner case or if there is a special implementation requirement? Since the default methods are non-virtual they cannot be overloaded in derived classes.
One way to work around this is to create a new unrelated method, lets call it  "my_compare", and call this instead of the default method call. Using this type of workaround should be considered only as a last resort as it can hinder reuse of existing or future code. For example, if the class is an inheritance of a sequence item and factory replaced, a "my_compare" implementation might limit an existing scoreboard receiving and checking these items with the default compare method.
Fortunately, the uvm framework has considered this use case where the default implementation should be circumvented while allowing the user defined code to run from the hooked method.

class my_sequence_item extends uvm_sequence_item;

  rand logic [31:0]  var1;
  rand logic [7:0]   var2;

  `uvm_object_utils_begin (my_sequence_item)
    `uvm_field_int( var1, UVM_ALL_ON | UVM_NOCOMPARE)
    `uvm_field_int( var2, UVM_ALL_ON | UVM_NOCOMPARE)
  `uvm_object_utils_end

  extern virtual function bit  do_compare(uvm_object rhs,uvm_comparer comparer);

endclass: my_sequence_item
//------------------------------------------------------------------------//
function bit my_sequence_item::do_compare(uvm_object rhs,uvm_comparer comparer);
.
// implementation here
.
endfunction: do_compare

Using the compare function as our example, class members var1 and var2 in the code snippet above are registered to be ignored when calling the default compare method. The  do_compare hook is still executed by the default method call executing the user specific code. This ability allows a call to the default compare method without any need to know that the inherited object has a specialized functionality implemented or that the compare method implementation is circumvented.

Monday, March 7, 2016

Connecting Bi-directional DUT Ports to an Interface

It isn't rare to find SOC top levels designs with bi-directions pins. It also isn't rare to use purchased VIPs to verify protocols requiring bi-directional connectivity.
VIPs come with pre-coded interfaces which are required to be used by the integrator. There are a number of different interface coding methods which include bi-direction ports and require different wire-up connection methods to a DUT. This post presents different interface/DUT connection implementations and to limitations found in each different methodology.

Interface Nets Defined in its Port List

When the interface is coded with connection nets in its port list, a connecting wire between the interface and DUT instances is sufficient to create a connection between the two.

interface some_interface(inout wire  bidir1, 
                         inout wire  bidir2,
                         input logic in1
                        );
endinterface: some_interface
//----------------------------//
module tb();

wire sig1;
wire sig2;
wire sig3;
.
.
some_interface s_if( .bidir1(sig1),
                     .bidir2(sig2),
                     .in1(   sig3)
                    );

dut my_dut(.sig1(sig1),
           .sig2(sig2),
           .sig3(sig3),
           .
           .
           );

endmodule: tb

An interface with nets defined in its port list gives the most flexibility for wire-up connections. In some cases the connection could be simplified even more if the DUT and interface have the same port names  by using the ".name" or ".*" conventions described in sections 23.3.2.3 and 23.3.2.4 of the SV2012 LRM. However, when using third party IPs and VIPs, the probability of design and interface ports having the same naming convention is not high and cannot be expected.

Interface Nets Defined in the Interface

Another popular method is to define the interface nets within the interface and then create a connection via a cross module reference between the DUT port and the interface signal. A number of different connection implementations for this coding method are detailed in the subsequent sections.

Use Assign Statements in the TB 

Connect the nets by assigning either the DUT or interface with the directional continuous assign statement. This method is feasible for directional nets, in or out, but will not suffice for a bi-directional net as only one of the sides can be the driver.

interface some_interface();
wire  bidir1; 
wire  bidir2;
logic in1;
endinterface: some_interface
//----------------------------//
module tb();

wire sig1;
wire sig2;
wire sig3;
.
.
some_interface s_if();

dut my_dut(.sig1(sig1),
           .sig2(sig2),
           .sig3(sig3),
           .
           .
           );

assign s_if.in1 = sig3;
// Can't connect bi-direction ports using this method!
//assign bidir1 
//assign bidir2

endmodule: tb

Direct Instantiation of interface signals to the DUT Instance

A solution which works for both directional and bi-directional port types is to embed the interface nets to the DUT instance ports using cross module references.

interface some_interface();
wire  bidir1; 
wire  bidir2;
logic in1;
endinterface: some_interface
//----------------------------//
module tb();

some_interface s_if();

// can be an issue for auto-instatiated DUT
dut my_dut(.sig1(s_if.bidir1),
           .sig2(s_if.bidir2),
           .sig3(s_if.in1),
           .
           .
           );

endmodule: tb

This implementation has a limitation: if the DUT is auto-instantiated, the connection to the DUT can be overridden when modifications are made to the port list.

Connecting Ports to Interface nets with Verilog Primitives

Verilog provides many primitives to model hardware behavior. These primitives, used in the TB, can bridge between the DUT and interface internal nets including bi-directional ports. The Verilog primitive tran can create connections which allow bi-directional drivers.

interface some_interface();
wire  bidir1; 
wire  bidir2;
logic in1;
endinterface: some_interface
//----------------------------//
module tb();

wire sig1;
wire sig2;
wire sig3;

some_interface s_if();

dut my_dut(.sig1(sig1),
           .sig2(sig2),
           .sig3(sig3),
           .
           .
           );

// create connection with tran primitive 
tran(s_if.bidir1, sig1);
tran(s_if.bidir2, sig2);

// directional nets can use continuous assign statements
assign s_if.in1 = sig3;

endmodule: tb

Controllable Connectivity 

Sometimes it is necessary to create different connections for pins with multiple functionality. The Verilog primitives of tranif0 or tranif1 provide a controlled connection to the DUT. These primitives behave the same as the tran primitive with the exception of an enable input connecting or disconnecting the terminals accordingly.

interface some_interface();
wire  bidir1; 
wire  bidir2;
logic in1;
endinterface: some_interface
//----------------------------//
module tb();

wire sig1;
wire sig2;
wire sig3;

wire s_if_control = 1; // default is to short the terminals

some_interface    s_if();

dut my_dut(.sig1(sig1),
           .sig2(sig2),
           .sig3(sig3),
           .
           .
           );

// create connection with tranif1 primitive 
tranif1(s_if.bidir1, sig1, s_if_control);
tranif1(s_if.bidir2, sig2, s_if_control);

// directional nets can use continuous assign statements
assign s_if.in1 = sig3;

endmodule: tb