UVM
The Passive Agent
The agent's passive mode — monitor only, observing an interface without driving it. Why passive is right for DUT-driven and internal interfaces, and how it preserves all checking and coverage while dropping only the stimulus when an interface becomes internal at integration.
Agents · Module 14 · Page 14.3
The Engineering Problem
The active agent drives and observes an interface the testbench controls (Module 14.2). This chapter is its counterpart — the passive agent, which builds only a monitor and observes without driving. Passive is not "a lesser active agent"; it's the correct, required configuration for an entire class of interfaces: ones the testbench must not drive. A DUT output the design masters; an internal interface between two DUT sub-blocks; and — most importantly — an interface that becomes internal at integration: a block's external interface (which the testbench drove at block level) turns into a subsystem's internal interface, now driven by the integrated neighbor block. The problem this chapter solves is how to keep checking an interface you can no longer drive. The naive move — remove the agent because "we don't drive it anymore" — throws away the monitor's protocol checking and coverage, leaving the now-internal interface unverified. The passive agent is the answer: keep observing, stop driving.
A passive agent is the agent configured to observe an interface without driving it — it builds only the monitor (no sequencer, no driver, no stimulus path) and exports the monitor's analysis port so the scoreboard and coverage still subscribe. It is the right (and only safe) choice wherever the testbench does not master the interface: DUT-driven outputs, internal inter-block interfaces, and interfaces that go internal at integration. Its defining value is that it preserves all monitoring — reconstruction, protocol checking, coverage (Modules 13.2–13.4) — while dropping only the stimulus: the same agent that drove an interface at block level can be configured passive at subsystem/SoC level to keep checking the now-internal interface without conflicting with the real block that now drives it. This chapter is the passive agent: its monitor-only structure, why DUT-driven and internal interfaces require it, the active→passive transition that makes agents reusable across integration levels, and the mistake of removing an agent instead of passivating it.
What is a passive agent — the monitor-only configuration that observes an interface without driving it — and why is it the right (and only safe) choice for interfaces the testbench doesn't master, preserving all checking and coverage while dropping only the stimulus?
Motivation — why an entire class of interfaces needs passive
Passive is not an afterthought to active — it's mandatory for interfaces the testbench can't drive, and valuable for keeping them checked. The reasons:
- Some interfaces are driven by the DUT, not the testbench. A DUT output — a result bus the design produces — is mastered by the DUT. The testbench must not drive it (that's contention, Module 14.2); it can only observe. That observation requires a passive agent.
- Internal inter-block interfaces must never be perturbed. An interface between two DUT sub-blocks carries real traffic the testbench watches but never touches. Any driving would corrupt the inter-block communication. A passive agent is the only safe observer there.
- Interfaces become internal at integration. A block's external interface — driven by the testbench at block level — becomes a subsystem's internal interface, now driven by the integrated neighbor block. The testbench's driving is now redundant and conflicting; the checking is still wanted. Passive mode is the transition that handles this.
- Checking and coverage are still needed on interfaces you don't drive. An interface you don't drive still carries bugs — protocol violations, wrong data, missed scenarios. The monitor's job (reconstruct, check, cover) is just as valuable there. Passive keeps that.
- Passive ≠ idle. A passive agent actively observes — it reconstructs transactions, checks the protocol, and feeds the scoreboard and coverage. It does everything an active agent's monitor does; it just doesn't drive.
The motivation, in one line: an entire class of interfaces — DUT-driven, internal, and internal-at-integration — must not be driven by the testbench but still needs checking and coverage, so UVM provides the passive agent: a monitor-only observer that keeps all the monitoring while dropping the stimulus, making it the required configuration where the testbench watches but does not master.
Mental Model
Hold the passive agent as the driving instructor who moves to the passenger seat:
A passive agent is the driving instructor who, once the student takes the wheel, moves to the passenger seat — stops driving, but keeps watching every move and grading it. The car still has an evaluator; it just isn't the one driving anymore. Picture a dual-control training car. During training (block-level verification), there's no student yet — so the instructor drives (the agent is active, providing the stimulus the interface needs). Then the student arrives — at integration, the real neighbor block is connected, and it now takes the wheel (drives the interface). The instructor does not leave the car and does not keep grabbing the wheel — grabbing the wheel now would fight the student and crash (two drivers, contention). Instead, the instructor moves to the passenger seat (the agent goes passive): they stop driving, but they keep doing the part that still matters — watching every move, checking that the student follows the rules of the road (protocol checking), and recording what maneuvers were practiced (coverage). The car is still being evaluated; the evaluator just isn't the driver anymore. The worst mistake would be to conclude "the instructor isn't driving, so remove them from the car" — then no one is grading, and the student's mistakes go unnoticed. The instructor in the passenger seat is exactly as vigilant as before — same eyes, same rulebook, same scorecard — only the driving has been handed over.
So the passive agent is the instructor in the passenger seat: it stops driving (the real block — the student — now drives the interface) but keeps watching, checking, and grading (the monitor still reconstructs, checks the protocol, and feeds coverage). The active→passive transition is moving from the driver's seat to the passenger seat — not leaving the car. And the cardinal error is removing the agent (taking the instructor out) instead of passivating it (moving them over): removing it loses the grading; passivating it keeps the grading and only yields the wheel.
Visual Explanation — monitor only, no stimulus path
The defining picture is what a passive agent lacks: the stimulus path (sequencer, driver) is gone; only the monitor and its analysis port remain.
The figure shows the passive agent as the active agent minus the stimulus path. There is no sequencer and no driver — nothing inside the agent drives the interface. The interface is not mastered by the testbench (it's warning-colored to mark that someone else — a DUT output or an integrated neighbor block — drives it). The agent's sole component is the monitor, which observes the interface (never driving it — Module 13.1's passivity) and exports its analysis port upward, so the scoreboard and coverage still subscribe. The crucial reading is what's present versus absent. Absent: the entire sequencer-driver stimulus path of the active agent (Module 14.2) — removed because the testbench does not drive this interface. Present: everything on the observe side — the monitor's reconstruction (Module 13.2), protocol checking (Module 13.3), and analysis-port broadcast (Module 13.4) to the scoreboard and coverage. The success-colored path from the analysis port to the scoreboard/coverage is the point: a passive agent is not idle — it fully feeds checking and coverage, exactly as an active agent's monitor does. The only difference from active is the missing brand-colored stimulus path. The diagram is the passive agent's essence: strip the driving, keep the watching — a complete observer for an interface the testbench can't or shouldn't drive, still delivering all the monitoring value up to the checkers.
RTL / Simulation Perspective — the active→passive transition across integration levels
The passive agent's most important use is the integration transition: the same agent that's active at block level is configured passive at subsystem/SoC level, where the interface has gone internal. The hierarchy shows the transition.
The figure shows why passive mode makes agents reusable across integration. At block level, the interface is external — there's no neighbor block yet, so the testbench drives it; the agent is active (full trio, providing stimulus). As the design is integrated up, the neighbor block — the one that will drive this interface in the real chip — is connected. Now, at subsystem level and SoC level, that neighbor drives the interface (it has become internal), so the same agent is configured passive: it stops driving (the neighbor block is the driver now — driving would be contention) but keeps observing — reconstructing, checking the protocol, and feeding coverage on the now-internal interface. The RTL/structural insight is the one agent class, configured per level: the agent is unchanged code — only its is_active setting changes, tracking whether the testbench or the design drives the interface at that level. This is the deep payoff of the active/passive knob (Module 14.1): the protocol-checking and coverage you built into the agent's monitor at block level keep running at every higher level for free, automatically following the interface as it goes internal — you don't rewrite anything, you flip a configuration bit. Without passive mode, you'd face a bad choice at integration: keep the agent active (and fight the neighbor → contention) or delete it (and lose the checking). Passive is the third, correct option: keep the agent, flip it passive — yielding the wheel while keeping the eyes. The figure is the argument that passive isn't a niche mode — it's the mechanism by which an interface's verification (its monitor) survives integration intact.
Verification Perspective — passive preserves checking, drops only stimulus
The passive agent's value is precisely what it keeps: all the monitor's checking and coverage. Removing the agent (instead of passivating it) loses that — seeing what survives is seeing why passive matters.
The figure shows the surgical precision of going passive: it removes one thing and keeps everything else. The active agent has two capabilities — a stimulus path (sequencer + driver) and the monitor's observe capabilities. Going passive removes only the stimulus path (warning-colored "dropped" — no driving, the sole loss). Everything the monitor provides survives: reconstruction (observed transactions → the scoreboard), protocol checking (violations still flagged, Module 13.3), and coverage (scenarios still sampled). The verification insight is that a passive agent retains the entire checking-and-coverage capability of the agent — it loses only the ability to generate stimulus, which on a DUT-driven or internal interface is exactly the part you don't want anyway. This is why passive is not a downgrade: on an interface the testbench shouldn't drive, the stimulus path is useless (or harmful), and the monitor is all you wanted — passive gives you precisely that. The contrast that makes the point: removing the agent entirely (a tempting "cleanup" at integration) would lose all of it — no reconstruction, no protocol checking, no coverage on that interface — leaving the now-internal interface completely unverified. So the choice at integration is not "active or delete"; it's passive — keep the monitor's full value, drop only the now-redundant stimulus. The figure is the argument against the delete instinct: passive is checking-and-coverage preserved, driving removed — and on an interface you don't master, that's all upside. The monitor's verification work is too valuable to throw away just because the driving became redundant.
The Agent, Written Out
Everything above turns on one configuration value and two if statements. Here
they are.
class apb_agent extends uvm_agent;
`uvm_component_utils(apb_agent)
apb_monitor mon; // always built
apb_driver drv; // active only
apb_sequencer sqr; // active only
// The monitor's output is the agent's output. It exists in both modes, and
// the env connects to it without knowing or caring which mode this is.
uvm_analysis_port #(apb_txn) ap;
function new(string name, uvm_component parent);
super.new(name, parent);
ap = new("ap", this);
endfunction
function void build_phase(uvm_phase phase);
// super.build_phase() is what resolves is_active from the config database.
// Call it FIRST. Read get_is_active() before this line and you get the
// class default (UVM_ACTIVE) no matter what the env configured.
super.build_phase(phase);
// The monitor is unconditional. That is the whole point of the mode:
// observation is never what gets dropped.
mon = apb_monitor::type_id::create("mon", this);
if (get_is_active() == UVM_ACTIVE) begin
sqr = apb_sequencer::type_id::create("sqr", this);
drv = apb_driver::type_id::create("drv", this);
end
endfunction
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
mon.ap.connect(ap); // always - the checking path
// Guard the connection with the SAME condition that guarded the build.
// An unguarded connect here dereferences a null handle in passive mode and
// the test dies during connect_phase, before any stimulus has run.
if (get_is_active() == UVM_ACTIVE)
drv.seq_item_port.connect(sqr.seq_item_export);
endfunction
endclassConfiguring it per integration level
class soc_env extends uvm_env;
`uvm_component_utils(soc_env)
apb_agent cpu_agent; // testbench masters this interface
apb_agent periph_agent; // the DUT's neighbour masters this one
apb_scoreboard sb;
function void build_phase(uvm_phase phase);
super.build_phase(phase);
// Set is_active BEFORE creating the agent - the agent reads it in its own
// build_phase, which runs after this one (build is top-down).
uvm_config_db #(uvm_active_passive_enum)::set(
this, "cpu_agent", "is_active", UVM_ACTIVE);
uvm_config_db #(uvm_active_passive_enum)::set(
this, "periph_agent", "is_active", UVM_PASSIVE);
cpu_agent = apb_agent::type_id::create("cpu_agent", this);
periph_agent = apb_agent::type_id::create("periph_agent", this);
sb = apb_scoreboard::type_id::create("sb", this);
endfunction
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
// Both agents feed the scoreboard identically. The scoreboard has no idea
// one of them is passive, and it should not need to.
cpu_agent.ap.connect(sb.cpu_imp);
periph_agent.ap.connect(sb.periph_imp);
endfunction
endclassThe line that carries the argument from earlier in this chapter is
periph_agent.ap.connect(sb.periph_imp). It is identical for the active and the
passive agent. Every check the scoreboard performs on that interface, and every
coverage bin the monitor samples, continues to work at integration without a
single edit — because the only thing the mode changed was whether a driver was
constructed.
An agent configured passive built its driver anyway, and two drivers fought over the bus
CONFIG-DB-SILENT-MISSAt subsystem integration, an interface that had been clean at block level started
showing X on its data bus during roughly one transfer in twenty. The scoreboard
reported mismatches whose payloads looked like two valid transactions bitwise
ORed together. The env had been updated to configure the relevant agent
UVM_PASSIVE, the change had been reviewed, and uvm_config_db::set was plainly
present in the code.
Raising verbosity showed both the DUT's neighbour block and the testbench driver logging a transfer at the same simulation time.
// In the env - note the type parameter:
uvm_config_db #(int)::set(this, "periph_agent", "is_active", UVM_PASSIVE);
// ...and in a hand-written agent that did its own lookup:
function void apb_agent::build_phase(uvm_phase phase);
super.build_phase(phase);
uvm_config_db #(uvm_active_passive_enum)::get(this, "", "is_active", is_active);
// ^
// return value discarded - a failed get leaves is_active at its default
mon = apb_monitor::type_id::create("mon", this);
if (is_active == UVM_ACTIVE) begin
sqr = apb_sequencer::type_id::create("sqr", this);
drv = apb_driver::type_id::create("drv", this);
end
endfunctionuvm_top.print_topology() settled it in one line: the agent that was supposed to
be passive had drv and sqr children in the printed hierarchy. Whatever the env
intended, the agent had built an active configuration.
The confirming check was +UVM_CONFIG_DB_TRACE, which prints every set and get
with its type. The trace showed the set recorded under type int and the get
requesting type uvm_active_passive_enum — two entries that never matched. The
X on the bus was then simply two drivers contending, which the OR-looking
payloads had already suggested.
Two failures compounding, and neither reported anything on its own.
uvm_config_db is type-parameterised, so a value stored as int is not
retrievable as uvm_active_passive_enum. The lookup simply found nothing. That
is not an error condition in the config database — a miss is a legitimate,
expected outcome, which is why it returns a status rather than raising one.
The second failure is that the status was discarded. get() leaves its output
argument untouched on a miss, so is_active retained the class default of
UVM_ACTIVE, and the agent proceeded to build a driver exactly as it would at
block level. Every subsequent step behaved correctly given that input. The
configuration intent existed only in the env, and nothing carried it across.
Note that using the built-in uvm_agent::build_phase resolution rather than a
hand-written lookup would have masked half of this: 1800.2 accepts both the
uvm_active_passive_enum and the legacy int form, so the set would have been
found. Hand-rolling the lookup narrowed what the agent would accept without
narrowing what the env was allowed to send.
// 1. Let the base class resolve it. uvm_agent::build_phase accepts both the
// enum and the legacy int form, so the env and agent cannot disagree.
function void apb_agent::build_phase(uvm_phase phase);
super.build_phase(phase);
mon = apb_monitor::type_id::create("mon", this);
if (get_is_active() == UVM_ACTIVE) begin
sqr = apb_sequencer::type_id::create("sqr", this);
drv = apb_driver::type_id::create("drv", this);
end
endfunction
// 2. If a component must do its own lookup, never discard the status.
if (!uvm_config_db #(my_cfg)::get(this, "", "cfg", cfg))
`uvm_fatal("NOCFG", "cfg not set for this component")Verifying the fix is a topology assertion rather than a stimulus test, because the defect is structural and exists before any transfer runs:
// In the env's check_phase, or a small dedicated test.
if (periph_agent.get_is_active() != UVM_PASSIVE)
`uvm_error("CFG", "periph_agent should be passive at this integration level")
if (periph_agent.drv != null)
`uvm_error("CFG", "passive agent built a driver")The durable rule is the one in step 2, and it is worth applying beyond this
agent: a discarded uvm_config_db::get status converts a configuration mistake
into a default-valued silent success. Making every non-optional lookup fatal on
miss turns a class of integration bug that surfaces as bus contention into one
that surfaces as a clear message during build_phase. The related failure modes
live in config_db gotchas; the analysis path
this agent keeps in both modes is covered in
analysis ports.
Runtime / Execution Flow — the passive agent at run time
At run time, a passive agent runs only the monitor's observe loop — it never drives, never needs a sequence — yet fully feeds the scoreboard and coverage. The flow shows its operation.
The flow shows that a passive agent's run time is just the monitor's loop — complete on the observe side, empty on the drive side. Someone else drives (step 1): the DUT or an integrated neighbor block masters the interface — the agent never drives, so there is no stimulus step at all. Monitor observes (step 2): the agent's only component does the full monitor job — reconstruct transactions (Module 13.2), check the protocol (Module 13.3) — exactly as it would in an active agent. Broadcast (step 3): the monitor's analysis port feeds subscribers (Module 13.4), identically to active. Consume (step 4): the scoreboard and coverage, subscribed, check and sample — full checking and coverage on the interface. The runtime insight is that steps 2–4 are identical to an active agent's observe side — a passive agent's monitor is not a reduced monitor; it's the same monitor doing the same work. The only difference is the absence of the stimulus steps (the active agent's "test runs a sequence → driver drives," Module 14.2): a passive agent has nothing to run a sequence on (no sequencer) and nothing to drive with (no driver), and it needs neither, because the interface is driven externally. This is why a passive agent is fully functional verification despite generating no stimulus: its value was always on the observe side, and that side is wholly intact. The flow is the passive agent delivering complete checking and coverage on an interface it watches but doesn't touch — the monitor's work, unchanged, with the driving simply not there.
Waveform Perspective — observing without driving
A passive agent's defining behavior is visible on a timeline: the interface is driven by someone else, and the agent observes it without contributing any drive. The waveform shows the passive agent watching a neighbor-driven interface.
A passive agent observes a neighbor-driven interface, driving nothing itself
12 cyclesThe waveform shows the passive agent observing without driving. The interface is driven by an integrated neighbor block — nbr_valid and the data come from the neighbor, not the agent. The crucial visual is what the agent does not contribute: there is no agent-driven signal on the interface — the passive agent drives nothing. Its monitor observes: mon_capture pulses when it samples the neighbor's activity, and it emits the observed transaction on its analysis port (ap_emit) — checking and coverage proceed (Modules 13.3–13.4). The contrast with the active agent (Module 14.2) is exactly the absence of the drive path: the active agent's waveform had drv_valid — its own driver asserting the interface — whereas here there is no such signal; the only activity attributable to the agent is observation (mon_capture, ap_emit). This is passivity made visible: the agent watches the neighbor-driven interface and checks it, but never touches the wheel. Reading a waveform for passive-agent behavior — is the agent driving anything (it must not)? is its monitor observing and emitting (it should)? — confirms the agent is a pure observer. The picture to carry: a passive agent appears on the interface only as observation — no drive whatsoever — while fully performing the monitor's reconstruction, checking, and coverage. The driving belongs to someone else (the DUT or the neighbor block); the watching and grading belong to the passive agent — eyes on, hands off.
DebugLab — the checking that vanished at integration
An internal interface went unverified because the agent was removed instead of made passive
At block level, an interface was thoroughly verified — its agent drove stimulus and its monitor flagged protocol violations and recorded coverage. After integration into the subsystem, a real bug on that now-internal interface (a protocol violation between the two integrated blocks) escaped: no violation was flagged, no coverage was recorded, the scoreboard saw nothing. The interface that had been the most thoroughly checked at block level was, at integration, completely unverified — its checking had silently disappeared.
When the interface became internal at integration, the engineer removed the agent — reasoning "the testbench no longer drives this interface, so we don't need the agent" — which also removed the monitor, deleting the protocol checking and coverage. They conflated "stop driving" with "stop watching":
✗ REMOVE the agent at integration (conflates 'don't drive' with 'don't watch'):
// env: "we don't drive this interface anymore, delete its agent"
// → driver/sequencer gone (correct: the neighbor drives now)
// → MONITOR also gone (WRONG: protocol checking + coverage DELETED)
// → the now-internal interface is UNVERIFIED → real protocol bug ESCAPES
✓ make the agent PASSIVE at integration (keep the monitor, drop only the stimulus):
uvm_config_db#(uvm_active_passive_enum)::set(this, "if_agent", "is_active", UVM_PASSIVE);
// → sequencer/driver gone (correct: the neighbor drives now — no contention)
// → MONITOR KEPT → protocol checking + coverage still run on the internal interface
// → the same checks that passed at block level keep guarding it at integrationThis is the remove-vs-passivate bug — throwing away the monitor's verification value by deleting the agent when the correct move was to passivate it. The engineer was half right: at integration, the neighbor block drives the interface, so the testbench should stop driving it (keeping the driver would be contention, Module 14.2). But they over-corrected: instead of dropping just the driver (via passive mode), they deleted the whole agent — including the monitor. The monitor was doing valuable, still-needed work: reconstructing the inter-block transactions, checking the protocol between the two integrated blocks, and recording coverage of the now-internal traffic. Deleting it removed all of that, leaving the now-internal interface — which still carries bugs — completely unchecked. So a real protocol violation between the integrated blocks escaped: nothing was watching. The fix is to make the agent passive (is_active = UVM_PASSIVE) instead of removing it: the sequencer and driver go (correctly — the neighbor drives now, no contention), but the monitor stays, continuing to check the protocol and feed coverage on the internal interface. The same checks that passed at block level keep guarding the interface at integration. The general lesson, and the chapter's thesis: passive ≠ removed — when an interface goes internal at integration, don't delete its agent; make it passive. Removing the agent conflates "we stop driving" (true) with "we stop watching" (false, and dangerous) — it throws away the monitor's protocol checking and coverage, leaving the now-internal interface unverified. Passive keeps the watching, drops only the driving — exactly the surgical change integration calls for. Move the instructor to the passenger seat; don't push them out of the car.
The tell is verification that disappears when an interface goes internal. Diagnose remove-vs-passivate mistakes:
- Check what happened to the agent at integration. If an interface's agent was deleted when it went internal, its monitor's checking and coverage went with it.
- Look for coverage that dropped at integration. Coverage on an interface that vanishes at the subsystem level signals a removed (not passivated) agent.
- Confirm whether internal interfaces are still monitored. An integrated, internal interface with no monitor has no protocol checking — a real bug there will escape.
- Distinguish 'stop driving' from 'stop watching'. If the reasoning was "we don't drive it, so remove the agent," the watching was lost unnecessarily — passive was the right move.
Passivate, never delete, when an interface goes internal:
- Make the agent passive at integration. Set
is_active = UVM_PASSIVEso the driver and sequencer drop (no contention) but the monitor keeps checking and covering. - Keep monitoring internal interfaces. Internal inter-block interfaces carry bugs; their monitors' protocol checking and coverage are still needed after integration.
- Never conflate driving with watching. Stopping the stimulus is correct at integration; stopping the observation is not — they are separate, and passive separates them.
- Verify coverage persists across levels. Confirm an interface's coverage and protocol checks survive integration — if they vanish, the agent was removed instead of passivated.
The one-sentence lesson: passive ≠ removed — when an interface goes internal at integration, make its agent passive (is_active = UVM_PASSIVE), never delete it; removing the agent conflates "stop driving" (correct) with "stop watching" (wrong), throwing away the monitor's protocol checking and coverage and leaving the now-internal interface unverified, while passive keeps the watching and drops only the driving.
Common Mistakes
- Removing an agent when an interface goes internal. Make it passive instead; deleting it loses the monitor's protocol checking and coverage on the now-internal interface.
- Driving an interface the testbench doesn't master. DUT outputs and internal interfaces must be observed with a passive agent — driving them causes contention or corrupts inter-block traffic.
- Thinking passive means idle. A passive agent fully reconstructs, checks the protocol, and feeds coverage; it does everything an active agent's monitor does, minus the driving.
- Trying to run a sequence on a passive agent. A passive agent has no sequencer; stimulus belongs to active agents, and the interface here is driven externally.
- Keeping an agent active after integration. Once the neighbor block drives the interface, an active agent fights it (contention); switch it to passive.
- Forgetting to export the passive agent's analysis port. Even passive, the monitor's port must be exported so the scoreboard and coverage subscribe — passive feeds checking too.
Senior Design Review Notes
Interview Insights
A passive agent is the agent configured to observe an interface without driving it — it builds only the monitor, with no sequencer and no driver, and exports the monitor's analysis port so the scoreboard and coverage still subscribe. You use it wherever the testbench does not master the interface. There are three main cases. First, a DUT output — a bus the design drives to report results; the testbench must not drive it, only observe it, so a passive agent watches it. Second, an internal interface between two DUT sub-blocks; that interface carries real inter-block traffic the testbench watches but must never perturb, so a passive agent is the only safe observer. Third, and most importantly, an interface that becomes internal at integration: a block's external interface that the testbench drove at block level becomes a subsystem's internal interface, now driven by the integrated neighbor block. There, the testbench's driving is redundant and would conflict, but the checking is still wanted — so the same agent is configured passive. The defining property of a passive agent is that it preserves all the monitoring — reconstruction, protocol checking, coverage — while dropping only the stimulus. It does everything an active agent's monitor does; it just doesn't drive. So passive is not a downgrade or an idle mode — it's a complete observer for an interface the testbench can't or shouldn't drive. The mental model is a driving instructor who moves to the passenger seat once the student takes the wheel: they stop driving but keep watching and grading. Eyes on, hands off.
Because it lets the same agent follow an interface as it goes internal, keeping the interface's verification intact without rewriting anything. At block level, an interface is external — there's no neighbor block yet, so the testbench drives it, and the agent is active, providing stimulus. As you integrate up to subsystem and SoC level, the real neighbor block that will drive that interface in the chip gets connected, and now it drives the interface — the interface has become internal. At that point, keeping the agent active would be wrong: its driver would fight the neighbor block, causing contention. But you still want the checking — protocol violations and coverage on the now-internal interface. Passive mode is the transition that resolves this. You configure the same agent passive: the sequencer and driver drop, so there's no contention, but the monitor stays, continuing to reconstruct transactions, check the protocol, and feed coverage. The crucial part is that this is one agent class, configured per level — the agent's code is unchanged; only its is_active setting changes, tracking whether the testbench or the design drives the interface. So all the protocol-checking and coverage you built into the agent's monitor at block level keep running at every higher level automatically, just by flipping a configuration bit. Without passive mode, integration would force a bad choice: keep the agent active and fight the neighbor, or delete it and lose the checking. Passive is the correct third option — keep the agent, flip it passive, yield the driving but keep the eyes. That's why passive isn't a niche mode; it's the mechanism by which an interface's monitor survives integration, which is exactly what makes agents reusable across the block-to-SoC hierarchy.
Going passive drops only the stimulus path and keeps everything the monitor provides. The active agent has two capabilities: a stimulus path, which is the sequencer and driver that generate and drive traffic, and the monitor's observe capabilities, which are reconstruction of transactions, protocol checking, and coverage sampling, all broadcast through the analysis port. Going passive removes only the stimulus path — no sequencer, no driver, no driving. Everything the monitor does survives: it still reconstructs observed transactions and sends them to the scoreboard, it still flags protocol violations, and it still samples coverage. So a passive agent retains the entire checking-and-coverage capability of the agent and loses only the ability to generate stimulus. That's why passive is not a downgrade — on an interface the testbench shouldn't drive, the stimulus path is useless or even harmful, so dropping it loses nothing you wanted, and the monitor, which is all you wanted, is fully intact. The important contrast is with removing the agent entirely. Removing it would lose all of it — no reconstruction, no protocol checking, no coverage on that interface — leaving it unverified. So at integration the choice is not active versus delete; it's passive, which keeps the monitor's full value and drops only the now-redundant stimulus. Concretely, the passive agent still exports its analysis port so the scoreboard and coverage subscribe exactly as before. The monitor in a passive agent is not a reduced monitor; it's the same monitor doing the same work. The driving is simply absent because the interface is driven externally, by the DUT or an integrated neighbor block.
Removing the agent is a mistake because it conflates stopping the driving with stopping the watching, and it throws away the monitor's still-needed checking and coverage. At integration, when an interface goes internal and the neighbor block drives it, it's correct that the testbench should stop driving — keeping the driver would cause contention. But the reasoning "we don't drive this interface anymore, so we don't need the agent" over-corrects: deleting the whole agent also deletes the monitor, which was doing valuable work — reconstructing the inter-block transactions, checking the protocol between the two integrated blocks, and recording coverage of the now-internal traffic. Removing it leaves the internal interface completely unchecked, so a real protocol violation between the integrated blocks escapes, with nothing watching. The interface that was the most thoroughly verified at block level becomes unverified at integration. What you should do instead is make the agent passive — set is_active to UVM_PASSIVE. That drops the sequencer and driver, which is correct because the neighbor drives now and you avoid contention, but it keeps the monitor, so the protocol checking and coverage keep running on the internal interface. The same checks that passed at block level keep guarding it at integration. The principle is that "stop driving" and "stop watching" are separate decisions, and passive mode separates them: it lets you stop driving while continuing to watch. Internal interfaces carry bugs too, so their monitors are still needed after integration. The image is moving the instructor to the passenger seat rather than pushing them out of the car — you yield the wheel but keep the evaluation. So passive, not delete, is the surgical change integration calls for.
Because uvm_agent::build_phase() is what resolves is_active from the configuration database. Until it has run, is_active holds its class default of UVM_ACTIVE, so an agent that reads get_is_active() first will build a driver and sequencer regardless of what the env configured.
The failure is quiet in the worst way. Nothing errors — the agent constructs a perfectly valid active configuration, the connect phase succeeds, and the test runs. At block level the behaviour is even correct, because active is what you wanted there. The bug only manifests at the integration level where the interface has an actual owner, and it manifests as bus contention: two drivers on one interface, X on the data lines, and scoreboard mismatches whose payloads look like two transactions merged.
The general shape is worth internalising beyond this one method. build_phase runs top-down, so a parent's build_phase executes before its children's, which is why the env can set configuration for an agent it has not created yet. Within a component, super.build_phase() is where the base class does its own configuration resolution. Skipping or reordering it does not skip the component's construction — it just makes that construction use stale defaults.
Print the topology. uvm_top.print_topology() shows the constructed hierarchy, and an agent that was meant to be passive will list drv and sqr children if it was not. That distinguishes "the configuration did not arrive" from "the configuration arrived and something else is driving the bus" in one step, which are entirely different investigations.
If the topology confirms an active build, the cause is almost always the configuration lookup, and +UVM_CONFIG_DB_TRACE names it directly by printing every set and get with its full path and type. The three things it exposes, in rough order of frequency: a type mismatch between the set and the get — uvm_config_db#(int)::set paired with uvm_config_db#(uvm_active_passive_enum)::get are two unrelated entries as far as the database is concerned; an instance-path mismatch, where the set names a scope that does not match where the agent actually sits; and ordering, where the set happens after the agent's build_phase has already run.
The reason any of these is silent is that uvm_config_db::get returns a status and leaves its output argument untouched on a miss. A miss is a legitimate outcome, not an error, so a caller that discards the return value converts a configuration mistake into a default-valued success. Make every non-optional lookup uvm_fatal on miss and this entire class of bug reports itself during build_phase instead of appearing as X on a bus.
Yes, it needs one — the monitor samples real signals, so it needs a handle to them exactly as a driver would. What changes is only the direction of use: the monitor reads the interface and never drives it, so the handle can be to the same physical interface instance that something else is mastering.
That is precisely the integration case. When a block's neighbour is connected and begins driving an interface that the testbench used to drive, the passive agent's monitor keeps its virtual interface handle pointing at that same interface and simply observes the neighbour's traffic instead of the driver's. Nothing about the handle changes; the source of the stimulus does.
Two practical points follow. First, a passive agent still fails at build or run time if its virtual interface was never configured, and the error can be confusing because people associate the virtual interface with driving. Configure it for passive agents exactly as for active ones. Second, the monitor should sample through a clocking block rather than reading the interface signals directly, which keeps it out of the race between the neighbour's drives and its own sampling — the same discipline that matters for any monitor, and the reason UVM environments rarely hit the scheduling problems that hand-written testbenches do.
Where This Is Specified
- IEEE 1800.2-2020 (UVM) —
uvm_agent. Theis_activemember of typeuvm_active_passive_enum, theget_is_active()accessor, and the basebuild_phasebehaviour that resolvesis_activefrom the configuration database, accepting both the enum form and the legacyintform. - IEEE 1800.2-2020 —
uvm_config_dbanduvm_resource_db. The type-parameterisedset/getinterface, the instance-path scoping rules, and the fact thatgetreports a miss through its return value while leaving the output argument unmodified. - IEEE 1800.2-2020 — phasing. The top-down ordering of
build_phasethat lets a parent configure a child it has not yet constructed, and the bottom-upconnect_phasein which analysis connections are made. - Accellera UVM User Guide — agent architecture. The active/passive convention itself, and the recommendation that an agent's analysis port is its interface to the environment in both modes.
Exercises
- Name what's absent. State which components a passive agent does not build, and what it still builds and exports.
- Trace the transition. Describe how the same agent is configured at block level versus after the interface goes internal at integration, and why.
- Defend passivation. An interface goes internal and a teammate proposes deleting its agent. Explain what would be lost and what to do instead.
- Classify the interfaces. For each, pick active or passive: a result bus the DUT outputs; an internal interface between two integrated blocks; a stimulus bus into the block under test.
Summary
- A passive agent is the agent configured to observe an interface without driving it — it builds only the monitor (no sequencer, no driver, no stimulus path) and exports the monitor's analysis port so the scoreboard and coverage still subscribe.
- It is the right (and only safe) choice wherever the testbench doesn't master the interface: DUT-driven outputs, internal inter-block interfaces, and interfaces that go internal at integration — driving any of these causes contention or corrupts inter-block traffic.
- Passive preserves all monitoring — reconstruction, protocol checking, coverage — and drops only the stimulus: it is not idle, it does everything an active agent's monitor does, minus the driving.
- The active→passive transition makes agents reusable across integration: the same agent is active at block level and passive at subsystem/SoC level, with only
is_activechanging — so an interface's verification follows it as it goes internal. - The durable rule of thumb: use a passive agent wherever the testbench doesn't drive the interface, and when an interface goes internal at integration, make its agent passive — never delete it; removing the agent conflates "stop driving" (correct) with "stop watching" (wrong), throwing away the monitor's protocol checking and coverage, while passive keeps the watching and drops only the driving. Passive ≠ idle, and passive ≠ removed.
Next — Agent Reuse: the active and passive modes are the foundation of the agent's greatest strength — reuse. The next chapter generalizes beyond mode-switching to the full picture: how a protocol agent, built once, is reused across blocks, subsystems, and projects — the conventions, packaging, and configuration that make an agent a true plug-and-play verification component.
Continue learning
Related tutorials
- Related topic
Agent Architecture
The agent — UVM's reusable, configurable container that bundles a sequencer, driver, and monitor into one self-contained unit for a single interface, built once and reused everywhere, switchable between active (drives) and passive (observes).
- Related topic
Protocol Agents
Building agents for real standardized protocols (AXI, APB) — how the protocol spec shapes the agent's transaction, driver, monitor, and sequences, the master/slave roles built per role, and why a protocol agent is the highest-leverage, widely-reused VIP.