105 questions covering the complete UVM series — architecture, phases, factory, sequences, RAL, debug, and advanced topics. Filter by topic or difficulty.
UVM (Universal Verification Methodology) is a standardised SystemVerilog class-library framework for building reusable, scalable verification environments. It provides a base class library, factory mechanism, phase system, and TLM communication infrastructure.
Key reasons: structured hierarchy scaling from block to system level; factory-based component substitution without modifying source; phase system synchronising construction, connection, and stimulus; constrained-random stimulus with functional coverage closing; and a common vocabulary enabling IP reuse across teams and projects.
From top to bottom: Test → Environment → Agent → Driver/Sequencer/Monitor → Sequence Item.
uvm_object and uvm_component?
uvm_component: fixed position in hierarchy, has a parent, persists entire simulation, participates in phases. Examples: driver, monitor, agent, env, test. Registered with `uvm_component_utils.
uvm_object: no parent, no phases, created/destroyed dynamically. Examples: sequence items, sequences, config objects, RAL objects. Registered with `uvm_object_utils.
run_test() do and where is it called?
run_test() is a global function that: reads the test name from +UVM_TESTNAME= plusarg; creates the test instance via the factory; then kicks off the UVM phase scheduler from build through final phase.
Always called inside an initial block in the top-level SystemVerilog module — the only module in a UVM testbench that is not a UVM component.
initial begin uvm_config_db #(virtual apb_if)::set(null, "uvm_test_top", "vif", APB); run_test(); // test name from +UVM_TESTNAME= end
`uvm_component_utils and `uvm_object_utils do internally?
Both macros register the class with the UVM factory and generate boilerplate:
uvm_factory::register() so the class can be overriddentype_id typedef and static get_type() method used in override callsget_type_name() returning the class name as a stringcreate(string name, uvm_component parent); object utils generates create(string name)SystemVerilog has two worlds: the static HDL world (modules, interfaces — elaborated at compile time) and the dynamic OOP world (UVM classes — created at runtime). Classes cannot directly hold references to interface instances.
Solution: virtual interface. A virtual interface is a class-compatible handle pointing to a specific interface instance:
// In tb_top (static world) — bridge into config_db uvm_config_db #(virtual apb_if)::set(null, "uvm_test_top", "vif", APB); // In driver (dynamic world) — retrieve virtual apb_if vif; uvm_config_db #(virtual apb_if)::get(this, "", "vif", vif);
super.new() and covergroup construction?
The build phase is top-down and deliberate so each level can push configuration to the level below before that level builds its children. Constructing children in the constructor happens before the phase system starts — before any config_db values exist.
Additionally, factory overrides may not yet be registered at construction time, so the wrong type gets created.
super.new() + covergroup new() ONLY. All component/port creation and config_db retrieval go in build_phase().uvm_top and what role does it play?
uvm_top is the singleton uvm_root instance sitting above all user-created components. It is the implicit parent of uvm_test_top (the test instance created by run_test()).
Roles: manages phase scheduling; provides print_topology(); serves as the root scope for config_db wildcard lookups when context is null; holds the global timeout setting.
When a driver needs to access signals in a legacy Verilog BFM module, a class cannot extend a module.
A factory instance override connects the two: the env creates the abstract type; the override replaces it with the concrete type. The concrete class is the exception to the “all classes in packages” rule.
uvm_event_pool is a singleton string-keyed pool of uvm_event objects accessible from anywhere:
// Producer (no direct handle to consumer) uvm_event e = uvm_event_pool::get_global("xfer_done"); e.trigger(); // Consumer (no direct handle to producer) uvm_event e = uvm_event_pool::get_global("xfer_done"); e.wait_trigger();
Use when direct TLM connections are impractical — synchronising a coverage component with a stimulus event, or notifying a watchdog sequence that a handshake completed.
Build group (top-down, functions): build_phase → connect_phase → end_of_elaboration_phase → start_of_simulation_phase
Run group (concurrent, tasks — consumes simulation time): run_phase (with reset → configure → main → shutdown sub-phases)
Cleanup group (bottom-up, functions): extract_phase → check_phase → report_phase → final_phase
Function phases (build, connect, end_of_elaboration, start_of_simulation, extract, check, report, final): zero-time functions, execute once per component in hierarchy order, cannot use delays or wait statements.
Task phases (run_phase and all sub-phases): can consume simulation time via delays, event waits, and clock edges. All components’ run_phase tasks execute concurrently — the scheduler forks them all and waits for objections to drain.
build_phase top-down: A parent must create its children before they can exist. The test’s build_phase creates the env; the env’s build_phase creates agents; agents create drivers and monitors. Top-down ensures parents exist first.
connect_phase bottom-up: A child’s ports must exist before a parent can connect to them. Bottom-up ensures leaf-level ports (driver’s seq_item_port, monitor’s ap) are fully constructed before the agent’s connect_phase wires them together.
Objections are reference-counted gatekeepers on task phases. A phase ends only when its count reaches zero. The test raises an objection at the start of run_phase and drops it when stimulus is complete:
task run_phase(uvm_phase phase); phase.raise_objection(this, "test running"); my_seq.start(m_env.m_agent.m_seqr); phase.drop_objection(this, "test done"); endtask
If no component ever raises an objection, the run_phase ends immediately at time 0 — stimulus never runs, the test appears to pass vacuously. This is the most common new-testbench bug.
Drain time is an additional wait after all objections are dropped but before the phase actually ends. It allows in-flight pipeline transactions to complete and monitors to capture final responses.
function void end_of_elaboration_phase(uvm_phase phase); uvm_objection obj = phase.get_objection(); obj.set_drain_time(this, 100ns); endfunction
phase_ready_to_end() in the scoreboard for precise FIFO drain control rather than a blanket time delay.phase_ready_to_end() and when do you override it?
Called on every component when the phase objection count first reaches zero. A component can raise a new objection inside this callback to delay the phase end while it finishes work (e.g. scoreboard FIFO drain):
function void phase_ready_to_end(uvm_phase phase); if (phase.get_name() != "run") return; if (m_fifo.size() > 0) begin phase.raise_objection(this); fork begin wait(m_fifo.size() == 0); phase.drop_objection(this); end join_none end endfunction
start_of_simulation_phase used for?
Runs after all connections are complete and before simulation time advances. Common uses:
`uvm_infouvm_top.print_topology() to verify the hierarchyuvm_factory::get().print(1) to verify overridesYes. phase.jump(uvm_reset_phase::get()) forces UVM to restart from the named phase across all components. The scheduler kills the current phase and re-runs from the jump target.
Typical use: a watchdog sequence detects a fatal bus error, triggers a DUT hardware reset, then calls phase.jump(reset_phase) to cleanly re-run the reset and configure sub-phases without ending the simulation.
No — the phase scheduler does not wait for run_phase tasks to return; it waits only for the objection count to reach zero. Once all objections are dropped, UVM kills all outstanding run_phase tasks (via disable fork) and moves to extract_phase.
A monitor’s forever begin ... end run_phase is the canonical example — it never returns, yet simulation ends cleanly.
A timeout kills simulation if run_phase does not end within the specified time:
uvm_top.set_timeout(500us, 1); // Or command-line: +UVM_TIMEOUT=500000,YES
When the timeout fires, UVM issues a `uvm_fatal showing which phases were still active and which components held outstanding objections. The second argument (1=YES) makes it fatal; 0=NO makes it a warning.
Without the factory, substituting a component (e.g. error-injecting driver) requires editing every line of source code that creates it. The factory decouples class creation from class identity: type_id::create() checks for an override and returns the override type instead, without any change to calling code.
Overrides can be registered from the test, command-line plusargs, or config objects — enabling per-test, per-instance component substitution with zero source changes.
Type override — replaces every instance everywhere:
my_driver::type_id::set_type_override(error_driver::get_type());
Instance override — replaces only at a specific hierarchy path:
my_driver::type_id::set_inst_override( error_driver::get_type(), "uvm_test_top.m_env.m_agent.*");
Instance overrides take precedence over type overrides when both match the same path.
type_id::create() instead of new()?
Using new() bypasses the factory entirely — no overrides are applied, the object cannot be inspected by factory debug tools, and print_topology() cannot display its type.
// Wrong — bypasses factory, overrides silently ignored m_drv = new("m_drv", this); // Correct — checks override table before creating m_drv = my_driver::type_id::create("m_drv", this);
uvm_analysis_port, etc.) are infrastructure — always use new("name", this) for these.The override type must be a subclass of the original type (Liskov Substitution Principle). The factory rejects overrides where the override is not derived from the original.
Override must be registered before the first create() call for that type — typically in the test’s build_phase, before creating the environment.
Three steps in order:
uvm_factory::get().print(1) in end_of_elaboration_phase. Check the override appears with correct type names.m_env = gpio_env::type_id::create(...). Order matters.// Simulate what factory would return at a specific path: uvm_factory::get().debug_create_by_type( my_driver::get_type(), "uvm_test_top.m_env.m_agent", "m_drv");
Use plusargs — no recompilation required:
// Type override +uvm_set_type_override=my_driver,error_driver // Instance override +uvm_set_inst_override=my_driver,error_driver, uvm_test_top.m_env.m_agent.* // Enable factory trace +UVM_FACTORY_TRACE
UVM processes these plusargs during factory initialisation, before build_phase runs. Useful in regression to test variants without separate test classes.
`uvm_component_utils directly?
Parameterised classes are templates — the factory uses a string name as the registration key, and a template has no single name until specialised with parameter values. Attempting to register base_driver_t #(32) and base_driver_t #(64) would conflict.
Solution: create a typedef specialisation and register that:
class base_drv_t #(parameter int DW=32) extends uvm_driver; // No `uvm_component_utils endclass typedef base_drv_t #(32) base_drv_32; `uvm_component_utils(base_drv_32)
Each hierarchy level has its own config object, and the parent config holds handles to child configs:
class gpio_env_cfg extends uvm_object; apb_agent_cfg m_apb_cfg; // env holds agent config gpio_reg_block gpio_rm; endclass
The test pushes only the top-level env_cfg into config_db. The env’s build_phase extracts agent configs from it and distributes them downward. Adding a new agent only requires a new handle in env_cfg — not a new config_db::set() in every test.
uvm_config_db::set()?
uvm_config_db #(T)::set( context, // 1. scope root (component or null) "instance_path", // 2. path relative to context "field_name", // 3. key name string value // 4. value to store );
The get() call uses the same four parts. The database resolves a match by walking up the component hierarchy from the getter’s path until a matching set() entry is found. Wildcards ("*") match any path segment.
The most specific path wins, then last-write among equal specificity. A set() with "uvm_test_top.m_env.m_agent" wins over one with "*" even if the wildcard was set later.
"*" and a child later sets the same field for a specific path, the child’s more specific entry wins regardless of call order. Add +UVM_CONFIG_DB_TRACE to see which value is returned by get().A sequence item (extends uvm_sequence_item) is a single bus transaction — the unit of communication between the sequence and driver. Fields fall into four categories:
start_item() / finish_item() handshake.
task body(); apb_seq_item req = apb_seq_item::type_id::create("req"); start_item(req); // ① wait for sequencer grant if (!req.randomize() with {addr == 12'h004;}) `uvm_fatal("SEQ", "randomize failed") finish_item(req); // ② send to driver; blocks until item_done() endtask
Randomisation happens between the two calls so callbacks and constraint overrides can affect the values. Never randomise before start_item().
`uvm_do and `uvm_do_with macros?
These macros combine item creation, randomisation, and sending into one opaque call. Problems:
Always use the explicit pattern: create → start_item → randomize with assertion → finish_item.
A virtual sequence coordinates stimulus across multiple agents. It holds handles to their sequencers and starts sub-sequences on each — enabling cross-interface ordering and synchronisation.
class sys_vseq extends uvm_sequence; apb_sequencer m_apb_seqr; spi_sequencer m_spi_seqr; task body(); fork apb_init.start(m_apb_seqr, this); spi_xfer.start(m_spi_seqr, this); join endtask endclass
Set with: m_seqr.set_arbitration(SEQ_ARB_STRICT_FIFO);
grab() and lock()?
Both give exclusive access to a sequencer, but differ in when exclusivity starts:
lock() — queued. Waits for currently-granted sequences to finish, then claims exclusive access. Polite.
grab() — immediate. Takes the sequencer right now, interrupting any pending requests. Used for ISR-style urgent sequences that must respond without delay.
Both released with unlock() / ungrab().
In-place update (most common): the driver writes response fields into the same req object before calling item_done(). The sequence reads them after finish_item() returns:
// In driver (before item_done): req.read_data = vif.PRDATA; req.error = vif.PSLVERR; seq_item_port.item_done(); // unblocks finish_item() // In sequence, after finish_item(): `uvm_info("SEQ", $sformatf("read=0x%08h", req.read_data), UVM_MEDIUM)
Response FIFO: driver calls item_done(rsp) with a separate object; sequence calls get_response(rsp). Used when request and response are different types.
Sequence layering decomposes high-level protocol transactions into lower-level physical transactions. A translator sequence runs on the downstream sequencer but holds a handle to the upstream sequencer:
task body(); forever begin up_seqr.get_next_item(hi_item); // pull high-level item foreach (hi_item.payload[i]) begin lo_item = lo_item::type_id::create("lo"); start_item(lo_item); lo_item.flit = hi_item.payload[i]; finish_item(lo_item); end up_seqr.item_done(); // AFTER all flits sent end endtask
this?
Sequences are uvm_object — not in the component hierarchy. Passing this as config_db context would use a non-component for path resolution, giving wrong results.
Use m_sequencer — the handle to the sequencer that started the sequence, which IS a component with a valid hierarchy path:
if (!uvm_config_db #(gpio_reg_block)::get( m_sequencer, // ← sequencer, not this "", "gpio_rm", rm)) `uvm_fatal("SEQ", "No register model!")
A master sequence initiates transactions — calls start_item/finish_item to push items to the driver.
A slave sequence responds to bus requests — models a slave device. The driver receives a request phase item (address/command), presents it to the sequence, waits for a response item, then drives the response back. The sequence provides the response (look up data, introduce errors).
Used to verify master DUTs where the testbench acts as the responding slave/memory.
Active agent (is_active = UVM_ACTIVE): contains driver + sequencer + monitor. Drives stimulus onto the interface AND observes.
Passive agent (is_active = UVM_PASSIVE): contains monitor ONLY. Observes but drives nothing. Used at integration level to watch interfaces driven by other blocks.
// In agent build_phase — zero code change to agent class: if (m_cfg.is_active == UVM_ACTIVE) begin m_seqr = uvm_sequencer::type_id::create("m_seqr", this); m_drv = my_driver::type_id::create("m_drv", this); end
get_next_item() / item_done() protocol.
task run_phase(uvm_phase phase); apb_seq_item req; vif.PSELx <= 0; vif.PENABLE <= 0; @(posedge vif.PRESETn); forever begin seq_item_port.get_next_item(req); // ① block until item ready drive_bus(req); // ② drive hardware seq_item_port.item_done(); // ③ release; unblocks finish_item() end endtask
item_done() must be called exactly once per get_next_item(). Too early = race. Never called = deadlock.
Before calling item_done(). item_done() unblocks the sequence which may immediately read the response fields. Fields populated after item_done() create a race — the sequence may read uninitialised values.
do @(posedge vif.PCLK); while (!vif.PREADY); req.error = vif.PSLVERR; // ① populate response if (req.read_not_write) req.read_data = vif.PRDATA; seq_item_port.item_done(); // ② THEN unblock
The agent acts as a black box — the environment connects to the agent’s stable public interface, not to internal sub-components. If the env connected directly to m_agent.m_mon.ap, any internal restructuring of the agent (e.g. splitting the monitor into two) would break all environments using it.
// In agent connect_phase — forward, not a new port: m_mon.ap.connect(this.ap);
try_next_item() and when do you use it instead of get_next_item()?
try_next_item(req) is non-blocking — returns immediately with a non-null req if available, or null if nothing is ready. Use it when the protocol requires the driver to actively drive idle signals on every clock even when no transaction is pending:
forever begin seq_item_port.try_next_item(req); if (req != null) begin drive_xfer(req); seq_item_port.item_done(); end else drive_idle_cycle(); // keep bus active with idle end
task run_phase(uvm_phase phase); forever begin fork begin: main @(posedge vif.PRESETn); // wait for reset deassert forever begin seq_item_port.get_next_item(req); drive_xfer(req); seq_item_port.item_done(); end end begin: rst @(negedge vif.PRESETn); // fire on reset assert idle_bus(); end join_any disable fork; end endtask
set_id_info() do?
A pipelined driver accepts new requests before previous responses arrive. It uses get() (non-blocking) — the sequence unblocks immediately. Responses return via put(rsp):
fork forever begin // request thread seq_item_port.get(req); drive_pipeline(req); end forever begin // response thread collect_rsp(rsp); rsp.set_id_info(req); // copy sequence + txn IDs seq_item_port.put(rsp); end join_none
set_id_info(req) copies the sequence ID and transaction ID from the request so the sequencer can route the response to the correct sequence instance.
Always at transaction completion — when all fields of the transaction are known.
Broadcasting at start means response fields (read_data, error, status) are not yet valid, making scoreboard comparisons incorrect.
For split-transaction protocols (AXI), the monitor assembles a complete transaction — both request and response phases — before calling ap.write(txn). This may require buffering partial transactions in an associative array keyed by transaction ID.
uvm_*_port): initiator end — the component that calls put/get/peek. Holds the call.uvm_*_export): pass-through connector — forwards a connection through a component boundary.uvm_*_imp): terminal end — implements the actual method (scoreboard’s write(), FIFO’s put). Lives in the final receiver.Connection: port.connect(export) or port.connect(imp). Exports chain to imps.
| Feature | Analysis port | TLM put port |
|---|---|---|
| Connectivity | 0 to many exports | Exactly one export |
| Blocking? | No — function | Yes (blocking variant) |
| Direction | Push broadcast | Push unicast |
| Typical use | Monitor → scoreboard/cov | Driver ↔ sequencer FIFO |
// In env connect_phase — three lines, one port m_agent.ap.connect(m_sb.analysis_export); m_agent.ap.connect(m_cov.analysis_export); m_agent.ap.connect(m_reg_pred.bus_in); // One ap.write() call broadcasts to all three
The analysis port maintains a dynamic list of subscribers. Each connect() adds to that list. Broadcast happens in connection order.
uvm_subscriber and how does it simplify code?
uvm_subscriber #(T) pre-declares an analysis_export and requires one method: write(T t). Saves three lines of boilerplate per subscriber:
// Without — you declare+build port yourself class my_sb extends uvm_component; uvm_analysis_imp #(my_item, my_sb) analysis_export; function void build_phase(uvm_phase phase); analysis_export = new("analysis_export", this); ... // With uvm_subscriber — just implement write() class my_sb extends uvm_subscriber #(my_item); function void write(my_item t); ... endfunction endclass
`uvm_analysis_imp_decl?
When a component needs multiple analysis inputs of the same type — e.g. a scoreboard receiving from two different monitors. Since only one write() function can exist per class, multiple imps need different method names.
`uvm_analysis_imp_decl(_expected) `uvm_analysis_imp_decl(_actual) class my_sb extends uvm_scoreboard; uvm_analysis_imp_expected #(my_item, my_sb) exp_export; uvm_analysis_imp_actual #(my_item, my_sb) act_export; function void write_expected(my_item t); ... endfunction function void write_actual(my_item t); ... endfunction endclass
tlm_analysis_fifo and when do you use it?
tlm_analysis_fifo #(T) bridges between the synchronous analysis broadcast (function, zero-time) and a component that processes items in simulation time (task, blocking).
analysis_export — receives write() calls, stores in internal queue. Non-blocking.get_export — scoreboard task calls blocking get() here.// Scoreboard run_phase can now block freely forever begin m_exp_fifo.get(exp_t); // blocks until expected arrives m_act_fifo.get(act_t); // blocks until actual arrives compare(exp_t, act_t); end
// In env connect_phase: m_reg_pred.map = gpio_rm.APB_map; // tell predictor which map m_reg_pred.adapter = m_adapter; // tell predictor which adapter m_agent.ap.connect(m_reg_pred.bus_in); // wire monitor → predictor
Every transaction observed by the monitor feeds the predictor’s bus_in analysis export. The predictor calls adapter.bus2reg() to decode the transaction, then calls predict() to update the RAL mirror values automatically.
Analysis ports allow zero connections — a disconnected port is legal; write() becomes a no-op. No compile-time or elaboration-time error. The symptom is silent failure: scoreboard receives nothing, all comparisons produce zero matches, tests pass vacuously.
+UVM_VERBOSITY=UVM_FULL and check for zero-subscriber warnings in the port summary.seq_item_port is a uvm_seq_item_pull_port #(REQ, RSP) pre-declared in uvm_driver. It provides: get_next_item(), item_done(), get(), put(), try_next_item().
TLM ports, exports, and imps are infrastructure objects — not registered with the factory and not user-extensible via overrides. Using type_id::create() would fail with “type not registered.”
// Always use new() for TLM infrastructure: ap = new("ap", this); put_port = new("put_port", this);
Predictor: subscribes to the stimulus path. For each transaction driven, computes the expected DUT output — a software model of the DUT’s functional behaviour.
Evaluator (comparator): receives both predicted (from predictor) and actual (from monitor) transactions. Compares and reports mismatches.
Keeping them separate: the predictor is DUT-specific (changes when spec changes); the evaluator is generic (FIFO-based comparison, always reusable). Different evaluation strategies (in-order, out-of-order) plug into the same predictor.
In-order: expected and actual must arrive in the same sequence. Two FIFOs (expected, actual) — dequeue the front of each and compare. Simple but fails for protocols allowing transaction reordering.
Out-of-order: uses an associative array keyed by transaction ID. When an actual arrives, looks up by ID and compares regardless of arrival order. Required for AXI, PCIe, or any protocol with multiple outstanding transactions that may complete out of order.
A shadow register is a software copy of the DUT’s register file maintained by the scoreboard. Every observed write is applied to the shadow (including W1C logic). Every observed read is compared against the shadow value.
function void write(apb_seq_item t); int idx = t.addr[3:2]; if (!t.read_not_write) begin if (idx == 3) shadow[3] &= ~t.write_data; // W1C else shadow[idx] = t.write_data; end else if (t.read_data !== shadow[idx]) `uvm_error("SB", "MISMATCH") endfunction
Override phase_ready_to_end(). When called, check if FIFOs or queues are non-empty. If so, raise a new objection and fork a drain thread:
function void phase_ready_to_end(uvm_phase phase); if (phase.get_name() != "run") return; if (m_exp_q.size() > 0 || m_act_q.size() > 0) begin phase.raise_objection(this, "SB draining"); fork begin wait(m_exp_q.size()==0 && m_act_q.size()==0); phase.drop_objection(this); end join_none end endfunction
`uvm_error not `uvm_fatal in write() for mismatches?
uvm_fatal kills simulation immediately. Calling it on the first mismatch loses all subsequent mismatch context — you get one error and stop. All cascade effects, subsequent violations, and error count statistics are hidden.
uvm_error logs the error and continues. All mismatches are reported in one run, making root cause identification far easier.
uvm_error in write(). In report_phase, if total error count > 0, optionally issue uvm_error to ensure a non-zero simulator exit code.Add minimum transaction count assertion in check_phase:
function void check_phase(uvm_phase phase); if (m_checks == 0) `uvm_error("SB", "Zero comparisons — possible disconnected analysis port!") else if (m_checks < min_expected) `uvm_error("SB", $sformatf( "Only %0d checks, expected ≥%0d", m_checks, min_expected)) endfunction
Guards against vacuous pass: disconnected analysis port, misconfigured test, or reset never deasserted.
Code coverage: automatic — simulator instruments RTL and measures which lines, branches, and expressions were executed. Tells you that every line ran, not that every meaningful scenario was tested.
Functional coverage: manual — the engineer writes covergroups based on the specification, modelling which features and corner cases must be exercised. 100% code coverage can still miss specification-level bugs if functional coverage was not defined for them.
Coverage-Driven Verification: Plan → Generate → Check → Measure → Refine.
covergroup apb_cg;
cp_rw: coverpoint txn.read_not_write {
bins rd={1}; bins wr={0};
}
cp_addr: coverpoint txn.addr {
bins data_reg={12'h000}; bins dir_reg={12'h004};
bins ien_reg ={12'h008}; bins isr_reg ={12'h00C};
}
// Cross: every addr×rw combination — 8 bins
cx_rw_addr: cross cp_rw, cp_addr;
endgroup
A cross bin is hit only when both constituent bins are hit in the same sample — verifying reads AND writes to each register independently.
At the env level, not inside the agent. The agent is protocol-focused and should not know about verification intent. Env-level placement allows:
SystemVerilog LRM requirement: covergroup instances must be created with new() in the class constructor. Attempting to construct in build_phase or any other method is a language violation.
function new(string name, uvm_component parent); super.new(name, parent); my_cg = new(); // ← ONLY in constructor endfunction
All other object construction (ports, sub-components) goes in build_phase. Covergroup construction is the exception.
Transition bins track sequences of consecutive values across sample events. A bin is hit only when the coverpoint transitions through a specific sequence:
cp_state: coverpoint apb_state {
bins setup_to_access = (SETUP => ACCESS);
bins idle_to_setup = (IDLE => SETUP);
bins back2back = (ACCESS => SETUP => ACCESS);
}
APB protocol example: verifying that back-to-back transfers (ACCESS→SETUP without returning to IDLE) are exercised, and that the illegal IDLE→ACCESS transition never occurs.
After measuring coverage, query which bins are uncovered and bias constraints toward them:
// Read individual bin coverage foreach (apb_cg.cp_addr.get_bins()[b]) if (apb_cg.cp_addr.b.get_coverage() < 1.0) `uvm_info("COV",$sformatf("Hole: %s",b.get_name()),UVM_LOW) // Directed constraint to target the hole if (!item.randomize() with { addr == 12'h008; read_not_write == 1; }) `uvm_fatal("SEQ", "rand fail")
Merge coverage databases across multiple regression seeds using the simulator’s coverage merge tool for cumulative results.
The Register Abstraction Layer provides protocol-independent register access. Without RAL, sequences contain APB/AHB/AXI-specific bus calls — changing the bus requires rewriting all sequences. With RAL, sequences call reg.write(status, value); the adapter translates to bus transactions. Swapping the bus only requires changing the adapter.
RAL also provides: automatic mirror tracking; built-in test sequences (hw_reset, bit_bash, access); field-level access with proper masking.
uvm_reg_predictor — subscribes to monitor’s ap and keeps the mirror in sync with hardware.Desired: what software intends the hardware to contain. Set by reg.set(value) with no bus activity. Committed to hardware with reg.update().
Mirrored: last known hardware state, updated by the predictor after each observed bus transfer.
| After… | Desired | Mirrored |
|---|---|---|
| Reset | reset value | reset value |
| set(0xFF) | 0xFF | 0x00 (no bus cycle) |
| update() | 0xFF | 0xFF (bus write done) |
write(), update(), poke(), and get()?
// ① Stimulus path: reg methods → sequencer → driver → DUT gpio_rm.APB_map.set_sequencer(m_agent.m_seqr, m_adapter); // ② Predictor knows which map and adapter to use m_reg_pred.map = gpio_rm.APB_map; m_reg_pred.adapter = m_adapter; // ③ Mirror update: monitor → predictor m_agent.ap.connect(m_reg_pred.bus_in);
Missing ①: reg method calls produce no bus activity. Missing ②: predictor crashes on null map dereference. Missing ③: mirror never updated, all mirror checks pass vacuously.
lock_model() do and what fails if you forget it?
lock_model() must be the last call in the register block’s build(): it resolves all address offsets, verifies no address collisions, marks the model immutable, and enables address-based register lookup (used by adapter and predictor).
Without it: every register appears to be at address 0; front-door accesses all go to offset 0; the predictor cannot find registers by address — null pointer errors at runtime.
provides_responses in the RAL adapter control?
If set wrong:
class gpio_isr_reg extends uvm_reg; uvm_reg_field int_status; virtual function void build(); int_status = uvm_reg_field::type_id::create("int_status"); // .configure(parent, size, lsb_pos, access, // volatile, reset, has_reset, is_rand, individually_accessible) int_status.configure(this, 32, 0, "W1C", 1, 32'h0, 1, 0, 1); endfunction endclass
RAL access types include: RO, RW, RC, WC, WS, W1C, W1S, W1T, W0C and others. volatile=1 indicates hardware can update this field, so the mirror may diverge from the desired value.
class pss_reg_block extends uvm_reg_block; rand gpio_reg_block gpio; rand uart_reg_block uart; uvm_reg_map AHB_map; virtual function void build(); AHB_map = create_map("AHB_map", 32'h0, 4, UVM_LITTLE_ENDIAN); gpio = gpio_reg_block::type_id::create("gpio"); gpio.configure(this); gpio.build(); AHB_map.add_submap(gpio.default_map, 32'h0000); uart = uart_reg_block::type_id::create("uart"); uart.configure(this); uart.build(); AHB_map.add_submap(uart.default_map, 32'h1000); lock_model(); endfunction endclass
Ascending verbosity: UVM_NONE (0) → UVM_LOW (100) → UVM_MEDIUM (200, default) → UVM_HIGH (300) → UVM_FULL (400) → UVM_DEBUG (500).
A message is printed only if its level ≤ the current verbosity setting. Set globally: +UVM_VERBOSITY=UVM_HIGH. Per-component: +uvm_set_verbosity=path,id,level,phase.
| Plusarg | Purpose |
|---|---|
+UVM_VERBOSITY=UVM_HIGH | Sets global verbosity threshold |
+UVM_CONFIG_DB_TRACE | Traces all config_db set/get calls |
+UVM_FACTORY_TRACE | Traces all factory create() calls |
+UVM_TESTNAME=xxx | Selects test class to run |
+UVM_TIMEOUT=500000,YES | Sets run_phase timeout in ps |
+UVM_MAX_QUIT_COUNT=10 | Stop after N uvm_errors |
+uvm_set_type_override=A,B | Factory type override from CLI |
+UVM_OBJECTION_TRACE | Traces all raise/drop objection calls |
+UVM_OBJECTION_TRACE — find any raise without a matching drop+UVM_TIMEOUT=1000000,YES — force kill and get a backtrace showing active phasesget_next_item() with no sequence runningfinish_item() because driver never called item_done()wait() or @(event) condition is waiting for a signal that never transitionsprint_topology() to debug build phase errors?
function void end_of_elaboration_phase(uvm_phase phase); uvm_top.print_topology(); uvm_factory::get().print(1); // show overrides too endfunction
print_topology() shows the full component hierarchy with each component’s type and instance name. If a component is missing (null handle), it will be absent from the tree. If a factory override did not take effect, the type name will reveal the issue.
m_agent.ap.connect(m_sb.analysis_export)class my_catcher extends uvm_report_catcher; virtual function action_e catch(); if (get_severity()==UVM_ERROR && get_id()=="KNWN") return CAUGHT; // suppress this known issue return THROW; // pass everything else through endfunction endclass // Register globally: uvm_report_cb::add(null, new my_catcher());
Useful in regression to suppress known expected errors, convert warnings to errors during gate-level simulation, or count specific error types without stopping simulation.
// Programmatically in test build_phase: uvm_root::get().set_report_max_quit_count(10); // Or command-line (no recompile): // +UVM_MAX_QUIT_COUNT=10,YES
After 10 uvm_errors, simulation terminates with a fatal. Useful in regressions where a single bug floods the log with thousands of identical errors — limiting to 10 keeps logs readable and simulation time short.
Instead of exposing virtual interface signals directly to sequences, timing helper tasks are added to the config object. The config holds the virtual interface handle, so it can implement the timing tasks:
class bus_agent_cfg extends uvm_object; virtual bus_if vif; task wait_for_clock(int n=1); repeat(n) @(posedge vif.clk); endtask task wait_for_interrupt(); @(posedge vif.irq); endtask endclass // In sequence body(): finish_item(item); m_cfg.wait_for_clock(4); // 4-cycle inter-transfer gap
Vertical reuse means block-level verification components (agent, scoreboard, coverage) work at integration level without modification.
The passive agent only instantiates the monitor. The monitor’s analysis port feeds the same scoreboard and coverage components used at block level. No sequences run (or run on null sequencer). The same agent package is compiled once and instantiated in two different modes at the two hierarchy levels.
bind expose internal DUT signals to a passive monitor?
bind attaches an interface inside a target RTL module without modifying its source:
bind core_block apb_if apb_probe ( .PCLK(clk), .PRESETn(rstn), .PADDR(core_block.apb_addr), .PWDATA(core_block.apb_wdata) // ... );
The passive agent’s virtual interface points to apb_probe, and the monitor observes internal DUT transactions non-invasively — no DUT port modifications required.
Fork two threads: background stimulus (low priority) and interrupt monitor. When IRQ fires, start a high-priority ISR sequence using grab():
task body(); fork bg_seq.start(seqr, this, 100); // low priority forever begin m_cfg.wait_for_interrupt(); isr_seq = apb_isr_seq::type_id::create("isr"); isr_seq.start(seqr, this, 500); // high priority end join_any endtask
ISR sequence: grab() → read ISR reg → W1C clear → ungrab().
Use a stop flag with a while(!stop) loop — the sequence exits cleanly at the end of its current item without interrupting a mid-handshake transfer:
class stoppable_seq extends uvm_sequence; bit stop_flag = 0; task body(); while(!stop_flag) begin start_item(req); if(!req.randomize()) `uvm_fatal("SEQ","rand fail") finish_item(req); end endtask endclass // From virtual sequence: seq.stop_flag = 1; // exits after current item completes
A class’s fully-qualified type is package::class_name. Including the same source file into two packages creates two distinct types — even though the code is identical.
Effects:
Fix: every class lives in exactly one package. Other packages import it — never `include it again.
rm.data_reg.write(status, 32'hDEAD, .parent(this)) — sequence calls RALuvm_reg_bus_op (kind=WRITE, addr=0x00, data=0xDEAD)adapter.reg2bus(rw) → creates apb_seq_item (read_not_write=0, addr=0x00, write_data=0xDEAD)uvm_pkg ← base
↑
agent_pkg / register_pkg ← import uvm_pkg only
↑
env_pkg / sequence_pkg ← import uvm_pkg + agents
↑
test_pkg ← imports everything
Why it matters: each class lives in exactly one package; other packages import it. This prevents type duplication, enables staged compilation (changed packages recompile only their dependents), and ensures factory registration when the test package is imported in tb_top.
class watchdog_seq extends uvm_sequence; `uvm_object_utils(watchdog_seq) int timeout_cycles = 1000; task body(); my_agent_cfg cfg; uvm_config_db #(my_agent_cfg)::get(m_sequencer,"","cfg",cfg); cfg.wait_for_clock(timeout_cycles); `uvm_fatal("WD", "DUT did not complete within timeout!") endtask endclass // In run_phase: start watchdog and main sequence in fork/join_any // If main sequence finishes first, watchdog abandoned cleanly
run_test() → factory creates test (uvm_test_top)`include "uvm_macros.svh" appear before all UVM class definitions?
SystemVerilog compilation is sequential. The UVM macros (`uvm_object_utils, `uvm_fatal, etc.) expand at the point of use — if the include hasn’t been processed yet, the compiler sees undefined identifiers. Similarly, import uvm_pkg::* brings UVM types into scope — any class definition extending uvm_object or uvm_component requires them visible at the point of definition.
design.sv = HDL kingdom: RTL modules (gpio_dut) and SystemVerilog interfaces (apb_if). No UVM includes. Compiled first.
testbench.sv = OOP kingdom: all UVM classes (driver, monitor, agent, env, test) plus the static top module that bridges the two kingdoms. The bridge is the initial block in gpio_tb_top — it calls config_db::set(null, ..., "vif", APB), crossing a handle from the HDL world (interface instance APB) into the UVM class world (config_db entry).
Three changes, none in sequence code:
parameter int DW = 32 to DW = 64logic [DW-1:0] PWDATA, PRDATArand logic [DW-1:0] write_data uses the parameterSequences call rm.data_reg.write() — the RAL handles field width internally. The scoreboard shadow is logic [DW-1:0]. Everything adapts automatically through the one parameter change.
Describing the factory as only a “registry” without explaining the override mechanism — which is the entire verification purpose.
A complete answer covers five points:
Second common mistake: saying “call new() through the factory”. You never do — new() bypasses the factory. Always type_id::create().
A UVM heartbeat is a watchdog that monitors whether registered components are making periodic progress. Components register with a uvm_heartbeat object and must call beat() within the configured timeout window — otherwise a fatal or error is issued.
Use cases: detecting hung drivers waiting forever for PREADY; detecting sequences deadlocked waiting for a response; catching hangs in long simulations where a simple phase timeout alone is insufficient because the timeout fires too early or too late.
uvm_resource_db and how does it relate to uvm_config_db?
uvm_config_db is actually a convenience wrapper around uvm_resource_db — it adds hierarchical scoping and priority resolution on top of the raw resource database. Directly using uvm_resource_db skips the scope matching and is used for global configuration that does not depend on component hierarchy.
Example use: setting RAL exclusion attributes on registers with the "REG::" prefix — these are global settings that any sequence can see regardless of where in the hierarchy it runs.
uvm_resource_db #(bit)::set(
{"REG::", rm.isr_reg.get_full_name()},
"NO_REG_HW_RESET_TEST", 1);In an ID-keyed associative array: if m_exp_map.exists(txn.id) returns false when an actual arrives, report an error — unexpected transaction with no matching prediction.
In check_phase, verify the expected map is empty — any remaining entries are predictions that never arrived (missing actual responses):
function void check_phase(uvm_phase phase); if (m_exp_map.size() > 0) `uvm_error("SB", $sformatf( "%0d expected txns never matched", m_exp_map.size())) endfunction
Automatic bins: the simulator partitions the value space into equal bins (default 64 bins). Fast to write but may create thousands of bins for wide signals, making reports unreadable and coverage uninformative.
Explicit bins: the engineer defines which values map to which bin with meaningful names:
cp_size: coverpoint txn.burst_len {
bins single = {0};
bins short = {[1:15]};
bins max_burst = {255};
bins illegal = default; // catches unspecified values
}Explicit bins are always preferred for protocol verification — they map directly to specification corner cases.
$cast() and direct assignment in UVM?
A parent class handle can be directly assigned from a child handle (upcast). But a child handle cannot be directly assigned from a parent handle (downcast) — this requires $cast().
In UVM, downcasting is critical in do_copy() and do_compare() where the argument type is uvm_object:
function void do_copy(uvm_object rhs); apb_seq_item r; if (!$cast(r, rhs)) `uvm_fatal("ITEM","Type mismatch in do_copy") super.do_copy(rhs); addr = r.addr; write_data = r.write_data; endfunction
When a driver needs to access signals in a legacy Verilog BFM module, a class cannot extend a module. The pattern:
pure virtual task drive_write(input logic [11:0] addr, input logic [31:0] data).A factory instance override connects them: the env creates the abstract type via type_id::create(); the override replaces it with the concrete class defined inside the BFM module. The concrete class is the exception to the “all classes in packages” rule — it intentionally lives inside the static world to access those signals.
do_compare() for a sequence item?
do_compare() must compare only protocol-meaningful stimulus fields — not timestamps, sequence IDs, or response fields that differ by construction:
function bit do_compare(uvm_object rhs, uvm_comparer c); apb_seq_item r; if (!$cast(r, rhs)) return 0; return (addr == r.addr && read_not_write == r.read_not_write && byte_en == r.byte_en && (read_not_write ? read_data == r.read_data // compare DUT response : write_data == r.write_data)); endfunction
Define a protected virtual configuration method in the base test. Derived tests override only the configuration, not the entire build_phase:
class gpio_base_test extends uvm_test; function void build_phase(uvm_phase phase); super.build_phase(phase); m_cfg = gpio_env_cfg::type_id::create("m_cfg"); configure_env(m_cfg); // virtual hook // ... rest of build endfunction virtual function void configure_env(gpio_env_cfg cfg); cfg.has_coverage = 1; cfg.has_scoreboard = 1; endfunction endclass class gpio_fast_test extends gpio_base_test; virtual function void configure_env(gpio_env_cfg cfg); super.configure_env(cfg); cfg.has_coverage = 0; // disable for speed endfunction endclass
super.build_phase() always be the first call in an overriding build_phase?
super.build_phase(phase) must be called first because the base class build_phase processes field automation set via `uvm_component_utils_begin/.._end macros, applies config_db values bound through the automation system, and performs infrastructure initialisation that subsequent factory create() calls depend on.
If not called, automated field population from config_db is skipped — certain tool-specific UVM infrastructure may not initialise — leading to subtle failures that are hard to trace because the component works correctly in simple cases but fails in regression.
Symptoms of duplicate includes: $cast() fails between identical-looking types; factory overrides silently ignored; analysis port type errors at connect_phase.
Prevention checklist:
`include "my_class.sv" across all package files and ensure no duplicates+UVM_FACTORY_TRACE and look for the same class name registered from two different package pathsget_type_name() — if they show the same name but cast fails, the type is in two packagesUse uvm_resource_db with the "REG::" prefix:
uvm_resource_db #(bit)::set(
{"REG::", m_cfg.gpio_rm.isr_reg.get_full_name()},
"NO_REG_HW_RESET_TEST", 1);Available attributes: NO_REG_HW_RESET_TEST, NO_REG_BIT_BASH_TEST, NO_REG_ACCESS_TEST. Common exclusion reasons: W1C registers (reset test fails because read clears them), write-only registers (readback always returns 0), clock-control registers (random writes halt the clock).
Via command-line plusarg targeting a specific component path:
+uvm_set_verbosity=uvm_test_top.m_env.m_agent.m_drv,_ALL_,UVM_FULL,runOr programmatically in the test:
m_env.m_agent.m_drv.set_report_verbosity_level(UVM_FULL);
// Global hierarchy:
uvm_root::get().set_report_verbosity_level_hier(UVM_HIGH);The component-specific verbosity overrides the global +UVM_VERBOSITY setting, allowing you to see full driver trace without drowning in monitor and scoreboard messages.
After start_item() and before finish_item(). This is the canonical UVM pattern.
Reason: start_item() may invoke a mid_do() callback that applies directed constraint overrides. If randomisation happens before the grant, these overrides cannot affect values. Randomising in the window between the two calls allows callbacks and constraint inheritance from the parent sequence to work correctly.
get_next_item() and try_next_item()? When does each one apply?
get_next_item(req): blocking — driver waits until the sequence provides an item. The driver is idle until stimulus arrives. Use for simple non-pipelined protocols where idle cycles are acceptable.
try_next_item(req): non-blocking — returns immediately with null if no item is ready. Use for protocols that require the bus to be driven on every clock cycle (e.g. AXI where TVALID must toggle, or protocols that detect idle by missing valid transitions).
forever begin seq_item_port.try_next_item(req); if (req != null) begin drive_xfer(req); seq_item_port.item_done(); end else drive_idle_cycle(); end
test_pkg.sv:
package test_pkg; import uvm_pkg::*; import gpio_agent_pkg::*; import gpio_env_pkg::*; `include "uvm_macros.svh" `include "gpio_base_test.sv" `include "gpio_rand_test.sv" endpackage
tb_top (essential elements):
`include "uvm_macros.svh" import uvm_pkg::*; import test_pkg::*; // registers all test classes initial begin uvm_config_db #(virtual apb_if)::set( null, "uvm_test_top", "vif", APB); run_test(); // +UVM_TESTNAME selects test end