Monday, March 31, 2014

Custom Field Access Policies in vr_ad

A big part of verifying a design is checking its registers. A register packages is usually used to accomplish this task. It provides an abstract way of describing registers, their fields and their specific access policies, reset values, etc. For Specman, the vr_ad package is the most widespread.

While register packages come with built-in support for standard access policies like read/write, read only, write only, read clear, and more, it's very common that a design employs some quirky access policies as well.

Let's take as an example an access policy defined as follows: writing to the field is allowed, except when trying to write the value 0; reading from the field is always allowed.

One way to do it in vr_ad is to use the side-effect hooks, in particular the post_access(...) method. Here's how this would look like:

<'
// define the register
reg_def EXAMPLE VGM 0x0 {
  reg_fld field1 : uint(bits : 16) : RW : 0;
  reg_fld field2 : uint(bits : 16) : RW : 0;
};

// setup quirkiness for field2
extend EXAMPLE vr_ad_reg {
  post_access(direction : vr_ad_rw_t) is also {
    if (direction == WRITE) {
      var prev_val := get_prev_value();
      var cur_val  := get_cur_value();
      
      // check value of field2
      if (cur_val[15:0] == 0) {
        write_reg_rawval(%{cur_val[31:16], prev_val[15:0]});
      };
    };
  };
};
'>

This code is pretty easy to understand. Whenever a write is performed, the value written to field2 is checked and in case it was 0 then the update is undone by writing the previous value back. This code has two main problems. Firstly, we make use of hard coded bit ranges to identify field2's bits inside the whole register. This is going to be a problem if the offset or width of field2 should change in the future. We could solve this by using some features that are buried a bit deeper inside the vr_ad implementation (but we're not going to do this here). Even after fixing the previous issue, the second problem still remains: when looking at the register definition, it isn't immediately apparent that field2 is quirky. Normally, the register definitions are generated from some sort of XML specification and the generated file is not edited by hand. Hook implementations are done in a separate file, which could lead to confusion.

Newer versions of vr_ad allow us to do the same thing in a more elegant manner. Users can now define their own custom access policies and specify what should happen on a write and on a read. Here is the new and improved version:

<'
// define a new access policy
extend vr_ad_field_attribute_t : [RWi0];

extend RWi0'fld_mask vr_ad_reg_field_info {

  // define what happens at write
  get_field_write_data_according_to_policy(read_data : vr_ad_data_t,
    ndata : vr_ad_data_t) : vr_ad_data_t is {
    if (ndata == 0) {
      result = read_data;
    }
    else {
      result = ndata;
    };
  };

  // define what happens at read
  get_field_read_data_according_to_policy(default_data : vr_ad_data_t,
    read_data : vr_ad_data_t) : vr_ad_data_t is {
    result = read_data;
  };
};


// define the register
reg_def EXAMPLE VGM 0x0 {
  reg_fld field1 : uint(bits : 16) : RW   : 0;
  reg_fld field2 : uint(bits : 16) : RWi0 : 0;
};
'>

What we first need to do is give our custom policy a name, RWi0. Then, we can extend vr_ad_reg_field_info and specify the behavior for operations on this field. The documentation for this feature is rather minimalistic, but browsing through the implementation one can discern the following:

  • the return value of get_field_write_data_according_to_policy will be used to update the field's value when performing a write operation
    • read_data contains the value that has been stored in the field prior to this write
    • ndata contains the value which is being tried to be written
  • the return value of get_field_read_data_according_to_policy will be compared with the value that was read from the DUT
    • default_data is the value that is returned for write-only fields; by default this is the reset value, but this can be changed using set_rmask_default_data(val: vr_ad_data_t)
    • read_data contains the value that is stored in the field

This code will behave in the same way as the previous one, but it has two main advantages. The first is that it it immediately visible from the register definition that field2 is quirky. The second is that if your company tends to use the same exotic policies in more designs, then the definitions of these policies could be stored in a central library somewhere and be reused. The old approach would have required a lot of duplicate code to implement the same type of quirkiness for fields in different registers.

You can find the code snippets, along with some test code, on the blog's SourceForge repository.

Stay tuned for the next post, where we'll do the same exercise in UVM RAL!

Monday, March 17, 2014

Keeping Your Constraints in One Basket

Doing constrained random verification is pretty easy on paper. You know what the transactions look like and what constraints you want to apply to their fields. It gets trickier when you want to convert this intent into a real life verification environment by building all the sequence items and sequences. If done improperly, this can lead to a lot of doubled up work. A problem I've hit a couple of times is how to handle my constraints so as to avoid scattering them all over the place.

Let's take as an example an AHB UVC. If you're not familiar with the AMBA High Performance Bus protocol, the specification is available here (only for registered ARM customers, but you can also find more info by searching the net).

Here is how a sequence item could look like:

class vgm_ahb_item extends uvm_sequence_item;
  rand bit[31:0]       addr;
  rand vgm_ahb_size_e  size;
  rand vgm_ahb_burst_e burst;
  
  // other fields (omitted for clarity)
  // ...
  
  // address must be aligned to the size of the transfer
  constraint aligned_address_c {
    size == VGM_AHB_BYTES_2   -> addr[0:0] == 0;
    size == VGM_AHB_BYTES_4   -> addr[1:0] == 0;
    size == VGM_AHB_BYTES_8   -> addr[2:0] == 0;
    size == VGM_AHB_BYTES_16  -> addr[3:0] == 0;
    size == VGM_AHB_BYTES_32  -> addr[4:0] == 0;
    size == VGM_AHB_BYTES_64  -> addr[5:0] == 0;
    size == VGM_AHB_BYTES_128 -> addr[6:0] == 0;
  }
  
  // bursts must not cross a 1kB address boundary
  constraint dont_cross_1kb_c {
    burst inside { VGM_AHB_INCR4, VGM_AHB_INCR8, VGM_AHB_INCR16 }
      -> addr[10:0] <= 'h4_00 - vgm_ahb_get_num_transfers(burst) * 2**size;
  }
endclass

The specification restricts the values of the addr field based on the values of the size and burst fields. This gets translated into the two constraints: aligned_address_c and  don't_cross_1kb_c.

Now that we have our groundwork laid out, we want to start verifying our DUT. We want to stress it by accessing consecutive address locations with different sizes and burst types. Our sequence will have the burst type, size and the first address location as a fields. An additional field will configure how many address clusters we access. Naturally we want to adhere to the protocol so, for example, starting a 4-byte wide transfer to address 0x431 would be illegal. The constraints we defined for the item have to hold here as well.

You might be tempted to do something like this:

class vgm_ahb_consec_sequence extends uvm_sequence #(vgm_ahb_item);
  rand int             count;
  rand bit[31:0]       addr;
  rand vgm_ahb_size_e  size;
  rand vgm_ahb_burst_e burst;
  
  // address must be aligned to the size of the transfer
  constraint aligned_address_c {
    size == VGM_AHB_BYTES_2   -> addr[0:0] == 0;
    size == VGM_AHB_BYTES_4   -> addr[1:0] == 0;
    size == VGM_AHB_BYTES_8   -> addr[2:0] == 0;
    size == VGM_AHB_BYTES_16  -> addr[3:0] == 0;
    size == VGM_AHB_BYTES_32  -> addr[4:0] == 0;
    size == VGM_AHB_BYTES_64  -> addr[5:0] == 0;
    size == VGM_AHB_BYTES_128 -> addr[6:0] == 0;
  }
  
  // bursts must not cross a 1kB address boundary
  constraint dont_cross_1kb_c {
    burst inside { VGM_AHB_INCR4, VGM_AHB_INCR8, VGM_AHB_INCR16 }
      -> addr[10:0] <= 'h4_00 - vgm_ahb_get_num_transfers(burst) * 2**size;
  }
  
  // body() definition
  // ...
endclass

Copy-pasting is rarely a good idea in software development. It just gives us more places to update our code should our constraints change (for example we want to add more because our initial constraints were not enough). It also creates more places where mistakes can creep in.

Ideally we wouldn't have to do any duplication. Couldn't we somehow make use of the AHB sequence item class? It already defines all the constraints we need...

It turns out we can. We can tweak our previous example by defining a vgm_ahb_item object  as a protected field. By adding constraints to our API fields that tie them to the fields of this field we are "exporting" the sequence item constraints to the sequence. Here's how all this would look like:

class vgm_ahb_consec_sequence extends uvm_sequence #(vgm_ahb_item);
  rand int             count;
  rand bit[31:0]       addr;
  rand vgm_ahb_size_e  size;
  rand vgm_ahb_burst_e burst;
  
  // body() definition
  // ...
  
  // constraint exporter
  protected rand vgm_ahb_item m_item = new();
  
  constraint item_constraints_c {
    addr  == m_item.addr;
    size  == m_item.size;
    burst == m_item.burst;
  }
endclass

The cool thing about this approach is that it can be retrofitted to existing code because there are no modifications to the API. I favor this approach over just defining one field of type vgm_ahb_item and using that for inline constraints. Should our sequence item contain any extra fields (for direction, data, protection, etc.) it would not be clear which ones are actually used in the sequence. By explicitly defining the sequence fields we make it clear what the API is.

A working example that includes the complete code can be found at the blog's SourceForge repository here.

Stay tuned for more SystemVerilog tips and tricks!

Monday, March 10, 2014

A Subtle Gotcha When Using fork...join

I want to start things out light with a recent experience I've had using fork...join statements.

My intention was to start several parallel threads inside a for loop and have them take the loop parameter as an input. I naively assumed the following code would do the job:

module top;

  initial begin
    for (int i = 0; i < 3; i++)
      fork
        some_task(i);
      join_none
  end

  task some_task(int i);
    $display("i = %d", i);
  endtask

endmodule

Do you see the problem with this code? If not, don't worry as I didn't at first either. Here is the output if you run it:

# i =           3
# i =           3
# i =           3

It seems that the for loop executed completely and only then did the spawned processes start, but they were given the latest value of i.

After digging around on the Internet I discovered the answer. The SystemVerilog LRM mentions that "spawned processes do not start executing until the parent thread executes a blocking statement". This explains why the for loop finished executing and why by the time the processes were spawned i had already reached the value '3'.

The LRM also shows exactly how to fix our problem: "Automatic variables declared in the scope of the fork...join block shall be initialized to the initialization value whenever execution enters their scope, and before any processes are spawned". Applying this to our code yields:

module top;

  initial begin
    for (int i = 0; i < 3; i++)
      fork
        automatic int j = i;
        some_task(j);
      join_none
  end

  task some_task(int i);
    $display("i = %d", i);
  endtask

endmodule

Now, for every loop iteration a new variable is allocated, which is then passed to the respective task. Running this example does indeed give the desired result:

# i =           2
# i =           1
# i =           0

I've been playing with parallel processes for quite some time now, but I didn't know about this until recently. I've mostly learned SystemVerilog from online tutorials, but I see that a quick read of the LRM might be of real use. Who knows what other tricks are in there?

I'll keep you updated with any more subtleties I find.

Saturday, March 8, 2014

Welcome to the Verification Gentleman Blog

My name is Tudor Timisescu. I am a Verification Engineer at Infineon Technologies, where I verify cryptographic co-processors and develop in house verification IP. While I mainly work with SystemVerilog and e, I also dabble in other topics such as formal verification, VPI applications, automation tools and more.

I started this blog to share my experiences as I learn about verification. I will be blogging about various problems I've encountered during my work and how I've solved them. I will also add tutorials on how to do cool stuff in UVM and I will post my thoughts on applying software development best practices to verification.

Feel free to use the blog's comments section to ask questions, post corrections or suggest new topics.

Stay tuned for my first technical post.