Skip to content

UVM

Reusable Verification Architecture

Building self-contained, configurable, position-independent agents and environments — config objects, the factory, and block-to-SoC composition — so verification IP reuses everywhere.

UVM Testbench Architecture · Module 3 · Page 3.5

The Engineering Problem

Reuse is the entire reason UVM exists (Module 1.9), and you now have the pieces — a named hierarchy (3.3) and a wired data-flow graph (3.4). But reuse does not happen automatically because you used UVM. An agent or environment is reusable only if it was designed to be: self-contained, configurable, and dependent on nothing specific to where it currently sits. Build it with one hard-coded assumption — an absolute path, a fixed mode, a baked-in type — and it works beautifully standalone and breaks the moment it is reused somewhere else.

This is the gap between "I used UVM" and "my verification IP is reusable." A block-level environment that verifies a UART perfectly is worthless at the SoC level if it assumes it is the top of the tree, or hard-codes the one configuration it was built for. The discipline of reusable architecture is a set of concrete rules — configure from above, depend on nothing absolute, parametrise, substitute via the factory — that make a unit position-independent and context-independent, so it drops into the next block, the SoC, and the next project unchanged.

What design discipline makes an agent or environment genuinely reusable — across blocks, SoC integration, and projects — rather than only working in the one place it was first built?

Motivation — why reuse is a design property, not a freebie

Reusability has to be engineered in; the payoff and the cost of skipping it are both large:

  • Reuse is the dominant cost lever. A verified agent reused across ten blocks is ten environments you didn't rebuild; an env reused at SoC is an integration you didn't re-verify from scratch. The economics of verification (Module 1.3) are dominated by what you can reuse — and that is decided by architecture.
  • "Works standalone" is not "reusable." The most expensive surprises happen at integration: a block env that passed in isolation breaks when nested in an SoC because it assumed it was the top. Designing for reuse moves that failure from integration day (expensive) to never.
  • Configurability is what makes one unit serve many contexts. The same agent must drive at block level and merely observe at SoC; the same env must run with different sizes, modes, and protocols. Configuration — from the top, via config objects and the config database — is the mechanism that lets one component behave differently without being edited.
  • Position-independence is what makes composition possible. An env reused inside another env sits at a different path. Only a unit that makes no absolute-path assumptions can be relocated in the tree, which is exactly what block→SoC composition requires.

The motivation, in one line: reuse is the whole point of UVM but it is a design property — earned by building self-contained, configurable, position-independent units, and lost by a single hard-coded assumption.

Mental Model

Hold reusable IP as a sealed appliance with a settings panel:

A reusable unit is a sealed appliance: it exposes a settings panel and a power plug, and assumes nothing about the house it's installed in. Inside, it is self-contained — it does not reach out and grab wires from the walls, hard-code the address of the room it's in, or assume it's the only appliance. Everything it needs to adapt — its mode (active/passive), its interface, its sizes and types — comes in through the settings panel (configuration from above), not by reaching into its surroundings. Because it makes no assumptions about its location or neighbours, you can install it in this house (block), that house (another block), or a mansion full of other appliances (SoC) without rewiring it. The moment an appliance hard-codes "I am in the kitchen at 123 Main Street," it can only ever work in that one kitchen.

So the test of reusability is always: does this unit reach out and assume something specific, or does everything it needs arrive through its settings panel? Configure-from-above and assume-nothing-absolute are the two halves of the same discipline.

Visual Explanation — the levels of reuse

Reuse is not one thing; it happens at every level of the architecture, each unit reusable in a wider context than the one below it.

Levels of reuse: transaction across tests, agent across envs, environment across block and SoCReuse units, from transaction to SoC environmentReuse units, from transaction to SoC environmentTransaction — reused across teststhe stable data unit; sequences and checkers all speak it (Module 3.2)the stable data unit; sequences and checkers all speak it (Module 3.2)Agent — the fundamental reuse unitsequencer + driver + monitor for one interface; self-contained, active/passive configurablesequencer + driver + monitor for one interface; self-contained, active/passive configurableEnvironment — reused block → SoCa container reused standalone and as a sub-env inside a larger env-of-envsa container reused standalone and as a sub-env inside a larger env-of-envsSoC environment — compositionmany block envs and agents composed; coordination via virtual sequencesmany block envs and agents composed; coordination via virtual sequences
Figure 1 — the levels of reusable verification IP. The transaction is reused across tests and sequences (the stable data unit). The agent — sequencer, driver, monitor for one interface — is the fundamental reuse unit, reused across environments and blocks. The environment is reused block→SoC as a sub-env (an env-of-envs). Reusable sequences and a config layer ride alongside. Each level is a self-contained, configurable unit reusable in a wider context than the one beneath it.

The levels share one property: each is a self-contained, configurable unit reusable in a wider scope. The transaction is reused by every test and sequence. The agent is the workhorse of reuse — package an interface's sequencer, driver, and monitor once, and it drops into any environment needing that interface. The environment is reused twice over: standalone to verify a block, and nested as a sub-env when that block becomes part of an SoC. And the SoC environment is composition — many reusable envs and agents assembled, coordinated from above. Designing each level to be self-contained is what lets the level above simply compose it rather than rebuild it.

RTL / Simulation Perspective — a reusable agent

The discipline is concrete in code: a reusable agent takes everything it needs through a config object delivered from above, and assumes nothing — its mode, its interface, and its parameters all arrive via configuration.

a reusable agent — configured from above, no hard dependencies
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class agent_cfg extends uvm_object;            // config object: all the agent needs, in one place
  `uvm_object_utils(agent_cfg)
  uvm_active_passive_enum is_active = UVM_ACTIVE;  // mode — set per context
  virtual my_if            vif;                     // interface — supplied from the top
  int unsigned             max_len = 16;            // a parameter — not hard-coded inside
endclass
 
class my_agent extends uvm_agent;
  `uvm_component_utils(my_agent)
  agent_cfg cfg;  my_sequencer sqr;  my_driver drv;  my_monitor mon;
 
  function void build_phase(uvm_phase phase);
    if (!uvm_config_db#(agent_cfg)::get(this, "", "cfg", cfg))   // everything arrives via config
      `uvm_fatal("NOCFG", "agent_cfg not provided from above")
    mon = my_monitor::type_id::create("mon", this);             // monitor: always (observe always)
    if (cfg.is_active == UVM_ACTIVE) begin                       // driver/sequencer: configurable
      sqr = my_sequencer::type_id::create("sqr", this);
      drv = my_driver   ::type_id::create("drv", this);
    end
  endfunction
endclass

Every reuse principle is visible. The agent depends on nothing specific: it doesn't hard-code its mode, its interface, or max_len — they come from agent_cfg, set by whatever instantiates the agent. It is configurable from above: a block-level test sets is_active = UVM_ACTIVE; an SoC-level test reuses the same agent with is_active = UVM_PASSIVE, and the agent builds the driver only when active. It uses relative config scoping (get(this, "", "cfg", ...) is relative to the agent itself), so it works at any path. Bundling everything into one agent_cfg object is the pattern: the settings panel is a single object handed in from the top, and the agent reads it rather than reaching out for anything.

Verification Perspective — configuration and the factory make units adaptable

Two mechanisms turn a self-contained unit into an adaptable one without editing it: configuration (the config database carrying config objects from the top down) and the factory (substituting a derived type for a base type across the whole environment). Together they let one architecture serve many contexts.

Test configures units via config database and overrides types via the factory; reusable units read config and are created by the factoryset config objectstypeoverridesget: adapt(mode/size/vif)create derivedtypeTest (the top)configures + overrides percontextConfig databasecarries config objects downFactorysubstitutes typesReusable unit (agent/env)reads config; created via factory12
Figure 2 — the two mechanisms that make a unit adaptable without editing it. Configuration flows down from the test through the config database: each reusable unit reads its config object and adapts (mode, interface, sizes). The factory lets a test substitute a derived type for a base type everywhere it is created, specialising behaviour without touching the environment. A reusable unit exposes itself to both — it reads config and is created via the factory — so the same code is reconfigured and re-specialised per context.

These two mechanisms are how one unit serves many situations. Configuration adapts behaviour by data: the same agent runs active or passive, with a 16-beat or 256-beat max, depending on the config object handed to it — no code change. The factory adapts behaviour by type: a test can register an override so that everywhere the environment creates a my_driver, it instead creates an error_injecting_driver — specialising the environment for an error test without editing the env at all. A reusable unit is built to expose itself to both: it reads its config in build_phase and is created through the factory (type_id::create). That exposure is what lets the unchanged unit be reconfigured (data) and re-specialised (type) from the top for each new context.

Runtime / Execution Flow — block-to-SoC composition

The headline reuse is block→SoC: the same environment that verified a block standalone is nested as a sub-env inside an SoC environment, alongside other block envs, and coordinated from above. This only works if each block env is self-contained and position-independent.

SoC environment composed of block A env and block B env as sub-environments, coordinated by a virtual sequencecontains (reused)contains (reused)coordinatesdrivesdrivesSoC environmentcomposes block envsBlock A envreused unchanged(reconfigured)Block B envreused unchanged(reconfigured)Virtual sequencecoordinates across blocks12
Figure 3 — block-to-SoC composition (env-of-envs). Each block environment (block A's env, block B's env) was built to be self-contained and configurable. At the SoC level, they are instantiated unchanged as sub-environments of one SoC env, each reconfigured for its SoC role (often the driving agents made passive where the real RTL now drives), and coordinated by a virtual sequence above them. Reusing the block envs as-is is possible only because they assume nothing about being the top of the tree.

This composition is the reuse payoff made concrete, and it is entirely dependent on the block envs being self-contained. The SoC env instantiates block A's env and block B's env exactly as written, reconfigures them for the SoC context (typically making their agents passive where actual RTL now drives those interfaces, and adjusting sizes/modes via config), and adds a virtual sequence on top to coordinate stimulus across them. Nothing inside the block envs is edited. But this only works because each block env reads its configuration from above and makes no absolute-path or top-of-tree assumptions — relocate a unit that hard-codes its position and the composition breaks, which is exactly the DebugLab below.

Waveform Perspective — one agent, two roles by configuration

The clearest runtime signature of reuse is the same agent serving opposite roles purely by configuration: active (it drives and observes) at block level, passive (it only observes) at SoC level, where real RTL drives the interface.

The same agent, reused active then passive — configuration alone changes its role

10 cycles
The same agent, reused active then passive — configuration alone changes its roleActive reuse: the agent (UVM_ACTIVE) drives AND observes these pinsActive reuse: the agen…Passive reuse: the SAME agent (UVM_PASSIVE) only observes — other RTL drivesPassive reuse: the SAM…one agent, two roles by configuration: monitor always observes, driver is contextualone agent, two roles b…clkvaliddata00A0A100B0B100000000agent_drivesagent_observest0t1t2t3t4t5t6t7t8t9
Figure 4 — in the active context the agent (configured UVM_ACTIVE) drives the pins and observes them (agent_drives high). Reused in the SoC context as UVM_PASSIVE, the same agent only observes — another block drives the interface — so agent_drives is low while the agent still observes. The monitor always observes (observation is always needed); the driver is contextual (built only when active). One agent, two roles, decided entirely by the config object handed in from above — no code change.

The contrast between agent_drives (on in the active context, off in the passive) and agent_observes (on in both) is reuse made visible. The same agent code, the same monitor, the same transaction model — only the is_active field of the config object differs between the two contexts, and that single setting decides whether the driver is built at all. This is the concrete realisation of "configure from above": a reusable unit exposes its variability as configuration, and the context (block test vs SoC test) supplies the values, so one unit covers both roles without an edit. Observation is constant because you always need it; driving is configurable because it is contextual.

DebugLab — the env that passed alone but broke inside the SoC

A block env that worked standalone and failed on integration — hard-coded absolute paths

Symptom

A UART block environment verified the UART perfectly in standalone regressions for months. When it was reused as a sub-environment inside the SoC environment — with no change to the block env's code — its agents stopped receiving their configuration: the virtual interface and mode settings silently didn't apply, drivers came up unconfigured, and lookups returned null. The exact same env, reused one level deeper, broke.

Root cause

Hard-coded absolute hierarchical paths. The block env (and its test) configured its agents using absolute paths that were only correct when the env sat at the top. Standalone, the agent's path was uvm_test_top.env.agent; nested in the SoC, the same env became a sub-env and its agent moved to uvm_test_top.soc_env.uart_env.agent — so every absolute-path config silently missed:

why the same env broke one level deeper
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
standalone path:  uvm_test_top.env.agent           ← absolute config scope matched here
SoC-nested path:  uvm_test_top.soc_env.uart_env.agent   ← path is now deeper
config used:      set(null, "uvm_test_top.env.agent", "cfg", cfg);   ← ABSOLUTE, hard-coded
result:           scope no longer matches the relocated agent → config silently not applied
fix:              set relative to `this`:  set(this, "agent", "cfg", cfg);  ← position-independent

Nothing in the block env changed — but its assumption about its position in the tree was wrong the moment it was nested. The unit was not position-independent, so it could only ever work as the top.

Diagnosis

The tell is a unit that works standalone and breaks when nested — almost always a position assumption. Diagnose reuse failures by hunting absolute dependencies:

  1. Search for absolute hierarchical paths. Any config scope or lookup that names uvm_test_top... or a fixed full path is position-dependent and breaks under nesting. Reusable units scope relative to this.
  2. Compare get_full_name() standalone vs nested. Print the component's path in both contexts; if config was scoped to the standalone path, the deeper nested path won't match — that's the silent miss.
  3. Look for top-of-tree assumptions. Anything that assumes it is directly under uvm_test_top, or that there is exactly one of it, breaks when composed. Self-contained units assume neither their depth nor their multiplicity.
Prevention

A reusable unit must be position-independent and assume nothing absolute:

  1. Scope configuration relative to this. Use set(this, "relative.path", ...) and get(this, "", ...) so a unit's configuration follows it wherever it is nested. Never hard-code an absolute hierarchical path.
  2. Pass everything through config objects from above. The interface, mode, and parameters arrive via a config object the parent supplies — the unit reaches out for nothing, so it doesn't matter where it sits.
  3. Test reuse early. Instantiate the env both standalone and nested (under an extra dummy parent) during development; a unit that only works at the top reveals itself immediately rather than on integration day.

The one-sentence lesson: a unit that hard-codes its absolute position works standalone and breaks when reused one level deeper — keep configuration relative to this and supply everything from above, so the unit is position-independent and composes anywhere.

Common Mistakes

  • Hard-coding absolute hierarchical paths. Config scopes or lookups naming a fixed full path break the instant the unit is nested. Scope relative to this so the unit is position-independent.
  • Baking in configuration instead of taking it from above. A unit that hard-codes its mode, interface, or sizes can serve only the one context it was built for. Take everything through a config object supplied by the parent.
  • Assuming top-of-tree or singularity. Code that assumes it is directly under uvm_test_top, or that there is only one instance, breaks under composition. Reusable units assume neither their depth nor their count.
  • Reaching out instead of being given. A component that hard-references a sibling or grabs a wire by hierarchical path couples itself to one structure. It should receive handles/interfaces through configuration, not reach for them.
  • Not designing the agent as the reuse unit. Spreading an interface's logic across the env instead of encapsulating it in a self-contained, active/passive agent prevents reuse of that interface elsewhere. The agent is the unit; keep it whole and configurable.
  • Skipping factory-friendliness. Creating components with new instead of type_id::create blocks factory overrides, so the env can't be specialised (e.g., an error-injecting driver) without editing it. Always create via the factory.

Senior Design Review Notes

Interview Insights

Three properties, all designed in. First, it must be self-contained: it encapsulates everything for its job (an agent bundles the sequencer, driver, and monitor for one interface) and doesn't spread its logic across the surrounding environment. Second, it must be configurable from above: everything that varies by context — mode (active/passive), the virtual interface, sizes, parameters, even component types — arrives through a config object and the config database supplied by the parent, rather than being hard-coded inside. Third, it must be position-independent: it makes no absolute-path or top-of-tree assumptions, scoping its configuration relative to this so it works at any depth in the tree. A unit with these properties drops into a new block, into an SoC as a sub-env, or into the next project unchanged. A unit missing any of them — one hard-coded path, one baked-in mode, one assumption that it's the top — works standalone and breaks the moment it's reused, which is why reusability is a design property rather than an automatic benefit of using UVM.

Exercises

  1. Spot the reuse blocker. For each, say whether it harms reuse and how to fix it: (a) uvm_config_db::set(null, "uvm_test_top.env.agent", "cfg", cfg); (b) an agent that hard-codes is_active = UVM_ACTIVE; (c) a driver created with new("drv", this); (d) a scoreboard that looks up a sibling by relative path.
  2. Design the config. Write (in prose) the config object for a reusable bus agent: list the fields it should carry so the agent needs to assume nothing, and explain who sets it and when.
  3. Compose to SoC. Describe how you would reuse two block envs inside an SoC env: what you instantiate, what you reconfigure (and to what), and what you add on top — and state the one property of the block envs that makes this possible.
  4. Make it position-independent. Given an env whose test configures its agent with an absolute path, rewrite the configuration to be relative to this, and explain why this fixes the standalone-passes-but-nested-fails bug.

Summary

  • Reuse is a design property, not an automatic benefit of UVM. A unit is reusable only if it is self-contained, configurable from above, and position-independent — and a single hard-coded assumption makes it work standalone yet break on reuse.
  • Reuse happens at every level: the transaction (across tests), the agent (the fundamental unit — self-contained and active/passive configurable), and the environment (reused block→SoC as a sub-env in an env-of-envs).
  • Configuration (config objects via the config database, scoped relative to this) adapts a unit by data, and the factory (type_id::create + overrides) adapts it by type — together letting one unchanged unit serve many contexts.
  • Block→SoC composition reuses block envs unchanged as sub-envs, reconfigured (often active→passive) and coordinated by a virtual sequence — possible only because each env is self-contained and makes no absolute-path or top-of-tree assumptions.
  • The durable rule of thumb: build sealed appliances with a settings panel — configure from above, scope relative to this, create via the factory, and assume nothing about where you sit — and test reuse by nesting, because "works standalone" is not "reusable."

Next — Layered Testbenches: reusable units compose into layers. The next chapter looks at how a UVM testbench is organised into abstraction layers — signal, command/transaction, functional, and scenario/test — and how each layer builds on the one below, so stimulus and checking are expressed at the right altitude.