Production-quality UVM testbench style — naming conventions, class definitions, factory rules, macro guidance, sequence rules, component best practices, package organisation, and the complete do/don’t checklist distilled from industry experience.
classname.svh`uvm_component_utils / `uvm_object_utils`uvm_info / `uvm_error / `uvm_fataldo_copy(), do_compare()seq.start(sequencer)start_item() + finish_item()uvm_config_db APIphase_ready_to_end() in scoreboardsrun_phase() in drivers and monitorssuper.new(name [,parent]) first in constructorsrandomize() and $cast() return values`uvm_field_* automation macros`uvm_do / `uvm_do_with sequence macrospre_body() and post_body() in sequencesset_config_string/int/object() (deprecated)uvm_resource_db API (use config_db instead)#0 procedural delays$unit scopeConsistent naming makes component hierarchies readable and enables path-based debugging. These conventions apply across the entire testbench:
| Item | Convention | Example |
|---|---|---|
| Package files | name_pkg.sv — suffix always _pkg | gpio_agent_pkg.sv |
| Class files | classname.svh — no import/include inside the file | gpio_driver.svh |
| Agent components | prefix_driver, prefix_monitor, prefix_agent, prefix_seq_item | apb_driver, apb_monitor |
| Component handles | m_ prefix for member component handles | m_drv, m_mon, m_seqr, m_agent |
| Config objects | prefix_agent_cfg or prefix_env_cfg | apb_agent_cfg, gpio_env_cfg |
| Sequence classes | prefix_action_seq — descriptive verb | gpio_write_seq, apb_init_seq |
| Test classes | prefix_test or feature_test | gpio_base_test, interrupt_test |
| Analysis ports | ap for the main port; error_ap for secondary | m_mon.ap |
| String names in create() | Match the handle name exactly | m_drv = gpio_driver::type_id::create("m_drv", this) |
| Message IDs | Short uppercase abbreviation matching component | "DRV", "MON", "SB", "CFG" |
create("m_drv", this) becomes the component’s dot-path segment — the handle name m_drv and the string "m_drv" should always be identical. This makes print_topology readable and config_db path matching predictable.Never `include a class definition directly into a module, interface, or tb_top. When a class is included into two different scopes, SystemVerilog treats them as different types — factory overrides fail silently, $cast() fails, and analysis port connections are rejected.
// ── Correct: class defined in a package ────────────────── package gpio_agent_pkg; `include "uvm_macros.svh" import uvm_pkg::*; `include "gpio_seq_item.svh" `include "gpio_agent_cfg.svh" `include "gpio_driver.svh" `include "gpio_monitor.svh" `include "gpio_agent.svh" endpackage : gpio_agent_pkg // ── Wrong: class included directly into tb_top ─────────── // module tb_top; // `include "gpio_driver.svh" // Creates gpio_driver in $unit scope // ... // NEVER DO THIS
Each class lives in its own .svh file with the same name as the class. The file contains only the class definition — no import or `include statements inside the file. Those belong in the package file that includes it.
// ── Correct constructor structure ───────────────────────── function new(string name, uvm_component parent); super.new(name, parent); // ← always first my_cg = new(); // covergroup construction (only allowed here) // Everything else goes in build_phase() endfunction // ── Objects (uvm_object): no parent argument ────────────── function new(string name = "class_name"); super.new(name); endfunction
The UVM factory calls constructors with exactly two arguments (name and parent for components, name only for objects). Any extra arguments with no default value will prevent the factory from creating the object. Even with defaults, the extra arguments are always the default — there is no way to pass custom values through type_id::create(). Use config_db to pass data to components.
Every class that extends a UVM base class must be registered. Without registration, the class cannot be overridden, cannot appear in print_topology, and cannot be debugged with factory debug tools. Registration has zero runtime performance cost.
// ── Components (uvm_component hierarchy) ───────────────── `uvm_component_utils(my_driver) `uvm_component_utils(my_monitor) `uvm_component_utils(my_agent) `uvm_component_utils(my_env) `uvm_component_utils(my_test) // ── Objects (uvm_object hierarchy) ──────────────────────── `uvm_object_utils(my_seq_item) `uvm_object_utils(my_sequence) `uvm_object_utils(my_agent_cfg) // ── Always create via factory — NEVER use new() directly ─ // Wrong: m_drv = new("m_drv", this); // Correct: m_drv = my_driver::type_id::create("m_drv", this);
If the test package is not imported into tb_top, the test classes are never registered with the factory. run_test("my_test") will fail with “type not found” even though the class exists.
Always use `uvm_info, `uvm_warning, `uvm_error, `uvm_fatal. Never use $display for testbench messages — UVM macros add time, component path, severity formatting, and are controlled by the verbosity filter system.
The `uvm_field_int, `uvm_field_string, and related macros auto-generate do_copy(), do_compare(), do_print(), and do_pack(). They appear to save time but have significant downsides:
do_copy() and do_compare() gives precise control over which fields are compared — essential for scoreboard correctness// ── Avoid: automation macros ────────────────────────────── // `uvm_field_int(addr, UVM_ALL_ON) // `uvm_field_int(data, UVM_ALL_ON) // ── Do: manual implementation ───────────────────────────── function void do_copy(uvm_object rhs); apb_seq_item rhs_; $cast(rhs_, rhs); super.do_copy(rhs); addr = rhs_.addr; read_not_write = rhs_.read_not_write; write_data = rhs_.write_data; byte_en = rhs_.byte_en; // read_data and error: response fields, copied too read_data = rhs_.read_data; error = rhs_.error; endfunction function bit do_compare(uvm_object rhs, uvm_comparer comparer); apb_seq_item rhs_; $cast(rhs_, rhs); // Only compare payload fields — NOT sequence metadata return (addr == rhs_.addr && read_not_write == rhs_.read_not_write && byte_en == rhs_.byte_en && (read_not_write ? read_data == rhs_.read_data : write_data == rhs_.write_data)); endfunction
`uvm_do and `uvm_do_with combine item creation, randomisation, and sending into a single macro. They hide the start_item/finish_item handshake and make it impossible to guard randomisation failure or perform per-item operations between start_item and finish_item. Always use the explicit pattern:
// ── Avoid: `uvm_do_with ─────────────────────────────────── // `uvm_do_with(item, {addr == 12'h004;}) // hides everything // ── Do: explicit pattern ────────────────────────────────── item = apb_seq_item::type_id::create("item"); start_item(item); if (!item.randomize() with { addr == 12'h004; }) `uvm_fatal("SEQ", "Randomize failed") finish_item(item);
// ── Wrong: silent randomization failure ────────────────── // item.randomize() with {addr inside {12'h000, 12'h004};} // ── Correct: check and report failure ──────────────────── if (!item.randomize() with { addr inside {12'h000, 12'h004}; }) `uvm_fatal("SEQ", "Randomization failed — check constraints")
Sequences should generate items and send them through the driver — not drive time directly with #delay or @(event). Time-consuming operations belong in the driver, not the sequence. A sequence with #100ns creates a dependency on the testbench clock that breaks reuse.
pre_body() and post_body() are called by start() only when call_pre_post = 1 (the default). When a sequence is started as a sub-sequence from another sequence’s body(), these hooks fire in ways that can cause unexpected behaviour. Put all pre/post logic directly in body().
Raising and dropping an objection for every sequence item creates enormous overhead in the objection counting mechanism and slows simulation significantly. Raise one objection at the test level before starting sequences, and drop it after all sequences complete.
// ── Wrong: objection per transaction ───────────────────── // task body(); // repeat(100) begin // phase.raise_objection(this); // ← terrible performance // ... drive item ... // phase.drop_objection(this); // end // endtask // ── Correct: one raise/drop per test ───────────────────── task run_phase(uvm_phase phase); phase.raise_objection(this); seq.start(m_seqr); // sequence runs to completion phase.drop_objection(this); endtask
Component creation (type_id::create) happens exclusively in build_phase. TLM connections (ap.connect) happen exclusively in connect_phase. Mixing them causes build failures because the connected-to component may not exist yet.
Drivers and monitors run their forever loops in run_phase, not in main_phase or custom phases. Using run_phase ensures compatibility with all UVM testbenches and tools.
// ── Avoid (deprecated) ──────────────────────────────────── // set_config_object("m_env.m_agent", "cfg", m_cfg); // get_config_object("", "cfg", m_cfg); // ── Use ─────────────────────────────────────────────────── uvm_config_db #(gpio_agent_cfg)::set( this, "m_env.m_agent", "cfg", m_cfg); if (!uvm_config_db #(gpio_agent_cfg)::get( this, "", "cfg", m_cfg)) `uvm_fatal("CFG", "No agent config!")
// ── Wrong: unchecked cast ───────────────────────────────── // $cast(my_obj, rhs); // silently sets my_obj to null if it fails // ── Correct: checked cast ───────────────────────────────── if (!$cast(my_obj, rhs)) begin `uvm_fatal("CAST", "$cast failed — type mismatch") return; end if (my_obj == null) `uvm_fatal("CAST", "cast succeeded but handle is null")
| Rule | Rationale |
|---|---|
Never place code in $unit scope | Timescale, visibility, and reuse problems. All code goes in packages. |
Never use #0 procedural delays | Indicates a race condition in the design. Fix the race — don’t paper over it. |
Avoid wildcard associative arrays [*] | Cannot be used with foreach, find_index, and other constructs. Use [int] instead. |
Avoid program blocks | Alters clocking/sampling semantics — confusing and unnecessary in UVM. |
Avoid checker blocks | Not consistently supported; use SVA in interfaces instead. |
| Do not rely on static variable initialization order | Order is undefined. Initialize statics on first instance via a function. |
Use if() not assert for method return checks | assert() adds unwanted entries to coverage database. |
| Use non-blocking assignment in drivers | <= on interface signals ensures correct simulation semantics at clock edges. |
A well-structured UVM project follows a strict package dependency hierarchy. Lower-level packages have no knowledge of higher-level ones, enabling staged compilation and preventing circular dependencies.
// ── Package dependency order (compile bottom to top) ───── // // uvm_pkg ← always at the bottom // ↑ // agent_pkg(s) ← import uvm_pkg only // register_pkg ← import uvm_pkg only // ↑ // env_pkg ← import uvm_pkg + agent_pkgs // sequence_pkg(s) ← import uvm_pkg + agent_pkgs + reg_pkg // ↑ // test_pkg ← import everything above // ── Agent package structure ─────────────────────────────── package gpio_agent_pkg; `include "uvm_macros.svh" import uvm_pkg::*; `include "gpio_seq_item.svh" // sequence item first `include "gpio_agent_cfg.svh" // config object `include "gpio_driver.svh" // components `include "gpio_monitor.svh" `include "gpio_agent.svh" // agent last (uses above) `include "gpio_api_seq.svh" // API sequences endpackage : gpio_agent_pkg // ── Test package structure ──────────────────────────────── package gpio_test_pkg; `include "uvm_macros.svh" import uvm_pkg::*; import gpio_agent_pkg::*; // agent classes import gpio_env_pkg::*; // env + scoreboard + coverage import gpio_seq_pkg::*; // sequences import gpio_reg_pkg::*; // register model `include "gpio_base_test.svh" `include "gpio_reg_test.svh" `include "gpio_interrupt_test.svh" endpackage : gpio_test_pkg // ── tb_top: import test package ─────────────────────────── module tb_top; import uvm_pkg::*; `include "uvm_macros.svh" import gpio_test_pkg::*; // ← MUST be imported for factory to see tests // ... interfaces, DUT, clock, vif config_db::set ... initial run_test(); endmodule
run_test("my_test") fails with “Cannot find test type ‘my_test’.” This is one of the most common testbench setup errors in new projects.Before publishing an agent or environment for reuse, verify every item in this checklist:
| Category | Checklist item |
|---|---|
| Factory | All classes registered with `uvm_component_utils or `uvm_object_utils |
| Factory | All objects created with type_id::create() — no bare new() |
| Constructor | super.new() is first statement; no extra arguments |
| Packages | All classes in packages; one class per .svh file |
| Config | Agent has a config object; all tuneable behaviour controlled through config |
| Config | Using uvm_config_db — not deprecated set_config_object() |
| Virtual interface | Interface retrieved from config_db in build_phase with `uvm_fatal on failure |
| Active/passive | Agent respects is_active — driver/sequencer only built when UVM_ACTIVE |
| Analysis port | Monitor broadcasts completed transactions via analysis_port |
| Sequence items | do_copy() and do_compare() implemented manually — no `uvm_field macros |
| Sequences | randomize() return value always checked |
| Messages | All messages use `uvm_info/warning/error/fatal — no $display |
| End-of-test | Scoreboard implements phase_ready_to_end if it uses FIFOs |
| No time in sequences | Sequences use only start_item/finish_item — no #delay or @event |
| Documentation | Config object fields documented; analysis port outputs documented |
| Guideline | Rule or Avoid? |
|---|---|
| All classes in packages | Rule — type uniqueness requires it |
| One class per .svh file | Rule — enables staged compilation |
| super.new() first in constructor | Rule — required by UVM base class |
| No extra constructor args | Rule — factory cannot pass them |
| Register all classes with factory | Rule — enables override, debug, topology |
| Match handle name to string in create() | Rule — enables path-based debug |
| Import test package in tb_top | Rule — without it, run_test() fails |
| Avoid `uvm_field_* macros | Avoid — slow, compare all fields indiscriminately |
| Avoid `uvm_do macros | Avoid — hides start_item/finish_item, no randomize check |
| Check randomize() return value | Rule — silent failures produce wrong stimulus |
| Check $cast() return value | Rule — unchecked cast may silently produce null |
| Avoid pre_body()/post_body() | Avoid — unpredictable when called as sub-sequence |
| Avoid objection per transaction | Avoid — extreme simulation slowdown |
| Avoid #0 delays | Avoid — masks a race; fix the underlying race |
| No code in $unit scope | Rule — timescale and reuse issues |