Saturday, May 23, 2015

Keeping Constraints and Covergroups in Sync

In the old days, people had to write all of their tests by hand. With chips getting bigger and bigger, it became clear that this painstaking process couldn't scale. Constrained random verification was invented to help us verification engineers deal with the increasing complexity of our DUTs. By describing the kind of stimulus we want to drive and letting the random generator do its thing we can verify more with less effort. Random tests are nice and all, mostly because they are easier to write, but this all comes at a price. It's much more difficult to say what a random test is really doing without letting it run. We typically write coverage to log what we are actually stimulating.

Constraints and coverage are two sides of the same coin; they both represent the legal state space. By writing constraints we decide what we want to stimulate, whereas coverage describes what we want to observe. The two should be in sync, since if we aren't driving something, it doesn't make any sense to try to observe it.

Let's look at a very basic example of an item with two integer fields and some constraints set on them:

class item;
  rand bit[2:0] x, y;

  constraint x_always_smaller {
    x < y;
  }

  constraint never_same_parity {
    x % 2 == 0 <-> y % 2 == 1;
  }

  constraint if_2_then_5 {
    x == 2 -> y == 5;
  }
endclass

We only want to generate pairs with x always smaller than y, both having different parities and if x is 2 we want y to be 5. These constraints range from very general to very specific.

As mentioned above, generating random items isn't going to help us much if we can't prove for certain that we've driven all legal values. We'll need to cover the values of x and y and ignore any illegal combinations. Specifying ignore bins used to be a very daunting task, as the language wasn't particularly rich in features. Things have gotten much better with the IEEE 1800-2012 standard. This post from AMIQ Consulting shows how to use expressions to specify cross-coverage bins. Here's how our coverage collector could look like:

class cov_collector;
  covergroup cov with function sample(bit[2:0] x, bit[2:0] y);
    coverpoint x;
    coverpoint y;

    cross x, y {
      function CrossQueueType create_x_greater_ignore_bins();
        for (int i = 0; i < 8; i++)
          for (int j = 0; j < 8; j++)
            if (i >= j)
              create_x_greater_ignore_bins.push_back('{ i, j });
      endfunction

      function CrossQueueType create_same_parity_ignore_bins();
        for (int i = 0; i < 8; i++)
          for (int j = 0; j < 8; j++)
            if (i % 2 == j % 2)
              create_same_parity_ignore_bins.push_back('{ i, j });
      endfunction

      function CrossQueueType create_if_2_and_not_5_ignore_bins();
        for (int i = 0; i < 8; i++)
          if (i != 5)
            create_if_2_and_not_5_ignore_bins.push_back('{ 2, i });
      endfunction

      ignore_bins x_greater = create_x_greater_ignore_bins();
      ignore_bins same_parity = create_same_parity_ignore_bins();
      ignore_bins if_2_and_not_5 = create_if_2_and_not_5_ignore_bins();
    }
  endgroup

  // ...
endclass

Writing this covergroup without expressions would have been a true test of one's patience. Previously we would have had to explicitly write out all of the values we wanted to ignore, since it wasn't possible to specify any relationships between values. The new language constructs are definitely a step in the right direction.

To make it all a bit easier to follow it makes sense to create one set of ignore bins for each constraint. With some looping and expression checking we can ignore all of the value pairs that would be restricted by each constraint. This way, we can ensure that everything is in sync. The unfortunate part, however, is that we get a lot of redundancy between the two classes. We've described our state space (the legal values of x and y) twice: once as the expressions inside the constraints and once again as the same expressions (albeit in negative form) inside the ignore bins. Should we want to add a new constraint or modify one of the existing ones, we'd need to modify both classes. Thus, the constraints and the coverage form a very fragile equilibrium.

Randomization can be used for more than just driving stimulus. This paper from Verilab shows how the constraint solver can be used in reverse gear to extract metadata from a collected packet. Constraints can also be leveraged as checkers, for example, to make sure that a collected packet contained a legal combination of fields. Using the constraint solver for these tasks means that we don't need to duplicate information inside the checking code.

We can integrate this idea into our coverage problem. We could just loop over all combinations of x and y and use the constraint solver to figure out if a certain combination is legal or not:

class better_cov_collector;
  covergroup cov with function sample(bit[2:0] x, bit[2:0] y);
    coverpoint x;
    coverpoint y;

    cross x, y {
      function CrossQueueType create_ignore_bins();
        item it = new();
        for (int i = 0; i < 8; i++)
          for (int j = 0; j < 8; j++) begin
            if (!it.randomize() with { x == i; y == j; })
              create_ignore_bins.push_back('{ i, j });
          end
      endfunction

      ignore_bins ignore = create_ignore_bins();
    }
  endgroup

  // ...
endclass

Now we can modify the constraints on our items as much as we like and our ignore bins will stay in sync. This is one of the few cases where we want randomization to fail. One small problem with that is that modern simulators have features where it is possible to stop or break on randomization failures. This will interfere with our code and might become really annoying. Turning such features off isn't an option either, since they can provide real benefit for cases where we actually overconstrain a randomization call.

We can adapt our code to use the inline constraint checker language feature:

function CrossQueueType create_ignore_bins();
  item it = new();
  for (int i = 0; i < 8; i++)
    for (int j = 0; j < 8; j++) begin
      it.x = i;
      it.y = j;
      if (!it.randomize(null))
        create_ignore_bins.push_back('{ i, j });
    end
endfunction

By calling randomize(null) we are turning off randomization for all fields inside our item and checking whether the values that we assigned to them conform to the constraints. This way, the simulator can distinguish that this isn't a regular randomization call and wouldn't need to break if it failed. I know of at least one simulator that doesn't do this, though. If yours also breaks here, it might be nice to open a support case with the vendor to check if they wouldn't want to implement this differentiation inside their tool.

While this approach works for cross-coverage, it wont work for regular coverpoints, since there it isn't possible to specify ignore bins using functions. For example, in our case it isn't possible to cover the value 7 for x, because x always has to be smaller than y. Such a language feature might be a nice addition in the next version of the standard.

One thing we still need to do is update our bin generating function if the ranges for our fields change. In our current example, if we would change the type of x and y to bit [15:0] we would need to change the endpoints of all the loops. If SystemVerilog (better)supported reflection we could figure out these endpoints automatically.

The approach we've looked at above, while not 100% bulletproof, is still very useful because it avoids the need for error prone manual modifications to the coverage code. When working with SystemVerilog, it keeps our coverage definitions in sync and it makes refining constraints a breeze. If you want to give it a try yourself, you can find the code on SourceForge.

Tuesday, May 5, 2015

Enum fields in UVM_REG

For some time now, I've been mulling over the problem of storing register field values as enumerations. Enumerations are a very handy tool to improve code readability. Design specifications often make use of them too. If our verification environments could handle enumerated values for register fields we wouldn't have to go back and forth to the specification to decode bits when trying to figure out, for example, how to start the operations that we want or what results we got.

As we already know, fields in UVM_REG are first class objects. There is an own class, uvm_reg_field, used to model them. On the other hand, in vr_ad, fields are merely members of the register struct. They can be of any scalar type and the register will handle packing and unpacking itself.

A big advantage that vr_ad has is that fields can be of enumerated types. This isn't possible out of the box when using UVM_REG. That's because uvm_reg_field is supposed to be as generic as possible and only stores values as bit vectors. What those bit vectors represent is supposed to be a layer on top of the physical representation. Let's build a new register field class that can do this. I've chosen the name vgm_reg_enum_field since I don't have the authority to create classes with the uvm_* prefix.

We'll make our class a child of uvm_reg_field, so that the base class can handle the heavy lifting of interacting with other register layer classes. Users of our classes would mostly be interested in working with the field's desired value, for which we'll define a new API.

The uvm_reg_field class provides a value field which can be used for randomizing the value that we want driven. This field is of type uvm_reg_data_t, which is simply a bit vector. We'll need another such field that is of the enumerated type our field is supposed to hold. Since we want our class to handle any enumerations, we'll need the type being stored to be a parameter:

class vgm_reg_enum_field #(type T) extends uvm_reg_field;
  rand T value;

  // ...
endclass

By defining a new class member called value inside our new class we're effectively hiding the one from uvm_reg_field. Generally, variable hiding is frowned upon and this post makes a good case as to why, but in this case it kind of feels right (though I'm pretty sure Puneet will be upset with me). I'd rather have the old member hidden so that it isn't possible to constrain raw values.

We've made our enum type a parameter, but we need to make sure that its width is compatible to the register field's size. The size is set within the configure(...) function, so we'll extend it to check it:

class vgm_reg_enum_field #(type T) extends uvm_reg_field;
  // ...

  function void configure(uvm_reg parent, int unsigned size,
    int unsigned lsb_pos, string access, bit volatile, uvm_reg_data_t reset,
    bit has_reset, bit is_rand, bit individually_accessible
  );
    if (size != $bits(T))
      `uvm_fatal("SIZERR", "Size and enum width don't match")
    super.configure(parent, size, lsb_pos, access, volatile, reset, has_reset,
      is_rand, individually_accessible);
  endfunction

  // ...
endclass

The configure(...) function is, however, non-virtual, so it won't be possible to do any sort of type overriding and have our consistency check executed. I'm sure glad they made get_parent() and other "useful" functions virtual, but not this one... We could also add the check inside the get_n_bits() function, since that will be called when the parent register is being created to check for overlaps:

  virtual function int unsigned get_n_bits();
    int unsigned size = super.get_n_bits();
    if (size != $bits(T))
      `uvm_fatal("SIZERR", "Size and enum width don't match")
    return size;
  endfunction

This isn't a nice solution, but it's pragmatic.

We also need to take into account the is_rand argument to configure(...). Again, since the function isn't virtual, doing anything there won't be enough. We can extend the pre_randomize() function and set the rand_mode based on the rand_mode configured for the base class' value field:

  function void pre_randomize();
    super.pre_randomize();
    value.rand_mode(super.value.rand_mode());
  endfunction

We also need to handle the field's reset value. We can cheat and pass it into configure(...), but note that this still allows us to pass in a raw value. Reset values can also be set using set_reset(...), but it also takes a uvm_reg_data_t argument. We need a function that can only accept an enumerated value. Since SystemVerilog doesn't allow function overloading to create a new set_reset(...) function that takes an enum argument, we'll need to use a new name. An idea would be set_reset_enum(...):

  virtual function void set_reset_enum(T value, string kind = "HARD");
    super.set_reset(uvm_reg_data_t'(value), kind);
  endfunction

This new function is a thin wrapper around the original set_reset(...) function, but it forces the user to pass in an enum argument. Since we're on the topic, the current implementation of uvm_reg_field blatantly ignores user errors when providing input. When setting stuff, it will merrily chop of most significant bits from arguments to truncate them to the field's size, leaving the user the fun of having to debug this. Not nice...

We can also create a complementary get_reset_enum() function to return the reset value in a strongly typed form:

  virtual function T get_reset_enum(string kind = "HARD");
    return T'(get_reset(kind));
  endfunction

Aside from randomizing the field's value, the user can set a desired value using the set(...) function. We'll need to extend this function to also update our new value field:

  virtual function void set(uvm_reg_data_t value, string fname = "",
    int lineno = 0
  );
    super.set(value, fname, lineno);
    this.value = T'(super.value);
  endfunction

For the corresponding get() function, we don't have to do anything, but to satisfy my paranoia we could add a check to make sure that both desired values (the original and the overridden one) are consistent:

  virtual function uvm_reg_data_t get(string fname = "", int lineno = 0);
    if (value != bits2enum(super.value))
      `uvm_fatal("VALERR", "Inconsistend desired values")
    return super.get(fname, lineno);
  endfunction

Similarly to the set/get_reset_enum(...) functions, we can provide strongly typed versions of set(...)/get():

  virtual function void set_enum(T value, string fname = "", int lineno = 0);
    set(uvm_reg_data_t'value, fname, lineno);
  endfunction


  virtual function T get_enum(string fname = "", int lineno = 0);
    return T'(get(fname, lineno));
  endfunction

The do_predict(...) function should also update the desired value, so it also needs to be extended:

  virtual function void do_predict(uvm_reg_item rw,
    uvm_predict_e kind = UVM_PREDICT_DIRECT, uvm_reg_byte_en_t be = -1
  );
    super.do_predict(rw, kind, be);
    this.value = bits2enum(super.value);
  endfunction

Finally, the get_mirrored_value() could use a strongly typed counterpart:

  virtual function T get_mirrored_value_enum(string fname = "",
    int lineno = 0
  );
    return bits2enum(get_mirrored_value(fname, lineno));
  endfunction

One thing I've been avoiding is asking what would happen if we tried to set a raw value that isn't defined in the enum.For example, if we had a 2-bit enum type with only 3 literals, what would happen if we set the value 3? I'd expect the simulator to flag an error, something along the lines of "can't convert". Unfortunately, this isn't what happens. I guess we'll have to handle this in a different way.

We'll need to replace all casts from uvm_reg_data_t to the enum type with our own function that can detect conversion errors. The LRM makes a distinction between using T'(...) (called a static cast) and $cast(...) (called a dynamic cast). In our conversion function we could call $cast(...) and if it fails we could issue a fatal error:

  protected virtual function T bits2enum(uvm_reg_data_t value);
    if (!$cast(bits2enum, value))
      `uvm_fatal("CASTERR", { "In field '", get_name(), "': ",
        $sformatf("Requested value 'b%0b not mapped to enum literal", value) })
  endfunction

By using this instead of static cast we might get slightly slower code, but it's a price worth paying.

The situation we looked at above does raise an interesting question. In some designs we can actually have the case that multiple bit representations of a register field do the same thing. For example, let's consider a field who's values are encoded using a priority encoding scheme, where the first set bit determines what operation will be performed:

typedef enum bit[2:0] {
  CONTINUE = 3'b001, STOP = 3'b010, START = 3'b100 } operation_e;

In this case, when bit 2 is set, a START will be executed, regardless of what the other bits contains. If bit 1 is set and bit 2 is cleared, then a STOP will be executed. A CONTINUE will only be executed if only bit 0 is set. For such a field, we actually want to test that writing 3'b101, for example, still triggers a START.

What we could do is map all of the values of the enum type. This is easily done in SystemVerilog:

typedef enum bit[2:0] {
  CONTINUE = 3'b001, STOP[2], START[4] } operation_e;

The operation_e enum will have a CONTINUE literal, followed by STOP0, STOP1, START0, START1, START3 and START4. Handling all of these values inside generation code (that users write in sequences) or modeling code (that is used for reference modeling) is going to be pretty difficult. We'd need a mess of if and/or case statements.

A better idea is to have functions inside the register field class that can convert to and from the bit and enum representations.This new field class could inherit from the vgm_reg_enum_field class and extend the bits2reg(...) function to do a many-to-one style conversion:

class multiply_mapped_enum_field extends vgm_reg_enum_field #(operation_e);
  protected virtual function operation_e bits2enum(uvm_reg_data_t value);
    if (value[2])
      return START;
    if (value[1])
      return STOP;
    if (value[0])
      return CONTINUE;
    `uvm_fatal("VALERR", { "In field '", get_name(), "': ",
      $sformatf("Illegal value 'b%0b", value) })
  endfunction

  // ...
endclass

The advantage to doing it this way is that any modeling code the user writes doesn't need to care that START can be executed via multiple bit representations.

At the same time, to simplify generation code, converting from enums to bits is a one-to-many problem. From an information theory point of view, this involves creating new "information" to fill the gaps. Constrained randomization can be used to fill those gaps:

  protected virtual function uvm_reg_data_t enum2bits(operation_e value);
    if(!std::randomize(enum2bits) with {
      enum2bits[`UVM_REG_DATA_WIDTH - 1:3] == 0;
      value == START -> enum2bits[2] == 1;
      value == STOP -> enum2bits[2:1] == 2'b01;
      value == CONTINUE -> enum2bits[2:0] == 3'b001;
    })
      `uvm_fatal("RANDERR", "Randomization error")
  endfunction

This implies changing the vgm_reg_enum_field base class to handle any such conversions using such a enum2bits(...) function. It's implementation is trivial when the values aren't multiply-mapped. We should be careful where we use this function though. It would make little sense to use any kind of random values when trying to set reset values, for example, but for set_enum(...) it definitely makes sense.

We could also add enum wrappers for the read/write/mirror/predict(...) tasks, but these are only used for starting accesses. It's pretty uncommon to only access a single field so we won't look at it now, but this should be pretty straightforward to implement.

Using enumerated values together with the register access layer has the potential to make sequences and modeling code much more readable. The UVM BCL implementation and the language itself don't make it easy on us, though. You can download the full code for vgm_reg_enum_field from SourceForge, including the example on how to implement multiply-mapped enum literals. It's not production grade just yet, as it would need to be beta tested on a real project, but if you do use it get in touch and share your experiences.