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;

// ...

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);

// ...

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;

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();

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);

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));

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);

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);

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);

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

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);

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));

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) })

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) })

// ...

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")

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.