Every configurable thing binds at one of three times. Compile time sets what structurally exists: widths, agent counts, sim-only code.
Run time selects among compiled behaviors per invocation: test name, seed, counts. Test time carries rich structured settings down through the environment.
Layer | Mechanism | Binds at | Change cost |
|---|---|---|---|
Structural | Parameter, define, +define+FLAG | Compile and elaborate | A recompile |
Invocation | +TESTNAME, +NUM_TXNS, +SEED | Time zero | A rerun only |
Test-internal | Config object built in the test | Build phase | A test edit |
Push every decision to the cheapest layer that can express it. Recompiling to change a verbosity is a smell.
When one setting arrives from several sources, fix the order once: command-line plusarg beats test override beats file value beats hard default. The command line wins because regression scripts reach for it last.
One central class implements the order: defaults in the declarations, an optional file load, a test-override hook, then a single plusarg parse as the final word. A test sets its overrides by extending the class, never after resolve(), or it would beat the plusarg. The class below has no top module, so it is not run in this course's checks.
// not-run: class-only listing with no top module to run (Verilator 5.048 compiles it)
class tb_config;
// Layer: hard defaults
int num_txns = 100;
int verbosity = 1;
bit en_scoreboard = 1;
string mem_image = "";
// Called once at time 0;
// precedence = default < file < test override < plusarg
function void resolve(string cfg_file = "");
if (cfg_file != "") load_file(cfg_file); // file layer
apply_test_overrides(); // test layer
void'($value$plusargs("NUM_TXNS=%d", num_txns)); // final word
void'($value$plusargs("VERBOSITY=%d", verbosity));
void'($value$plusargs("MEM_IMAGE=%s", mem_image));
if ($test$plusargs("NO_SCOREBOARD")) en_scoreboard = 0;
print();
endfunction
// A test extends tb_config and overrides this hook.
virtual function void apply_test_overrides();
endfunction
function void load_file(string fname);
int fd; string line, key; int val;
fd = $fopen(fname, "r");
if (fd == 0) return; // file is optional
while ($fgets(line, fd))
if ($sscanf(line, "%s %d", key, val) == 2)
case (key)
"num_txns": num_txns = val;
"verbosity": verbosity = val;
endcase
$fclose(fd);
endfunction
function void print();
$display("CFG: txns=%0d verb=%0d sb=%0b img=%s",
num_txns, verbosity, en_scoreboard, mem_image);
endfunction
endclassOnce resolved, the single object travels down the environment. Plain testbenches pass it through constructors or a package handle, and layered ones use a config database. Components read the object, and none of them touch the command line.
The classic failure is scattered plusargs: $value$plusargs calls buried in drivers, monitors and scoreboards. Nobody can list the knobs, two sites parse one knob with different defaults, and reproducing a run means grep archaeology.
Discoverability: one class lists every knob, instead of grep across the codebase.
Consistency: one default per knob, not one per call site drifting apart.
Reproducibility: the printed banner plus the seed fully describes the run.
Testability: unit checks construct and override the object directly, with no command line.
500 runs, which is wrong. Load order decided precedence by accident instead of by design. Parse the command line last so the plusarg wins.
Widths bind at compile time, so a run-time knob cannot change them. The plusarg silently does nothing, which is why structural settings never travel as plusargs.
A short precedence chain runs without plusargs. The test-override layer sets 200, the absent plusarg leaves it alone, and the checks confirm both facts.
module tb_precedence;
integer num_txns = 100; // hard default
integer errors = 0;
integer matched;
initial begin
num_txns = 200; // test-override layer
matched = $value$plusargs("NUM_TXNS=%d", num_txns); // command line wins when present
if (matched != 0) errors++; // no plusargs in this run, so 0 is expected
if (num_txns != 200) errors++; // override layer survived
if (errors == 0) $display("PASS: test override held at 200 with no plusarg present");
else $fatal(1, "FAIL: %0d precedence checks did not hold", errors);
$finish;
end
endmodule