Tuesday, September 22, 2015

Of Copies and Clones

Let's take a walk down memory lane and remember the fun times we had in college. For a few weeks at the end of the semester, though, things had to get serious, because exam season was starting. I wasn't a really big note taker, so I found myself having to loan notes from my friends. While studying, I couldn't well start scribbling on my friends' notes, as that would have made them really mad. The solution was to make copies on which I could then write to my heart's content. This way I could give the originals back to their owners in the same state as I got them.

In programming we're often in the same situation. We get some object passed via a function call from another collaborator in the program. For example, inside a scoreboard we get a transaction from a monitor. We might need to fiddle with this object, but before we do this, it's wise to copy it so that the original stays untouched. Much has been written on object copying, but I want to touch on how this is handled in SystemVerilog and, in particular, when using UVM.

According to section 8.12 Assignment, renaming, and copying of the IEEE 1800-2012 standard, it's possible to use the new keyword to create a shallow copy of an object, like so:

some_class obj = new()
some_class copy = new obj;

This will create a new object whose fields are identical to those of the first one. Let's look at a concrete example. Assume we have an APB transfer class, which extends uvm_sequence_item:

package vgm_apb;

class transfer extends uvm_sequence_item;
  rand direction_e direction;
  rand bit [31:0] address;
  rand bit [31:0] data;
  rand int unsigned delay;

  // ...
endclass

Copying a transfer would be conceptually equivalent to:

copy = new();
copy.direction = orig.direction;
copy.address = orig.address;
copy.data = orig.data;
copy.delay = orig.delay;

The compiler would "create" all of this code for us under the hood, which would save us a lot of typing. It would take all of the fields that our transfer class has and create such assignment statements for each and every one of them. When I say all fields, I mean all fields. Notice that transfer extends uvm_sequence_item, which means it will contain all fields defined in that class and in all other classes in the inheritance hierarchy. A grandparent class of uvm_sequence_item is uvm_transaction, which contains the following field definitions:

class uvm_transaction extends uvm_object;
  const uvm_event_pool events = new;
  uvm_event begin_event;
  uvm_event end_event;

  // ...
endclass

This means that our long list of assignments that the copy expands to would also contain:

copy.events = orig.events;
copy.begin_event = orig.begin_event;
copy.end_event = orig.end_event;

Notice that events, begin_event and end_event are objects themselves. An assignment like this wouldn't result in new objects being created inside the copy, but in the same objects getting referenced. Does this make sense? Not really. A transfer should have its own private events, totally independent of the events of other transfers. Unfortunately, it's impossible to exclude such fields from the copy procedure. In C++, it's possible to define a custom copy constructor to handle such special situations, but in SystemVerilog, when using the new operator, it's either all or nothing.

Because the language doesn't allow for flexibility when implementing copying, UVM introduced it's own functions to handle this task, called copy(...) and clone(). Let's look at copy(...) first. The way copy(...) is supposed to work is that it updates the fields of the caller object with the values contained in the object passed as its argument:

vgm_apb::transfer orig_transer = new();
vgm_apb::transfer copied_transfer = new();
copied_transfer.copy(orig_transfer);

Notice that we had to first create the object in which we stored the copy. After the call, some of the fields of copied_transfer will be set to the values contained in orig_transfer. Which fields get copied can be configured using the `uvm_field_*(...) macros:

class transfer extends uvm_sequence_item;
  // ...

  `uvm_object_utils_begin(transfer)
    `uvm_field_enum(direction_e, direction, UVM_ALL_ON)
    `uvm_field_int(address, UVM_ALL_ON)
    `uvm_field_int(data, UVM_ALL_ON)
    `uvm_field_int(delay, UVM_ALL_ON)
  `uvm_object_utils_end
endclass

The macros define which fields from the transfer class get copied. It's the responsibility of subclasses to define which of their fields participate in the copy. For example, uvm_transaction doesn't allow the events, begin_event and end_event fields to get copied. I don't want this post to be a tutorial on how to implement copying, as there are plenty of excellent resource already available, but a bit of introduction will be useful.

The use model for clone() is that, when called on an object, it will return a new object which is a copy of it:

vgm_apb::transfer orig_transer = new();
vgm_apb::transfer copied_transfer;
$cast(copied_transfer, orig_transfer.clone());

Notice that this time we didn't need to create a transfer to store the copy. The clone() function did this for us and subsequently called copy(...) on it to update its fields. We had to cast the return value, though, because the function's prototype is:

virtual function uvm_object clone();

Since the method allocated a vgm_apb::transfer, the cast will be successful.

What I want to investigate in this post is what happens when we try to copy and clone across the inheritance tree. For this purpose, we'll need a class that extends vgm_apb::transfer. The APB2 protocol defines an extra PSTRB signal which enables sparse write accesses. We may have a mixture of APB and APB2 slaves in our system and we want our UVC to be available in both flavors. The best way to do this would be to have another separate package for APB2. The APB2 transfer class would extend the previous one:

package vgm_apb2;

class transfer extends vgm_apb::transfer;
  rand bit strobe[4];

  // ...
endclass

To have the new strobe field get copied we could use a field macro for it. At the same time, the uvm_object class provides a hook for users to add their own code to extend the copy(...) function. This do_copy(...) hook is called after the code the field macros expand to:

class transfer extends vgm_apb::transfer;
  // ...

  virtual function void do_copy(uvm_object rhs);
    transfer rhs_cast;
    if (!$cast(rhs_cast, rhs))
      `uvm_fatal("CASTERR", "Cast error")
    this.strobe = rhs_cast.strobe;
  endfunction
endclass

The function is defined as virtual in uvm_object and it takes a uvm_object as an argument. This means we need to cast to our class to be able to access the strobe field. The copy(...) function is not virtual, so users are instructed to extend the hook. They're also not supposed to call the do_copy(...) function (or any of the other hooks for printing, recording, packing and unpacking) directly. It's kind of silly that the developers didn't define these methods are protected. One of the core tenets of OOP is encapsulation. According to this mantra, you know what's better than telling the user to not call certain functions directly? Not allowing the user to call certain functions directly (at least from outside of the class itself).

Copying a vgm_apb2::transfer works exactly as for the previous one. It's transparent to us whether the copy is performed via the field macros or the do_copy(...) hook:

vgm_apb2::transfer orig_transer = new();
vgm_apb2::transfer copied_transfer = new();
copied_transfer.copy(orig_transfer);

Now that we have a small inheritance hierarchy, let's see what happens when we try to mix copies and clones of different classes. Since any vgm_apb2::transfer is also a vgm_apb::transfer, it means that we can copy the former into the latter:

vgm_apb::transfer apb_trans_copy;
vgm_apb2::transfer apb2_trans = new("apb2_trans");

apb_trans_copy = new("apb_trans_copy");
apb_trans_copy.copy(apb2_trans);

At the same time, it's also possible to clone a vgm_apb2::transfer into a vgm_apb::transfer variable:

vgm_apb::transfer apb_trans_copy;
vgm_apb2::transfer apb2_trans = new("apb2_trans");

$cast(apb_trans_copy, apb2_trans.clone());

Now let's try and break stuff. Let's try and copy a vgm_apb::transfer into a  vgm_apb2::transfer:

vgm_apb::transfer apb_trans = new("apb_trans");
vgm_apb2::transfer apb2_trans_copy;

apb2_trans_copy = new("apb2_trans_copy");
apb2_trans_copy.copy(apb_trans);

Remember that the argument of copy(...) is passed to our do_copy(...) function, where it gets cast to vgm_apb2::transfer. Since a vgm_apb::transfer isn't a vgm_apb2::transfer, the cast will fail and cause a nice run time error. If, however, we would have implemented the copy of the strobe field using the field macros, we wouldn't be so lucky. We wouldn't get any fatal error (not even a warning). The vgm_apb::transfer fields would get copied to the target object, but strobe would be left untouched, leaving us with a pseudo-copy. This isn't nice at all.

Let's also try to clone a vgm_apb::transfer into a vgm_apb2::transfer variable:

vgm_apb::transfer apb_trans = new("apb_trans");
vgm_apb2::transfer apb2_trans_copy;

$cast(apb2_trans_copy, apb_trans.clone());

This time, the $cast(...) from this code snippet will fail, since clone() will return a vgm_apb::transfer. As we can see, misuse of copy(...) or clone() will cause either funky behavior in the worst case or run time errors in the best case. These are errors that could have been easily caught at compile time, but we need to make a few small changes to our classes.

Let's go back to a time when there wasn't any UVM and people wrote vanilla SystemVerilog code (and dinosaurs roamed the Earth). Let's forget about UVM objects and sequence items and let's implement our transfer as a stand-alone class:

package vgm_apb;

class transfer;  rand direction_e direction;
  rand bit [31:0] address;
  rand bit [31:0] data;
  rand int unsigned delay;

  // ...
endclass

If we want to implement a copy(...) method, the argument it's going to take will be of class vgm_apb::transfer (instead of uvm_object like in the previous case):

class transfer;
  // ...

  function void copy(transfer rhs);
    this.direction = rhs.direction;
    this.address = rhs.address;
    this.data = rhs.data;
    this.delay = rhs.delay;
  endfunction
endclass

The clone() method will also return a vgm_apb::transfer directly, instead of a downcast uvm_object:

class transfer;
  // ...

  virtual function transfer clone();
    clone = new(name);
    clone.copy(this);
    return clone;
  endfunction
endclass

Since our copy(...) and clone() functions are designed to work with objects of this class (and it's ancestors), we should have much more compile time safety. Moreover, when we're cloning, we don't need to do any more casting. Let's implement the APB2 transfer as well:

package vgm_apb2;

class transfer extends vgm_apb::transfer;
  rand bit strobe[4];

  // ...
endclass

Let's implement the copy(...) method to also be strongly typed:

class transfer extends vgm_apb::transfer;
  // ...

  function void copy(transfer rhs);
    super.copy(rhs);
    this.strobe = rhs.strobe;
  endfunction
endclass

I hope no one needs convincing that it's still possible to copy a vgm_apb2::transfer into a vgm_apb::transfer. Let's try to do it the other way around again and see what happens:

vgm_apb::transfer apb_trans = new();
vgm_apb2::transfer apb2_trans_copy;

apb2_trans_copy = new();
apb2_trans_copy.copy(apb_trans);

Since we're calling copy(...) on a vgm_apb2::transfer, where the argument is supposed to also be of type vgm_apb2::transfer, we'd expect to get a compile error here because we're passing it an object of type vgm_apb::transfer, right? Well, yes and no. Remember that vgm_apb:::transfer, which is the base class of vgm_apb2::transfer also defined a copy(...) function, that took a vgm_apb::transfer as an argument. What happened to that method when we overrode it in the sub-class? The LRM isn't really clear about what to expect when overriding methods and changing their arguments. In my simulator it seems that both versions of copy(...) exist and that the old one gets called. Since this is a gray area in the standard, other simulators might fail during compile, either when compiling the vgm_apb2 package (because they won't allow methods to be overridden like this) or when compiling the code snippet from above (because they will only keep the most recent definition of a method when it's overridden). I've tried it in a second simulator and there the latter case happened. Since we have three major EDA vendors, wouldn't it be really awesome if each one of them would have their own interpretation here and we could see all three behaviors I described above? If you ask me, I say the third option should be the legal one, since SystemVerilog doesn't explicitly support function overloading (in contrast to C++ or Java).

We've also got a clone() to implement. Luckily, SystemVerilog supports covariant return types, so it's possible to refine the clone() function to return a vgm_apb2::transfer.

class transfer extends vgm_apb::transfer;
  // ...

  virtual function transfer clone();
    clone = new();
    clone.copy(this);
    return clone;
  endfunction
endclass

This way, we don't need to do any casting of the return value for this class either. At the same time, compilers should be able to flag the following code as problematic:

vgm_apb::transfer apb_trans = new();
vgm_apb2::transfer apb2_trans_copy;

apb2_trans_copy = apb_trans.clone();

My tool merrily compiles it, but flags a run time error, even though It should have figured it out earlier while parsing the code. Other simulators will throw an error during compilation.

We did this exercise to show you that it's possible to write our copying routines in such a way that their improper use will result in a compile time error (that is, if the simulator allows). Now let's apply this knowledge about method overrides to our UVM sequence items. For both our transfers, we can override the copy(...) function to take an argument of the respective transfer type. Since I've called both classes transfer (but placed them in different packages), the code looks the same for both:

class transfer extends uvm_sequence_item;
  // ...

  function void copy(transfer rhs);
    super.copy(rhs);
  endfunction
endfunction

For clone(), I initially tried to call the base implementation (from uvm_object) and cast the result to the appropriate transfer class:

class transfer extends uvm_sequence_item;
  // ..

  virtual function transfer clone();
    void'($cast(clone, super.clone()));
    return clone;
  endfunction
endclass

As a side note, for those of you who don't know, it's possible to use the name of a value returning function as a temporary variable whose type is the return type. This is why we're trying to cast the result into clone. While I was developing the code, I made a mistake and forgot to add the `uvm_object_utils(...) macro to the vgm_apb2::transfer class. It was very surprising to see that the cast was always failing, until I looked in the implementation of uvm_object's clone() function. It seems that it calls a virtual method called create(...), which is defined for us when using the utils macro. Since I had forgotten to add the macro, the vgm_apb2::transfer class didn't have an own create(...) function and the one from the base class was getting called. This returned an object of vgm_apb::transfer, which is why the cast wasn't working. Since we're anyway overriding clone(), why not keep things simple and just create a new object using the class's constructor:

class transfer extends uvm_sequence_item;
  // ..

  virtual function transfer clone();
    clone = new(get_name());
    clone.copy(this);
    return clone;
  endfunction
endclass

This way it doesn't matter if we use the utils macro at all. As we can see, we don't need much more code to achieve compile time safety when copying. The code we do need to write, though, is all boilerplate. It could just as well be added to the utils macros, since the only thing that varies is the name of the class, which is already passed as an argument.

I've uploaded the code I used to prototype these ideas on SourceForge. Feel free to try it out in your simulator and see what you get. If it doesn't work to your liking (i.e. flag compile errors when trying to do improper copies), then be sure to write your friendly application engineer. Tools are supposed to help us catch such silly errors early, without having to resort to time consuming debugging.

Monday, September 14, 2015

Be More Assertive about Your Testbench Code

Developing verification environments revolves around writing checks. We need to separate the concepts of checking the DUT from checking testbench code. DUT checks represent the "business logic" of our verification software. The code we write isn't perfect, though. Sprinkling the testbench with checks of its own helps to ensure its correctness by catching programming errors at their source.

Both SystemVerilog and e provide language constructs to reason about the DUTs behavior. In SystemVerilog we have the assert keyword, while e programmers use check in procedural code and expect to verify temporal behavior. These keywords are tightly integrated with EDA tools, allowing users to tag individual checks, inspect their states (for example, using an assertion browser) or annotate them to their verification plans.

If you've ever read up on SystemVerilog, chances are you've seen code snippets similar to this one:

byte some_var;
assert (std::randomize(some_var) with { some_var == 1000; });

Checking the return value of randomize() is in general a good idea, because it helps us find cases where we have contradicting constraints. It's pretty clear that randomization will fail in the code snippet above, since a byte can only hold values up to 255. The reason it fails is because we made a mistake when setting our constraint, resulting in buggy testbench code.

While we will see see an error message when executing this code, using assert to do implement such checks is not the way to go. This is because the IEEE 1800-2012 LRM states that "Assertions are primarily used to validate the behavior of a design.". It also says that the assert statement is supposed to be used "to specify the property as an obligation for the design that is to be checked to verify that the property holds.". The fact that the randomization call was successful doesn't relate in any way to the DUT. It is purely a testbench issue, so we shouldn't be using assert to check it.

There are multiple problems that misusing assert like this will cause. First, since EDA tools interpret assert statements as DUT checks and track them, any such testbench checks will appear alongside "real" assertions and pollute the overview. This is more of an annoyance than a major problem. The problems come when we realize that assertions can be disabled using the $assertoff(...) system task. If before executing the randomize() call above the simulator would encounter an $assertoff(...), we wouldn't get any error flagged since the check would be disabled. This means that in cases where we would expect assertion errors (like error injection or fault simulations) and would disable some DUT checks, we might accidentally disable some our testbench's checks in the process. Let's also look at what happens when we disable assertions that would pass. Consider the following code snippet:

byte some_var;
assert (std::randomize(some_var) with { some_var == 10; });

This randomize() call will always be successful, but if we were to disable all assertions, then we'd have the nice surprise of seeing that some_var will remain 0. This is because the randomize() doesn't get executed anymore. There was also a rumor at one point that some simulators might execute the statement, while others might not, leading to more potential for inconsistency between different vendors (as if there wasn't enough variation in SystemVerilog simulator implementations...). I'm not sure what the status right now is (all the ones I've tested won't execute the randomize() statement), but I hope this and the other reasons above convinced you that using assert in this way is a very bad idea.

The assert keyword is also part of the e language, where it's meant to be used to check e code for correct behavior (remember that the keywords to check the design for correct behavior were check and expect). SystemVerilog doesn't have such a language construct dedicated to checking our own code, but then again neither does C. In C, assertions are implemented using the preprocessor. Programmers include the assert.h header, which defines the assert(...) macro. If the expression passed as an argument to the macro fails, an error message is printed which contains the location of the error (file and line) and the program is stopped.

We can implement something similar for SystemVerilog. Since assert is already taken, I've had the not so original idea of calling our macro prog_assert (for program, not progressive). If you've got a better name for it, please let me know in the comments. Our header will be called "prog_assert.svh". The macro needs to check the expression and in case of a fail, trigger a $fatal(...) call:

`define prog_assert(expr) \
  begin \
    if (!(expr)) \
      $fatal(0, $sformatf("Assertion '%s' failed.", `"expr`")); \
  end

The $fatal(...) message generated by the tool will already contain the location of the message (the file, line and scope - this is mandated by the standard). In addition to this, we can also print the expression that caused the fail. Let's see the macro in action. Let's say that we want to implement a rectangle class that takes the sides as constructor arguments:

class rectangle;
  extern function new(int unsigned side0, int unsigned side1);
  // ...
endclass

It doesn't make any sense to pass negative numbers for their lengths, so we can enforce them to be positive by declaring them as int unsigned. It also doesn't make any sense to allow any of the sides to be 0. This is something that we need to check at run time, when the constructor gets called:

function rectangle::new(int unsigned side0, int unsigned side1);
  `prog_assert(side0 > 0)
  `prog_assert(side1 > 0)
  // ...
endfunction

This way we can ensure that the code that is instantiating a rectangle isn't buggy.

Another feature of the C assert "library" is the ability to disable checks for deployed code. The idea behind this is that while software is being developed, it has bugs. We want to be able to track down those bugs quickly when they cause an assertion to fail and fix them. Production software should (ideally) be free of bugs, so any checks we have will only slow us down without any added benefit (since we know they're all going to pass anyway). Assertions are disabled when the NDEBUG symbol is defined. We can have our macro work the same way:

`ifdef NDEBUG
  `define prog_assert(expr) \
    begin \
    end
`else
  // ...
`endif

When NDEBUG is defined before including prog_assert.svh, the prog_assert macro will expand to basically nothing (as compilers should be able to optimize the empty begin...end block away). This means that the code passed as the expression won't be seen by the compiler. This makes it interesting to look at what happens if we use prog_assert with a randomize() call:

byte some_var;
`prog_assert(std::randomize(some_var) with { some_var == 10; })
$display("some_var = %0d", some_var);

If we simply execute this code, we won't see any error message (since the randomize() call can't fail) and we'll see that some_var got the value 10. If however we define the NDEBUG symbol beforehand, we'll notice that some_var stays 0. This is because the randomize() call never happens. This is a feature, not a bug as the C library also works like this. Programmers are only supposed to use expressions without any side-effects inside assert statements.

After a bit of research I learned that the Unreal engine (a big library used by a lot of video games) has some very nice assertion mechanisms in place. Aside from the assert style statement provided by assert.h (which they call check), it also defines two others. Most of them do basically the same thing, with some extra sugar on top. The more interesting one is called verify and the difference between it and assert is that the expression it operates on also gets executed in production builds, i.e. in cases where assert would expand to nothing. This is exactly the behavior we need to check the status of randomize():

`ifdef NDEBUG
  `define prog_verify(expr) \
    begin \
      void'(expr); \
    end
`else
  `define prog_verify(expr) \
    `prog_assert(expr)
`endif

During the development stage, prog_verify(...) acts just like prog_assert(...) (it checks the expression and issues an error when it evaluates to false). After deployment, it merely evaluates the expression. Why do we need both macros? Wouldn't prog_verify(...) suffice? Well, evaluating the expression uses up processor time, but if it doesn't have any side-effects there's no point in doing it. The safest bet would be to always use prog_verify(...), but for cases where we know that executing the expression doesn't change the state of the testbench we can gain more performance in production mode by using prog_assert(...).

If you want to use these macros in your own code, I intend to maintain them on GitHub. Feel free to follow the micro-project, download it and suggest improvements. I'm still considering adding another macro called prog_ensure(...) that always checks its expression argument, regardless of whether NDEBUG is defined.

What I don't like at all about SystemVerilog is that there isn't any concept of a standard library. This is exactly the kind of thing that should be contained in such a library, that should come packaged with the simulator. The closest thing to this is UVM, but I'm not particularly thrilled by the design decisions taken there (i.e. building big monoliths that will eventually topple and crush us all!) and I don't want to suggest adding any new features. You might not want to create extra dependencies when developing UVCs by having to also specify the path to "prog_assert.svh". A pragmatic solutions would be to just copy the code for the macros inside the UVC (there isn't much code to copy anyway) and just change the prefix from prog to <uvc_name>.

We have all of these nice features to find bugs inside the DUT and point us in the direction of where to look to fix them. It's a shame to not pay our own code the same amount of attention. Software programmers have been using assertions for quite some time now to check the validity of their own code or that of their clients. If you want to write more robust verification software, whether it's UVCs or testbenches, give prog_assert a try.

Sunday, September 6, 2015

My Take on SVA Usage with UVM

For verifying complex temporal behavior, SystemVerilog assertions (SVAs) are unmatched. They provide a powerful way to specify signal relationships over time and to validate that these requirements hold. One limitation of SVAs is that they can only be used in static constructs (module, interface or checker). Since modern verification is class based, this leads to segregation between the assertions and the testbench. There have been many papers written about how to bring these two parts of the verification environment closer together, particularly when using UVM.

Let's start our exploration of SVAs with some simple assertions for the Wishbone protocol. To keep it simple, we'll only consider a subset of signals:

interface vgm_wb_slave_interface(input bit RST_I, input bit CLK_I);
  logic STB_I;
  logic [32:0] ADR_I;
  logic ACK_O;

  default clocking @(posedge CLK_I);
  endclocking
endinterface

The STB_I signal initiates a transfer and in classic Wishbone it's supposed to stay high until it is acknowledged by the slave:

  stb_held_until_ack : assert property (
    $rose(STB_I) |->
      STB_I throughout ACK_O [->1]
  )
  else
    $error("STB_I must be held until ACK_O");

The assertion above states that once STB_I goes high, it's supposed to stay high until the first occurrence of ACK_O.

A first step to closer collaboration between the testbench and the SVAs is to integrate assertion messaging with UVM's reporting mechanism. The SVA Bible recommends replacing severity system tasks with calls to their corresponding `uvm_* macros:

  stb_held_until_ack : assert property (
    // ...
  )
  else
    `uvm_error("WBSLV", "STB_I must be held until ACK_O")

This is a nice idea in theory, but there are more subtle points to consider in practice. The approach works fine when there's only one instance of the interface, but not as well when we have more. For the fail messages for $error(...) the simulator will print the scope where the error happened. This makes it easy to trace the source of a failure. Simple calls to `uvm_error(...) won't do this anymore. This is because `uvm_*(...) calls outside of UVM report objects get forwarded to the topmost node of the hierarchy, uvm_root, making it impossible to distinguish between callers.

To work around this limitation, we can add the scope to the error message ourselves:

  stb_held_until_ack : assert property (
    // ...
  )
  else
    `uvm_error("WBSLV", $sformatf("%s\n  In scope %m",
      "STB_I must be held until ACK_O"))

The %m format specifier is a placeholder for the hierarchical path of the current scope. Let's add another assertion that checks that all address bits are at valid levels during a transfer:

  adr_not_unknown : assert property (
    STB_I |-> !$isunknown(ADR_I)
  )
  else
    `uvm_error("WBSLV", $sformatf("%s\n  In scope %m",
      "ADR_I must be at a known level during a transfer"))

Passing around the scope like this in every assertion can get a bit tedious. It' also makes it difficult to change the format of our messages should we so desire (like printing the scope before the error message). To compact things a bit more, we can wrap the `uvm_error(...) macro with an own macro that handles printing the scope:

  `define error(MSG) \
    `uvm_error("WBSLV", $sformatf("%s\n  In scope %m", MSG))

We've integrated assertion reporting with UVM, so now we'll see assertion fails contribute to the report at the end of the simulation. In addition to this, it should also open up new possibilities.

Sometimes we want to disable select assertions in certain tests where we are intentionally causing a fail scenario. Such situations could be when we are doing error testing or fault injection (for example for ISO 26262 certification). SystemVerilog provides the $assertoff(...) system task for this.

Ideally, we want to do any kind of disabling from inside our UVM environment, i.e. from our UVM test. Normally we have a reference to the interface supplied to us as a virtual interface:

class test_vif extends test_base;
  virtual vgm_wb_slave_interface vif;

  virtual function void start_of_simulation_phase(uvm_phase phase);
    $assertoff(0, vif.stb_held_until_ack);
  endfunction

  // ...
endclass

Trying to call $assertoff(0, vif.stb_held_until_ack) gives different results depending on the simulator, but all of them are disappointing. One one simulator I've seen it throw a fatal run time error, while on another it just silently refused to work.

UVM provides a way of fiddling with report messages. Among others, one thing it allows us to do is to change the severity of certain messages we choose. This is done through a report catcher. We can define our own report catcher that intercepts the error message from the stb_held_untils_ack assertions and demotes them to warnings:

class no_stb_until_ack_error_catcher extends uvm_report_catcher;
  function action_e catch();
    if (get_severity() == UVM_ERROR && uvm_is_match("*STB_I*", get_message()))
      set_severity(UVM_WARNING);
    return THROW;
  endfunction

  // ...
endclass

We then attach it to the root of the hierarchy, where we said the messages get routed:

class test_report_catcher extends test_base;
  virtual function void end_of_elaboration_phase(uvm_phase phase);
    no_stb_until_ack_error_catcher catcher = new("catcher");
    uvm_report_cb::add(uvm_root::get(), catcher);
  endfunction

  // ...
endclass

This will mean that all errors for this assertion will get demoted, regardless of where they come from. If we had two instances of the interface and we'd only want to relax one of them, this wouldn't do. We could change the catcher to also match against the message content against the desired scope, but this is too flaky and it's also not tractable (e.g. what if we have 20 instances and we want to ignore the assertion in 10 of them).

This paper shows us how to embed a UVM component inside the interface so that it can participate in the UVM phasing and configuration mechanisms. There's no reason why such a component couldn't also participate in reporting. We can declare a light class that inherits from uvm_component and instantiate it:

interface vgm_wb_slave_interface(input bit RST_I, input bit CLK_I);
  class message_reporter extends uvm_component;
    function new(string name, uvm_component parent);
      super.new(name, parent);
    endfunction
  endclass

  message_reporter reporter = new($sformatf("%m.reporter"), null);

  // ...
endinterface

This creates a component parallel to the testbench whose name contains the hierarchical path of it's parent interface's instance. Instead of dispatching messages to uvm_root, we could send them through this component. This also has the added benefit that we don't need to specify the scope anymore:

  `define error(MSG) \
    begin \
      if (uvm_report_enabled(UVM_NONE, UVM_ERROR, "WBSLV")) \
        reporter.uvm_report_error("WBSLV", MSG, UVM_NONE, \
          `uvm_file, `uvm_line); \
    end

We can now attach the report catcher to the interface of interest, while leaving the other one untouched:

class test_report_catcher extends test_base;
  virtual function void end_of_elaboration_phase(uvm_phase phase);
    no_stb_until_ack_error_catcher catcher = new("catcher");
    uvm_root top = uvm_root::get();
    uvm_component slave_if0_reporter = top.find("*slave_if0.reporter");
    uvm_report_cb::add(slave_if0_reporter, catcher);
  endfunction

  // ...
endclass

What I don't like about this approach is that it creates multiple tops under uvm_root. Normally we have a UVC for a certain protocol (in our case Wishbone) and the assertions are conceptually part of that UVC, even though they live in the static world. Our goal should be to somehow bring these assertions into the UVC agent. Instead of having the interface's reporter be instantiated under uvm_root, it would be really neat if we could make it a child of the agent. To do this, it has to be created inside the agent instead of getting new-ed in the interface. This is going to be problematic since the reporter class is defined in the interface.

This idea of instantiating classes inside interfaces and referencing them in the UVM hierarchy is suspiciously similar to what we looked at in the previous post on how to achieve interface polymorphism. There I mentioned that the idea came from older papers that favored the idea of abstract BFMs. As luck would have it, one of those papers (namely this one) shows exactly how to make such a BFM a part of the agent.

The first step is to define an abstract class that replaces the interface, a so called proxy:

virtual class checker_proxy extends uvm_component;
  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction
endclass

virtual class sva_checker_wrapper;
  pure virtual function checker_proxy get_proxy(string name,
    uvm_component parent);
endclass

We also need another helper class whose only task is to instantiate the proxy. Inside the interface we define the concrete implementations of these classes:

interface vgm_wb_slave_interface(input bit RST_I, input bit CLK_I);
  typedef class checker_proxy;
  checker_proxy proxy;

  class checker_proxy extends vgm_wb::checker_proxy;
    function new(string name, uvm_component parent);
      super.new(name, parent);
    endfunction
  endclass

  class sva_checker_wrapper extends vgm_wb::sva_checker_wrapper;
    virtual function checker_proxy get_proxy(string name, uvm_component parent);
      if (proxy == null)
        proxy = new(name, parent);
      return proxy;
    endfunction
  endclass

  sva_checker_wrapper checker_wrapper = new();

  // ...
endinterface

Notice that we defined a field for the proxy object, but we didn't instantiate it yet. This is will be done in the get_proxy(...) function, where it gets passed the name and the parent. We want to pass this wrapper class to the agent so that it can call this function, effectively passing itself back to the interface and becoming the proxy's parent. We  can do this via the config DB:

module top;
  vgm_wb_slave_interface slave_if0(rst, clk);

  initial
    uvm_config_db #(vgm_wb::sva_checker_wrapper)::set(null, "*.slave_if0_agent",
      "checker_wrapper", slave_if0.checker_wrapper);

  // ...
endmodule

In the agent we call get_proxy(...), passing it a name and itself as a parent:

class agent extends uvm_agent;
  checker_proxy sva_checker;

  virtual function void build_phase(uvm_phase phase);
    sva_checker_wrapper checker_wrapper;
    if (!uvm_config_db #(sva_checker_wrapper)::get(this, "",
      "checker_wrapper", checker_wrapper)
    )
      `uvm_fatal("CFGERR", "No checker wrapper received")

    sva_checker = checker_wrapper.get_proxy("sva_checker", this);
  endfunction

  // ...
endclass

This way we've separated the act of declaring the proxy from instantiating it. We've let the agent know that the interface exists and asked it to create the proxy as a child component. Now, if we change the `error(...) macro to use the proxy, messages reported from the interface will seem like they originated from inside the agent:

  `define error(MSG) \
    begin \
      if (uvm_report_enabled(UVM_NONE, UVM_ERROR, "WBSLV")) \
        proxy.uvm_report_error("WBSLV", MSG, UVM_NONE, \
          `uvm_file, `uvm_line); \
    end

When we want to disable assertions, we can attach the report catcher to the SVA checker proxy inside the agent:

class test_agent_report_catcher extends test_base;
  vgm_wb::agent slave_if0_agent;
  vgm_wb::agent slave_if1_agent;

  virtual function void end_of_elaboration_phase(uvm_phase phase);
    no_stb_until_ack_error_catcher catcher = new("catcher");
    uvm_report_cb::add(slave_if0_agent.sva_checker, catcher);
  endfunction

  // ...
endclass

No more parallel hierarchies and no more fiddling with children of uvm_root.

One of the main motivations in the Verilab paper for having an embedded UVM component inside the interface is so that we could use the configuration database to tweak various settings inside it. There's no reason why we couldn't do it now as well. We don't even need the config DB. For example, the Wishbone protocol also defines the so called pipelined mode. In this mode, the STB signal doesn't need to stay high until the transfer is completed. A CYC signal (which we've ignored until now) is supposed to stay asserted from start (STB) to finish (ACK):

interface vgm_wb_slave_interface(input bit RST_I, input bit CLK_I);
  logic CYC_I;

  bit m_is_pipelined;

  cyc_held_until_end : assert property (
    $rose(STB_I) |-> CYC_I
      ##0 (ACK_O or ##1 CYC_I throughout
        (!m_is_pipelined && STB_I || ACK_O) [->1])
  )
  else
    `error("CYC_I must be held until transfer end");

  // ...
endinterface

The m_is_pipelined variable controls the mode we are in. We could control its value from the UVM environment via the proxy. We first need to declare a function inside the abstract proxy class to set this variable's value:

virtual class checker_proxy extends uvm_component;
  // ...

  pure virtual function void set_pipelined(bit is_pipelined);
endclass

The abstract proxy class advertises to its users that it's authorized to configure the mode of its interface. Inside the interface, this function's implementation will reference the m_is_pipelined variable:

interface vgm_wb_slave_interface(input bit RST_I, input bit CLK_I);
  class checker_proxy extends vgm_wb::checker_proxy;
    virtual function void set_pipelined(bit is_pipelined);
      m_is_pipelined = is_pipelined;
    endfunction
  endclass

  // ...
endinterface

The test can now easily configure the interface associated with a certain agent via its proxy:

class test_agent_report_catcher extends test_base;
  virtual function void start_of_simulation_phase(uvm_phase phase);
    slave_if1_agent.sva_checker.set_pipelined(1);
  endfunction

  // ...
endclass

Now we've got the interface fully under our control. If you want to see the complete example in action, you can download it from SourceForge.

Let's take a quick look back and see what we've managed to do. We've achieved much tighter integration between our SVAs defined in the interface (static) and our UVC agent (dynamic). By forwarding fail messages through a child component of the agent we've made it seem like the assertions are instantiated inside the UVC. This proxy component takes the place of the static interface for tasks such as disabling individual assertions (using a report catcher) or configuring various parameters. Now we can tweak SVAs to our heart's desire directly from the UVM testbench.