Skip to main content

Configuration and Reuse

Every component across this topic has been built once, for one test. Real verification needs many tests reusing the same environment with different settings — this page looks honestly at what's actually available to do that, with nothing but plain SystemVerilog, and where each option gets uncomfortable.

Simple knobs: constructor arguments work fine​

The Environment already does this correctly — num_txns flows in through the constructor, so a smoke test and a long regression run just construct the environment differently:

environment env_smoke = new(fifo_if_inst, 20);
environment env_regression = new(fifo_if_inst, 2000);

For a single scalar knob known at construction time, this is genuinely fine — no complaint here.

An ad-hoc alternative: a global config class​

Some hand-rolled testbenches reach for a different pattern instead — a plain class with static properties, read directly by whichever component cares:

class tb_config;
static int num_txns = 20;
static bit inject_errors = 0;
endclass
task generator::run();
repeat (tb_config::num_txns) begin
// ...
end
endtask

This avoids threading a value through every constructor — but it's worth naming exactly what it costs: every component that reads tb_config::num_txns sees the identical value, globally, with no way to configure just one instance differently from another. And structurally, this is nothing new — it's the singleton pattern UVM's own curriculum names directly, just applied here to configuration data instead of a phase or a printer object, with every downside that page describes: no hierarchy, no scoping, one shared value everywhere.

The real problem: swapping a component's behavior for one test​

Say one test needs a driver that occasionally corrupts write data — without touching the driver class every other test still uses. Two options exist, and neither is comfortable:

Option A — edit environment's constructor directly, hardcoding which driver class gets built for this one test:

function new(virtual fifo_if vif, int num_txns, bit use_error_driver = 0);
// ...
if (use_error_driver) begin
driver_error_inject err_drv = new(vif, gen2drv);
drv = err_drv; // upcast: driver_error_inject extends driver
end else begin
drv = new(vif, gen2drv);
end
// ...
endfunction

This works, but it means environment — the one class every test is supposed to share, unmodified — now has to know about every driver variant any test will ever need, forever, edited again each time a new one shows up.

Option B — construct the driver outside the environment and pass it in. This sounds cleaner, until the actual dependency shows up: the driver needs gen2drv, the mailbox environment's own constructor creates. Passing a pre-built driver in from outside means either creating that mailbox outside the environment too (undoing the "environment owns all construction" property The Environment was built around), or accepting an awkward two-step construction the rest of this topic's environment doesn't need.

Neither option is wrong, exactly — both are genuinely what hand-rolled testbenches do in practice. But both cost something real: Option A means editing shared code per test; Option B means breaking encapsulation the environment was specifically built to have.

What's next​

Three real friction points now exist, named honestly rather than glossed over: a global config class with no scoping, and a component-swap problem with no comfortable answer. The next page takes stock of exactly these, and where every one of them already has a real, shipped solution elsewhere on this site.