Creating UVM Tests Dynamically

Everyone who uses UVM knows that using the library ofter requires large amounts of boilerplate code. Tests are no exception.

A UVM test usually starts a certain sequence during its run phase (but other run-time phases could also be used). Let’s assume that we have three sequences: some_sequence, some_other_sequence and yet_another_sequence, which we each want to start from its own test.

Here’s an example of a test that starts some_sequence:

class test_that_executes_some_sequence extends uvm_test;

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction

  virtual task run_phase(uvm_phase phase);
    some_sequence seq = some_sequence::type_id::create("seq", this);
    phase.raise_objection(this);
    seq.start(null);
    phase.drop_objection(this);
  endtask

  `uvm_component_utils(test_that_executes_some_sequence)

endclass

A test that starts some_other_sequence is:

class test_that_executes_some_other_sequence extends uvm_test;

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction

  virtual task run_phase(uvm_phase phase);
    some_other_sequence seq = some_other_sequence::type_id::create("seq", this);
    phase.raise_objection(this);
    seq.start(null);
    phase.drop_objection(this);
  endtask

  `uvm_component_utils(test_that_executes_some_other_sequence)

endclass

Finally, a test that starts yet_another_sequence is:

class test_that_executes_yet_another_sequence extends uvm_test;

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction

  virtual task run_phase(uvm_phase phase);
    yet_another_sequence seq = yet_another_sequence::type_id::create("seq", this);
    phase.raise_objection(this);
    seq.start(null);
    phase.drop_objection(this);
  endtask

  `uvm_component_utils(test_that_executes_yet_another_sequence)

endclass

That’s a pretty impressive amount of code just to say “start each of the three sequences in its own test”. The amount is not necessarily the problem, but coupled with the fact that there is a lot of duplication makes it become one. It’s not immediately obvious whether the three files differ by more than just the sequence and test names or whether there are any hidden tweaks which make them behave slightly differently.

We would like to achieve something like the following pseudo-code:

create_test_that_starts(some_sequence)
create_test_that_starts(some_other_sequence)
create_test_that_starts(yet_another_sequence)

This way we know that the tests only differ in the sequence being started. Even cooler would be to be able to specify tests using loops:

for seq in [some_sequence, some_other_sequence, yet_another_sequence]:
    create_test_that_starts(seq)

This matches the natural language description of our problem statement: “start each of the three sequences in its own test”.

One possible solution would be to use macros, but these generally suffer from an array of issues, like lack of clarity as to how and where they should be used, poor error messages when writing code that doesn’t compile, lack of debug capabilities, etc.

Another widespread solution for such problems in the industry is to use code generation. I’m generally skeptical of code generation of this sort, which usually means passing the buck to some other half baked scripts. These scripts would be in a different language, so they would require us to either spin off or duplicate information we need in SystemVerilog. They also wouldn’t integrate as well into the overall development and execution flow.

We’ll look at an alternative way of defining tests, which doesn’t suffer from the problems of the “traditional” approaches. We won’t just look at the finished code, but take a journey through the steps required to build it.

Before we begin, let’s do a crash course in how UVM starts tests. UVM uses the value of +UVM_TESTNAME as a type name when it instructs the factory to create the test. Conceptually, it does the following:

string test_type_name = get_value_of_plusarg("UVM_TESTNAME");
uvm_component test = uvm_factory()::get().create_component_by_name(test_type_name);

Users define their test classes and register them with the factory using the uvm_component_utils macro. This makes it possible for the test type name to be used as an argument to +UVM_TESTNAME.

To reduce the amount of code we need, we want to consolidate the duplicated code into a single place. If we look at the tests, they only differ by the type of the seq variable, which holds the sequence to start. We can create a shared template by using a class that is parameterized by the sequence type:

class test_that_executes_sequence_via_param #(type SEQ = int) extends uvm_test;

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction

  virtual task run_phase(uvm_phase phase);
    SEQ seq = SEQ::type_id::create("seq", this);
    phase.raise_objection(this);
    seq.start(null);
    phase.drop_objection(this);
  endtask

  `uvm_component_param_utils(test_that_executes_sequence_via_param #(SEQ))

endclass

While the uvm_component_param_utils macro will register the class with the UVM factory, it will not register it using a type name. Since we don’t have type names, we won’t have anything to pass to +UVM_TESTNAME to select tests.

We could create subclasses of the parameterized class, where we pass the desired sequence type for the SEQ parameter, and register those with the factory. For example, for some_sequence we would have:

class test_that_executes_some_sequence_using_param
    extends test_that_executes_sequence_via_param #(some_sequence);

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction

  `uvm_component_utils(test_that_executes_some_sequence_using_param)

endclass

This is still a pretty large amount of boilerplate. Granted, this is code that might not ever change. We could do a little better, though.

The uvm_component_utils macro maps type names to “real” classes using the uvm_component_registry class. If we were to expand the code for uvm_component_utils(test_that_executes_some_sequence_using_param) we would see uvm_component_registry #(test_that_executes_some_sequence_using_param, "test_that_executes_some_sequence_using_param"). This instructs the factory to create a component of type test_that_executes_some_sequence_using_param whenever the string "test_that_executes_some_sequence_using_param" is supplied as a type name.

The exact way this is done is kind of all over the place. The underlying class that the factory uses is called uvm_object_wrapper. It declares two functions, create_object() and create_component(), which the factory uses to create instances. The uvm_component_registry class extends uvm_object_wrapper and implements its create_component() function, but it also has many other functions related to registering itself with the factory and handling type overrides. A cleaner implementation would have been for uvm_component_registry to be its own class, that doesn’t extend uvm_object_wrapper, that instead creates an instance of a uvm_object_wrapper and registers it with the factory instead of itself. This would have provided a better separation of concerns.

We want to create our own mapping, which instructs the factory to create a component of type test_that_executes_sequence_via_param #(some_sequence) whenever the string "test_that_executes_some_sequence_using_param" is supplied as a type name. The easiest way to do this is by creating own own class that extends uvm_component_registry with the appropriate parameters:

class wrapper_for_test_that_executes_some_sequence_using_param
    extends uvm_component_registry #(test_that_executes_sequence_via_param #(some_sequence), "test_that_executes_some_sequence_using_param");
endclass

This class just has to exist. The code in uvm_component_registry will take care of instructing the UVM factory to produce an instance of the test class parameterized with some_sequence when test_that_executes_some_sequence_using_param is passed as a value for +UVM_TESTNAME.

While using parameterization solves a part of the boilerplate issue by having the aspect of “starting sequences” in a single class, it still requires us to write the wrapper classes necessary for factory registration by hand. We can’t implement any kind of looping over the sequences we want to start from tests, as we set out to do.

We have to transpose our code from using types and parameterization to some kind of dynamic constructs. If we go back to test_test_that_executes_sequence_via_param, we see that the type of sequence is not at all relevant to starting it. All uvm_sequences have a start() task, which can be used by the test to start it:

  virtual task run_phase(uvm_phase phase);
    uvm_sequence seq = create_seq();
    phase.raise_objection(this);
    seq.start(null);
    phase.drop_objection(this);
  endtask

We just need a way of creating sequences without hard-coding the exact type. Luckily, the UVM factory already has us covered here. Remember that uvm_object_utils creates a subclass of uvm_object_wrapper that can create the “real” type:

uvm_sequence seq;
uvm_object_wrapper seq_wrapper = some_sequence::get_type()
$cast(result, seq_wrapper.create_object("seq"));

Instead of hard coding the sequence type in the test, so the test can create an instance of that sequence itself, we can instead supply it with a uvm_object_wrapper that produces that sequence:

class test_that_executes_sequence_via_constructor extends uvm_test;

  local const uvm_object_wrapper seq_wrapper;

  function new(string name, uvm_component parent, uvm_object_wrapper seq_wrapper);
    super.new(name, parent);
    this.seq_wrapper = seq_wrapper;
  endfunction

  virtual task run_phase(uvm_phase phase);
    uvm_sequence seq = create_seq();
    phase.raise_objection(this);
    seq.start(null);
    phase.drop_objection(this);
  endtask

  local function uvm_sequence create_seq();
    uvm_sequence result;
    if (!$cast(result, seq_wrapper.create_object("seq")))
      `uvm_fatal("TEST", "Cannot construct sequence from supplied wrapper")
    return result;
  endfunction

endclass

Notice that we have to check at run time whether the seq_wrapper we got can really produce an object of type uvm_sequence. In other programming languages, which take class parameterization seriously and where it’s not just an afterthought, it is possible to enforce this compile time. This would be done by declaring the seq_wrapper argument as being a uvm_object_wrapper of a type that extends uvm_sequence. The syntax could look something like the following:

function new(uvm_object_wrapper #(? extends uvm_sequence) seq_wrapper)

For reference, in Java this language feature is called an upper bounded wildcard.

The tricky part is figuring out how to supply the sequence wrapper. We’ve already talked about how the factory uses uvm_object_wrappers to perform the actual creation and maps type names to such objects.

Let’s look at how we would create a uvm_object_wrapper that creates a test that starts some_sequence and registers itself with the name "test_that_executes_some_sequence_using_constructor". The first thing we need to implement is the create_component() function:

class wrapper_for_test_that_executes_some_sequence_using_constructor
    extends uvm_object_wrapper;

  virtual function uvm_component create_component(string name, uvm_component parent);
    test_that_executes_sequence_via_constructor result = new(name, parent, some_sequence::get_type());
    return result;
  endfunction

endclass

The uvm_object_wrapper class also provides a get_type_name() function, which is supposed to return the name of type being wrapped. I noticed that if we don’t implement this function, we get some annoying prints in the console, so let’s do this as well:

class wrapper_for_test_that_executes_some_sequence_using_constructor
    extends uvm_object_wrapper;

  virtual function string get_type_name();
    return "test_that_executes_some_sequence_using_constructor";
  endfunction

  // ...

endclass

Last, but not least, we should also register this wrapper with the UVM factory, so it knows how to produce instances when "test_that_executes_some_sequence_using_constructor" is used as a type name. This is more or less a copy/paste of how uvm_component_registry does it:

class wrapper_for_test_that_executes_some_sequence_using_constructor
    extends uvm_object_wrapper;

  local static wrapper_for_test_that_executes_some_sequence_using_constructor me = get();

  local function new();
  endfunction

  static function wrapper_for_test_that_executes_some_sequence_using_constructor get();
    if (me == null) begin
  	  uvm_coreservice_t cs = uvm_coreservice_t::get();
  	  uvm_factory factory = cs.get_factory();
      me = new();
      factory.register(me);
    end
    return me;
  endfunction

  // ...

endclass

It’s worth discussing about how registration is actually triggered. Notice that the me variable is static, but has a initial value. This value is computed during static initialization, which happens very early within a simulation, before any processes (e.g. from initial or always blocks) are executed.

If we were to have to write a uvm_object_wrapper for each of the sequences we want to start, we’d end up with a lot of boilerplate. This is exactly what we want to avoid.

We notice that we can pass the sequence wrapper as an argument to the test wrapper itself:

class wrapper_for_test_that_executes_sequence_using_constructor
    extends uvm_object_wrapper;

  local const uvm_object_wrapper seq_wrapper;

  function new(uvm_object_wrapper seq_wrapper);
    this.seq_wrapper = seq_wrapper;
  endfunction

  virtual function string get_type_name();
    return $sformatf("test_that_executes_%s_using_constructor", seq_wrapper.get_type_name());
  endfunction

  virtual function uvm_component create_component(string name, uvm_component parent);
    test_that_executes_sequence_via_constructor result = new(name, parent, seq_wrapper);
    return result;
  endfunction

endclass

We can extract the code for factory registration to a different class, which is responsible for registering tests for the sequences we want to start:

class tests_that_execute_each_sequence;

  local static bit test_for_some_other_sequence_registered = register_test(some_other_sequence::get_type());
  local static bit test_for_yet_another_sequence_registered = register_test(yet_another_sequence::get_type());

  local static function bit register_test(uvm_object_wrapper seq_wrapper);
    uvm_coreservice_t cs = uvm_coreservice_t::get();
  	uvm_factory factory = cs.get_factory();
    wrapper_for_test_that_executes_sequence_using_constructor test_wrapper = new(seq_wrapper);
    factory.register(test_wrapper);
    return 1;
  endfunction

endclass

Notice that the register_test() function returns a bit, but the value is always 1. This is so that it can be used as an initial value for a static helper variable, so that the function is called during static initialization.

The tests_that_execute_each_sequence class does a bit much, though. It knows how to register tests with the factory, but is also registers the tests we want.

We can extract the code for registration into an own class:

class test_builder;

  local const wrapper_for_test_that_executes_sequence_using_constructor test_wrapper;

  local function new(uvm_object_wrapper seq_wrapper);
    test_wrapper = new(seq_wrapper);
  endfunction

  static function test_builder for_sequence_type(uvm_object_wrapper seq_wrapper);
    test_builder result = new(seq_wrapper);
    return result;
  endfunction

  function bit register();
    uvm_coreservice_t cs = uvm_coreservice_t::get();
  	uvm_factory factory = cs.get_factory();
    factory.register(test_wrapper);
    return 1;
  endfunction

endclass

Registering tests for the sequences is a bit cleaner, because we’re not distracted by the actual test builder code.

class tests_that_execute_each_sequence;

  local static bit test_for_some_other_sequence_registered
      = test_builder::for_sequence_type(some_other_sequence::get_type()).register();
  local static bit test_for_yet_another_sequence_registered
      = test_builder::for_sequence_type(yet_another_sequence::get_type()).register();

endclass

Now it starts to become obvious that we can use a loop to register the tests:

class tests_that_execute_each_sequence;

  local static bit tests_registered = register_tests();

  local static function bit register_tests();
    uvm_object_wrapper seqs[] = '{
        some_sequence::get_type(),
        some_other_sequence::get_type(),
        yet_another_sequence::get_type() };

    foreach (seqs[i])
      void'(test_builder::for_sequence_type(seqs[i]).register());
  endfunction

endclass

While it’s very convenient to use loops, being able to use procedural code to define tests is extremely powerful. It makes it possible to do things like to conditionally define tests based on conditions known to the testbench (for example, whether a feature is present in the DUT) or to read some input file and define tests based on that. The space of possibilities is huge.

To reap the benefits of dynamic test creation, we would want to integrate the code from above into our own projects. The test_builder class and its associated classes feel as if they shouldn’t depend on any kind of user code and that they could be reusable.

One thing a uvm_test also does, that we completely left out of the examples, is to instantiate the rest of the verification environment (i.e. the agents, scoreboards, etc.). The examples we’ve looked at so far completely ignore this fact. We’ll emulate it with an info message in the build phase:

virtual class abstract_test extends uvm_test;

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction

  virtual function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    `uvm_info("TEST", "Building TB...", UVM_NONE)
  endfunction

endclass

The test_builder registers tests of the type test_that_executes_sequence, which only knows how to start sequences, but knows nothing about TB creation. In order to combine the two, test_that_executes_sequence can become a mixin, allowing the user to include it onto their abstract_test class:

class test_that_executes_sequence #(type T = uvm_test) extends T;

    // ...

endclass

Naturally, the parameter has to propagate to the other classes as well:

class wrapper_for_test_that_executes_sequence #(type T = uvm_test) extends uvm_object_wrapper;

  virtual function uvm_component create_component(string name, uvm_component parent);
    test_that_executes_sequence #(T) result = new(name, parent, seq_wrapper);
    return result;
  endfunction

  // ...

endclass
class test_builder #(type T = uvm_test);

  local const wrapper_for_test_that_executes_sequence #(T) test_wrapper;

  static function test_builder #(T) for_sequence_type(uvm_object_wrapper seq_wrapper);
    test_builder #(T) result = new(seq_wrapper);
    return result;
  endfunction

  // ...

endclass

This allows users to layer test building onto their own code:

class tests_that_execute_each_sequence;

  local static function bit register_tests();
    // ...
    foreach (seqs[i])
      void'(test_builder #(abstract_test)::for_sequence_type(seqs[i]).register());
  endfunction

  // ...

endclass

At the moment, I see the test builder as more of a pattern, instead of a reusable library, because there’s not much code there to reuse directly. A full example can be found at https://github.com/verification-gentleman-blog/creating-uvm-tests-dynamically. It can serve as the basis for implementing the pattern in your own projects.

In a future post, I’m going to show you how to use dynamic test creation to layer constraints onto a random test, in order to target certain areas of the design space. Be sure to subscribe if you don’t want to miss it.

Comments