UVM: The Factory Powers Reuse

As I mentioned in my last UVM post, UVM allows engineers to create modular, reusable, randomized self-checking testbenches. In that post, we covered the “modularity” aspect of UVM by discussing TLM interfaces, and their value in isolating a component from others connected to it. This modularity allows a sequence to be connected to any compatible driver as long as they both are communicating via the same transaction type, or allows multiple coverage collectors to be connected to a monitor via the analysis port. This is incredibly useful in creating an environment in that it gives the environment writer the ability to mix and match components from a library into a configuration that will verify the desired DUT.

Of course, it is often the case that you may want to have multiple environments that are very similar, but may only differ in small ways from each other. Every environment has a specific purpose, and also shares most of its infrastructure with the others. How can we share the common parts and only customize the unique portions? This is, of course, one of the principles of object-oriented programming (OOP), but UVM actually takes it a step further.

A naïve OOP coder might be tempted to instantiate components in an environment directly, such as:
class my_env extends uvm_env;
  function void build_phase(…);
    my_driver drv = new("drv", this); //extended from uvm_driver
    …
  endfunction
  …
endclass

The environment would be similarly instantiated in a test:
class my_test extends uvm_test;
  function void build_phase(…);
    my_env env = new("env", this); //extended from uvm_env
  endfunction
  …
endclass

Once you get your environment running with good transactions, it’s often useful to modify things to find out what happens when you inject errors into the transaction stream. To do this, we’ll create a new driver, called my_err_driver (extended from my_driver) and instantiate it in an environment. OOP would let us do this by extending the my_env class and overloading the build_phase() method like this:
class my_env2 extends my_env;
  function void build_phase(…);
    my_err_driver drv = new(“drv”, this);
    …
  endfunction
  …
endclass

Thus, the only thing different is the type of the driver. Because my_err_driver is extended from my_driver, it would have the same interfaces and all the connections between the driver and the sequencer would be the same, so we don’t have to duplicate that other code. Similarly, we could extend my_test to use the new environment:
class my_test2 extends my_test;
  function void build_phase(…);
    my_env2 env = new(“env”, this); //extended from my_env
    …
  endfunction
  …
endclass

So, we’ve gone from one test, one env and one driver, to two tests, two envs and two drivers (and a whole slew of new build_phase() methods), when all we really needed was the extra driver. Wouldn’t it be nice if there were a way that we could tell the environment to instantiate the my_err_driver without having to create a new extension to the environment? We use something called the factory pattern to do this in UVM.

The factory is a special class in UVM that creates an instance for you of whatever uvm_object or uvm_component type you specify. There are two important parts to using the factory. The first is registering a component with the factory, so the factory knows how to create an instance of it. This is done using the appropriate utils macro (which is just about the only macro I like using in UVM):
class my_driver extends uvm_driver;
  `uvm_component_utils(my_driver) // notice no ‘;’
  …
endclass

The uvm_component_utils macro sets up the factory so that it can create an instance of the type specified in its argument. It is critical that you include this macro in all UVM classes you create that are extended (even indirectly) from the uvm_component base class. Similarly, you should use the uvm_object_utils macro to register all uvm_object extensions (such as uvm_sequence, uvm_sequence_item, etc.).

Now, instead of instantiating the my_driver directly, we instead use the factory’s create() method to create the instance:
class my_env extends uvm_env;
  `uvm_component_utils(my_env)
  function void build_phase(uvm_phase phase);
    drv = my_driver::type_id::create(“drv”, this);
    …
  endfunction
  …
endclass

The “::type_id::create()” incantation is the standard UVM idiom for invoking the factory’s static create() method. You don’t really need to know what it does, only that it returns an instance of the my_driver type, which is then assigned to the drv handle in the environment. Given this flexibility, we can now use the test to tell the environment which driver to use without having to modify the my_env code.

Instead of extending my_test to instantiate a different environment, we can instead use an extension of my_test to tell the factory to override the type of object that gets instantiated for my_driver in the environment:
class my_factory_test extends uvm_test;
  `uvm_component_utils(my_factory_test);
  function void build_phase(…);
my_driver::type_id::set_type_override(my_err_driver::get_type());
    super.build_phase(…);
  endfunction
endclass

This paradigm allows us to set up a base test that instantiates the basic environment with default components, and then extend the base test to create a new test that simply consists of factory overrides and perhaps a few other things (for future blog posts) to make interesting things happen. For example, in addition to overriding the driver type, the test may also choose a new stimulus sequence to execute, or swap in a new coverage collector. The point is that the factory gives us the hook to make these changes without changing the env code because the connections between components remain the same due to the use of the TLM interfaces. You get to add new behavior to an existing testbench without changing code that works just fine. It all fits together…

Comments

0 thoughts on “UVM: The Factory Powers Reuse

Leave a Reply