You fix a timeout bug in the stream VIP, rename a badly named config field while you are there, and push the release. The next morning three projects report regressions. One set the old field name through config_db, which now matches nothing, so their agents run on defaults. One compiles with a UVM 1.2 library and your new code used a call that only exists in the IEEE 1800.2 library. The third is fine, but cannot tell from anything in the log which VIP version it is running. A VIP release is a contract with people you will never meet. How do you change it without breaking them?
A version number is a promise about compatibility. With semantic versioning, a major release may break the API and comes with migration notes; a minor release adds features without breaking anything; a patch fixes behaviour without changing the interface at all. Consumers can then upgrade patches without reading anything, minors with a glance at the changelog, and majors on purpose. Renaming a config field is a breaking change, so either it waits for a major, or it ships with a shim that keeps the old name working for a while.
Think of a public library moving a shelf. You do not simply move the books and let readers find empty space; you leave a sign at the old spot pointing to the new one, and take it down only after everyone has had time to learn the new place.
Step | What it contains | What it protects against |
|---|---|---|
Version constant |
| Not knowing which VIP a failing log used |
Changelog entry | Every change, tagged major, minor or patch | Surprise behaviour changes |
Deprecation shim | Old names still work, warn once, removal date stated | Silent breakage of consumer configs |
Compatibility checks | Library version checks, required-version checks | Compiling against an unsupported UVM or VIP |
Qualification run | Smoke, feature and compatibility tiers on every supported simulator | Shipping something only your bench exercises |
Delivery bundle | Sources (or encrypted sources), filelist, user guide, examples | Consumers guessing how to compile and configure |
The renamed field's old name stays in the config object for one release. The shim must notice that a consumer set it, warn once (not every cycle), and use it. A shim that only exists, and is never consulted, is a silent break with extra steps.
// snippet
class axis_cfg extends uvm_object;
`uvm_object_utils(axis_cfg)
int unsigned ready_timeout = 1000; // the new name
int unsigned rdy_tmo = 0; // DEPRECATED in 2.3, removed in 3.0; 0 means unset
local bit warned;
function new(string name = "axis_cfg");
super.new(name);
endfunction
virtual function int unsigned effective_ready_timeout();
if (rdy_tmo != 0) begin // an old consumer set the old name
if (!warned) begin
`uvm_warning("AXIS_DEPRECATED", "rdy_tmo is deprecated and removed in 3.0: set ready_timeout")
warned = 1;
end
return rdy_tmo;
end
return ready_timeout;
endfunction
endclassThe agent reads effective_ready_timeout(), never the fields directly. The model below runs the same logic with no UVM library and checks the three cases: the default, a consumer using the new name, and a consumer using the old one, which must warn exactly once.
class axis_cfg_model;
int unsigned ready_timeout = 1000;
int unsigned rdy_tmo = 0; // deprecated name; 0 means unset
int unsigned warnings = 0;
function int unsigned effective_ready_timeout();
if (rdy_tmo != 0) begin
if (warnings == 0) warnings++; // warn once
return rdy_tmo;
end
return ready_timeout;
endfunction
endclass
module tb_deprecation_shim;
axis_cfg_model c;
int bad = 0;
initial begin
c = new();
if (c.effective_ready_timeout() != 1000) bad++; // default
c.ready_timeout = 2000;
if (c.effective_ready_timeout() != 2000) bad++; // a consumer on the new name
c.rdy_tmo = 500;
if (c.effective_ready_timeout() != 500) bad++; // a consumer on the old name
if (c.effective_ready_timeout() != 500) bad++; // still routed
if (c.warnings != 1) bad++; // but warned only once
if (bad == 0) $display("PASS: old name routed, new name honoured, one warning");
else $display("FAIL: %0d shim checks failed", bad);
$finish;
end
endmoduleThe package carries a version constant, and the agent prints it once at build. When a consumer's log arrives with a bug report, the first line you look for says which VIP they ran. The same place checks the library: code that differs between UVM 1.2 and the IEEE 1800.2 reference library is selected at compile time with the library's own defines. The 1800.2 library defines UVM_VERSION_POST_2017; UVM 1.2 does not, and defines UVM_VERSION_1_2 instead.
// snippet: in axis_vip_pkg
localparam string AXIS_VIP_VERSION = "2.3.1";
// In axis_agent::build_phase, once:
`uvm_info("AXIS_VIP", $sformatf("axis VIP %s on %s", AXIS_VIP_VERSION, uvm_revision_string()), UVM_LOW)
`ifdef UVM_VERSION_POST_2017
// IEEE 1800.2 library only
`else
// UVM 1.2 path
`endifA consumer env that needs a feature added in a minor release can check the constant in its own build and fail with a clear message ("needs axis VIP 2.2 or later") instead of failing later with a missing method.
Commercial VIP is often shipped encrypted with the IEEE 1735 protection pragmas, so consumers can compile and simulate it but not read it. That changes what you owe them: they cannot debug inside your code, so your messages, your print output and your user guide carry the whole load. Whatever the delivery, publish the matrix you actually qualify: simulators and their versions, UVM library versions, and any compile options the VIP needs. A combination not in the matrix is not supported, however likely it is to work.
Tier | What it runs | What it proves |
|---|---|---|
Smoke | Active, passive and mixed topologies, one test each | The build shapes are right; passive drives nothing |
Feature | One test per knob, at its edges | Every documented option does what the guide says |
Compatibility | Configurations from previous releases, unchanged | Old consumer code still builds and passes, with deprecation warnings only |
Matrix | The smoke tier on every simulator and UVM library in the matrix | The support claim is true |
A release ships when every tier passes and the changelog, version constant and user guide agree. That list is a checklist with a sign-off, not a feeling of readiness.
The model above is compiled and run by the course's checker. A correct shim prints one line:
PASS: old name routed, new name honoured, one warningDelete the warnings == 0 guard and the last check fails (one warning per call); return ready_timeout unconditionally and the old-name checks fail. Those two mistakes are the ones real shims ship with.
A VIP team renamed rdy_tmo to ready_timeout in a minor release, without a shim, judging it too small to matter. Consumers that set the field through the config object got a compile error, which was annoying but honest. Consumers that set it as a separate config_db entry, an older style many still used, got nothing at all: the set with the old name simply matched no get. Their agents ran with the default timeout, and long-latency ports started failing with timeout errors.
To the consumers it looked exactly like a functional regression in the new VIP. Three teams filed bugs against the timeout logic before anyone diffed the config field names. The VIP team re-released with a shim that also read the old config_db key and warned, and added a compatibility tier that runs the previous release's example configurations unchanged. That tier would have caught the rename the day it was made.
rdy_tmo and, in the same test, the new ready_timeout. Which value does the shim above use, and is that the right policy?rdy_tmo, because any non-zero old value wins. That keeps old consumers working unchanged, but it surprises a consumer half-way through migrating. A stricter shim reports an error when both are set, which forces the conflict to be resolved; whichever policy you choose, write it in the deprecation note.
The smoke tier, in the passive topology. It is the cheapest tier and runs first, and an agent that drives anything in passive mode fails it before any feature or compatibility test runs.
Next: the VIP is reusable and releasable. Error and fault injection uses its hooks to make the DUT's error paths run on purpose.