UVM Factory Revealed, Part 1

Introduction

When you first learn UVM, most of the concepts make sense, even if you are new to Object-Oriented Programming. Except one, the UVM Factory. Why do you need all that extra code, class::type_id::create(), just to make an object? What’s wrong with just calling new()? The answer is teamwork!

The Small Problem

Imagine you are trying to verify a design with the X bus protocol. You write UVM testbench components that speak that protocol, including the agent, driver, and monitor. Here is agent code that constructs the driver.

class x_agent extends uvm_agent;
  x_driver drv;

  function void build_phase(…);
    drv = new("drv", this); // Construct the X bus driver
  endfunction
endclass

In the build phase you call new(). SystemVerilog says that this constructs the object based on the type of the handle on the left side, and so you end up with an x_driver object. Job done and you didn’t have to type all that type_id::create() stuff.

But then halfway through the project, your manager says the design must work with the next generation of the protocol, called the Xplus bus. She even gave you the xplus_driver class that extends x_driver.

No problem, just add a flag variable and some code and your agent constructs both types. Since the new class extends the original one, you can reuse the drv handle. You can even get fancy with the typed constructor.

class x_agent extends uvm_agent;
  x_driver drv;
  bit use_xbus;  // True: use X bus, False: XPLUS-bus

  function void build_phase(…);
    if (use_xbus)
      drv = new(…); // Contruct the X bus driver
    else
      drv = xplus_driver::new(…); // Typed constructor builds xplus_driver object
  endfunction
endclass

With this new agent and driver, you write a Xplus test, set the flag use_xbus, and it passes. Who needs the factory when you can make a change this easily? Time to commit your changes and release them to the team.

When you come in the next morning, there are several angry people outside your cubicle. There is a bug in your code – the default value of use_xbus is 0, so the agent always constructs xplus_driver. Everyone who tried to use your revised agent got the wrong driver and now all their tests fail.

The Big Problem

The issue here is not the code bug. You are working in a team, so changes you make affect everyone. Every time you change shared code, adding more control variables, if– and case-statement for all the new variants, you potentially cause bugs. How can your tests inject the new xplus_driver class without changing the x_agent class?

The Solution

Many UVM techniques involve polymorphism. Sounds tricky, but it just means you define a base class with virtual methods. These become “hooks” to inject new behavior. How? Define a derived class and replace the base object with the derived one. Now when you call handle.action(), you can get the base action or the derived, depending on the object type.

The factory pattern is a little more subtle. Imagine that the factory is a printing press. If you load a drum with a base image, it will print base text. If you load one with a derived image, it prints the derived text.

The UVM Factory prints objects
The UVM type-based Factory can print base and derived objects

The UVM Factory prints objects. In the base x_driver and derived xplus_driver, use the `uvm_component_utils(x_driver) macro to define type_id. This is a “proxy class”, which means it is a “helper” of these classes, building their objects. Each type_id is a unique class that, together, are the UVM Factory. The key is that all these proxy classes share a static array which is lookup table of base and derived types. By default, the following call builds an x_driver object.

drv = x_driver::type_id::create("drv", this);

The key concept is that from the test class, you can decide which object is built by the agent, without having to modify the agent’s code. In the test’s build_phase(), add the following call.

x_driver::type_id::set_type_override(xplus_driver::get_type());

You can read this as follows. Tell the x_driver’s factory that when it is asked to create an object, use the type defined in the xplus_driver class, where get_type() returns the type xplus_driver::type_id.

The OOP hook here to inject new behavior is the virtual method create(). The subtle difference is that the method is in the type_id proxy class, not in any of the driver classes.

Conclusion

If you write your UVM testbench with the factory create pattern, instead of directly calling the new() constructor, you can inject new behavior by overriding a base class with a derived. Not only can you override components, but you can also extend a sequence item class, override the base type, and now all your sequences will get this new behavior, without changing their code. The factory pattern enables all this, plus a stable code base, so you can inject new features, without affecting your team.

Look for a more technical post on the UVM Factory next week.

To learn more

You can learn more about these topics including Oriented Programming with the Siemens SystemVerilog for Verification course. It is offered in instructor led format by our industry expert instructors, or in a self-paced on-demand format. It can also be tailored to address your specific design goals and show you how to set up an environment for reuse for additional designs.  Also, you can now earn a digital badge/level 1 certificate by taking our Advanced Topics Badging Exam. This will enable you to showcase your knowledge of this topic by displaying the badge in your social media and email signature.

Want to stay up to date on news from Siemens Digital Industries Software? Click here to choose content that's right for you

Leave a Reply

This article first appeared on the Siemens Digital Industries Software blog at https://blogs.sw.siemens.com/verificationhorizons/2023/01/20/uvm-factory-revealed-part-1/