An APB agent written for one block in 2019 was copied into nine projects over four years. Each copy was edited a little: a timeout here, a wider data bus there, a fix for a protocol corner case in one project that the other eight never received. When a tenth project asked which copy to take, nobody could say which one was correct, and three of them reported the same protocol error with three different message IDs.
This lesson is about the step from "an agent that works in my bench" to verification IP: a packaged, versioned, regressed and documented product that many projects adopt without forking. Integrating someone else's VIP into your env is covered in Integrating Third-Party VIP, and semantic versions and deprecation shims in Versioning, Delivery and Release Readiness; this lesson is the producer's side at the scale of many projects.
Software teams stopped copying library source between projects long ago. They depend on a named, versioned package, they read its documented interface rather than its code, and they upgrade on purpose. VIP deserves the same treatment, because a testbench agent is a library: its configuration object is its API, its message IDs are its error codes, and its behaviour is a contract that every consumer's tests depend on.
Four things make that work: a configuration contract that says what can be set and checks it, a version that says what changed, a regression of the VIP itself that runs before every release, and documentation good enough that a new project integrates it without reading the source.
One SystemVerilog package per VIP, including every class file, plus the interfaces outside it (interfaces cannot live in packages). Ship a file list, so consumers compile it the same way you do.
Prefix everything: classes, macros, typedefs, event-pool names, message IDs. Two VIPs that both define cfg_t, DATA_W or an event called "irq" collide only at SoC level, where both meet for the first time.
No global side effects. The VIP never installs a report server, sets the global timeout, changes verbosity or actions on uvm_root, or registers type overrides. Those are the test's decisions, and at SoC level several VIPs making them means the last one silently wins.
Declare the version in code, for example a localparam string APB_VIP_VERSION = "2.4.0"; in the package, printed at UVM_LOW in the agent's build_phase, so every log says which VIP produced it.
The config object is the VIP's API. Every field is documented, has a safe default, and is validated in one place that the agent calls before it builds anything. The bench's agents keep their knobs simple (is_active through config_db, a job object for the test), which is right for one block; a VIP shared by many projects writes the contract down:
// snippet: a VIP configuration object with a validated contract
class apb_vip_cfg extends uvm_object;
`uvm_object_utils(apb_vip_cfg)
uvm_active_passive_enum is_active = UVM_ACTIVE;
int unsigned addr_width = 32; // 8..32
int unsigned data_width = 32; // 8, 16 or 32
int unsigned max_wait = 16; // PREADY wait states before PROTO_TIMEOUT
bit has_checker = 1; // protocol assertions in the interface
bit has_coverage = 0; // off by default: costs time at SoC scale
function new(string name = "apb_vip_cfg");
super.new(name);
endfunction
// Called by the agent in build_phase; every violation is a uvm_error with one ID.
function bit validate(uvm_component ctx);
validate = 1;
if (!(data_width inside {8, 16, 32})) begin
ctx.uvm_report_error("APB_CFG", $sformatf("data_width=%0d: must be 8, 16 or 32", data_width));
validate = 0;
end
if (addr_width < 8 || addr_width > 32) begin
ctx.uvm_report_error("APB_CFG", $sformatf("addr_width=%0d: must be 8..32", addr_width));
validate = 0;
end
if (is_active == UVM_PASSIVE && max_wait == 0)
ctx.uvm_report_warning("APB_CFG", "max_wait=0 in passive mode disables the PREADY timeout check");
endfunction
endclassCompile-time shape (bus widths) and run-time behaviour (timeouts, checkers) are different kinds of contract; which belongs where is the subject of VIP Parameterization. Whatever the split, a consumer must never need to set an undocumented config_db key to make the VIP work.
A VIP is regressed against itself before any project sees a release. The standard shape is a loopback bench: an active instance of the agent drives an interface, a passive instance of the same agent monitors it, and a small responder or reference model plays the other side. That checks the driver against the monitor, which is the pairing every consumer relies on.
Test class | What it proves |
|---|---|
Loopback, all modes | Driver and monitor agree on every transfer type, width and wait-state count the contract allows. |
Error injection with expected errors | The protocol checker fires on each violation, with the documented ID. Each test expects its error and fails if it does not appear (checking expected faults). |
Configuration sweep | Every documented config value builds and runs; every invalid value is rejected by |
Library and tool matrix | The same tests on each supported simulator and on UVM 1.2 and IEEE 1800.2, since consumers differ. |
Coverage of the VIP's features | Every transfer type, response and mode in the contract was exercised, so a release is not "tested" in name only. |
A ten-line quick start: compile, instantiate the interface, set the config, create the agent, connect the analysis port.
A configuration reference with every field, its default, its legal range and the version that added it.
A sequence catalogue: what each provided sequence does and which fields it randomizes.
A message ID catalogue, with severity and meaning. Consumers write waivers and regression buckets against IDs; renaming one is a breaking change.
Known issues with ticket numbers, and release notes per version that say what a consumer must change.
Most VIP failures at scale are not bugs in the protocol logic. They are assumptions that held in the first bench and do not hold in someone else's:
Failure at SoC level | Assumption that broke | Where it is covered |
|---|---|---|
The agent drives a bus that the SoC's real master drives | Active by default, never tested passive | |
config_db lookups find nothing one level deeper | Absolute paths such as | |
Elaboration error: no such scope | Hierarchical references into the DUT | Lesson 1 of this topic |
Two VIPs' classes or events collide | Unprefixed names and global pool keys | Packaging, above |
The SoC test's verdict line disappears | The VIP installed its own report server | |
Tests end with traffic still in flight | End of test owned by the block test | |
Hangs or false errors after a mid-test reset | Single reset at time zero | |
Regression time triples with 40 instances | Field macros, recording and per-beat strings on by default |
The integration side of the same list, adapting a vendor's transactions, mapping its configuration and fencing its messages, is in Integrating Third-Party VIP. The bench's own proof that reuse can work is Vertical reuse: dma_env used twice, unchanged, inside dma_subsys_env, with one input agent switched to passive.
A team packaged their AXI VIP with a convenience: its agent's build_phase installed a report server that printed a coloured summary. In block benches nobody noticed. At SoC level, the integration test installed its own verdict server in its constructor, as in the bench's install_verdict_server(), and then the AXI agents were built and installed theirs. The last call to set_server() won. The SoC regression, which keyed off the TEST VERDICT line, saw no line on any test and reported the whole nightly run as failed.
The run was not wrong, and the fix was not in the SoC bench. The VIP removed the server install and documented a rule that it now checks in its own regression: the VIP touches no global state. Its coloured summary became an optional function the test can call. The general lesson is the one this topic keeps returning to: in reusable code, every global setting is a decision taken on behalf of a bench you have never seen.
APB_TIMEOUT to APB_PREADY_TIMEOUT to be clearer. Why is that a major version change for consumers?Consumers' waivers, report catchers, verbosity settings and regression buckets match the old ID. After the rename, waived issues fail again and failure triage groups break. A message ID is part of the VIP's interface; renaming it is a breaking change, or needs a deprecation period.
It checks that the driver produces exactly what the monitor reconstructs, for every mode the contract allows, which is the pairing every consumer relies on (consumers drive with one and check with the other, or use only the passive monitor at SoC level). It needs no DUT, so it runs on every release and every supported tool.
uvm_event_pool::get_global("irq"). What happens, and what packaging rule prevents it?They share one event: each VIP's waiters wake on the other's interrupts, which shows up as spurious or missed interrupt handling. Prefixing every global name with the VIP's prefix (apb_vip_irq), or keeping events in the VIP's config object instead of the global pool, prevents it.
Next: this completes Testbenches at Scale. The Advanced UVM Topics hub lists the other advanced topics.