How UVM Factory Works..??

In my personal opinion, one of the most appealing word in UVM is “Factory”. It stimulates our thinking & our imagination to fall down on industrial settings and mechanical setup of machinaries & processing units/pipes with products getting made and packaged before running on the transport rail & to be delivered to the warehouses. Hence the word “Factory” makes us curious to know about this jargon in Verification Methodology Context i.e. UVM.

Since UVM primarily deals with the the high level of data abstraction e.g. Transactions represented by OOPs based Classes, hence it looks like Factory has to do something with OOPs based components & objects, their generation and/or replacements in the UVM based Verification Environment. Lets dig deep into “Factory” & know more about it?

First of all, its important to understand that as per the recommended UVM methodology, we should never construct new components and/or transactions using new() class constructor. Instead it is recommended that – we should make calls to a look-up table to create the requested components and transactions. This special look-up table is called “Factory” in UVM. Entries to this look-up table is made by registering the components and/or transactions while defining them. To create a component/transaction using Factory, create() command is used. We’ll see all the process steps in detail using the applicable UVM code below.

From the application point of view, UVM Factory facilitates an object of one type to be substituted with an object of derived type without having to change the structure of the Testbench or modify the Testbench code. This behavior is called “overriding” and there are 2 types of overriding is possible with UVM Factory (described below). Overriding helps to replace one transaction/sequence with another to generate new scenarios or conditions without making any change in the Testbench structure/code. Similarly, new version of the components can be brought into the Testbench without any change in the structure of the Testbench & beauty of all this is that – it happens all the fly at the run time.

To achieve & enable this capability of the UVM Testbench, it is required to follow certain steps while defining & creating our components/sequences/transactions.  Lets go through these required steps:

 1. Factory Registration:

Every UVM component or transactions must contain Factory registration code & this registration code can be generated by using Factory registration macros, as shown below in the code:


  //// Registration for a component
   class my_comp extends uvm_component;
      `uvm_component_utils(my_comp)
     ...
     ...
     ...
   endclass: my_comp

It is recommended to extend your component class from UVM base classes available e.g uvm_env, uvm_agent, uvm_driver, uvm_monitor, uvm_sequencer etc. for the the corresponding physical component.


  //// Registration for a sequence
   class my_seq extends uvm_sequence #(my_txn);
      `uvm_object_utils(my_seq)
     ...
     ...
     ...
   endclass: my_seq

  //// Registration for a transaction
   class
      `uvm_object_utils(my_txn)
     ...
     ...
     ...
   endclass: my_txn

After registration macro usage, another important coding convention for UVM Factory is the constructor syntax in components and objects, let see it next:

2. Default Constructor:

Since uvm_component and uvm_object constructors are virtual methods hence user have to follow their prototype template. As we know that in UVM components/objects are constructed during build phase but Factory constructor should contain default arguments in the definition of the components/objects. This allows Factory registered component/object to be created inside Factory initially & later to be re-assigned to the class properties passed via the create() command as arguments. The default arguments are different for components and objects.


  class my_comp extends uvm_component;
      `uvm_component_utils(my_comp)

      // Component Default Constructor
      function new (string name = "my_comp", uvm_component parent = null);
         super.new(name, parent);
      endfunction: new

  endclass: my_comp

  class my_seq extends uvm_seqeuence #(my_txn);
      `uvm_object_utils(my_seq)

      // Sequence Default Constructor
      function new (string name = "my_seq");
         super.new(name);
      endfunction: new

  endclass: my_seq

  class my_txn extends uvm_sequence_item;
      `uvm_object_utils(my_txn)

      // Transaction Default Constructor
      function new (string name = "my_txn");
         super.new(name);
      endfunction: new

  endclass: my_txn

3. Component & Object Creation:

Using Factory, hierarchically below components/objects (childs) are created by the immediate higher components/objects (parent). Hence, next goal would be the Factory supported component and object creation code entry for the child components inside the parent components and objects. Lets see how to do that –


  class my_agent extends uvm_agent;
     `uvm_component_utils(my_agent)

     my_driver drvr;
     my_monitor moni;

    // Component Constructor
     function new (string name = "my_agent", uvm_component parent = null);
       super.new(name, parent);
     endfunction: new

    // Build Phase
     function void build_phase (uvm_phase phase);
       super.build_phase(phase);
       drvr = my_driver::type_id::create("drvr", this);
       moni = my_monitor::type_id::create("moni", this);
     endfunction: build_phase

  endclass: my_agent

  class my_driver extends uvm_driver #(my_txn);
     `uvm_component_utils(my_driver)

    // Driver Constructor
     function new (string name = "my_driver", uvm_component parent = null);
       super.new(name, parent);
     endfunction: new

    // Run Task
     task run_phase (uvm_phase phase);
       my_txn txn;
       txn = my_txn::type_id::create("txn", this);
       .......;
     endtask: run_phase

  endclass: my_driver

As described above regarding “overriding” behavior of Factory, here lets elaborate it in detail now –

As we discussed it before, The UVM Factory can be thought of as a look-up table, so when normal component construction takes place using <type>::type_id::create(“<name>”, <parent>); approach, what happens is that the type_id is used to pick up the Factory component wrapper for the class, construct its contents & pass the resultant handle back again to the LHS.

The Factory override changes the way in which lookup happens so that looking up the original type_id results in a different type_id being used. Consequently, a handle to a different type of constructed object is returned. This technique is primarily based on Polymorphism which is the ability to be able to refer to the derived types using a base type handle. In practice, an override will only work if the parent class is being overridden by one of its derived classes.

Now lets discuss the supported 2 types of overriding:

Type Overriding:

A type overriding means that every time a component class type is created in the Testbench hierarchy, a substitute type i.e. derived class of the original component class, is created in its place. It applies to all the instances of that component type.

Syntax:

<original_type>::type_id::set_type_override(<substitute_type>::get_type(), replace);

where “replace” is a bit which is when set equals to 1, enables the overriding of an existing override else existing override is honoured.

Example UVM Code:

  class my_driver extends uvm_driver #(my_txn);
     `uvm_component_utils(my_driver)

     ..
     ..

  endclass: my_driver

  class my_updated_driver extends my_driver #(my_txn);
     `uvm_component_utils(my_updated_driver)

     ..
     ..

  endclass: my_updated_driver

  class my_agent extends uvm_agent;
     `uvm_component_utils(my_agent)

     my_driver drvr;

    // Agent Constructor
     function new (string name, uvm_component parent);
       super.new(name, parent);
     endfunction: new

    // Agent Build Phase
     function void build_phase (uvm_phase phase);
       super.build_phase(phase);
       drvr = my_driver::type_id::create("drvr", this);
     endfunction: build_phase

  endclass: my_agent

  class my_test extends uvm_test;
     `uvm_component_utils(my_test)

     env e;

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

     function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        my_driver::type_id::set_type_override(my_updated_driver::get_type(),1);
        e = env::type_id::create("e", this);
     endfunction: build_phase

    task run_phase (uvm_phase phase);
       ...
       ...
       ...
    endtask: run_phase

  endclass: my_test

An important point to note here is the order of 2 commands i.e. set_type_override()  to be placed before the create() command inside the build_phase() of the my_test class. Only with this order of commands the substitution will get into effect. In case the order is reversed, original driver in the code i.e. my_driver will be constructed instead of the intended driver i.e. my_updated_driver.

Instance Overriding:

In Instance Overriding, as name indicates it substitutes ONLY a particular instance of the component OR a set of instances with the intended component. The instance to be substituted is specified using the UVM component hierarchy.

Syntax:

<original_type>::type_id::set_inst_override(<substitute_type>::get_type(), <path_string>);

Where “path_string” is the hierarchical path of the component instance to be replaced.

Example UVM Code:

We can use the same code as mentioned above for the “Type Override” except the following highlighted line:


 function void build_phase(uvm_phase phase);
      super.build_phase(phase);
        my_driver::type_id::set_inst_override(my_updated_driver::get_type(), "top.e.agent.drvr");
      e = env::type_id::create("e", this);
 endfunction: build_phase

Objects or sequence related objects are generally only used with type overrides since the instance override approach relates to a position in the UVM Testbench component hierarchy which objects do not take part in.

Debugging the UVM Testbench Structure & Factory Content:

After assembling a rather complex UVM Testbench Environment, it is often useful to print out the structure of the testbench in tabular form and to query the types that were registered with the Factory. A great technique to view the structural composition of the Testbench classes and the Factory setup is to call the this.print() and factory.print() methods in the end_of_elaboration_phase() (as shown in Code below) from the top-level testbench. By the time the end_of_elaboration_phase() executes, the entire environment has already been built and connected, so these print() methods show what had been built in the Testbench and the types that were registered with the factory.


  function void end_of_elaboration_phase(uvm_phase phase);

    super.end_of_elaboration_phase(phase);

    this.print();

    factory.print();

  endfunction: end_of_elaboration_phase

At this point of time, I’m going to pen down & conclude this topic about UVM Factory. I hope this shall provide you friends a fair amount of information and wish you would be able to get benefited by this post. Thank you for your time to go through it. Please feel free to write down your feedback and/or suggestions.

I wish all the success in your work & life, See you again with another topic soon..till then take care, Bye!