Reusable Verification Components
fifo_agent and the PWM's reg_bus_agent were both written for one specific testbench in this curriculum, and both happened to work — but "works in the one environment it was written for" and "safe for someone else to drop into a different project" are genuinely different bars. A verification component (VC) — an agent plus the sequence library and transaction that go with it — meant for reuse across projects or teams needs discipline beyond correctness. This page covers what that discipline actually is; the next page covers the other half: how a reusable VC gets consumed once it's built this way.
Transaction discipline: protocol data only, nothing project-specific
A transaction class like fifo_txn should describe the protocol — what a legal write or read looks like — and nothing about how any particular project happens to use it. A project-specific constant hardcoded into the transaction (an address range only one specific SoC uses, a constraint only one specific test scenario needs) quietly ties a supposedly generic VC to the one project it was extracted from. Where something is genuinely variable across projects — data width, address width — parameterizing the class (the same class fifo #(type T) mechanic from SystemVerilog's Static and Parameterized Classes) keeps it reusable instead of forked per project.
Agent discipline: standalone, not tree-shaped around one environment
A reusable agent has to work correctly dropped into any environment, not just the one it was designed alongside. Two concrete implications:
- No hardcoded assumptions about sibling components. An agent's driver or monitor should never reach across the hierarchy to directly access another component by absolute or assumed relative path — it should only know about what's configured into it via
uvm_config_db, the same mechanism Resource DB & Config DB covered. A driver that assumes"uvm_test_top.env.scoreboard"exists at that exact path breaks the moment it's instantiated inside a larger environment with a different shape. - Active/passive still has to work in isolation. The Agent & Environment's
is_activeguard already does this correctly — a passive instance of the agent should be fully usable on its own, monitoring traffic in an environment that has nothing to do with driving it.
Sequence library discipline: protocol scenarios, not test scenarios
A reusable VC's sequence library should stay protocol-focused — generic read/write sequences, burst patterns, error-injection variants, anything that's a property of the protocol rather than of one specific test's story. Sequences that compose several agents together to enact one particular test scenario — a virtual sequence, per Virtual Sequences & Sequencers — belong in the environment or test that's using the VC, not inside the VC itself. Bundling scenario-specific orchestration into the VC's own sequence library ties it to one project's test plan instead of leaving it generically usable by the next one.
Factory discipline: the requirement that's invisible until someone else needs it
Every class in a reusable VC has to use `uvm_component_utils/ `uvm_object_utils — covered as early as Utility & Field Macros — but for reusable VCs specifically, this isn't a formality. It's what lets a downstream user override any piece of the VC via The UVM Factory without editing the VC's source at all.
create() call breaks reusability quietly, not loudlyIf any class inside a VC calls new() directly instead of type_id::create() anywhere in its own internals — building a helper object, say — that one spot becomes un-overridable by anyone reusing the VC, and nothing about the VC's own testbench will ever reveal it, since it never needed to override that spot itself. The break only surfaces on someone else's project, when their factory override silently doesn't take effect for exactly that one piece — a genuinely hard bug to trace back to "a new() three files deep inside a VC nobody on this project wrote."
Callback discipline: hooks for behavior the VC's author couldn't anticipate
Factory overrides solve "replace this whole class with a different one." A narrower, more common need is "run one extra bit of logic at a specific point inside an otherwise-unmodified component" — inject an error on a specific transaction, log something project-specific right after a driver sends data, without forking the driver's source to do it. UVM's callback mechanism — Callbacks, Events & Pools covers the full `uvm_register_cb/ `uvm_do_callbacks mechanics — exists exactly for this: a reusable VC's author defines named callback hook points — empty virtual methods with descriptive names — at the exact spots in its driver or monitor where a downstream user might reasonably want to intervene, and a project using the VC subclasses uvm_callback, overrides just that hook, and registers an instance, without touching the VC's own source at all.
This is the discipline this page's other sections have been building toward from a different angle: a VC that's disciplined about transactions, agents, sequences, and the factory but exposes no callback hooks still forces every project-specific tweak into either editing the VC directly (breaking reuse for everyone else) or an awkward factory override of an entire class just to change one line of behavior inside it. Deciding where a VC's callback hooks belong is itself part of designing it for reuse — placed at points a downstream user is plausibly likely to need (before/after driving a transaction, on a protocol error, at the end of a sequence item), not scattered everywhere defensively.
Anti-patterns worth naming directly
- Hardcoded addresses or paths inside a driver or monitor — a register offset, an absolute hierarchical path — instead of receiving it through configuration. RAL Testbench Integration's adapter deliberately has no addresses in it at all; the register map owns every address, exactly so the bus-level driver underneath stays protocol-generic.
- Environment-specific checking embedded inside a protocol-level driver or monitor — a monitor that also happens to check DUT-specific data integrity is doing two jobs at once, and the second one doesn't belong to it at all (the next page covers exactly where it does belong).
- Skipping utility macros on "internal" helper classes because they feel too minor to matter — breaking factory overrides and
print()/compare()/copy()uniformly, for exactly the class someone reusing the VC is most likely to need one of those on.
What's next
This page covered what makes a VC safe to reuse in isolation. The next page covers the other side of the same discipline: how a reusable VC actually gets consumed by a larger environment, and where the line falls between what the VC owns and what the environment composing it owns instead.