UVM Basics

Chapter 2 of 10

The UVM Component Tree and Phasing

uvm_component as a standardized class hierarchy on top of class/extends, the fixed order UVM runs build_phase/connect_phase/run_phase in, the objection mechanism that controls when a test ends, and UVM's reporting macros.

Chapter 1's first gap was concrete: the SV Basics capstone's driver and generator() were just a class and a task, with no shared naming convention and nothing a tool could walk to report or debug. This chapter is where UVM closes that gap — uvm_component, the base class nearly every structural part of a UVM testbench extends.

uvm_component: a class with a standardized shape

uvm_component is a class, the same keyword SV ch10 (oop-classes) introduced, extended the same way SV ch11 (oop-inheritance-polymorphism) taught:

class my_driver extends uvm_component;
  `uvm_component_utils(my_driver)
 
  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction
endclass

Every uvm_component's constructor takes exactly two arguments: name (a string, this instance's name) and parent (a handle to the component that owns it, or null for the top-level component). super.new(name, parent) hands both up to uvm_component's own constructor, which is what actually builds the tree — it records parent, and appends this instance's name to the end of parent's own path to build a full hierarchical name like uvm_test_top.env.agent.driver. That's the entire payoff of chapter 1's first promise: every component gets a name and a place in a standardized hierarchy for free, just by extending uvm_component and calling super.new() correctly — no hand-rolled naming convention required.

Phasing: what UVM runs, and in what order

Chapter 1's capstone used initial (SV ch8, always-blocks) and fork...join (SV ch14, interprocess-communication) to sequence its testbench by hand: construct the driver, then fork the generator and the driver's run() task together. UVM replaces that hand-sequencing with phasing: a fixed set of methods that UVM itself calls, in a fixed order, on every component in the tree. You don't call these methods — you override them, and UVM calls them for you:

PhaseKindTypical use
build_phasefunctionconstruct sub-components, read configuration
connect_phasefunctionconnect TLM ports between sibling components
run_phasetaskthe testbench's actual behavior — driving, monitoring, checking

(A few more phases exist — end_of_elaboration_phase, start_of_simulation_phase before run_phase, and extract_phase/check_phase/report_phase/final_phase after it — for elaboration-time setup and post-run bookkeeping. This track doesn't use them directly, so they're mentioned here only so the names aren't a surprise if you see them elsewhere.)

None of these run on their own — a top-level module has to call run_test() to actually start the phase engine, which is where chapter 3 picks up.

super.build_phase(phase): don't forget it

Overriding build_phase is common — most components construct their children there. But uvm_component's own build_phase does real work too, and skipping the call to it silently breaks that work:

function void build_phase(uvm_phase phase);
  super.build_phase(phase);  // always call this first
  // ... construct sub-components, read config, etc. ...
endfunction

This is easy to forget and hard to notice when you do — the symptom shows up elsewhere (a config value that mysteriously never arrives, for instance), not as an error at the call site. The habit: every phase method you override starts with a call to super.<phase_name>(phase).

Phase direction: build runs top-down, connect runs bottom-up

build_phase runs top-down — a parent's build_phase runs before its children's, so a parent can publish configuration before a child's build_phase goes looking for it. connect_phase runs the opposite direction, bottom-up — children connect first, so that by the time a parent's connect_phase runs, its children already have something to connect to.

This isn't just trivia: it's the exact reason chapter 5's config_db pattern works (a value set() at the top, before any child exists, is guaranteed visible by the time a child's build_phase calls get()), so chapter 5 will point back here instead of re-deriving it.

The objection mechanism: how run_phase actually ends

run_phase is a task, not a function — and it's usually written as a forever loop with no natural stopping point, the same way SV ch8's always-style processes never end on their own. Every component's run_phase gets forked as a concurrent process when the phase starts. So what makes the phase — and the whole test — actually end?

The answer is the objection mechanism: phase.raise_objection(this) tells UVM "don't end this phase yet," and phase.drop_objection(this) withdraws that hold. UVM ends run_phase the instant nothing is objecting to it ending:

task run_phase(uvm_phase phase);
  phase.raise_objection(this);
  `uvm_info("RUN", "starting work", UVM_MEDIUM)
  #10;
  `uvm_info("RUN", "work done", UVM_MEDIUM)
  phase.drop_objection(this);
endtask

Two ways this breaks if you get it wrong:

  • No objection ever raised: every component's run_phase process is still forked (they briefly exist), but with nothing objecting, UVM considers the phase satisfied essentially immediately and tears those processes down at time 0 — so it looks like nothing ran, even though run_phase technically started.
  • An objection raised but never dropped: the phase — and the simulation — never ends.

This is UVM's standardized answer to a question SV ch8/ch14's fork...join/disable fork material already raised informally: how does a concurrent process know when it's safe to stop? Instead of every testbench inventing its own answer, every component in the tree uses the same raise_objection/drop_objection pair.

Reporting: `uvm_info, `uvm_warning, `uvm_error, `uvm_fatal, and verbosity

SV ch2 (basic-syntax) covered $display/$warning/$error/$fatal, graded by severity. UVM has its own version of the same idea, used inside any uvm_component or uvm_object:

MacroSeverityStops simulation?
`uvm_infoInfoNo
`uvm_warningWarningNo
`uvm_errorErrorNo (but counted as a failure)
`uvm_fatalFatalYes
`uvm_info("DRV", $sformatf("driving sel=%0b", sel), UVM_MEDIUM)
`uvm_error("DRV", "unexpected value on the DUT output")

`uvm_info's three arguments are an ID (a short string tag, usually the component's role, like "DRV"), the message, and a verbosity level (UVM_LOW, UVM_MEDIUM, or UVM_HIGH). Two things these macros add on top of SV ch2's plain $display-family:

  • The reporting component's full hierarchical name is included automatically — a direct payoff of this chapter's naming-tree material. A `uvm_info call inside uvm_test_top.env.agent.driver prints that path without you ever formatting it yourself.
  • Verbosity can filter output at runtime, without touching code. A `uvm_info call only prints if its verbosity level is at or below the run's configured threshold — pass +UVM_VERBOSITY=UVM_HIGH on the simulator command line to see everything, or leave it at the default (UVM_MEDIUM) to see less.

Summary

  • uvm_component is a class (SV ch10) you extends (SV ch11); its constructor always takes name and parent, which is how UVM builds a standardized, fully-named component tree for free.
  • UVM calls build_phase, connect_phase, run_phase (and a few less commonly used phases) on every component, in a fixed order, instead of you sequencing things by hand with initial/fork...join (SV ch8/ch14). You override these methods; UVM calls them.
  • Always call super.build_phase(phase) (and the equivalent for any phase method you override) first — skipping it silently breaks the parent class's own setup.
  • build_phase runs top-down, connect_phase runs bottom-up — chapter 5's config_db pattern depends on this ordering.
  • run_phase is a task, typically a forever loop with no natural end. phase.raise_objection(this)/phase.drop_objection(this) control when it (and the test) actually ends — zero objections means the phase ends at time 0 before anything meaningful runs; an objection never dropped means it never ends.
  • `uvm_info/`uvm_warning/`uvm_error/`uvm_fatal are UVM's version of SV ch2's severity-graded printing, with two things added: the reporting component's hierarchical name is included automatically, and `uvm_info's verbosity level can be filtered at runtime with +UVM_VERBOSITY, without touching code.

What are the two arguments every uvm_component's constructor takes, and what does UVM do with them?

A component overrides build_phase but never calls super.build_phase(phase). What's the most accurate description of what happens?

A run_phase task never calls phase.raise_objection(this) at all. What happens?

Which macro would you use to print a message severe enough that simulation should stop immediately? (lowercase, no backtick)