75 curated questions across all major UVM topics — from basics to RAL, sequences, TLM, and debug. Filter by topic or difficulty.
UVM (Universal Verification Methodology) is a standardised, class-library-based SystemVerilog framework for building modular, scalable testbenches.
Key reasons:
From top to bottom:
uvm_test) — configures the environment; starts sequences.uvm_env) — contains agents, scoreboards, coverage collectors, register model.uvm_agent) — groups driver + sequencer + monitor for one interface protocol.Build phases (top-down, functions): build → connect → end_of_elaboration → start_of_simulation
Run-time (task-based, consumes time): run_phase (contains reset → configure → main → shutdown sub-phases)
Cleanup phases (bottom-up, functions): extract → check → report → final
uvm_component — base for structural testbench elements. Has a parent, participates in phases, lives for the entire simulation. Examples: driver, monitor, agent, env, test. Registered with `uvm_component_utils.
uvm_object — base for transient data objects. No parent, no phases, created and destroyed on demand. Examples: sequence items, sequences, config objects, register fields. Registered with `uvm_object_utils.
Active agent (is_active = UVM_ACTIVE) builds driver, sequencer, AND monitor. Drives stimulus and observes.
Passive agent (is_active = UVM_PASSIVE) builds monitor ONLY. Observes but drives nothing. Used at integration level to monitor internal buses.
The switch is made through the config object — zero code changes to the agent class itself:
if (m_cfg.is_active == UVM_ACTIVE) begin m_seqr = ...; m_drv = ... end
uvm_config_db is a global typed key-value database. Any component can store data for any other component without direct handle references.
// set() — store a value uvm_config_db #(virtual apb_if)::set( null, "uvm_test_top", "vif", APB); // get() — retrieve it if (!uvm_config_db #(virtual apb_if)::get( this, "", "vif", vif)) `uvm_fatal("DRV", "No vif!")
The lookup matches the set() scope against the component’s path. Wildcards like "*" match any path. The key (4th argument of set, 3rd of get) must match exactly.
The UVM build phase is top-down and deliberate: the test builds the env, the env builds agents, agents build drivers. This ordering lets each level push config to the level below before it constructs its children.
If children are built in the constructor, construction happens before the phase system starts — before the parent has received any configuration. config_db lookups fail, virtual interfaces are null, and the is_active flag has not been set.
Additionally, factory overrides may not yet be registered. Constructing children before elaboration means the override table is incomplete — the wrong type gets created.
super.new() + covergroup construction only. Everything else goes in build_phase().SystemVerilog has two worlds that cannot reference each other: the static world (modules, interfaces — created at elaboration) and the dynamic world (classes — created at runtime).
A driver (class) needs to drive signals on an interface (static). Classes cannot directly hold references to module/interface instances.
Solution: virtual interface. A virtual keyword makes an interface handle class-compatible, bridging the two kingdoms:
// tb_top: set interface into config_db uvm_config_db #(virtual apb_if)::set(null, "uvm_test_top", "vif", APB); // driver: get it back as a class-type variable virtual apb_if vif; uvm_config_db #(virtual apb_if)::get(this, "", "vif", vif);
The UVM factory is a registry mapping type names to class definitions. When a component is created via type_id::create(), the factory checks for overrides and returns the overriding type if one exists.
Purpose: substitute component types without modifying testbench code. An error-injecting driver can replace a normal driver for a specific test with a single override call. Requires registration (`uvm_component_utils) and creation via type_id::create().
Type override — replaces every instance of a type everywhere:
my_driver::type_id::set_type_override(error_driver::get_type());
Instance override — replaces only at a specific component hierarchy path:
my_driver::type_id::set_inst_override(
error_driver::get_type(),
"uvm_test_top.m_env.m_agent.m_drv");Instance overrides take precedence over type overrides. Both are also available from the command line via +uvm_set_type_override and +uvm_set_inst_override.
Using new() directly bypasses the factory — no overrides are applied, the object cannot be debugged via factory tools, and print_topology cannot inspect it.
// Wrong — bypasses factory m_drv = new("m_drv", this); // Correct — goes through factory m_drv = my_driver::type_id::create("m_drv", this);
uvm_analysis_port, etc.) are infrastructure objects — always use new("name", this) for these.Three steps:
uvm_factory::get().print(1); in end_of_elaboration_phase. Verify the override appears and type names match exactly.// Check what type would be created at a specific path uvm_factory::get().debug_create_by_type( my_driver::get_type(), "uvm_test_top.m_env.m_agent", "m_drv");
Parameterised classes are templates, not concrete types. The factory registration macros use the class name as a string key — a template has no single name until specialised with parameter values.
Solution: create a typedef specialisation and register that:
class base_drv_t #(parameter int DW=32) extends uvm_driver; // No `uvm_component_utils — cannot register template endclass typedef base_drv_t #(32) base_drv; `uvm_component_utils(base_drv)
Each parameter combination requires a separate typedef.
When a driver needs to access signals in a Verilog/VHDL BFM module (the static world), a class cannot extend a module.
The pattern:
A factory instance override connects them: the env creates the abstract type; the override replaces it with the concrete type defined inside the module. Concrete classes are the exception to the ‘define all classes in packages’ rule.
start_item(item) blocks until the sequencer grants access to the driver (arbitration). finish_item(item) sends the item to the driver and blocks until the driver calls item_done().
task body(); item = my_item::type_id::create("item"); start_item(item); // wait for arbitration if (!item.randomize() with {addr==12'h004;}) `uvm_fatal("SEQ","randomize failed") finish_item(item); // send, wait for done endtask
Stimulus (rand) — values the sequence drives: address, write data, byte enables. Randomised.
Response (non-rand) — values the driver fills in after the bus transfer: read data, error status. Not randomised — they represent what the DUT returned.
Control — transaction type, delay counts.
Constraints — restrict stimulus to legal values.
The `uvm_do/`uvm_do_with macros hide the start_item/finish_item handshake. Problems:
Always use the explicit pattern:
item = my_item::type_id::create("item"); start_item(item); if (!item.randomize() with {addr==12'h000;}) `uvm_fatal("SEQ","randomize failed") finish_item(item);
A virtual sequence coordinates stimulus across multiple sequencers without itself generating items. It holds handles to real sequencers and starts sub-sequences on each.
Used when a test needs synchronised activity on more than one interface — e.g. APB config while simultaneously driving SPI data.
class sys_vseq extends uvm_sequence; apb_sequencer apb_seqr; spi_sequencer spi_seqr; task body(); fork apb_seq.start(apb_seqr, this); spi_seq.start(spi_seqr, this); join endtask endclass
When multiple sequences call start_item() simultaneously, the sequencer arbitrates. Six built-in modes:
Priority is the third argument of seq.start(seqr, parent, priority). Higher value = higher priority.
Both give exclusive access to a sequencer, blocking all other sequences.
Both released with unlock()/ungrab(). Grab is appropriate for ISR-style sequences that must respond immediately to an interrupt.
Sequence layering models multi-layer protocols (USB, PCIe, MIPI). A layering component contains intermediate sequencers and runs translator sequences that pull high-level items from an upstream sequencer and decompose them into low-level items for the downstream sequencer.
task body(); forever begin up_seqr.get_next_item(hi_item); foreach (hi_item.payload[i]) begin 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
Sequences are uvm_object — not in the component hierarchy. Passing this as context in config_db::get() 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, "", "gpio_rm", rm)) `uvm_fatal("SEQ","No register model!")
A slave sequence models the responding side of a protocol. Unlike normal sequences that generate stimulus, a slave sequence waits for the driver to present a request item, then randomises and returns a response.
The driver uses two get_next_item() calls — once for the request phase (address/command), once for the response phase (slave provides data).
Used to verify master DUTs (DUT initiates transactions; testbench acts as the slave/memory).
Calling up_seqr.item_done() unblocks the upstream sequence, which immediately generates its next item. If called before all downstream items (flits) for the current upstream item are sent, the upstream sequence produces the next item while current flits are still in flight.
Result: flits from two different upstream items are interleaved at the physical layer, causing ordering violations and scoreboard mismatches.
up_seqr.item_done() after the last finish_item() for all downstream items from the current upstream item.get_next_item() + item_done(): Holds the sequence in a suspended state until item_done(). The sequence cannot generate the next item until item_done() is called. Response is in the same item object. Used for non-pipelined drivers.
get() + put(): Non-holding. The sequence unblocks immediately after get() — it can generate the next item while the driver processes the current one. Response returned via separate put(rsp). Used for pipelined drivers.
task run_phase(uvm_phase phase); forever begin fork begin : drive_loop @(posedge vif.PRESETn); forever begin seq_item_port.get_next_item(req); drive_xfer(req); seq_item_port.item_done(); end end begin : reset_detect @(negedge vif.PRESETn); end join_any disable drive_loop; drive_idle_signals(); end endtask
try_next_item(req) is non-blocking. If a sequence item is available it populates req and returns non-null. If nothing is ready it returns null immediately.
Use it when the protocol requires the driver to actively drive idle signals on every clock even when no transaction is pending:
@(posedge vif.PCLK); seq_item_port.try_next_item(req); if (req != null) begin drive_xfer(req); seq_item_port.item_done(); end else drive_idle();
Response fields (read data, error) must be populated before calling item_done(). item_done() unblocks the sequence, which may immediately read the response fields. If fields are populated after item_done(), the sequence reads 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
A pipelined driver uses get() (immediate sequence unblock). Responses arrive later via put(rsp). Two forked threads handle stimulus and response separately:
fork forever begin // stimulus thread seq_item_port.get(req); drive_request(req); end forever begin // response thread get_pipeline_rsp(rsp); rsp.set_id_info(req); // REQUIRED seq_item_port.put(rsp); end join_none
set_id_info(req) is mandatory — copies sequence ID and transaction ID so the sequencer routes the response to the correct sequence.
provides_responses tells the RAL how the driver returns response data:
Mismatched:
An analysis port is one-to-many and non-blocking. ap.write(t) calls write(t) on ALL connected exports simultaneously and returns immediately — the analysis path cannot stall the driver.
Other TLM ports are point-to-point (exactly one export) and can be blocking (operation may wait). Analysis ports allow 0 to N connections; other TLM ports require exactly 1.
uvm_subscriber #(T) pre-declares a uvm_analysis_imp named analysis_export and requires one method: write(T t). Extend it for simple single-input subscribers — scoreboards, coverage collectors, predictors.
Extend uvm_component directly when you need more than one analysis input (e.g. a scoreboard with separate expected and actual streams requiring two different write() implementations).
Use `uvm_analysis_imp_decl to create uniquely-named imp classes with unique write() method suffixes:
`uvm_analysis_imp_decl(_from_a) `uvm_analysis_imp_decl(_from_b) class my_sb extends uvm_scoreboard; uvm_analysis_imp_from_a #(txn_a, my_sb) a_export; uvm_analysis_imp_from_b #(txn_b, my_sb) b_export; function void write_from_a(txn_a t); /*...*/ endfunction function void write_from_b(txn_b t); /*...*/ endfunction endclass
The monitor’s ap.write() is a function — it must return immediately. A scoreboard run_phase needs to block on get() calls. tlm_analysis_fifo buffers between the two:
analysis_export — receives write() calls, stores in queue. Non-blocking.get_export — scoreboard task calls blocking get() from here.task run_phase(uvm_phase phase); forever begin m_exp_fifo.get(exp_t); m_act_fifo.get(act_t); compare(exp_t, act_t); end endtask
put(t) blocks until responder accepts. Direction: initiator → responder.get(t) blocks until data available. Direction: responder → initiator.transport(req, rsp) blocks for the complete round-trip. For request-response pairs without pipelining.seq_item_port is a uvm_seq_item_pull_port #(REQ,RSP) built into every uvm_driver. It provides the complete driver-sequencer protocol: get_next_item, item_done, get, put, try_next_item, peek_next_item.
Unlike standard TLM ports, it is declared and constructed automatically by the uvm_driver base class. You never create it — just connect it: m_drv.seq_item_port.connect(m_seqr.seq_item_export) in agent connect_phase.
TLM ports are infrastructure objects — not user-extensible types that would ever need factory overrides. They are not registered with `uvm_component_utils and cannot be created via the factory.
// Correct — always use new() for TLM infrastructure ap = new("ap", this); put_port = new("put_port", this); m_fifo = new("m_fifo", this);
Predictor — receives the same stimulus as the DUT and computes expected responses. A model of what the DUT should produce.
Evaluator (comparator) — receives predicted and actual transactions, compares them, reports mismatches.
Separation makes maintenance easier: specification changes only require updating the predictor. The comparator is generic and reusable. For simple register-like DUTs, a shadow register model in write() is often sufficient.
When the test drops its last objection, UVM calls phase_ready_to_end() on each component. If the scoreboard has uncompared transactions in its FIFOs, it raises a temporary objection to delay the phase end:
function void phase_ready_to_end(uvm_phase phase); if (phase.get_name() != "run") return; if (m_exp_fifo.size() > 0 || m_act_fifo.size() > 0) begin phase.raise_objection(this); fork begin wait(m_exp_fifo.size()==0 && m_act_fifo.size()==0); phase.drop_objection(this); end join_none end endfunction
Store expected transactions in an associative array keyed by unique identifier. When actual arrives, look up by key instead of by FIFO position:
class ooo_sb extends uvm_scoreboard; my_txn m_exp[int]; // key = txn.tag function void write_expected(my_txn t); m_exp[t.tag] = t; endfunction function void write_actual(my_txn t); if (!m_exp.exists(t.tag)) `uvm_error("SB","No matching expected!") else if (!t.compare(m_exp[t.tag])) `uvm_error("SB","MISMATCH") m_exp.delete(t.tag); endfunction endclass
Fields like timestamps, sequence IDs, and internal counters are per-item metadata — they differ between the expected (predictor-generated) and actual (monitor-observed) transaction naturally, with no bug. Including them in do_compare() causes false failures on every comparison.
Compare only semantically meaningful protocol fields: addresses, data values, command types, and protocol-specific flags that the DUT is supposed to preserve or modify according to the spec.
function void report_phase(uvm_phase phase); if (m_errors > 0) `uvm_error("SB", $sformatf( "%0d matches, %0d MISMATCHES — FAILED", m_matches, m_errors)) else if (m_matches == 0) `uvm_warning("SB", "No comparisons made") else `uvm_info("SB", $sformatf( "%0d comparisons, 0 errors — PASSED", m_matches), UVM_LOW) endfunction
Using `uvm_error in report_phase increments the count seen by regression scripts. The zero-comparison warning catches empty tests that pass vacuously.
uvm_in_order_class_comparator #(T) provides two analysis exports, two FIFOs, and an in-order comparison run task. Suitable for simple, single-stream, in-order DUTs where all comparison logic fits in do_compare().
Use a custom scoreboard when:
Code coverage — measures which lines, branches, and expressions in the RTL were executed. Automatic — simulator instruments RTL. Tells you every line ran, but not that every meaningful scenario was tested.
Functional coverage — measures whether specific design scenarios and feature combinations were exercised. Written manually by the verification engineer based on the spec. 100% code coverage can still miss important corner cases if functional coverage was not defined for them.
This is a SystemVerilog LRM requirement. Covergroups are special constructs — instances must be created with new() in the 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(); // covergroup: ONLY in constructor endfunction
All other object construction goes in build_phase.
Cross coverage measures the Cartesian product of two or more coverpoints — verifying that specific combinations of values occurred together.
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};
}
cx_rw_addr: cross cp_rw, cp_addr;
// 4 bins: rd+data, rd+dir, wr+data, wr+dir
endgroupUse when the spec says every register must be both read and written, or all error conditions tested in both read and write modes.
Option 1 — iff clause:
covergroup apb_cg;
cp_addr: coverpoint txn.addr iff(coverage_enabled) {...}
endgroupOption 2 — guard in write() (preferred in UVM):
function void write(my_txn t); if (!m_cfg.reset_active) begin cov_txn = t; my_cg.sample(); end endfunction
Option 2 is preferred — the coverage component gets its reset state from config_db without tight coupling to any specific signal.
CDV loop:
UVM support: rand fields + constraint blocks for stimulus; coverage collectors connected to monitor ap (data flows automatically); factory overrides to swap stimulus sequences; config_db to enable/disable coverage per test.
Problem: Without RAL, every register sequence must know the bus protocol. RAL provides protocol-independent register access — reg.write() works regardless of bus type; swapping the adapter changes the bus.
Five objects (smallest to largest):
uvm_reg_field — one named bit-groupuvm_reg — one registeruvm_mem — a memory regionuvm_reg_map — named address map with byte offsetsuvm_reg_block — top-level containerDesired — what software intends the hardware to contain. Modified by set() with no bus activity. Committed with update().
Mirrored — last known hardware state, updated by the predictor after each observed bus transfer.
set(0xFF): desired=0xFF, mirror=0x00 (no bus cycle).update(): both=0xFF (bus write done).The adapter translates between generic uvm_reg_bus_op and bus-specific sequence items (e.g. apb_seq_item).
reg2bus(rw) — converts uvm_reg_bus_op → bus item. Returns the item to send.
bus2reg(bus_item, rw) — converts bus item back → uvm_reg_bus_op after the driver completes. Extracts read data and status.
virtual function void bus2reg(uvm_sequence_item item, ref uvm_reg_bus_op rw); apb_seq_item a; $cast(a, item); rw.data = a.read_data; // ← PRDATA for reads rw.status = a.error ? UVM_NOT_OK : UVM_IS_OK; endfunction
uvm_reg_predictor #(T) subscribes to the monitor’s analysis port, decodes every bus transfer using the adapter, and calls predict() to update the mirror.
Auto prediction — updates mirror after each RAL-initiated access only.
Explicit prediction (default) — observes ALL bus traffic including accesses from other bus masters, DMA, or raw bus sequences. The only mode that keeps the mirror accurate in a multi-master environment.
lock_model() resolves all address offsets, verifies no address collisions, marks the model as immutable, and enables address-based register lookup.
Without it: every register appears to be at address 0; front-door accesses all go to address 0; the predictor cannot find registers by address; null pointer errors at runtime.
Multiple block register models are composed using add_submap():
class pss_reg_block extends uvm_reg_block; rand gpio_reg_block gpio; rand spi_reg_block spi; 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); spi = spi_reg_block::type_id::create("spi"); spi.configure(this); spi.build(); AHB_map.add_submap(spi.default_map, 32'h1000); lock_model(); endfunction endclass
// ① Stimulus path: reg.write/read → sequencer → driver → DUT m_cfg.gpio_rm.APB_map.set_sequencer(m_agent.m_seqr, m_adapter); // ② Predictor config: map + adapter for decoding bus items m_reg_pred.map = m_cfg.gpio_rm.APB_map; m_reg_pred.adapter = m_adapter; // ③ Mirror update path: monitor → predictor m_agent.ap.connect(m_reg_pred.bus_in);
Missing ①: reg methods produce no bus activity. Missing ②: predictor crashes on null map. Missing ③: mirror never updated — all mirror checks pass vacuously.
Use 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: clock-control registers (random writes halt the clock), W1C status registers (reset test fails — read clears the register), write-only registers (readback always returns 0).
Most likely cause: an objection was raised but never dropped. Other causes: a sequence blocking on an event that never fires, or a forever loop with no exit condition.
Diagnosis: add +UVM_OBJECTION_TRACE to the run command. It logs every raise/drop with component path and running count. Find the last raise without a matching drop.
Also useful: +UVM_TIMEOUT=5ms,YES to force a timeout — the fatal message shows which phases were still active and which components held outstanding objections.
Add +UVM_CONFIG_DB_TRACE to the run command. This logs every set() and get() with scope, key, and result.
Common mismatches to look for:
"vif" but get() asked for "APB_vif". Keys are case-sensitive.virtual apb_if but get() requested virtual spi_if.uvm_top.print_topology() prints the complete component hierarchy in tree form with every component’s type and dot-path. Call from end_of_elaboration_phase — all components are built but simulation has not started.
Verifies: all expected components were created; factory overrides took effect (type shown = override type); hierarchy matches what config_db paths expect.
function void end_of_elaboration_phase(uvm_phase phase); uvm_top.print_topology(); uvm_factory::get().print(1); // show overrides too endfunction
+UVM_VERBOSITY=UVM_HIGH sets the global verbosity (default UVM_MEDIUM). Messages at UVM_HIGH and below are printed when this flag is set.
Per-component via command line:
// +uvm_set_verbosity=path,id,level,phase // e.g. +uvm_set_verbosity=uvm_test_top.m_env.m_agent.m_drv,DRV,UVM_HIGH,run
From inside the testbench:
m_drv.set_report_verbosity_level(UVM_HIGH); uvm_root::get().set_report_verbosity_level_hier(UVM_HIGH);
The objection mechanism is a reference counter. The run phase ends when count reaches zero. Pattern:
Drain time adds a delay after the last objection drops before the run phase actually ends. It allows in-flight pipeline transactions to complete and be observed by the monitor before the phase closes.
function void end_of_elaboration_phase(uvm_phase phase); uvm_objection obj = phase.get_objection(); obj.set_drain_time(this, 100ns); endfunction
Without drain time, the run phase ends immediately on last drop, leaving the monitor’s last transaction unbroadcast and uncompared. A better alternative for predictable drain is phase_ready_to_end() in the scoreboard — explicit FIFO drain logic.
uvm_top.set_timeout(5ms, 1); // from testbench // +UVM_TIMEOUT=5ms,YES // from command line
When the timeout fires, UVM issues a `uvm_fatal and the simulation ends immediately. The message identifies which phases were still active and which components held outstanding objections.
Always set a timeout in every base test to prevent runaway simulations from consuming infinite regression compute time.
Maximum number of `uvm_error messages before simulation stops. Default = 5.
uvm_root::get().set_report_max_quit_count(50); // +UVM_MAX_QUIT_COUNT=50,YES
Vertical reuse means block-level verification components continue working at integration level without modification.
is_active = UVM_PASSIVE in config. Zero agent code changes.bind attaches an interface to internal RTL hierarchy without modifying the RTL source.bind instantiates an interface inside a target RTL module’s scope without touching the RTL:
bind pss_dut.u_ahb_apb_bridge apb_if apb_probe (
.PCLK(PCLK), .PRESETn(PRESETn),
.PADDR(PADDR), .PSELx(PSEL),
.PWDATA(PWDATA), .PRDATA(PRDATA)
);The bound instance is accessible in tb_top as pss_tb_top.apb_probe and set into config_db as a virtual interface. Internal APB signals are now visible to a passive APB monitor without any DUT port additions.
Problems with `uvm_field_int and related macros:
Use manually implemented do_copy(), do_compare(), and convert2string() for precise control.
Add timing tasks to the agent config object. Sequences retrieve the config via m_sequencer and call these tasks:
// In config object task wait_for_clock(int n=1); repeat(n) @(posedge vif.clk); endtask task wait_for_interrupt(); @(posedge vif.irq); endtask // In sequence body() uvm_config_db #(bus_agent_cfg)::get(m_sequencer,"","cfg",m_cfg); finish_item(item); m_cfg.wait_for_clock(4);
uvm_pkg
↑
agent_pkg(s) / register_pkg ← import uvm_pkg only
↑
env_pkg / sequence_pkg ← import uvm_pkg + agents
↑
test_pkg ← import everything aboveWhy it matters:
Two forked threads: background traffic (low priority) and interrupt monitor. When IRQ fires, high-priority ISR sequence grabs exclusive access:
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() for immediate exclusive access → read ISR reg → W1C clear → ungrab(). Set SEQ_ARB_STRICT_FIFO arbitration so high-priority ISR runs before pending background requests.
A class’s fully qualified type is package::class_name. Including the same file into two packages creates two distinct types with the same source code.
Effects:
Fix: every class lives in exactly one package. Other packages import it — never `include it again.
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); 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; 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
The most common mistake: describing the factory only as a ‘registry’ without explaining the override mechanism — which is the entire verification purpose of the factory.
A complete answer covers five points:
Second common mistake: saying ‘you call new() through the factory’. You never do — new() bypasses the factory. Always use type_id::create().