A block-level UVC for, say, an AXI slave port is built to do two things: generate legal (and sometimes illegal) traffic on that port, and check whatever comes back.
At the SoC level, that same port is no longer an open input waiting for a testbench to drive it - it's wired to a real AXI master inside the DUT, whether that's a CPU, a DMA engine, or another block's bus interface. The UVC still belongs on that interface, but its stimulus-generating half now has nothing useful to do.
13 min read | Intermediate Level
What You'll Learn
- Why a UVC that drove traffic at block level can't keep driving that same traffic at SoC level
- What the
is_activefield on auvm_agentactually controls - How to flip a reused agent from active to passive without touching the UVC's source
- What verification actually focuses on once the same UVC is watching an integrated block, and how that reshapes coverage and closure
The Same Interface, a Different Driver
At block level, the UVC's job is to stand in for everything the block will eventually be connected to. An AXI slave UVC drives read and write transactions at the block's slave port because, on that bench, nothing else is connected there - the DUT is just the one block, in isolation.
Integrate that block into the SoC, and the picture changes completely. The AXI master on the other end of that port is no longer imaginary - it's real RTL, sitting inside the same DUT, driven by whatever software or hardware sequencer normally drives it.
If the SoC-level UVC kept actively driving transactions onto that port, it would be contending with the real master for control of the same signals, which is not a corner case to debug around - it's an illegal bus configuration.
The interface itself, and everything the UVC already knows about its protocol, is exactly as valid at SoC level as it was at block level. Only the direction of control changes - the UVC stops generating traffic and starts observing traffic that a real block inside the DUT is now generating instead.
Active vs Passive: What is_active Actually Controls
A well-written uvm_agent checks a single field, is_active, during its own build_phase, and uses it to decide whether to construct a driver and sequencer at all. The monitor gets built either way - it has no opinion about who is driving the bus, it just watches:
class my_axi_agent extends uvm_agent;
`uvm_component_utils(my_axi_agent)
my_axi_driver drv;
my_axi_sequencer sqr;
my_axi_monitor mon;
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
mon = my_axi_monitor::type_id::create("mon", this);
// Driver and sequencer only exist when the agent is active -
// in passive mode there is nothing for either one to do
if (get_is_active() == UVM_ACTIVE) begin
drv = my_axi_driver::type_id::create("drv", this);
sqr = my_axi_sequencer::type_id::create("sqr", this);
end
endfunction
endclass
get_is_active() just reads the agent's is_active field, which defaults to UVM_ACTIVE and can be overridden through the config database before the agent's build_phase runs. Reusing a block-level UVC at SoC level, almost always, means setting that one field to UVM_PASSIVE and changing nothing else in the UVC's source:
// In the SoC-level environment's build_phase, before the agent
// underneath it is built:
uvm_config_db #(uvm_active_passive_enum)::set(
this, "axi_agent", "is_active", UVM_PASSIVE);
Mistake: Setting is_active after the agent's build_phase has already run.
The config database entry has to land before the agent reads it, which means the set call belongs in a component higher up the hierarchy, executing in its own build_phase before the environment builds the agent underneath it. A set that happens later - say, in connect_phase - is simply too late; the agent already built a driver and sequencer it didn't need.
What the Passive Agent Still Has to Do
Going passive removes the driver and sequencer, but the monitor's job doesn't get any smaller - if anything, it becomes the only source of visibility into that interface.
The monitor still has to decode every transaction correctly, still drives its analysis port on every observed beat, and still feeds whatever scoreboard or coverage model was built around it at block level. None of that logic needs to change; it was already designed to observe the bus, not assume who was driving it.
Reusing a UVC passively is exactly why a monitor should never have been written to assume it's watching its own agent's driver. A monitor that only decodes what it can see on the physical interface - and never reaches into the driver or sequencer for shortcuts - ports over to SoC level with zero changes.
A Practical Pattern: Default the UVC to Passive
Flipping is_active through the config database works, but it puts the burden on every SoC-level environment to remember to set it. A pattern used widely in industry UVCs inverts that default: the agent's is_active field defaults to UVM_PASSIVE in the UVC itself, and it's the block-level test - the one place actively driving traffic is actually wanted - that explicitly overrides it to UVM_ACTIVE.
class my_axi_agent extends uvm_agent;
`uvm_component_utils(my_axi_agent)
// Passive by default. A block-level test opts in to UVM_ACTIVE
// instead of every SoC-level env having to opt out of it.
function new(string name, uvm_component parent);
super.new(name, parent);
is_active = UVM_PASSIVE;
endfunction
// ... build_phase unchanged from the earlier example ...
endclass
The reasoning behind the inversion is about failure direction. Forgetting to set passive at SoC level, with an active-by-default agent, produces a driver fighting the real DUT for control of a bus - a signal-integrity bug that can be confusing to root-cause.
Forgetting to set active at block level, with a passive-by-default agent, produces a test that generates no stimulus and reports zero coverage immediately - a loud, obvious failure that gets caught the first time the test runs.
Not every UVC in circulation follows this convention, and changing an existing UVC's default is a decision for whoever owns it, not something to assume. The point worth taking from it is the failure-mode reasoning: when a mode has to be set correctly across many environments, default to whichever mistake is easiest to notice.
What Verification Actually Focuses On After Integration
Block-level verification on that same UVC spent most of its effort on the protocol itself: every legal transaction type, every illegal one the block is supposed to reject, back-to-back transactions, narrow timing corners on that one interface. By the time a block is ready to integrate, that space is supposed to be closed.
At SoC level, re-running that same protocol space adds little - the interface behaves the way it always did, because the RTL behind it didn't change.
What's new, and what the passive agent's data now gets used for, is everything that only exists once multiple blocks are wired together: does a DMA transfer to memory land correctly while the CPU is concurrently fetching from the same region, does an address that decodes to the wrong target get caught by the interconnect instead of silently succeeding, does an interrupt asserted by one block actually reach the handler running on another. None of those questions could even be asked with one block in isolation.
The same passive monitor that used to feed block-level protocol coverage still runs - it still has to prove the interface behaves legally - but the scenarios that get built around it are drawn from how blocks interact, not from the interface's own corner cases.
Coverage and Closure Look Different Too
Because the interface-level corner cases were the point of block-level closure, chasing the same covergroups again at SoC level mostly measures whether the interconnect and other blocks still exercise the same legal transaction types - which they almost always do, since the RTL is unchanged. Treating that re-measurement as the SoC coverage goal burns regression time without answering a new question.
What SoC-level coverage is actually built to answer is whether the interesting cross-block combinations were exercised: concurrent access from two masters to the same target, an interrupt arriving while its handler's DMA transfer is still in flight, a configuration register change that takes effect mid-transaction.
A coverage model for this is usually a set of scenario-level bins - built from data the reused passive monitors already supply through their analysis ports - rather than a repeat of the protocol-level covergroups those same monitors fed at block level.
Closure follows the same shift. A block-level UVC's covergroups are typically inherited as-is and kept passing, as a regression health check that nothing broke during integration, but they stop being the metric anyone is actively chasing bins on. The metric that matters at SoC level is the cross-block scenario coverage built on top of that reused data.
Scoreboarding: One Big Checker, or Several Small Ones
A tempting design is a single, monolithic SoC-level scoreboard that swallows every passive monitor's analysis-port output and checks the whole chip end to end. In practice, most environments do better reusing each block's own scoreboard - the same one that checked that block in isolation - and feeding it from the reused passive monitor, then adding a thinner, separate checker on top for behavior that only exists across blocks.
Keeping each block's scoreboard responsible for its own interface, instead of collapsing everything into one checker, keeps a failure's scope obvious: a block-level scoreboard flagging a mismatch points straight at that block's data path, while the cross-block checker only needs to worry about things no single block's scoreboard could see on its own.
Common Mistakes
Mistake: Assuming a passive agent needs no configuration at all.
Interface widths, protocol variants, and other static configuration fields on the agent still have to match the DUT exactly. Going passive removes the driver and sequencer; it doesn't remove the need to configure the monitor correctly for the interface it's now watching.
Mistake: Leaving a default sequence bound to the agent's sequencer.
A sequencer that no longer exists, because the agent went passive, can't start a sequence that used to run on it automatically. Any test or virtual sequence that referenced the block-level UVC's default sequence needs that reference removed for the SoC-level configuration, not just disabled.
Mistake: Reusing a monitor that silently depends on driver-internal state.
A monitor that reads a variable inside the driver, instead of decoding the interface directly, only worked because the driver happened to be present at block level. That dependency breaks the moment the driver disappears in passive mode, and it's a sign the monitor wasn't fully self-contained to begin with.
Mistake: Treating block-level protocol coverage as the SoC-level coverage goal.
Re-closing the same interface-level covergroups at SoC level mostly confirms that unchanged RTL still behaves the way it did at block level. It answers a question that was already answered, at the cost of regression time that cross-block scenario coverage could have used instead.
Summary
In this article, you learned:
- A block-level UVC's driver has nothing valid to do once the interface it drove is wired to a real block inside the SoC DUT
- The
is_activefield onuvm_agentdecides whether a driver and sequencer get built at all, and many UVCs default it toUVM_PASSIVEso a block-level test has to opt in to driving traffic, rather than every SoC-level environment having to opt out - Once integrated, verification effort shifts from an interface's own protocol corners to cross-block application scenarios - concurrent access, address decode, and interrupt routing between blocks
- Coverage and closure shift the same way: reused passive monitors still feed protocol covergroups as a regression health check, but the coverage actively chased at SoC level is built from cross-block scenario bins, often checked through each block's own reused scoreboard plus a thinner cross-block checker on top