Factory creation and type substitution

Ask the factory for objects and components, replace a class everywhere or at one path, and find out why an override did nothing.

Lesson
U2.2
Time
25 min
Runs on
Verilator with UVM
Check
5 questions

The incrementer bench now has two ports, each with its own agent. An error test should corrupt traffic on port 1 only, so it registers a type override from incr_driver to err_driver. That night the port-0 scoreboard reports hundreds of mismatches, because a type override replaces the class at every path, not at one.

What each create call returns
01234create callenvport0port0.drvport1port1.drvrequestedincr_envincr_agentincr_driverincr_agentincr_drivertype overrideincr_envincr_agenterr_driverincr_agenterr_driverinstance overrideincr_envincr_agentincr_driverincr_agenterr_driverport 0 gets the error driver toothe only path that should

The build creates five components in this order. A type override changes every incr_driver; an instance override for env.port1.drv changes one.

Registering a class with the factory

The UVM factory is a single object that builds other objects on request. Before it can build a class, the class must be registered. The uvm_component_utils and uvm_object_utils macros do that, using the class name as a string key.

The macro also adds a nested type called type_id, a small proxy for that class. incr_driver::type_id::create(...) asks the factory for an incr_driver. incr_driver::get_type() returns the proxy itself, which is how you name a class in an override call. get_type_name() returns the name of the class an object really is, which is the quickest way to check what the factory built.

The string key is also how a test is started. run_test("err_test") asks the factory to create a component from the name err_test. A test class without the macro cannot be found that way, and the run stops at time 0 with an INVTST fatal error saying the requested test was not found.

What create does

A create call passes a name and, for a component, its parent. From those the factory works out the full path of the new object. Then it decides which class to build.

  1. Work out the path

    The parent's full name plus the new name, such as uvm_test_top.env.port1.drv.

  2. Check instance overrides

    Each instance override names a requested type and a path pattern. The first one that matches both is used.

  3. Check type overrides

    With no instance match, the type override for the requested type is used, if there is one.

  4. Follow chains

    The replacement class may itself be overridden, so the factory repeats the search for it.

  5. Build and cast

    The factory builds the final class and casts it back to the requested type. If that cast fails, the run stops with a fatal error.

The last step is why a replacement must extend the class it replaces. The caller holds an incr_driver handle, so whatever arrives must be an incr_driver or something derived from it. Overriding incr_driver with an unrelated class compiles fine, but the first create call stops the run with an FCTTYP fatal error.

Type overrides change every path

incr_driver::type_id::set_type_override(err_driver::get_type()); tells the factory to build an err_driver whenever anyone asks for an incr_driver. Every path is affected, which suits a test that wants a different driver throughout.

A second type override for the same requested type replaces the first. That is the default, and you rarely want anything else. Type overrides also work for objects: override incr_item with a derived wrap_item, and every item a sequence creates through the factory becomes a wrap_item.

Instance overrides change one path

An instance override adds a path pattern. Called from the test, incr_driver::type_id::set_inst_override(err_driver::get_type(), "env.port1.drv", this); affects only the driver at uvm_test_top.env.port1.drv. The third argument makes the path relative: the factory puts the test's own full name in front of it.

Leave out the third argument and the path is taken as complete, from the root. "env.port1.drv" then matches nothing, because every real path starts with uvm_test_top. The call succeeds, nothing warns you, and the override never applies. Patterns may also use * and ? as wildcards, so "*.port1.*" matches everything below port 1.

When an instance override and a type override both match, the instance override wins. The order in which the test registered them makes no difference. So a test can use a type override for the common case and an instance override for the one path that needs something different.

Overrides apply at create time

The factory checks overrides when an object is created, never afterwards. An object that already exists keeps its class. So an override must be registered before the create call it is meant to change.

For components, that leaves more room than it seems. The test's build_phase runs to the end before the environment's build_phase starts. An override registered anywhere in the test's build_phase is therefore in place before the environment creates its agents and drivers, even if the line comes after env = incr_env::type_id::create("env", this);.

The environment object itself is different. It was created on that line, so an override for incr_env registered below it is too late. The habit that avoids all of this is simple: register overrides at the top of the test's build_phase, before super.build_phase(phase) and before any create call. Objects follow the same rule at their own create call: in [U1.4](U1-4-uvm-test-family.html), an item override set during the run still works because the sequence creates its item later, in body().

When an override does nothing

An override that silently fails is one of the most common UVM debugging sessions. Work through the causes in this order.

CauseHow to see it
The class is built with new, which never asks the factoryget_type_name() on the object shows the original class
The override was registered after the create callMove the override to the top of the test's build_phase
The instance path does not matchCompare the override path with the object's get_full_name()
The object was created without a contextIts full path is only its name; pass a context to create

Why a factory override has no effect.

Printing the factory shows what it holds. Call uvm_factory::get().print(0); at the end of elaboration, and it lists every instance override with its full path, then every type override. An instance path with a typo, or one missing uvm_test_top, is easy to spot in that table.

Try it

err_test extends the normal test and only adds two overrides before calling the base class's build_phase. The instance override gives port 1 the error driver and leaves port 0 alone. The type override turns every factory-created incr_item into a wrap_item, but the item made with new stays an incr_item. The factory table at the end lists both overrides.

Change set_inst_override to incr_driver::type_id::set_type_override(err_driver::get_type()); and run again: both ports now get err_driver, as in the opening story. Then remove this from the original instance override call and watch port 1 lose its error driver.

RUNVerilator + UVM 2020.3.1 ยท simulator service
Edit the code, then run it
Expected among the UVM messages: uvm_test_top.env.port0.drv: incr_driver uvm_test_top.env.port1.drv: err_driver create: wrap_item, x=255 new: incr_item, x=0 Instance Overrides: Type Overrides: UVM_ERROR : 0

Check yourself

Predict the outputWhat line does the $display print? UVM's own messages are not part of the answer.
`include "uvm_macros.svh"
import uvm_pkg::*;
class base_drv extends uvm_component;
  `uvm_component_utils(base_drv)
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
endclass
class err_drv extends base_drv;
  `uvm_component_utils(err_drv)
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
endclass
class base_env extends uvm_env;
  `uvm_component_utils(base_env)
  base_drv drv;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    drv = base_drv::type_id::create("drv", this);
  endfunction
endclass
class big_env extends base_env;
  `uvm_component_utils(big_env)
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
endclass
class my_test extends uvm_test;
  `uvm_component_utils(my_test)
  base_env env;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    env = base_env::type_id::create("env", this);
    base_env::type_id::set_type_override(big_env::get_type());
    base_drv::type_id::set_type_override(err_drv::get_type());
  endfunction
  function void end_of_elaboration_phase(uvm_phase phase);
    $display("env=%s drv=%s", env.get_type_name(), env.drv.get_type_name());
  endfunction
endclass
module tb;
  initial run_test("my_test");
endmodule
Spot the bugThe test should give the driver at port 1 the error driver and leave port 0 alone. Each option is the first line of the test's build_phase, before the environment is created. Which one leaves port 1 with the plain driver?
ConceptA test first calls set_inst_override from incr_driver to slow_driver for env.port1.drv (relative to the test), then calls set_type_override from incr_driver to err_driver. What does port 1 get?
ConceptA test overrides incr_driver with apb_driver, which extends uvm_driver #(apb_item) and not incr_driver. What happens when the agent calls incr_driver::type_id::create("drv", this)?
ConceptA test sets an instance override for incr_item at uvm_test_top.env.port1.*. The port-1 sequence creates its items with incr_item::type_id::create("it"), and they stay plain incr_item objects. Why?

Remember

  • The utils macros register a class with the factory; type_id::create asks the factory for it, and new bypasses it.
  • A type override replaces a class at every path; an instance override replaces it only where the path pattern matches.
  • A matching instance override beats a type override, whatever order they were registered in.
  • Overrides apply at create time: register them at the top of the test's build_phase, before anything is created.
  • An instance path without a parent argument is a full path from the root, starting with uvm_test_top; print the factory to check it.