UVM Interview Questions

If you’re looking to enter the world of ASIC or FPGA verification, then chances are you’ve heard of Universal Verification Methodology (UVM). UVM has become the industry standard for verifying digital designs, and as such, it is a crucial skill set for anyone looking to break into the field.

Whether you’re a recent graduate or a seasoned professional, preparing for a UVM interview can be a daunting task. To help you prepare, we’ve put together a list of common UVM interview questions that you’re likely to encounter during the interview process.

In this blog post, we’ll go over some of the most frequently asked UVM interview questions and provide tips on how to answer them effectively. We’ll cover topics such as UVM basics, transaction-level modeling (TLM), sequences, and more.

So, if you’re ready to take your UVM knowledge to the next level and ace that interview, read on!

1. Explain TLM ports and exports in UVM with examples.

In the Universal Verification Methodology (UVM), TLM (Transaction Level Modeling) ports and exports are used for communication between different components of the testbench, such as the testbench itself and the DUT (Design Under Test).

TLM ports are interfaces that allow the testbench components to initiate transactions to the DUT, while TLM exports are interfaces that allow the DUT to initiate transactions to the testbench.

Here is an example to illustrate the use of TLM ports and exports in UVM:

Assume that we have a testbench component called “driver” that generates stimulus for the DUT and a testbench component called “monitor” that monitors the DUT’s behavior. We want to use TLM ports and exports to allow communication between the driver and the DUT, and between the DUT and the monitor.

First, we define a TLM port in the driver component:

class driver extends uvm_driver #(transaction);
  // Define a TLM port for the driver
  `uvm_analysis_port #(transaction) analysis_port;
  ...
endclass

This TLM port is used to send transactions from the driver to the DUT.

Next, we define a TLM export in the DUT:

class dut extends uvm_component;
  // Define a TLM export for the DUT
  `uvm_analysis_export #(transaction) analysis_export;
  ...
endclass

This TLM export is used to send transactions from the DUT to the monitor.

Finally, we connect the TLM port and export in the testbench’s build phase:

class my_testbench extends uvm_test;
  // Instantiate the driver, DUT, and monitor components
  driver my_driver;
  dut my_dut;
  monitor my_monitor;
  ...

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    // Connect the driver's analysis port to the DUT's analysis export
    my_driver.analysis_port.connect(my_dut.analysis_export);
    // Connect the DUT's analysis port to the monitor's analysis export
    my_dut.analysis_port.connect(my_monitor.analysis_export);
    ...
  endfunction
endclass

This code connects the driver’s TLM port to the DUT’s TLM export, and the DUT’s TLM port to the monitor’s TLM export. Now the driver can send transactions to the DUT, which can then send transactions to the monitor for analysis.

Overall, TLM ports and exports provide a powerful mechanism for communication between different components of a testbench in UVM, allowing for modular and flexible testbench design.

For more details regarding UVM TLM Concepts please refer below mentioned blog post:

2. How analysis ports/FIFOs and TLM ports/FIFOs differs? Explain the usecase of analysis ports/FIFOs being used in the testbench?

In UVM, analysis ports and TLM (Transaction Level Modeling) ports are both used for communication between UVM components, but they serve slightly different purposes.

Analysis ports are used for one-to-many communication, where a single UVM component sends data to multiple other components. An analysis port is typically implemented as a SystemVerilog interface with an associated write method. Components that want to receive the data connect to the analysis port using a `uvm_analysis_port object, which provides a write method that forwards the data to the analysis port. The analysis port then distributes the data to all connected components. Analysis ports are typically used for broadcasting events or passing data to multiple subscribers, such as for logging or coverage collection.

TLM ports, on the other hand, are used for point-to-point communication between UVM components. TLM ports are also implemented as SystemVerilog interfaces, but they typically provide a set of transaction-level methods (such as write, read, peek, etc.) that allow components to exchange transaction-level data directly. Components connect to TLM ports using a uvm_tlm_port or `uvm_tlm_fifo object. TLM ports are typically used for more fine-grained communication between components, such as passing transactions between a driver and a monitor.

Similarly, analysis FIFOs and TLM FIFOs are used for one-to-many and point-to-point communication, respectively. Analysis FIFOs are implemented as SystemVerilog interfaces with an associated write method that allows components to push data into the FIFO. Components that want to receive the data connect to the analysis FIFO using a `uvm_analysis_fifo object, which provides a get method that retrieves the data from the FIFO. TLM FIFOs, on the other hand, are implemented as SystemVerilog classes that provide a set of transaction-level methods (such as write, read, peek, etc.) that allow components to exchange transaction-level data directly. Components connect to TLM FIFOs using a `uvm_tlm_fifo object.

The use case of analysis ports/FIFOs and TLM ports/FIFOs in UVM depends on the specific needs of the design being tested. In general, analysis ports/FIFOs are useful for broadcasting events or passing data to multiple subscribers, while TLM ports/FIFOs are useful for fine-grained communication between specific pairs of components. For example, analysis ports/FIFOs might be used to log data or collect coverage information from multiple components in a testbench, while TLM ports/FIFOs might be used to pass transaction-level data between a driver and a monitor.

3. Sequence Vs Sequence item in UVM? Explain with Example

In the Universal Verification Methodology (UVM), a sequence is a class that defines a series of operations or stimuli to be applied to a DUT (Design Under Test) to test its functionality. Each operation or stimulus in a sequence is referred to as a sequence item.

To illustrate the difference between a sequence and sequence item in UVM, consider the following example:

class ReadWriteSequence extends uvm_sequence#(ReadWriteSequenceItem);
  `uvm_object_utils(ReadWriteSequence)

  virtual task body();
    ReadWriteSequenceItem item;

    // Read sequence
    item = ReadWriteSequenceItem::type_id::create("read_item");
    item.operation = READ;
    start_item(item);
    finish_item(item);

    // Write sequence
    item = ReadWriteSequenceItem::type_id::create("write_item");
    item.operation = WRITE;
    item.data = 0xABCD;
    start_item(item);
    finish_item(item);
  endtask
endclass

class ReadWriteSequenceItem extends uvm_sequence_item;
  `uvm_object_utils(ReadWriteSequenceItem)

  typedef enum {READ, WRITE} operation_e;
  operation_e operation;
  rand int data;

  // ...
endclass

In this example, ReadWriteSequence is a UVM sequence that defines a sequence of operations to be applied to a DUT. It includes two sequence items: a read item and a write item. Each item is of type ReadWriteSequenceItem, which is a subclass of uvm_sequence_item. The ReadWriteSequenceItem class defines the attributes for each item, including the type of operation (read or write) and the data to be written in the case of a write operation.

So, to summarize, a sequence is a collection of sequence items that defines a series of operations to be applied to a DUT, while a sequence item is an individual operation or stimulus within a sequence, with its own set of attributes and behaviors.

4. Explain how uvm_transaction differs from a uvm_sequence_item in UVM with example

In the Universal Verification Methodology (UVM), a transaction is a collection of data items that represent a single unit of information being transferred between two components. On the other hand, a sequence item is an object that represents a single transaction that needs to be sent or received.

The main difference between a uvm_transaction and a uvm_sequence_item is their level of abstraction. A uvm_sequence_item is a more abstract representation of a transaction, while a uvm_transaction is a more concrete representation.

A uvm_sequence_item is typically used to specify the behavior of a particular transaction that needs to be generated or monitored. It contains the necessary data fields and methods to generate or check a transaction, but it does not contain any implementation details. The implementation details are provided by the uvm_driver or uvm_monitor that handle the uvm_sequence_item.

Here is an example of a uvm_sequence_item that represents a read transaction:

class read_sequence_item extends uvm_sequence_item;
  rand bit [31:0] addr;
  rand bit [7:0] data;

  `uvm_object_utils(read_sequence_item)

  function new(string name = "read_sequence_item");
    super.new(name);
  endfunction

  task pre_randomize();
    // Set defaults for the data fields
    addr = 0;
    data = 0;
  endtask
endclass

In this example, the read_sequence_item contains two data fields, addr and data, that represent the address and data of the read transaction. It also has a pre_randomize task that sets default values for the data fields before they are randomized.

On the other hand, a uvm_transaction is a more concrete representation of a transaction. It is used to represent the actual transaction that is being generated or monitored by the uvm_driver or uvm_monitor. It contains the same data fields as the uvm_sequence_item, but it also includes implementation details such as timing information and status information.

Here is an example of a uvm_transaction that represents the read transaction:

class read_transaction extends uvm_transaction;
  bit [31:0] addr;
  bit [7:0] data;
  bit [31:0] timestamp;
  bit error;

  `uvm_object_utils(read_transaction)

  function new(string name = "read_transaction");
    super.new(name);
  endfunction
endclass

In this example, the read_transaction contains the same data fields as the read_sequence_item, but it also includes a timestamp field that represents the time when the transaction was generated or received, and an error field that represents whether an error occurred during the transaction.

In summary, a uvm_sequence_item is a more abstract representation of a transaction that is used to specify the behavior of a transaction, while a uvm_transaction is a more concrete representation of a transaction that includes implementation details such as timing and status information. It is recommended to use uvm_sequence_item for implementing sequence-based stimulus.

5. copy() Vs clone() Vs create() method in a UVM components?

In UVM (Universal Verification Methodology), copy(), clone(), and create() are methods used for creating new objects. The main difference between these methods is:

1) The create() method is used to construct an object.

2) The copy() method is used to copy an object to another object.

3) The clone() method is to create and copy an existing object to a new object handle. It will first create an object by calling the create() method and then calls the copy() method to copy an existing object to the new handle.

6. How get_name() differs from get_full_name() method in a UVM?

In UVM (Universal Verification Methodology), get_name() and get_full_name() are methods used to retrieve the name of an object. The main difference between these methods is:

  1. get_name(): The get_name() method retrieves the name of an object, but only the name of the object itself, without any hierarchy information. This method returns a string that represents the name of the object.
  2. get_full_name(): The get_full_name() method retrieves the full hierarchical name of an object, including the names of all the parent objects, separated by dots. This method returns a string that represents the full hierarchical name of the object.

In other words, get_name() returns the name of the object itself, while get_full_name() returns the full hierarchical name of the object, including the names of all the parent objects in the hierarchy.

For example, suppose you have a UVM object with the name “my_object” that is contained within a parent object with the name “parent_object”. In this case, get_name() would return “my_object”, while get_full_name() would return “parent_object.my_object”.

7. Explain the difference between UVM ACTIVE agent and UVM PASSIVE agent?

In the context of the Universal Verification Methodology (UVM), an ACTIVE agent is an agent that initiates transactions on its own, whereas a PASSIVE agent waits for the other side to initiate transactions.

An ACTIVE agent is responsible for generating transactions and driving them onto the bus or interface. It contains the logic to create the stimulus and control the timing of the transactions. ACTIVE agents are commonly used for stimulus generation, protocol checking, and coverage collection.

On the other hand, a PASSIVE agent is responsible for receiving transactions and responding to them. It waits for the other side to initiate transactions and reacts to them accordingly. PASSIVE agents are commonly used for monitoring the protocol, collecting protocol statistics, and checking for errors or violations.

An Agent is normally configured ACTIVE in a block level verification environment where stimulus is required to be generated. Same agent can be configured PASSIVE as we move from block level to chip level verification environment in which no stimulus generation is needed, but we still can use same for monitoring activity in terms of debug or coverage.

To summarize, the key difference between ACTIVE and PASSIVE agents in UVM is that ACTIVE agents initiate transactions, while PASSIVE agents wait for transactions to be initiated by the other side.

8. How is an Agent configured as ACTIVE or PASSIVE?

UVM agents have a variable of type UVM_ACTIVE_PASSIVE_e which defines whether the agent is active (UVM_ACTIVE) with the sequencer and the driver constructed, or passive (UVM_PASSIVE) with neither the driver nor the sequencer constructed. This parameter is called active and by default, it is set to UVM_ACTIVE. This can be changed using set_config_int() while the agent is created in the environment class. The build phase of the agent should then have the code as below to selectively construct driver and sequencer.

function void build_phase(uvm_phase phase);
if(m_cfg.active == UVM_ACTIVE) begin
//create driver, sequencer
end
endfunction

9. In UVM how a monitor differs from a scoreboard in UVM?

In the Universal Verification Methodology (UVM), monitors and scoreboards are two distinct components used for verifying the design under test (DUT).

A monitor is responsible for observing signals or transactions on the DUT’s interface and converting them into a higher-level abstraction that can be easily understood by the testbench. The monitor typically operates at the physical layer and captures the data being sent or received by the DUT. It can also perform additional tasks such as protocol checking, error detection, and data extraction.

On the other hand, a scoreboard is responsible for comparing the actual results generated by the DUT with the expected results provided by the testbench. It takes in the data produced by the monitor and performs comparisons against the expected data. The scoreboard typically operates at the transactional or functional layer and can identify errors such as incorrect data, missing transactions, or out-of-order transactions.

In summary, while both monitors and scoreboards are used for verification in UVM, they have distinct roles and operate at different layers of abstraction. The monitor captures the data being sent or received by the DUT, while the scoreboard performs comparisons against the expected results provided by the testbench.

10. What steps are needed to run a sequence?

There are three steps needed to run a sequence as follows:

1) Creating a sequence. A sequence is created using the factory create method as shown below:

my_sequence_c seq;
seq = my_sequence_c::type_id::create(“my_seq“)

2) Configuring or randomizing a sequence. A sequence might have several data members that might need configuration or randomization. Accordingly, either configure values or call seq.randomize()

3) Starting a sequence. A sequence is started using sequence.start() method. The start method takes an argument which is the pointer to the sequencer on which sequence has to be run. Once the sequence is started, the body() method in the sequence gets executed and it defines how the sequence operates. The start() method is blocking and returns only after the sequence completes execution.

11. What are pre_body() and post_body() functions in a sequence? Do they always get called?

pre_body() is a method in a sequence class that gets called before the body() method of a sequence is called. post_body() method in sequence gets called after the body() method is called.

The pre_body() and post_body() methods are not always called. The uvm_sequence::start() has an optional argument which if set to 0, will result in these methods not being called. Following are the formal argument of start() method in a sequence.

virtual task start (
uvm_sequencer_base sequencer, // Pointer to sequencer
uvm_sequence_base parent_sequencer = null, // parent sequencer
integer this_priority = 100, // Priority on the sequencer
bit call_pre_post = 1); // pre_body and post_body called

12. Is the start() method on a sequence blocking or nonblocking?

The start() method is a blocking call. It blocks execution until the body() method of the sequence completes execution.

13. How can a sequence get exclusive access to a sequencer?

When multiple sequences are run on a sequencer, the sequencer arbitrates and grants access to each sequence on a sequence item boundary. Sometimes a sequence might want exclusive access to sequencer until all the sequence items part of it are driven to the driver

(for example: if you want to stimulate a deterministic pattern without any interruption).

There are two mechanisms that allow a sequence to get exclusive access to sequencer.

1) Using lock() and unlock( ): A sequence can call the lock method of the sequencer on which it runs. The calling sequence will be granted exclusive access to the driver when it gets the next slot via the sequencer arbitration mechanism. If there are other sequences marked as a higher priority, this sequence needs to wait until it gets its slot. Once the lock is granted, no other sequences will be able to access the driver until the sequence issues an unlock() call on the sequencer which will then release the lock. The lock method is blocking and does not return until the lock has been granted.

2) Using grab() and ungrab(): The grab method is similar to the lock method and can be called by the sequence when it needs exclusive access. The difference between grab and lock is that when grab() is called, it takes immediate effect and the sequence will grab the next sequencer arbitration slot, overriding any sequence priorities in place. The only thing that can stop a sequence from grabbing a sequencer is an already existing lock() or grab() condition on the sequencer.

14. What is the difference between a pipelined and a non-pipelined sequence-driver model?

Based on how a design interface needs to be stimulated, there can be two modes implemented in an UVM driver class.

1) Non-pipelined model: If the driver models only one active transaction at a time, then it is called a non-pipelined model. In this case, the sequence can send one transaction to the driver and the driver might take several cycles (based on the interface protocol) to finish driving that transaction. Only after that, the driver will accept a new transaction from the sequencer

class nonpipe_driver extends uvm_driver #(req_c);
task run_phase(uvm_phase phase);
req_c req;
forever begin
get_next_item(req); // Item from sequence via sequencer
// drive request to DUT which can take more clocks
// Sequence is blocked to send new items til then
item_done(); // ** Unblocks finish_item() in sequence
end
endtask: run_phase
endclass: nonpipe_driver

2) Pipelined model: If the driver models more than one active transaction at a time, then it is called a pipelined model. In this case sequence can keep sending new transactions to driver without waiting for driver to complete a previous transaction. In this case, on every transaction send from the sequence, driver will fork a separate process to drive the interface signals based on that transaction, but will not wait until it is completed before accepting a new transaction. This modelling is useful if we want to drive back to back requests on an interface without waiting for responses from design.

class pipeline_driver extends uvm_driver #(req_c);
task run_phase(uvm_phase phase);
req_c req;
forever begin
get_next_item(req); // Item from sequence via sequencer
fork
begin
//drive request to DUT which can take more clocks
//separate thread that doesn’t block sequence
//driver can accept more items without waiting
end
join_none
item_done(); // ** Unblocks finish_item() in sequence
end
endtask: run_phase
endclass: pipeline_driver

15. How do we make sure that if multiple sequences are running on a sequencer-driver, responses are send back from the driver to the correct sequence?

If responses are returned from the driver for one of several sequences, the sequence id field in the sequence is used by the sequencer to route the response back to the right sequence. The response handling code in the driver should use the set_id_info() call to ensure that any response items have the same sequence id as their originating request.

Here is an example code in driver that gets a sequence item and sends a response back

(Note that this is a reference pseudo code for illustration and some functions are assumed to be coded somewhere else)

class my_driver extends uvm_driver;
//function that gets item from sequence port and
//drives response back
function drive_and_send_response();
forever begin
seq_item_port.get(req_item);
//function that takes req_item and drives pins
drive_req(req_item);
//create a new response item
rsp_item = new();
//some function that monitors response signals from dut
rsp_item.data = m_vif.get_data();
//copy id from req back to response
rsp.set_id_info(req_item);
//write response on rsp port
rsp_port.write(rsp_item);
end
endfunction
endclass

16. What is the difference between early randomization and late randomization while generating a sequence?

In Early randomization, a sequence object is first randomized using randomize() call and then the sequence calls start_item() to request access to a sequencer, which is a blocking call and can take time-based upon how busy the sequencer is. The following example shows an object (req) randomized first and then sequence waits for arbitration

task body()
assert(req.randomize());
start_item(req); //Can consume time based on sequencer arbitration
finish_item(req);
endtask

In Late randomization, a sequence first calls start_item() , waits until arbitration is granted from the sequencer, and then just before sending the transaction to sequencer/driver, randomize is called. This has the advantage that items are randomized just in time and can use any feedback from design or other components just before sending an item to driver. Following code shows late randomization of a request (req)

task body()
start_item(req); //Can consume time based on sequencer arbitration
assert(req.randomize());
finish_item(req);
endtask

17. What is the difference between get_next_item() and try_next_item() methods in UVM driver class?

The get_next_item() is a blocking call (part of the driver-sequencer API) which blocks until a sequence item is available for the driver to process, and returns a pointer to the sequence item.

The try_next_item() is a nonblocking version which returns a null pointer if no sequence item is available for driver to process.

18. What is the difference between get_next_item() and get() methods in UVM driver class?

The get_next_item() is a blocking call to get the sequence item from the sequencer FIFO for processing by driver. Once the sequence item is processed by a driver, it needs to call item_done() to complete the handshake before a new item is requested using get_next_item().

The get() is also a blocking call which gets the sequence item from sequencer FIFO forprocessing by driver. However, while using get(), there is no need to explicitly call item_done() as the get() method completes the handshake implicitly.

19. What is the difference between get() and peek() methods of UVM driver class?

The get() method part of driver class is a blocking call which gets the sequence item from sequencer FIFO for processing by driver. It unblocks once an item is available and completes handshake with sequencer.

The peek() method is similar to get() and blocks until a sequence item is available. However, it will not remove the request from the sequencer FIFO. So calling peek() multiple times will return same sequencer item in driver.

20. What is the difference in item_done() method of driver-sequencer API when called with and without arguments?

The item_done() method is a nonblockingmethod in driver class that is used to complete handshake with the sequencer after a get_next_item() or try_next_item() is successful.

If there is no need to send a response back, item_done() is called with no argument which will complete the handshake without placing anything in the sequencer response FIFO. If there is a need to send a response back, item_done() is passed with a pointer to a response sequence_item as an argument. This response pointer will be placed in the sequencer response FIFO which can be processed by the sequence as a response to the request it drove.

21. Which of the following driver class methods are blocking calls and which are nonblocking?

1) get()

2) get_next_item()

3) item_done()

4) put()

5) try_next_item()

6) peek()

get(), get_next_item(), peek() are blocking calls.

try_next_item(), item_done(), and put() are nonblocking calls

22. How can you stop all sequences running on a sequencer?

The sequencer has a method stop_sequences() that can be used to stop all sequences. However, this method does not check if the driver is currently processing any sequence_items. Because of this, if the driver calls an item_done() or put(), there can be a Fatal Error as the sequence pointer might not be valid. So a user needs to take care of making sure that once stop_sequence() is called, the sequencer thread is disabled (if started in a fork).

23. Which method in the sequence gets called when user calls sequence.print() method?

convert2string() : It is recommended to implement this function which returns a string representation of the object (values of its data members). This is useful to get debug information printed to simulator transcript or log file.

24. How do we register an uvm_component class and uvm_sequence class with factory?

The uvm_sequence class is registered with the factory using uvm_object_utils() macro and passing the class name as an argument. An example below:

class test_seq_c extends uvm_sequence;
`uvm_object_utils(test_seq_c)

The uvm_component class is registered with the factory using uvm_component_utils() macro and passing the class name as argument. An Example below:

class test_driver_c extends uvm_component;
`uvm_component_utils(test_driver_c)

25. Why should we register a class with factory?

A factory is a special look-up table used in UVM for creating objects of component or transaction types. The benefit of object creation using the factory is that a testbench build process can decide at run-time which type of object has to be created. Based on this, a class type could be substituted with another derived class type without any real code change. To ensure this feature and capability, all classes are recommended to be registered with the factory. If you do not register a class with the factory, then you will not be able to use the factory method::type_id::create() to construct an object.

26. What is meant by factory override?

The UVM factory allows a class to be substituted with another derived class at the point of construction. This can be useful for changing the behavior of a testbench by substituting one class for another without having the need to edit or re-compile the testbench code.

27. What is the difference between instance override and type override?

A type override means that every time a component class type is created in a testbench hierarchy, a substitute type is created in its place. This applies to all instances of that component type. On the other hand, an instance override means, overriding only a specific instance of a component class. A specific instance of a component is referenced by the position of that component in the UVM component hierarchy. Since only UVM component classes can have a hierarchy in UVM test benches, only component classes can be overridden on an instance granularity while sequences (or UVM objects) can be the only type overridden.

28. Can instance override and type override be used for both uvm_component class and transaction types ?

No, only UVM_component classes are part of UVM testbench hierarchy and can be overridden on an instance granularity. The sequence items or sequences are not a part of UVM testbench hierarchy and hence can only be overridden using type override which will override all objects of that type.

29. What is the concept of objections and where are they useful?

The uvm_objection class provides a means for sharing a counter between multiple components and sequences. Each component/sequence may “raise” and “drop” objections asynchronously, which increases or decreases the counter value. When the counter reaches zero (from a non-zero value), an “all dropped” condition is said to occur. The objection mechanism is most commonly used in the UVM phasing mechanism to coordinate the end of each run-time phase. User-processes started in a phase raises an objection first and drops the objection once the process completes. When all processes in a phase drops the objects, the phase’s objection count goes to zero. This “all dropped” condition indicates to the phasing mechanism that every participant agrees the phase should be ended.

Following is an example of how a sequence ( my_test_sequence ) is started on a sequencer ( my_sequencer ) and the objection is dropped after sequence completes execution

task main_phase( uvm_phase phase);
phase.raise_objection( this );
my_test_sequence.start(my_sequencer);
phase.drop_objection( this );
endtask

30. How can we implement a simulation timeout mechanism in UVM methodology?

A simulation time out mechanism helps to stop the simulation if the test doesn’t progress because of some bug beyond a maximum time.

In UVM, set_global_timeout(timeout) – is a convenience function that sets uvm_top.phase_timeout variable with the timeout value. If the run() phase doesn’t end before this timeout value, then the simulation is stopped and an error is reported. This function is called in the top-level module which also starts the test as follows

module test;
initial begin
set_global_timeout(1000ns);
end
initial begin
run_test();
end
endmodule

31. What is the concept of phasing in UVM methodology and explain the different phases of a UVM Component.

Please go through the below post to understand the Phasing concept in detail.

32. Why is build_phase() executed top down in uvm_component hierarchy?

In UVM, all the testbench components like a test, Env, Agent, Driver, Sequencer are based on the uvm_component class and there is always a hierarchy for the testbench components. The build_phase() method is part of the uvm_component class and is used to construct all the child components from the parent component. So, to build the testbench hierarchy you always need to have a parent object first, which can then construct its children, and that can further construct its children using build_phase. Hence, build_phase() is always executed top-down.

For Example: The top level uvm_test class calls build_phase which should construct all the uvm_env components part of this test, and the build_phase() of each uvm_env class should construct all the uvm_agent components that are part of that uvm_env, and this goes on. For all other phases it really doesn’t matter in which order it is called. The run_phase() for all components runs in parallel.

33. What is the use of phase_ready_to_end() method in a uvm_component class?

phase_ready_to_end(uvm_phase phase) is a callback method available in a component class that gets called when all objections are dropped for that corresponding phase and the phase is going to end.

A component class can use this callback method to define any functionality that it needs to perform when the phase is about to end.

One example is if a component want to delay ending of phase until some condition even after all objections are dropped, it can be done using this callback method.

Another example is if an irritator or reactive sequence is running until a main sequence is complete, phase_ready_to_end() callback method in main_phase() can be used to stop those irritator sequences.

34. What is uvm_config_db and what is it used for?

The UVM configuration mechanism supports sharing of configurations and parameters across different testbench components. This is enabled using a configuration database called uvm_config_db. Any testbench component can populate the configuration database with variables, parameters, object handles, etc. Other testbench components can get access to these variables, parameters, and object handles from the configuration database without really knowing where it exists in the hierarchy. For Example, a top-level testbench module can store a virtual interface pointer to the uvm_config_db. Then any uvm_driver or a uvm_monitor component can query the uvm_config_db to get a handle to this virtual interface and use it for actually accessing the signals through the interface.

35. How do we use the get() and set() methods of uvm_config_db?

The get() and set() are the primary methods used to populate or retrieve information from the uvm_config_db. Any verification component can use the set() method to populate the config_db with some configuration information and can also control which other components will have visibility to the same information. It could be set to have global visibility or visible only to one or more specific testbench components. The get() function checks for a shared configuration from the database matching the parameters.

The syntax for the get() and set() methods are as follows:

uvm_config_db#(<type>)::set(uvm_component context, string inst_name, string field_name,<type> value)
uvm_config_db#(<type>)::get(uvm_component context, string inst_name, string field_name, ref value)

The context specifies the current class or component from which get/set is called.

The inst_name is the name of the instance of a component from which get/set is called.

The field_name is the name of the object or parameter or variable which is set/get in config_db.

The <type> identifies the type of the configuration information set/get in config_db. For object handles, this will have the class name while for other variables; it will be the type of that variable.

36. Is it possible for a component lower in testbench hierarchy to pass a handle to a component in higher level of hierarchy using get/set config methods?

This is not a recommended way of passing configuration objects in UVM. Normally the higher level component sets up configuration data base with handles and the lower level components do get them using get/set methods.

37. What is the recommended way of assigning virtual interfaces to different components in a UVM verification methodology?

The top-level testbench module which instantiates the DUT and interfaces will set the virtual interface in the uvm_config_db. A test class or any other component in the UVM component hierarchy can then query the uvm_config_db using the get() method to get handles to this virtual interface and use them for accessing signals. The following shows an example of how this is done. The module test actually instantiates a DUT and physical interface for an APB bus master. It then sets the virtual interface handle to the uvm_config_db.

module test;
logic pclk;
logic [31:0] paddr;
//Instantiate an APB bus master DUT
apb_master apb_master(.pclk(pclk),*);
//Instantiate a physical interface for APB interface
apb_if apb_if(.pclk(pclk), *);
initial begin
//Pass this physical interface to test class top
//which will further pass it down to env->agent->drv/sqr/mon
uvm_config_db#(virtual apb_if)::set(null, “uvm_test_top”, “vif”, apb_if);
end
endmodule

Following shows a APB Env class that uses the get() method in uvm_config_db to
retrieve the virtual interface that was set in the top level test module.

class apb_env extends uvm_env;
`uvm_component_utils(apb_env);
//ENV class will have agent as its sub component
apb_agent agt;
//virtual interface for APB interface
virtual apb_if vif;
//Build phase - Construct agent and get virtual interface handle from test and
pass it down to agent
function void build_phase(uvm_phase phase);
agt = apb_agent::type_id::create(“agt”, this);
if (!uvm_config_db#(virtual apb_if)::get(this, ””, “vif”, vif))
begin
`uvm_fatal(“config_db_err”, “No virtual interface specified for this env instance”)
end
uvm_config_db#(virtual apb_if)::set( this, “agt”, “vif”, vif);
endfunction: build_phase
endclass : apb_env

38. Explain how simulation ends in UVM methodology?

UVM has a phased execution which consists of a set of build phases, run phases and check phases. The run() phase is where the actual test simulation happens and during this phase every component can raise an objection in beginning and hold it until it is done with its activity. Once all components drops the objection, the run() phase completes and then check() phase of all components execute and then the test ends.

This is how a normal simulation ends, but there are also controls on simulation timeouts to terminate the run() phase if some component hangs due to a bug in design or testbench. When the run() phase starts, a parallel timeout timer is also started. If the timeout timer reaches one of the specified timeout limits before the run() phase completes, the run() phase will timeout, an error message will be issued and then all phases post run() will get executed and test ends after that.

39. What is UVM RAL (UVM Register Abstraction Layer)?

UVM RAL (Register Abstraction Layer) is a feature supported in UVM that helps in verifying the registers in a design as well as in configuration of DUT using an abstract register model.

The UVM register model provides a way of tracking the register content of a DUT and a convenience layer for accessing register and memory locations within the DUT. The register model abstraction reflects the structure of the design specification for registers which is a common reference for hardware and software engineers working on the design.

Some other features of RAL include support for both front door and back door initialization of registers and built in functional coverage support.

40. What is UVM Call back?

The uvm_callback class is a base class for implementing callbacks, which are typically used to modify or augment component behavior without changing the component class. Typically, the component developer defines an application-specific callback class that extends from this class and defines one or more virtual methods, called as callback interface. The methods are used to implement overriding of the component class behavior. One common usage can be to inject an error into a generated packet before the driver sends it to DUT. Following pseudo code shows how this can be implemented.

1) Define the packet class with an error bit

2) Define the driver class that receives this packet from a sequence and sends it to DUT

3) Define a driver callback class derived from the base uvm_callback class and add a virtual method which can be used to inject an error or flip a bit in the packet.

4) Register the callback class using `uvm_register_cb() macro 5) In the run() method of driver that receives and send the packet to the DUT, based on a probability knob, execute the callback to cause a packet corruption

class Packet_c;
byte[4] src_addr, dst_addr;
byte[] data;
byte[4] crc;
endclass
//User defined callback class extended from base class
class PktDriver_Cb extends uvm_callback;
function new (string name = “PktDriver_Cb”);
super.new(name);
endfunction
virtual task corrupt_packet (Packet_c pkt);
//Implement how to corrupt packet
//example - flip one bit of byte 0 in CRC
pkt.crc[0][0] = ~pkt.crc[0][0]
endtask
endclass : PktDriver_Cb
//Main Driver Class
class PktDriver extends uvm_component;
`uvm_component_utils(PktDriver)
//Register callback class with driver
`uvm_register_cb(PktDriver,PktDriver_Cb)
function new (string name, uvm_component parent=null);
super.new(name,parent);
endfunction
virtual task run();
forever begin
seq_item_port.get_next_item(pkt);
`uvm_do_callbacks(PktDriver,PktDriver_Cb, corrupt_packet(pkt))
//other code to derive to DUT etc
end
endtask
endclass

41. What is uvm_root class?

The uvm_root class serves as the implicit top-level and phase controller for all UVM components. Users do not directly instantiate uvm_root. The UVM automatically creates a single instance of uvm_root that users can access via the global (uvm_pkg-scope) variable, uvm_top .

42. What is the parent class for uvm_test?

uvm_test class is the top-level class that a user can implement and there is no explicit parent that is mentioned. However, UVM has a special component called uvm_top and it is assigned as the parent of the test class.

I tried to reframe questions and answers from Semiconductor Industry experts and all Credit goes to the original author’s work is a crucial part of ethical writing and research who’s nonother than Mr. Robin Garg and Mr. Ramdas Mozhikunnath‘s knowledge and exposure to the semiconductor industry over the last so many years. I really like the way they shared their knowledge with the engineers who are in VLSI Domain.

I didn’t cover a few topics in this section just because I already posted multiple conceptual posts in UVM sections so please refer to that as well. See you guys 🙂

Books to refer to learn UVM in more detail:

  1. Getting Started with UVM: A Beginner’s Guide by Vanessa Cooper – https://amzn.to/39H4PMF
  2. A Practical Guide to Adopting the Universal Verification Methodology (UVM) – https://amzn.to/3NapXZl
  3. The UVM Primer: A Step-by-Step Introduction to the Universal Verification Methodology by Ray Salemi – https://amzn.to/3QE2FOf
  4. Practical UVM: Step by Step Examples – https://amzn.to/3zYilGj
  5. Advanced UVM – https://amzn.to/3yayPda
  6. Cracking Digital VLSI Verification Interview: Interview Success – https://amzn.to/39Ec1Jy
  7. Writing Testbenches: Functional Verification of HDL Models – https://amzn.to/3yazivW
  8. Formal Verification: An Essential Toolkit for Modern VLSI Design – https://amzn.to/3QDPE7t

8 thoughts on “UVM Interview Questions”

  1. Appreciate your effort to explain each question’s answers in detail and layman language.

  2. Unique set of questions that I have come accross various blogs. Great effort in putting these questions together.

Comments are closed.