UVM (Universal Verification Methodology) interviews often go beyond definitions such as sequence, sequencer, driver, and monitor. Interviewers may ask you to write a transaction class, implement a sequence, connect a monitor to a scoreboard, debug a configuration failure, or explain why a test ends before all transactions are processed.
For functional verification engineers, the goal is to demonstrate that you understand how UVM components work together in a reusable testbench—not simply that you can remember class names and macros.
This guide covers practical UVM coding interview questions with SystemVerilog examples, common coding mistakes, and follow-up questions relevant to VLSI verification interviews. Here are the concepts covered in UVM interview preparation material.
A sequence item represents an individual transaction that a verification environment sends to the DUT through a driver. For example, a simple bus transaction may contain an address, data, and a read/write control field.
class bus_item extends uvm_sequence_item;
rand bit [7:0] addr;
rand bit [31:0] data;
rand bit write;
`uvm_object_utils(bus_item)
function new(string name = “bus_item”);
super.new(name);
endfunction
endclass
The class extends uvm_sequence_item, while rand allows the fields to be randomized. The factory registration macro makes the class available for factory-based creation.
Likely follow-up: Why use uvm_sequence_item instead of uvm_component?
A sequence item represents transaction data and does not participate in the UVM component hierarchy. A driver, monitor, agent, or environment is a component because it participates in that hierarchy.
A sequence defines the stimulus scenario by creating and sending one or more sequence items to a sequencer.
class bus_sequence extends uvm_sequence #(bus_item);
`uvm_object_utils(bus_sequence)
function new(string name = “bus_sequence”);
super.new(name);
endfunction
task body();
bus_item req;
repeat (10) begin
req = bus_item::type_id::create(“req”);
start_item(req);
if (!req.randomize())
`uvm_fatal(“RAND”, “Randomization failed”)
finish_item(req);
end
endtask
endclass
Here, the sequence generates ten randomized transactions. start_item() and finish_item() participate in the sequence-to-sequencer handshake.
Likely follow-up: What happens if randomize() fails?
The sequence should not silently continue with invalid or unintended stimulus. Check the return value and investigate contradictory constraints or other randomization problems.
A driver receives transaction-level requests and converts them into pin-level activity on the DUT interface.
For this example, assume a simplified interface with valid, ready, addr, and data signals.
class bus_driver extends uvm_driver #(bus_item);
`uvm_component_utils(bus_driver)
virtual bus_if vif;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
task run_phase(uvm_phase phase);
bus_item req;
forever begin
seq_item_port.get_next_item(req);
// Drive transaction fields using vif.
// Wait for the protocol’s completion condition.
seq_item_port.item_done();
end
endtask
endclass
The omitted interface-driving logic depends on the actual protocol. A production driver must drive the correct signals, handle reset, wait for the required handshake, and avoid completing a transaction prematurely.
Likely follow-up: What is the difference between get_next_item() and get()?
With get_next_item(), the driver explicitly calls item_done() when it finishes processing the request. With get(), the item retrieval operation includes completion of the request handshake. The choice depends on the driver’s transaction-processing requirements.
A monitor observes DUT activity and converts sampled signals into transaction objects. It should not drive the DUT.
class bus_monitor extends uvm_monitor;
`uvm_component_utils(bus_monitor)
uvm_analysis_port #(bus_item) ap;
function new(string name, uvm_component parent);
super.new(name, parent);
ap = new(“ap”, this);
endfunction
// In the sampling task:
// 1. Observe the protocol handshake.
// 2. Create a bus_item.
// 3. Populate the observed fields.
// 4. Publish the transaction with ap.write(item).
endclass
The analysis port allows the monitor to publish transactions to a scoreboard, coverage collector, or other subscribers without knowing their internal implementation.
Likely follow-up: Why should the monitor avoid modifying a transaction after publishing it?
Subscribers may retain the object handle. Modifying the same object for another transaction can cause consumers to observe changed data unexpectedly. Creating a fresh object for each sampled transaction is a straightforward way to avoid this problem.
A scoreboard checks observed behavior against expected behavior. UVM analysis communication lets the monitor and scoreboard remain independent.
A simplified scoreboard can receive transactions using an analysis implementation:
class bus_scoreboard extends uvm_scoreboard;
`uvm_component_utils(bus_scoreboard)
uvm_analysis_imp #(bus_item, bus_scoreboard) analysis_export;
function new(string name, uvm_component parent);
super.new(name, parent);
analysis_export = new(“analysis_export”, this);
endfunction
function void write(bus_item item);
`uvm_info(“SB”,
$sformatf(“Observed addr=%0h data=%0h”,
item.addr, item.data),
UVM_MEDIUM)
endfunction
endclass
In the environment’s connect_phase, connect the monitor’s analysis port to the scoreboard’s implementation:
monitor.ap.connect(scoreboard.analysis_export);
The example logs observed transactions. A functional scoreboard must also obtain expected results and compare them with the observed results.
Likely follow-up: The monitor detects transactions, but the scoreboard receives none. What would you check?
Confirm that ap.write() is reached, the analysis port is constructed, the connection is made in connect_phase, and the producer and consumer use compatible transaction types.
This question checks whether you understand the basic UVM class hierarchy.
uvm_object | uvm_component |
Used for data objects and sequences | Used for hierarchical testbench components |
Examples: transactions and sequences | Examples: drivers, monitors, agents and environments |
Does not have a component parent-child hierarchy | Participates in the component hierarchy |
Does not use component phase callbacks | Participates in UVM component phases |
A sequence item typically extends uvm_sequence_item, while a driver extends uvm_driver.
Interview tip: Explain the responsibility of the class as well as its base class. Memorizing the inheritance relationship alone does not demonstrate that you understand the architecture.
The factory enables a testbench to substitute one registered implementation for another without changing every location where the component is created.
Suppose a base driver is extended by a specialized driver:
class debug_driver extends base_driver;
`uvm_component_utils(debug_driver)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
endclass
A type override can be registered before the affected component is created:
base_driver::type_id::set_type_override(
debug_driver::get_type()
);
This substitution works when the original component is created through the factory, for example with base_driver::type_id::create().
Likely follow-up: Why might an override not take effect?
Check whether both classes are registered correctly, whether the override is set before construction, and whether the original object is created through the factory rather than directly with new().
UVM components are class-based, while a SystemVerilog interface instance exists in the static simulation hierarchy. A virtual interface lets a class-based component access that interface.
The top-level module can publish the interface instance:
uvm_config_db#(virtual bus_if)::set(
null,
“uvm_test_top.env.agent.driver”,
“vif”,
bus_vif
);
The driver can retrieve it in build_phase:
if (!uvm_config_db#(virtual bus_if)::get(
this, “”, “vif”, vif))
`uvm_fatal(“NOVIF”, “Virtual interface not configured”)
The component path and field name must match the intended configuration scope.
Likely follow-up: What would you check if get() fails?
Check the interface type, the field name (“vif”), the component hierarchy, and whether the configuration is set before it is retrieved.
Objections help control when a task-based phase can complete. A test can raise an objection before starting its main sequence and drop it after the sequence completes.
task run_phase(uvm_phase phase);
bus_sequence seq;
phase.raise_objection(this);
seq = bus_sequence::type_id::create(“seq”);
seq.start(env.agent.sequencer);
phase.drop_objection(this);
endtask
If the test drops its objection before required activity finishes, the run phase may end prematurely. If an objection is never dropped, the simulation can remain active longer than intended.
Likely follow-up: Why is the objection raised before starting the sequence?
It prevents the phase from completing while the intended stimulus is running. The objection should be dropped only when the work protected by it is complete.
Constraints restrict random values to legal scenarios.
class aligned_item extends uvm_sequence_item;
rand bit [7:0] addr;
rand bit [31:0] data;
constraint addr_alignment {
addr[1:0] == 2’b00;
}
constraint addr_range {
addr inside {[8’h10:8’hF0]};
}
`uvm_object_utils(aligned_item)
function new(string name = “aligned_item”);
super.new(name);
endfunction
endclass
The address must fall within the specified range and be aligned to a four-byte boundary.
A useful follow-up is to ask what happens when two constraints conflict. For example, requiring an address to be both below 10 and above 20 leaves no legal solution. In that case, randomize() fails, so the calling code should check the return value.
An agent groups components associated with a DUT interface or protocol.
An active agent generally contains a sequencer, driver, and monitor. It can generate and drive stimulus while observing the response.
A passive agent typically contains a monitor and observes activity generated elsewhere.
For example, a subsystem may already have another component controlling a bus. A passive agent can monitor that bus and provide transactions to checking and coverage components without driving it.
Likely follow-up: Can a passive agent contribute to a scoreboard?
Yes. Its monitor can publish observed transactions through an analysis port, just as a monitor in an active agent can.
Do not immediately assume the DUT is faulty. Trace the expected and actual transaction paths and identify where they first differ.
Check:
A useful scoreboard report should include the expected value, actual value, transaction identifier, and relevant protocol context. A generic “comparison failed” message is much less useful for debugging.
Start by checking objection handling and sequence execution.
Common causes include:
Add informative messages at the beginning and end of the sequence, around transaction retrieval in the driver, and when the driver completes each item. This helps locate where execution stops.
These three components work together but have different responsibilities.
A typical flow is:
Sequence → Sequencer → Driver → DUT
The monitor observes the resulting DUT activity and publishes transactions to checking or coverage components.
An interviewer may ask you to explain this flow using a simple bus transaction. Be prepared to describe where the transaction is created, randomized, handed to the driver, driven onto the interface, and observed.
Practice by building a small verification environment in stages:
For broader preparation, review the practical VLSI verification interview questions.
For project-oriented questions, also practise explaining how you developed a testbench, isolated a failure, or improved checking. The guide to explaining a VLSI project in a technical interview can help structure those answers.
UVM coding interviews test whether you can build a verification environment that is correct, reusable, and debuggable. Prioritize transaction creation, sequence-driver communication, monitor-to-scoreboard connections, factory overrides, virtual interfaces, objections, and constrained randomization.
The strongest answers explain not only what the code does, but also why the component is needed, how it communicates with the rest of the environment, and what could go wrong.
Practise writing the code from a blank file, tracing transactions through the environment, and debugging realistic failures. That is more valuable than memorizing isolated definitions.