UVM-27: UVM Coding Guidelines — VLSI Trainers
VLSI Trainers UVM Series · UVM-27
UVM Series · UVM-27

UVM Coding Guidelines

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.

📋 Do’s and Don’ts at a Glance

✓ Do these

  • Define all classes within packages
  • One class per file, named classname.svh
  • Use `uvm_component_utils / `uvm_object_utils
  • Use `uvm_info / `uvm_error / `uvm_fatal
  • Manually implement do_copy(), do_compare()
  • Use seq.start(sequencer)
  • Use start_item() + finish_item()
  • Use uvm_config_db API
  • One config object per agent
  • Use phase objection mechanism
  • Implement phase_ready_to_end() in scoreboards
  • Use run_phase() in drivers and monitors
  • Call super.new(name [,parent]) first in constructors
  • Check randomize() and $cast() return values

✗ Avoid these

  • Including a class in multiple locations
  • Extra constructor arguments beyond name/parent
  • `uvm_field_* automation macros
  • `uvm_do / `uvm_do_with sequence macros
  • pre_body() and post_body() in sequences
  • Consuming simulation time directly in sequences
  • set_config_string/int/object() (deprecated)
  • uvm_resource_db API (use config_db instead)
  • UVM callbacks
  • User-defined phases
  • Phase jumping and phase domains
  • Raising/dropping objections per transaction
  • #0 procedural delays
  • Code in $unit scope

📋 Naming Conventions

Consistent naming makes component hierarchies readable and enables path-based debugging. These conventions apply across the entire testbench:

ItemConventionExample
Package filesname_pkg.sv — suffix always _pkggpio_agent_pkg.sv
Class filesclassname.svh — no import/include inside the filegpio_driver.svh
Agent componentsprefix_driver, prefix_monitor, prefix_agent, prefix_seq_itemapb_driver, apb_monitor
Component handlesm_ prefix for member component handlesm_drv, m_mon, m_seqr, m_agent
Config objectsprefix_agent_cfg or prefix_env_cfgapb_agent_cfg, gpio_env_cfg
Sequence classesprefix_action_seq — descriptive verbgpio_write_seq, apb_init_seq
Test classesprefix_test or feature_testgpio_base_test, interrupt_test
Analysis portsap for the main port; error_ap for secondarym_mon.ap
String names in create()Match the handle name exactlym_drv = gpio_driver::type_id::create("m_drv", this)
Message IDsShort uppercase abbreviation matching component"DRV", "MON", "SB", "CFG"
Match handle names to string names in create(). The string name passed to 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.

📋 Class Definition Rules

Rule: Define all classes within packages

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

Rule: One class per file, named classname.svh

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.

Rule: super.new() must be the first statement in every constructor

// ── 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

Rule: No extra constructor arguments beyond name and parent

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.

📋 Factory Rules

Rule: Register ALL classes with the factory

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);

Rule: Import the package that contains test classes into tb_top

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.

📋 Macro Guidance

Use: Message macros

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.

Avoid: `uvm_field_* automation macros

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:

// ── 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

Avoid: `uvm_do sequence macros

`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);

📋 Sequence Rules

Rule: Always check randomize() return value

// ── 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")

Rule: Do not consume simulation time directly in sequences

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.

Rule: Avoid pre_body() and post_body()

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().

Rule: Do not raise/drop objections per transaction

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 Rules

Rule: Build in build_phase, connect in connect_phase

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.

Rule: Use run_phase in drivers and monitors

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.

Rule: Use uvm_config_db — not deprecated set/get_config_*

// ── 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!")

Rule: Always check $cast() return value

// ── 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")

📋 SystemVerilog Language Rules

RuleRationale
Never place code in $unit scopeTimescale, visibility, and reuse problems. All code goes in packages.
Never use #0 procedural delaysIndicates 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 blocksAlters clocking/sampling semantics — confusing and unnecessary in UVM.
Avoid checker blocksNot consistently supported; use SVA in interfaces instead.
Do not rely on static variable initialization orderOrder is undefined. Initialize statics on first instance via a function.
Use if() not assert for method return checksassert() adds unwanted entries to coverage database.
Use non-blocking assignment in drivers<= on interface signals ensures correct simulation semantics at clock edges.

📋 Package Organisation

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
Always import the test package into tb_top. If the test package is not imported, the test class definitions are never seen by the simulator, the factory has no record of them, and 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.

📋 Reuse Checklist

Before publishing an agent or environment for reuse, verify every item in this checklist:

CategoryChecklist item
FactoryAll classes registered with `uvm_component_utils or `uvm_object_utils
FactoryAll objects created with type_id::create() — no bare new()
Constructorsuper.new() is first statement; no extra arguments
PackagesAll classes in packages; one class per .svh file
ConfigAgent has a config object; all tuneable behaviour controlled through config
ConfigUsing uvm_config_db — not deprecated set_config_object()
Virtual interfaceInterface retrieved from config_db in build_phase with `uvm_fatal on failure
Active/passiveAgent respects is_active — driver/sequencer only built when UVM_ACTIVE
Analysis portMonitor broadcasts completed transactions via analysis_port
Sequence itemsdo_copy() and do_compare() implemented manually — no `uvm_field macros
Sequencesrandomize() return value always checked
MessagesAll messages use `uvm_info/warning/error/fatal — no $display
End-of-testScoreboard implements phase_ready_to_end if it uses FIFOs
No time in sequencesSequences use only start_item/finish_item — no #delay or @event
DocumentationConfig object fields documented; analysis port outputs documented

📋 Quick Reference

GuidelineRule or Avoid?
All classes in packagesRule — type uniqueness requires it
One class per .svh fileRule — enables staged compilation
super.new() first in constructorRule — required by UVM base class
No extra constructor argsRule — factory cannot pass them
Register all classes with factoryRule — enables override, debug, topology
Match handle name to string in create()Rule — enables path-based debug
Import test package in tb_topRule — without it, run_test() fails
Avoid `uvm_field_* macrosAvoid — slow, compare all fields indiscriminately
Avoid `uvm_do macrosAvoid — hides start_item/finish_item, no randomize check
Check randomize() return valueRule — silent failures produce wrong stimulus
Check $cast() return valueRule — unchecked cast may silently produce null
Avoid pre_body()/post_body()Avoid — unpredictable when called as sub-sequence
Avoid objection per transactionAvoid — extreme simulation slowdown
Avoid #0 delaysAvoid — masks a race; fix the underlying race
No code in $unit scopeRule — timescale and reuse issues
Scroll to Top