Skip to content

UVM

The UVM Component Ecosystem

The cast of UVM base classes you extend for each role — test, env, agent, driver, monitor, sequencer, scoreboard, subscriber, sequence_item, sequence — and how each maps to components vs objects.

Introduction to UVM · Module 2 · Page 2.7

The Engineering Problem

You have the architecture (the fixed hierarchy) and the mental model (the five recurring ideas). Now meet the cast — the actual UVM base classes you extend to build that hierarchy. Because here is where many learners stumble: UVM has a specific base class for each role, and choosing the right one is not a detail. Extend uvm_driver and you inherit the sequencer connection for free; extend uvm_subscriber and you inherit the write() hook that receives broadcasts; extend uvm_sequence and you get a transient stimulus generator that runs on a sequencer rather than living in the tree.

The trap is treating these names as interchangeable boilerplate. They are not — each base class is one of the mental-model ideas, pre-packaged. The whole library organises under a single split you already know: everything is either a component (persistent structure) or an object (transient data). Map each base class onto that split and the "ecosystem" stops being a vocabulary list and becomes the five ideas wearing names.

What are the UVM base classes you extend for each role, and how does each one map onto the components-vs-objects mental model — so the library's cast attaches to concepts you already understand?

Motivation — why the right base class matters

Choosing the correct base class is not ceremony; it determines what your class is and can do:

  • You inherit the role's machinery. uvm_driver#(T) comes with a seq_item_port already wired to pull transactions from a sequencer; uvm_subscriber#(T) comes with an analysis_export and a write() method to receive broadcasts; uvm_sequencer#(T) comes with arbitration. Extend the right base and the plumbing is done; extend the wrong one and you reimplement it or fight the framework.
  • The base class encodes object-vs-component. Extending uvm_component makes a thing part of the persistent tree with phases; extending uvm_object (or uvm_sequence_item) makes it transient data. Choosing correctly is choosing the right kind of thing — the mental model's first idea, enforced by inheritance.
  • It prevents whole bug classes. The classic "my sequence won't run" bug is extending or instantiating a sequence as if it were a component. Knowing that uvm_sequence is an object that you start() on a sequencer — not build in build_phase — avoids it entirely.
  • It makes environments legible. When every component extends the conventional base for its role, anyone can read the class declarations and know the architecture before reading a line of behaviour. The base classes are a shared vocabulary.

The motivation, in one line: the base class you extend is the role you are filling — choose it from the mental model, and the library hands you the right tools while keeping the components-vs-objects line crisp.

Mental Model

Hold the cast as one split with two branches:

Everything descends from uvm_object, and the first fork is the whole story. One branch stays datauvm_objectuvm_transactionuvm_sequence_item (a transaction) → uvm_sequence (a generator of transactions). These are transient objects: created, randomised, passed by handle, run, discarded. The other branch becomes structureuvm_object → … → uvm_component, and from there the testbench cast: uvm_test, uvm_env, uvm_agent, uvm_driver, uvm_monitor, uvm_sequencer, uvm_scoreboard, uvm_subscriber. These are persistent components: built once, placed in the tree, given phases. To pick a base class, ask the mental model's first question — persistent structure or transient data? — and you are already on the right branch.

The one counter-intuitive fact to burn in: a sequence is an object, a sequencer is a component. The sequence is the transient what-to-send; the sequencer is the permanent thing it runs on. Confusing them is the most common ecosystem error, and the split above is what prevents it.

Visual Explanation — the two-branch class tree

The entire UVM cast hangs off one root and one fork. Drawing it once makes every base class' nature obvious: which branch it is on tells you whether it is persistent structure or transient data.

UVM class tree: uvm_object splits into the object/data branch (transaction, sequence_item, sequence) and the component branch (test, env, agent, driver, monitor, sequencer, scoreboard, subscriber)transientdatapersistent structuregeneratorthe testbench castuvm_objectthe common rootuvm_transactionOBJECT branch (data)uvm_sequence_itema transactionuvm_sequencestimulus generator (object!)uvm_componentCOMPONENT branch (structure)test · env · agent · driver · monitor ·sequencer · scoreboard · subscriberpersistent components12
Figure 1 — the UVM base class tree, the components-vs-objects split made concrete. Everything descends from uvm_object. One branch stays data: uvm_transaction → uvm_sequence_item (a transaction) → uvm_sequence (a stimulus generator) — transient objects. The other branch becomes uvm_component and the testbench cast: test, env, agent, driver, monitor, sequencer, scoreboard, subscriber — persistent structure. Which branch a class sits on tells you what kind of thing it is.

The fork is the lesson. The data branch is everything that flows — transactions and the sequences that generate them — all transient uvm_objects passed by handle. The component branch is everything that persists — the boxes of the hierarchy, each with a place in the tree and a set of phases. Note where uvm_sequence sits: on the data branch, because a sequence is transient stimulus, not structure. That single placement is the most-missed fact in the whole ecosystem, and the tree puts it where it belongs.

RTL / Simulation Perspective — the cast in code

Here is the whole cast as class declarations, each extending the conventional base for its role. Read the extends clause of each and you can state, before any behaviour, what kind of thing it is and what it does.

the UVM cast — each role extends its conventional base class
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── OBJECT branch (transient data) ──
class my_item      extends uvm_sequence_item;        // a transaction (data)
class my_seq       extends uvm_sequence #(my_item);  // stimulus generator — runs ON a sequencer
class my_cfg       extends uvm_object;               // a configuration object (data)
 
// ── COMPONENT branch (persistent structure) ──
class my_driver    extends uvm_driver    #(my_item); // has seq_item_port (pulls items)
class my_monitor   extends uvm_monitor;              // observes; we add an analysis port
class my_sequencer extends uvm_sequencer #(my_item); // arbitrates items to the driver
class my_sb        extends uvm_scoreboard;           // checks observed vs expected
class my_cov       extends uvm_subscriber#(my_item); // write() receives broadcasts (coverage)
class my_agent     extends uvm_agent;                // container: sequencer + driver + monitor
class my_env       extends uvm_env;                  // container: agents + scoreboard + coverage
class my_test      extends uvm_test;                 // top: configures + starts sequences

Each extends clause is a declaration of intent. my_driver extends uvm_driver#(my_item) says "I am a component that consumes my_item transactions" — and inherits seq_item_port to do it. my_cov extends uvm_subscriber#(my_item) says "I am a component that receives broadcast my_items" — and inherits the write() hook. my_seq extends uvm_sequence#(my_item) says "I am a transient generator of my_items" — an object you will start() on a sequencer, not a component you build. The base classes are not boilerplate; they are how you declare each piece's kind and get its machinery.

Verification Perspective — the component cast and their hooks

The component branch is the testbench's working cast, and each base class exists to give one role exactly the hooks it needs. Knowing what each provides is knowing why you extend it.

UVM component base classes: test, env, agent (containers); sequencer, driver, monitor, scoreboard, subscriber (workers) and their hooksUVM component base classes and what each gives youUVM component base classes and what each gives youuvm_test — the topselected by +UVM_TESTNAME; configures the env and starts sequencesselected by +UVM_TESTNAME; configures the env and starts sequencesuvm_env / uvm_agent — containersorganise the hierarchy; the agent bundles sequencer + driver + monitor for one interfaceorganise the hierarchy; the agent bundles sequencer + driver + monitor for one interfaceuvm_sequencer#(T) — arbitrationroutes sequence items from sequences to the driverroutes sequence items from sequences to the driveruvm_driver#(T) — drivesinherits seq_item_port: get_next_item / item_done to pull items and drive pinsinherits seq_item_port: get_next_item / item_done to pull items and drive pinsuvm_monitor — observesa component intended to observe and broadcast (you add the analysis port)a component intended to observe and broadcast (you add the analysis port)uvm_scoreboard / uvm_subscriber#(T) — checking & coveragescoreboard checks; subscriber inherits analysis_export + write() to receive broadcastsscoreboard checks; subscriber inherits analysis_export + write() to receive broadcasts
Figure 2 — the component cast and the hook each base class provides. Containers (test, env, agent) organise the tree. Workers do the verification: the sequencer arbitrates items, the driver pulls and drives them (seq_item_port), the monitor observes and broadcasts (analysis port), the scoreboard checks, the subscriber receives broadcasts (write()). Extending the right base class hands you the role's machinery for free.

Two groups live here. The containersuvm_test, uvm_env, uvm_agent — exist to organise and configure; they mostly build and connect children. The workers — sequencer, driver, monitor, scoreboard, subscriber — each do a verification job and each base class supplies the matching hook: uvm_driver brings the seq_item_port, uvm_subscriber brings write(), uvm_sequencer brings arbitration. (uvm_monitor and uvm_scoreboard are thin — essentially uvm_component with a declared role — so you add the analysis port or checking logic yourself.) The point is that extending the role's base class is how you receive its machinery, which is why the choice is meaningful rather than cosmetic.

Runtime / Execution Flow — the object branch and how it runs

The object branch is smaller but conceptually loaded, because it contains the one thing that runs without being a component: the sequence. A sequence is a transient object that executes on a sequencer to produce a stream of sequence items.

uvm_object to uvm_transaction to uvm_sequence_item (data) and uvm_sequence (generator started on a sequencer)The object branch — from data to generatorThe object branch — from data to generator1uvm_objectthe data root — copy/clone/print/compare; no place in the tree, nophases.2uvm_transaction → uvm_sequence_itema transaction: the unit of stimulus/observation data passed byhandle.3uvm_sequencea generator of sequence items — a transient object, not acomponent.4started on a sequencerseq.start(sequencer) in run_phase produces a stream of items to thedriver. Started, not built.
Figure 3 — the object branch lineage and how it operates at run time. uvm_object → uvm_transaction → uvm_sequence_item is the data lineage (a transaction is data). uvm_sequence extends sequence_item but plays a different role: it is a transient generator that, when started on a sequencer (a component), produces a stream of sequence items. The sequence is the 'what to send'; the sequencer is the component it runs on. Objects don't have phases — a sequence is started, not built.

The lineage carries the most important ecosystem subtlety. A uvm_sequence_item is plainly data — a transaction. A uvm_sequence extends uvm_sequence_item, so it is also on the object branch — yet it behaves like an actor, generating items. The resolution is that a sequence is a transient object that runs, not a persistent component: you create it and call seq.start(sequencer) during run_phase, and it executes on the sequencer (which is a component). It has no phases, no place in the hierarchy, no build_phase. This is why "where does the sequence go in the tree?" is the wrong question — it doesn't go in the tree; it runs on a component that does.

Waveform Perspective — a sequence (object) running on a sequencer (component)

The object/component split is visible at run time: a single sequence object generates a stream of sequence-item objects, which the sequencer routes and the driver — a component — turns into pins. The waveform shows one sequence producing several transactions.

One sequence (object) generates a stream of items; the driver (component) drives each

12 cycles
One sequence (object) generates a stream of items; the driver (component) drives eachthe sequence object produces item #1 → driver (component) drives itthe sequence object pr…same sequence, item #2 — one generator object, a stream of data objectssame sequence, item #2…item #3 — the sequencer/driver persist; the items are transient objectsitem #3 — the sequence…clkvaliddata00A000B100C2000000000000seq_itemt0t1t2t3t4t5t6t7t8t9t10t11
Figure 4 — a single my_seq object runs on the sequencer and produces three sequence-item objects over the run (seq_item pulses). Each item is pulled by the driver (a component) and translated into pin activity on valid/data. The sequence is transient — created and started, not built into the tree; the sequencer and driver are persistent components. One generator object, a stream of data objects, driven by a component: the ecosystem at run time.

The three seq_item pulses are three transient uvm_sequence_item objects, all produced by one uvm_sequence object running on the persistent sequencer, each driven by the persistent driver. This is the ecosystem in motion: objects stream, components persist. Notice the asymmetry — the sequence and its items come and go within the run, while the sequencer and driver were built once at elaboration and remain. Reading any UVM run is reading this: a flow of objects through a standing structure of components, each piece an instance of the base class that matches its role.

DebugLab — the sequence that wouldn't run, because it was built like a component

Created in build_phase, added to the tree — and it never drove a thing

Symptom

An engineer wrote a sequence, created it in the environment's build_phase with the factory (seq = my_seq::type_id::create("seq", this)), and expected it to generate stimulus. The simulation ran, the DUT sat idle, and nothing was ever driven. No error pointed at the sequence; it simply never executed. The most basic thing — "run my stimulus" — silently did nothing.

Root cause

A base-class / object-vs-component error. A uvm_sequence is on the object branch — it is transient stimulus, not a component. It has no place in the hierarchy and no phases, so building it into the tree accomplishes nothing: there is no phase that will ever "run" it, because objects don't have phases. It must be started on a sequencer:

what was done vs what a sequence requires
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
done:     seq = my_seq::type_id::create("seq", this);   // built into the tree like a component
expected: the framework would 'run' it via phases      // but objects have NO phases
reality:  uvm_sequence is a uvm_object (transient) — it never phases, never auto-runs
correct:  in run_phase:  seq = my_seq::type_id::create("seq");
                         seq.start(agent.sequencer);     // STARTED on a sequencer (a component)

The sequence was treated as a persistent component when it is a transient object. Components are built and phased; objects are created and (for sequences) started. The category was wrong, so the framework never ran it.

Diagnosis

The tell is something that "should run" silently doing nothing — often an object mistaken for a component. Diagnose base-class errors by checking the branch:

  1. Confirm the base class' branch. Is it a uvm_component (has phases, lives in the tree) or a uvm_object/uvm_sequence (transient, no phases)? A sequence is an object — it will never run from build_phase.
  2. For sequences, look for start(). A sequence runs only when seq.start(sequencer) is called, normally in the test's run_phase. No start() means no stimulus — building it changes nothing.
  3. Don't look for a phase on an object. If you're waiting for an object to "phase," that's the bug. Objects are created and used imperatively; only components are driven by phasing.
Prevention

Match the base class to the kind of thing, and operate it accordingly:

  1. Know each base class' branch. Sequences and transactions are objects (transient, no phases); drivers, monitors, sequencers, scoreboards, agents, envs, tests are components (persistent, phased). Extend the conventional base for the role.
  2. Build components, start sequences. Put components in build_phase via the factory; create sequences and start() them on a sequencer in run_phase. Never build a sequence into the tree expecting it to run.
  3. Raise objections around running stimulus. A sequence started in run_phase typically needs objections so the phase stays alive while it runs — another reason it belongs in run_phase, not build_phase.

The one-sentence lesson: a sequence is a transient object, not a component — it has no phases and is never built into the tree; you create it and start() it on a sequencer, and forgetting that is why stimulus silently never runs.

Common Mistakes

  • Treating a sequence as a component. uvm_sequence is on the object branch — transient, no phases. Building it in build_phase does nothing; you create it and start() it on a sequencer in run_phase. This is the single most common ecosystem error.
  • Extending the wrong base for the role. Each base class brings its role's machinery (uvm_driver's seq_item_port, uvm_subscriber's write(), uvm_sequencer's arbitration). Extending a generic uvm_component where a specific base exists means reimplementing what you'd have inherited.
  • Making a transaction a component. A transaction is data — extend uvm_sequence_item/uvm_object, not uvm_component. A transaction that's a component can't flow as transient data and breaks the object model.
  • Forgetting the parametrisation. uvm_driver#(T), uvm_sequencer#(T), uvm_subscriber#(T), uvm_sequence#(T) are parametrised by the transaction type. Omitting or mismatching the type breaks the connections that the base classes provide.
  • Putting checking in a uvm_monitor. The monitor base class signals "observe and broadcast"; checking belongs in uvm_scoreboard. Extending uvm_monitor and adding scoreboard logic re-creates the separation-of-concerns bug from the architecture chapter.

Senior Design Review Notes

Interview Insights

Everything in UVM descends from uvm_object, and the first fork splits the whole library into two branches that mirror the components-vs-objects mental model. The object branch is transient data: uvm_objectuvm_transactionuvm_sequence_item (a transaction) → uvm_sequence (a stimulus generator), plus configuration objects. The component branch is persistent structure: uvm_object → … → uvm_component, and from there the testbench cast — uvm_test, uvm_env, uvm_agent, uvm_driver, uvm_monitor, uvm_sequencer, uvm_scoreboard, and uvm_subscriber. Components have a place in the hierarchy and run through phases; objects are created, passed by handle, and discarded. You build a testbench by extending the conventional base class for each role, which hands you that role's machinery — for example uvm_driver brings the seq_item_port, uvm_subscriber brings the write() analysis hook.

Exercises

  1. Branch each class. For each, name the branch (object or component) and the conventional base class to extend: (a) a bus transaction; (b) a coverage collector; (c) a stimulus generator; (d) the thing that arbitrates items to the driver; (e) the per-interface container.
  2. Match the hook. Connect each base class to the machinery it provides: uvm_driver / uvm_subscriber / uvm_sequencer ↔ arbitration / seq_item_port / write() + analysis_export. State what you'd have to build by hand without it.
  3. Fix the non-running sequence. A teammate created a sequence in build_phase and it never drives. State the root cause in object/component terms and the two lines that fix it (create + start), including which phase they belong in.
  4. Thread the type. For a my_item transaction, write the extends clause for the sequence, sequencer, driver, and coverage subscriber, and explain what breaks if one uses a different type parameter.

Summary

  • The UVM cast is one split with two branches, mirroring the mental model's first idea: everything is a uvm_component (persistent structure) or a uvm_object (transient data).
  • The object branch is data and generators: uvm_objectuvm_transactionuvm_sequence_item (a transaction) → uvm_sequence (a stimulus generator), plus config objects — all transient, no phases, passed by handle.
  • The component branch is the testbench cast: uvm_test, uvm_env, uvm_agent (containers) and uvm_sequencer, uvm_driver, uvm_monitor, uvm_scoreboard, uvm_subscriber (workers) — each base class supplying its role's machinery (seq_item_port, write(), arbitration).
  • The most-missed fact: a sequence is an object, a sequencer is a component. A sequence is created and start()ed on a sequencer in run_phase, never built into the tree — building it like a component is why stimulus silently never runs.
  • The durable rule of thumb: pick the base class from the mental model — persistent structure or transient data? — and you land on the right branch, inherit the right hooks, and keep the object/component line crisp.

Next — Package Structure & Compilation: you know the cast and how it maps to the mental model. The next chapter covers how these classes are actually packaged and compiled — the import uvm_pkg, the package and file structure, and the compilation order that lets a multi-file UVM environment build cleanly.