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 aseq_item_portalready wired to pull transactions from a sequencer;uvm_subscriber#(T)comes with ananalysis_exportand awrite()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_componentmakes a thing part of the persistent tree with phases; extendinguvm_object(oruvm_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_sequenceis an object that youstart()on a sequencer — not build inbuild_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 data —uvm_object→uvm_transaction→uvm_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 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,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.
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.
// ── 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 sequencesEach 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.
Two groups live here. The containers — uvm_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.
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 cyclesThe 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
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.
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:
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.
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:
- Confirm the base class' branch. Is it a
uvm_component(has phases, lives in the tree) or auvm_object/uvm_sequence(transient, no phases)? A sequence is an object — it will never run frombuild_phase. - For sequences, look for
start(). A sequence runs only whenseq.start(sequencer)is called, normally in the test'srun_phase. Nostart()means no stimulus — building it changes nothing. - 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.
Match the base class to the kind of thing, and operate it accordingly:
- 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.
- Build components, start sequences. Put components in
build_phasevia the factory; create sequences andstart()them on a sequencer inrun_phase. Never build a sequence into the tree expecting it to run. - Raise objections around running stimulus. A sequence started in
run_phasetypically needs objections so the phase stays alive while it runs — another reason it belongs inrun_phase, notbuild_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_sequenceis on the object branch — transient, no phases. Building it inbuild_phasedoes nothing; you create it andstart()it on a sequencer inrun_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'sseq_item_port,uvm_subscriber'swrite(),uvm_sequencer's arbitration). Extending a genericuvm_componentwhere 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, notuvm_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 inuvm_scoreboard. Extendinguvm_monitorand 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_object → uvm_transaction → uvm_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
- 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.
- 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. - Fix the non-running sequence. A teammate created a sequence in
build_phaseand 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. - Thread the type. For a
my_itemtransaction, write theextendsclause 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 auvm_object(transient data). - The object branch is data and generators:
uvm_object→uvm_transaction→uvm_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) anduvm_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 inrun_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.