Showing posts with label Assertion Based Verification. Show all posts
Showing posts with label Assertion Based Verification. Show all posts

Sunday, June 25, 2017

Testing SVA Properties and Sequences

After a pretty long absence, it’s finally time to complete the series on unit testing interface UVCs. I meant to write this post in October/November 2016. While writing the code, I got bogged down by a simulator bug and tried to find an elegant work around, but failed. I got frustrated and shelved the work for a while. In the meantime I got caught up with technical reading and with taking online courses. I’ve also been pretty busy at work, putting in quite a bit of overtime, which left without much energy to do anything blog related. Well, enough excuses, it’s time to get to it…

Aside from monitoring and driving signals, an interface UVC is also responsible for checking that the DUT conforms to the protocol specification. This is typically done with SystemVerilog assertions, which provide a compact and powerful syntax for describing temporal behavior at the signal level. I already wrote a bit about unit testing SVAs using SVAUnit, a library from our friends at AMIQ Consulting.

I’ve since then had a bit of an epiphany while working on an interface UVC for a new proprietary on-chip interface. Now on a previous module, I needed to write some bigger SVA properties of the type “when register X is written via AHB, then Y happens”. The AHB UVC I was using (also written by me a while back) only had assertions embedded in a checker, but it didn’t export any SVA sequences for users to combine into their own properties. I ended up doing a bit of clipboard based inheritance and defining the needed sequences in my module SVA checker. I had a similar problem when I was trying to write some simple formal properties for a different module, where I also just copied (gasp!) and patched the needed code. Now AHB isn’t such a complicated and/or dynamic protocol, but my actions were in direct violation of the DRY princple. For the new UVC I was developing, I decided to not make the same mistake and build a nice hierarchy of SVA sequences, properties and assertions. These would form part of the UVC API, sort of an SVA API which users could “call” in their own code. As any important parts of the exported API, such members have to be unit tested.

Let’s look at some simple AHB SVA constructs. Since this protocol is so popular, I assume that most of you are already aquainted with it and for those of you who aren’t I’ll try to keep things simple, but I won’t explain any protocol details. Since the spec isn’t open, I can’t link it here, but a quick search should net you some useful resources. Nevertheless, I’m pretty sure you’ll be able to follow the post without becoming an expert in the protocol.

One of the first non-trivial protocol rules for AHB is that “[when] the slave is requesting wait states, the master must not change the transfer type […]”. Let’s write a property for this with the following signature:

property trans_held_until_ready(HTRANS, HREADY);
  // ...
endproperty

In the previous post we saw how we can use the expect statement to check that signal toggles happen as desired. The unit test supplied the property, while the code being tested handled the signals. To check a property, we can reverse the two roles and have the test supply the signal toggles, while the code under test is exactly our property of interest. The same `FAIL_UNLESS_PROP(…) macro can help us check if the property passes for a legal trace:

    `SVTEST(trans_held_until_ready__trans_stable__passes)
      cb.HTRANS <= NONSEQ;
      cb.HREADY <= 0;
      cb.HREADY <= ##3 1;

      `FAIL_UNLESS_PROP(trans_held_until_ready(HTRANS, HREADY))
    `SVTEST_END

I’ve ommited the definitions of the signals, which are bundled in a clocking block called cb. Declaring this clocking block as default also allows use to use the cycled delay operator, ##n, which makes the code a bit more readable.

Just checking that properties pass is rather boring though. Not only that, but a property that doesn’t pass when it should results in a false negative, which is instantly visible in the simulation log. It’s much more valuable that a property fail when it should, because false positives are much more insidious and less likely to be caught. We can do this also with an expect statement, but we’ll need to trigger a fail if the corresponding pass block is triggered. As with `FAIL_UNLESS_PROP(…), we can wrap this check inside a macro:

`define FAIL_IF_PROP_PASS(prop) \
  expect (prop) \
    `FAIL_IF(1)

Having HTRANS change before an occurence of HREADY should cause our property to fail:

    `SVTEST(trans_held_until_ready__trans_changes__fails)
      cb.HTRANS <= NONSEQ;
      cb.HREADY <= 0;
      cb.HTRANS <= ##3 SEQ;

      `FAIL_IF_PROP(trans_held_until_ready(HTRANS, HREADY))
    `SVTEST_END

Here’s how a property that satisfies both tests could look:

property trans_held_until_ready(HTRANS, HREADY);
  HTRANS inside { NONSEQ, SEQ } |=>
    $stable(HTRANS) throughout HREADY [->1];
endproperty

Those of you who’ve worked with AHB before might raise an eyebrow looking at that code. What if, for example, HTRANS comes together with HREADY and changes in the following cycle? The property shouldn’t fail, as the first transfer was accepted and a new one can begin. We can add a test for this exact situation:

    `SVTEST(trans_held_until_ready__trans_stable__passes)
      cb.HTRANS <= NONSEQ;
      cb.HREADY <= 0;
      cb.HREADY <= ##3 1;

      `FAIL_UNLESS_PROP(trans_held_until_ready(HTRANS, HREADY))
    `SVTEST_END

With this test, we can fix the property:

  property trans_held_until_ready(HTRANS, HREADY);
    HTRANS inside { NONSEQ, SEQ } |->
      HREADY or ##1 ($stable(HTRANS) throughout HREADY [->1]);
  endproperty

Some of you might argue that when HTRANS comes at the same time as HREADY, we’re not really “holding” anything. It could be argued that this is a special case and what we’re really interested in for this property are the cases where we actually see some wait states. We could exclude the instant grant case by tweaking the antecedent in such a way that it doesn’t match. This would lead to a vacuous pass of the property. A vacuous pass means that we have’t really checked anything, because we didn’t particularly care what happened in that situation. Vacuous passes aren’t usually shown in the assertion statistics, so we could “misuse” the number of times an assertion of this property passes as coverage for how many times we’ve seen (correctly) stalled transfers.

A vacuous pass is still a pass though and as per the LRM it should also trigger the execution of an assert/expect statement’s pass block. Some tools don’t work like this, though, choosing instead to not execute pass blocks on vacuous successes (unless the user explictly enables this, maybe via some command line switch or simulator setting). We can use this to our advantage to distinguish between a “real” pass and a vacuous pass. What we  have then, is a sort of ternary logic, where a property can result in one of the following:

  • (nonvacuous) pass, where the pass block is executed
  • fail, where the fail block is executed
  • vacuous pass, where neither block is executed

Note that there’s no concept of vacuous fails. Something either works, it doesn’t or it isn’t “important”.

Even if a simulator does execute pass block for vacuous successes, this behavior can either be turned off via a switch or, in a more portable fashion, via the $assertcontrol(…) system task (if it’s supported by the tool). This means that we can rather safely rely on the behavior described in the outcome list to determine vacuity.

As before, we can wrap such a check inside a macro. It’s definition is a bit trickier, since we need to check that neither block was executed. We can do this using variables:

`define FAIL_UNLESS_PROP_VAC(prop) \
  begin \
    bit pass_called; \
    bit fail_called; \
    \
    expect (prop) \
      pass_called = 1; \
    else \
      fail_called = 1; \
    \
    if (pass_called || fail_called) \
      `FAIL_IF(1) \
  end

If any of the two blocks gets executed, one of the variables will be set and we can issue an error. This code, while deceptively simple, fails to compile in some simulators, with them complaining that they can’t find the definition of pass_called inside the pass block (and, of course, the same for fail_called). This is the part where I got bogged down trying to find a suitable workaround. The only way I could get this to work was to define the *_called variables inside a package and use the scope operator to reference them in the pass/fail blocks:

package vgm_svunit_utils_sva;

  bit pass_called;
  bit fail_called;

endpackage

This seems rather crazy, because not only does it require a user to include the file with the macro definition, but to also compile the extra support package. It’s a bit much for just a couple of measly variables, but it’s either this or nothing…

Since we’re going to rely on global variables with a persistent lifetime, we’ll need to make sure to set them to 0 before executing the expect:

`define FAIL_UNLESS_PROP_VAC(prop) \
  begin \
    vgm_svunit_utils_sva::pass_called = 0; \
    vgm_svunit_utils_sva::fail_called = 0; \
    \
    expect (prop) \
      vgm_svunit_utils_sva::pass_called = 1; \
    else \
      vgm_svunit_utils_sva::fail_called = 1; \
    \
    if (vgm_svunit_utils_sva::pass_called || \
        vgm_svunit_utils_sva::fail_called) \
      `FAIL_IF(1) \
end

Using this macro, we can tweak our test for instantly granted transfers to require a vacuous pass for the property:

    `SVTEST(trans_held_until_ready__trans_stable__vacuous)
      cb.HTRANS <= NONSEQ;
      cb.HREADY <= 0;
      cb.HREADY <= ##3 1;

      `FAIL_UNLESS_PROP_VAC(trans_held_until_ready(HTRANS, HREADY))
    `SVTEST_END

This will also mean that we have to fix the property:

property trans_held_until_ready(HTRANS, HREADY);
  HTRANS inside { NONSEQ, SEQ } && !HREADY |=>
    $stable(HTRANS) throughout HREADY [->1];
endproperty

Let’s take a step back now. Remember, that for the `FAIL_IF_PROP(…) macro we were checking whether the pass block is executed and if it was we issued an error. This doesn’t fit into the whole “ternary logic” scheme we discussed above when talking about vacuity. If we only did this, we wouldn’t be able to distinguish a fail from a vacuous pass. The macro name is also kind of misleading. What do we want here? Do we want the property to fail? Do we want it to fail or be vacuous, but under no circumstances result in a nonvacuous pass? What I intended was the former, but others could just as well interpret it as the latter.

More explicit macros would better clarify our intent. In this case, what we want is a `FAIL_UNLESS_PROP_FAIL(…) macro, because we are explicitly checking that the property can catch errors. What we should check is that the fail block gets executed:

`define FAIL_UNLESS_PROP_FAIL(prop) \
  begin \
    vgm_svunit_utils_sva::fail_called = 0; \
    \
    expect (prop) \
    else \
      vgm_svunit_utils_sva::fail_called = 1; \
    \
    if (!vgm_svunit_utils_sva::fail_called) \
      `FAIL_IF(1) \
  end

In some cases, we would want to forbid a property to fail, but it wouldn’t be important if the pass is vacuous or not. Here we would have a `FAIL_IF_PROP_FAIL(…) macro, that check that the fail block wasn’t executed. There are six such macros that we can define, a pair for each of the three possible outcomes. We won’t look at their definitions here, but their construction is pretty simple now that we know the behavior of the pass/fail blocks.

It’s time for another realization about our property: the trigger condition is slightly off. Once we assert it what’s going to happen is that a new evaluation thread is going to be started on each clock cycle where a transfer is stalled. All of these threads are going to run in parallel, perform the same check and end at the very same time – when HREADY finally comes. This isn’t good for perfomance, especially if we have many AHB interfaces and very long stalls.

The start of an AHB transaction is a pretty interesting event, not only for this property, but potentially for others. A UVC user might be interesed in writing an own property that triggers once a transfer has started. We can fix our property and at the same time provide a reusable building block by defining a sequence:

sequence trans_started(HTRANS);
  // ...
endsequence

Just as for properties we wanted to check whether they fail or pass when we want them, for sequence we want to make sure that they match when they should and don’t match when they shouldn’t. We can check for a sequence match (or lack thereof) by treating it as a property and checking its pass/fail state. Doing the following would look a bit weird, though:

`FAIL_UNLESS_PROP_PASS(trans_started(HTRANS))

The intent of the code becomes a bit muddied: “Are we testing a property? But I thought trans_started(…) was a sequence…”. It would be better to have separate macros for sequence testing:

`define FAIL_IF_SEQ(seq) \
  `FAIL_UNLESS_PROP_FAIL(seq ##0 1)

`define FAIL_UNLESS_SEQ(seq) \
  `FAIL_UNLESS_PROP_PASS(seq ##0 1)

Using these will make the code a bit clearer. Also, notice the extra ##0 1 fused after the sequence. This is to ensure that we can’t accidentally pass a property as an argument, because the fusion operator will cause a compile error unless what comes before it is a sequence.

Coming back to our trans_started(…) sequence, the first thing we would like it to do is to not match when HTRANS is IDLE:

    `SVTEST(trans_started__idle__doesnt_match)
      HTRANS <= IDLE;
      `FAIL_IF_SEQ(trans_started(HTRANS))
    `SVTEST_END

We would also like it to match then HTRANS becomes active after an IDLE:

    `SVTEST(trans_started__coming_from_idle__matches)
      cb.HTRANS <= IDLE;
      ##1;

      cb.HTRANS <= NONSEQ;

      `FAIL_UNLESS_SEQ(trans_started(HTRANS))
    `SVTEST_END

With our tests in place, we can write the sequence implementation that fulfils them:

sequence trans_started(HTRANS);
  HTRANS inside { NONSEQ, SEQ } &&
    $past(HTRANS inside { IDLE, BUSY });
endsequence

Something still doesn’t feel right, though. What about back to back transfers? It’s perfectly legal to start a new transfer immediately after the previous one was granted. In this case, there isn’t any IDLE cycle to use as an anchor. What we can, however, use is the occurrence of HREADY in the previous cycle, which we’ll have to feed to the property. Here’s how this could be tested:

    `SVTEST(trans_started__after_done_trans__matches)
      cb.HTRANS <= NONSEQ;
      cb.HREADY <= 1;
      ##1;

      `FAIL_UNLESS_SEQ(trans_started(HTRANS, HREADY))
    `SVTEST_END

The fixed sequence would then be:

sequence trans_started(HTRANS, HREADY);
  HTRANS inside { NONSEQ, SEQ } &&
    $past(HTRANS inside { IDLE, BUSY } || HREADY);
endsequence

If we try to run this in the simulator, though, the test is still going to fail, even though there isn’t anything obviously wrong with our fix. This is because the test contains a very subtle mistake. When the underlying expect statement from the `FAIL_*(…) macro kicks in, in its very first cycle the value returned by $past(HREADY) is 0, because we haven’t actually let it run long enough for there to have been an actual previous cycle. The LRM states that in this case $past(…) returns the default value of the expression passed to it. What we need to do is move the delay operator into the `FAIL_*(…) macro, to allow the expect to sample HREADY first and look for a match of trans_started(…) afterwards:

    `SVTEST(trans_started__after_done_trans__matches)
      cb.HTRANS <= NONSEQ;
      cb.HREADY <= 1;

      `FAIL_UNLESS_SEQ(##1 trans_started(HTRANS, HREADY))
    `SVTEST_END

This way the test does what we intend it to do. We can now instantiate the sequence inside our trans_held_until_ready(…) property to have it trigger at the appropriate times. Since trans_started(…) has already been tested, we don’t have to write too many tests for the property, focusing just on what’s important. Also, by breaking the problem into smaller parts its easier to notice what the corner cases might be. As we’ve seen, writing even such a small property can be tricky, so we should make sure that our code works before trusting it to find design bugs.

Regarding the testing of assertions, I’m not saying that this isn’t important as well. A lot of the more complicated assertions we need to write will have to rely on support code (for example when pipelining comes into the mix) and we’re going to want to check that all parts of an assertion – the property, the support code and the connections between them – fit properly together. For protocol assertions and other simple assertions, I favor breaking down into smaller properties and sequences and testing those, not only to make testing easier, but to also provide a set of reusable elements that UVC users can integrate into their own code.

You can find the full code for the examples here and you can also download the SVUnit utils package if you want to start using these techiques for your own code.

There’s still some work to be done regarding unit tests and SVA constructs. For one, we strongly relied on the assumption that vacuous passes don’t trigger action blocks. We could add some code that tests this assumption by doing a trial run of a known vacuous property (e.g. 0 –> 1). If this isn’t the case, we could try calling the $assertcontrol(…) system task to disable vacuous success execution of pass blocks, if the task is available. Finally, if all else fails, we could inform the user to change the tool invocation to match our required behavior. This plan makes me feel less bad about the extra *_utils_sva package, which we had to use for the workaround with the status variables, because this is where we’d put all of this extra code. I’d also like to see this code merged into SVUnit at some point, but I’m not sure if now is the right time, due to the differences in tool capabilities across simulator vendors.

Now I’d like to conclude this series on unit testing UVCs. The tips in the past few posts should help you develop your UVCs faster and with higher quality, thereby increasing your confidence that they are ready for life in the harsh and unforgiving world of verification.

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.