A UVM testbench sits around a DUT (design under test) and does three jobs: generate stimulus, drive it in, and independently check what comes out. Here's how that splits across components — and just as importantly, which component actually contains which.
tb_top (the plain module, dashed) contains the DUT and the test. The test creates the env. The env assembles the agent (sequencer, driver, monitor) and the scoreboard as siblings. A sequence sends seq_item transactions into the sequencer, which hands them to the driver one at a time.The two root classes everything else extends
Before naming individual components, it's worth seeing the split they all fall into. Every class in the UVM library traces back to one of two roots:
uvm_object— the base for anything that's just data, with no fixed place in the testbench and no phasing. A sequence item is auvm_object. So is a sequence, and so is a configuration object.uvm_component— the base for anything structural: it has a permanent position in the testbench hierarchy (a parent, a name, children), and it's the only branch that gets build_phase, run_phase, and every other phase from Part 7 called on it automatically. Driver, monitor, sequencer, agent, env, and test are alluvm_component.
A useful shorthand: if it flows through the testbench, it's a uvm_object. If it's part of the testbench's fixed shape, it's a uvm_component. Every component below is one of these two; nothing in UVM sits outside this split.
Sequence & sequence item
A sequence item (extending uvm_sequence_item, which itself extends the more general uvm_transaction) is one unit of stimulus — one transaction, like "a single write of this data to this address." A sequence (extending uvm_sequence) is the logic that decides what stream of sequence items to generate — randomized, directed, or some mix — and sends them one at a time to the sequencer.
One detail worth knowing rather than being surprised by later: uvm_sequence itself extends uvm_sequence_item. A sequence is a kind of sequence item. That's not an accident — it's exactly what lets one sequence start another sequence inside it, layering smaller, reusable sequences into larger ones.
Sequencer
The sequencer's only job is traffic control: it takes sequence items from whichever sequence is currently running, and hands them to the driver, one at a time, exactly when the driver asks for the next one. It doesn't know or care what the DUT actually is.
Driver
The driver (extending uvm_driver) is the only component that actually toggles pins. It pulls a sequence item from the sequencer, and translates it into the exact pin-level activity the DUT's interface expects — asserting a write-enable signal for one clock cycle, holding data steady, whatever the protocol requires.
Monitor
The monitor watches the DUT's interface passively — it never drives a signal, only observes. It reconstructs transactions from raw pin activity (the reverse of what the driver does) and broadcasts them, usually over a uvm_analysis_port, to anything that wants to observe DUT activity — most importantly, the scoreboard.
Scoreboard
The scoreboard is where "is this correct?" actually gets answered. It receives transactions from one or more monitors, independently determines what the correct result should have been, and flags a mismatch as a failure. This is the component that turns "we ran some traffic" into "we know whether the DUT is right or wrong."
Agent
An agent (extending uvm_agent) is simply a container that bundles a sequencer, driver, and monitor for one interface into a single reusable unit. If a DUT has two interfaces (say, a write side and a read side — exactly the situation in the async FIFO project later in this series), you'd typically have two agents, one per interface.
Environment (env) and test
The env (extending uvm_env) instantiates and connects the agents and the scoreboard — it's the top-level structure of the testbench itself. The test (extending uvm_test) sits one level above the env: it configures the environment and starts the specific sequence that defines what this particular test run is actually trying to exercise.
The test itself isn't the actual top of the hierarchy, though it's the top of what you write. When a simulation calls run_test("my_test"), UVM first creates a single implicit node called uvm_root, and your test becomes a child of it. You never instantiate uvm_root yourself — it exists purely so that every component in the testbench, no matter how deeply nested, has a single common ancestor.
class my_driver extends uvm_driver #(my_item);
`uvm_component_utils(my_driver)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
task run_phase(uvm_phase phase);
forever begin
seq_item_port.get_next_item(req);
// drive DUT pins from req here
seq_item_port.item_done();
end
endtask
endclass
class my_agent extends uvm_agent;
`uvm_component_utils(my_agent)
my_driver drv;
my_monitor mon;
uvm_sequencer #(my_item) sqr;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
drv = my_driver::type_id::create("drv", this);
mon = my_monitor::type_id::create("mon", this);
sqr = uvm_sequencer#(my_item)::type_id::create("sqr", this);
endfunction
function void connect_phase(uvm_phase phase);
drv.seq_item_port.connect(sqr.seq_item_export);
endfunction
endclass
Communication objects: ports, exports, and imps
None of the components above are physically wired together — they talk over TLM (transaction-level modeling) objects, connected once in connect_phase. Two pairs cover almost everything in this series:
uvm_seq_item_pull_port/uvm_seq_item_pull_export— the driver-to-sequencer link. The driver's port pulls: it callsget_next_item()and blocks until the sequencer's export actually has one ready. This is auvm_driver's built-inseq_item_port, already used in the code above.uvm_analysis_port/uvm_analysis_imp— the monitor-to-scoreboard link, used the opposite way. The monitor's port broadcasts: callingap.write(item)fans that same item out to every analysis imp connected to it, with no blocking and no reply expected. One monitor can feed several listeners this way, not just one scoreboard.
The general pattern behind both: a port is the calling side, an export or imp is the side that actually implements the method being called. You connect a port to an export/imp once, and from then on the port's method calls are simply forwarded across.
Supporting infrastructure — what makes it configurable
A few more objects don't sit in the stimulus/response path at all, but every environment in this series relies on them:
- The factory (
uvm_factory). Every`uvm_component_utils/`uvm_object_utilsmacro registers that class with a single global factory. Callingmy_driver::type_id::create(...)asks the factory for an instance, rather than callingnew()directly — which is what lets a test later override one type with another (set_type_override_by_type) without touching the env's code at all. - Configuration (
uvm_config_db). A way to pass a value or handle — most commonly the virtual interface — from a high level down into deeply nested components, without manually threading it through every constructor in between. - Objections (
uvm_objection). Covered in depth in Part 7 — this is the object behindphase.raise_objection()/drop_objection(), deciding whenrun_phaseis actually allowed to end. - Reporting (
uvm_report_server,uvm_report_object). Every`uvm_info,`uvm_warning,`uvm_error, and`uvm_fatalcall routes through this. It's also what's actually counting theUVM_ERRORtotal that Part 7 mentioned as the real pass/fail signal.
A few more names you'll meet later
These don't come up in this series, but they're worth recognizing on sight so a real codebase doesn't feel unfamiliar:
uvm_subscriber— a ready-made component that already has an analysis imp built in. For a simple "just react to whatever the monitor sends" component, extending this can save writing the imp declaration by hand.- Callbacks (
uvm_callback/uvm_callbacks) — a mechanism for injecting extra behavior into an existing component from outside it, without editing or re-deriving that component's class. - The Register Abstraction Layer (
uvm_reg,uvm_reg_block,uvm_reg_field, and related classes) — a separate object family purpose-built for verifying register-mapped DUTs. It's a large enough topic on its own that it's intentionally out of scope for this series.
| Class | Role, in one line |
|---|---|
uvm_object | Root of anything that's data, not structure |
uvm_component | Root of anything with a fixed place in the testbench + phasing |
uvm_transaction / uvm_sequence_item | One unit of stimulus or observed activity |
uvm_sequence | Decides what stream of items to generate |
uvm_sequencer | Hands items from the active sequence to the driver, on request |
uvm_driver | Turns an item into pin-level activity on the DUT |
uvm_monitor | Turns pin-level activity back into an item, passively |
Scoreboard (typically extends uvm_component) | Decides if the DUT's behavior was correct |
uvm_agent | Bundles one interface's sequencer, driver, and monitor |
uvm_env | Assembles the agents and scoreboard(s) |
uvm_test | Configures the env and starts the sequence(s) for this run |
uvm_root | Implicit top of the hierarchy, created by run_test() |
| TLM ports/exports/imps | Connect components without hard-wiring method calls |
uvm_factory | Creates registered types; allows swapping implementations |
uvm_config_db | Passes values/handles down the hierarchy without manual threading |
uvm_objection | Decides when run_phase is allowed to end |
uvm_report_server | Routes and counts every info/warning/error/fatal message |
Takeaway
Everything in UVM is a uvm_object (data) or a uvm_component (structure). Sequence and sequencer decide what to send; driver and monitor are the only components that touch pins; scoreboard decides correctness; agent, env, and test assemble the structure. TLM ports/exports/imps carry data between components; the factory, config_db, objections, and reporting are the infrastructure that makes all of it configurable and reusable, not just functional.