Part 3 · Constraint Randomization · Intermediate
The Knobs Pattern
Control knobs gating constraints, weights as knobs feeding dist, config-driven knob setting, and a full knobbed bus transaction.
What a knob is
A knob is a control variable that shapes randomization without being the randomized payload . Instead of editing constraints to change stimulus character, you set a variable: max burst length, error percentage, address-region weights. Knobs come in two kinds. State knobs (non-rand) are set procedurally by the test or config before randomize() and enter the solver as constants. Rand knobs are themselves randomized — typically at a coarser cadence (once per sequence, not per item) — so the bench explores knob space automatically. Both kinds appear inside constraints as gates and as weights.
class knobbed_txn;
// ---- payload (rand) ----
rand bit [31:0] addr;
rand bit [3:0] len;
rand bit err;
// ---- knobs (state: set before randomize) ----
bit [3:0] len_max = 4'd15; // gate: ceiling on len
bit allow_err = 1'b0; // gate: enable error path
int unsigned err_pct = 0; // weight: error rate 0..100
int unsigned w_low_addr = 8; // weights: address regions
int unsigned w_high_addr = 2;
constraint len_c { len inside {[1:len_max]}; }
constraint err_c {
allow_err == 0 -> err == 0; // gate
err dist { 1 := err_pct, 0 := (100 - err_pct) }; // weight
}
constraint addr_c {
addr dist { [32'h0000_0000:32'h0000_FFFF] :/ w_low_addr,
[32'hF000_0000:32'hFFFF_FFFF] :/ w_high_addr };
}
endclassTwo mechanics deserve attention. First, dist weights can be variables — the solver reads their current values at randomize() time, so err_pct is a genuine runtime dial. Second, the gate idiom (knob implies field behavior) is solver-cheap and readable; because knobs are state, the implication direction is fixed and there is no implication-skew hazard from the knob itself (skew arises when both sides are rand — covered in distribution debugging).
Setting knobs: procedural and config-driven
// --- direct procedural setting (simple benches) ---
knobbed_txn t = new();
t.len_max = 4'd4; // bring-up: short bursts only
t.allow_err = 0;
repeat (100) assert(t.randomize());
t.len_max = 4'd15; // stress phase: full range, 5% errors
t.allow_err = 1;
t.err_pct = 5;
repeat (1000) assert(t.randomize());
// --- config-object pattern (scales to real benches) ---
class stim_cfg;
rand bit [3:0] len_max;
rand int unsigned err_pct;
rand int unsigned w_low_addr, w_high_addr;
constraint sane_c {
len_max inside {[1:15]};
err_pct <= 20;
w_low_addr + w_high_addr > 0; // dist needs nonzero total
}
function void apply(knobbed_txn t);
t.len_max = len_max;
t.allow_err = (err_pct > 0);
t.err_pct = err_pct;
t.w_low_addr = w_low_addr;
t.w_high_addr = w_high_addr;
endfunction
endclass
stim_cfg cfg = new();
assert(cfg.randomize() with { err_pct == 5; len_max >= 8; });
cfg.apply(t); // knobs themselves were randomized - two-level randomThe config-object version is the important one: the knobs are randomized at sequence granularity (one cfg.randomize per test phase), then applied to every transaction in that phase. This creates two-level randomization — the bench explores knob space across runs while each run has coherent character — and it gives knobs their own constraints (sane_c) so illegal knob combinations are unrepresentable. In UVM the same shape becomes a config object distributed via uvm_config_db; the pattern is identical, the plumbing changes.
Full worked example: knobbed bus write generator
class bus_txn;
rand bit [31:0] addr;
rand bit [3:0] len;
rand bit [2:0] size;
rand bit write;
rand bit err;
// knobs
bit [3:0] len_max = 15;
int unsigned wr_pct = 50; // write/read mix
int unsigned err_pct = 0;
bit lock_size = 0; // gate: pin size to size_val
bit [2:0] size_val = 2;
constraint valid_c {
len inside {[1:15]};
size <= 3'd2;
addr % (1 << size) == 0;
}
constraint knob_len_c { len <= len_max; }
constraint knob_dir_c { write dist { 1 := wr_pct, 0 := (100 - wr_pct) }; }
constraint knob_err_c { err dist { 1 := err_pct, 0 := (100 - err_pct) }; }
constraint knob_size_c { lock_size -> size == size_val; }
endclass
module demo;
initial begin
bus_txn t = new();
int writes = 0;
// Phase: 80% writes, word-locked, short, clean
t.wr_pct = 80; t.len_max = 4;
t.lock_size = 1; t.size_val = 2;
repeat (1000) begin
assert(t.randomize());
writes += t.write;
end
$display("write ratio: %0d/1000 (expect ~800)", writes);
end
endmoduleKNOB DATA FLOW
test / config file / plusargs
|
v
stim_cfg.randomize() <- knobs get their own constraints
|
v apply()
txn knobs (state vars) len_max=4 wr_pct=80 err_pct=0
|
v enter solver as constants
txn.randomize() payload solved under knob gates/weights
|
v
driver -> DUT
one knob change reshapes 1000s of txns - zero constraint editsMeasure the knob effect (the write-ratio counter above) at least once when bringing up a knobbed class — a dist weight pointing at a variable that is accidentally zero, or a gate left enabled from a previous phase, produces silently wrong stimulus character, and a ten-line histogram catches it immediately.
Interview angle
Knob questions usually arrive as “how would you make one sequence produce bring-up traffic and stress traffic without code changes?” The expected shape: state-variable knobs gating constraints and feeding dist weights, set from a randomized config object so knob space itself is explored; mention that dist weights may be variables (many candidates do not know this) and that knobs-as-state avoids bidirectional-solving surprises. The senior-level addition: knob cadence — knobs randomize per phase or per sequence, payload per item — which is what gives runs coherent character instead of white noise. If UVM is in scope, map the pattern to config objects via uvm_config_db rather than claiming it is UVM-specific.
Key takeaways
Knobs are control state shaping randomization: gates (implications) and weights (variable dist).
dist weights can be variables read at randomize() time — err_pct is a true runtime dial.
Randomize the knobs themselves in a config object with sanity constraints — two-level randomization.
Knob cadence: per-phase/per-sequence for knobs, per-item for payload — coherent stimulus character.
Histogram once after bring-up to confirm knob effect — zero weights and stale gates fail silently.
Common pitfalls
Both dist weights zero (e.g. err_pct=0 with weight 100-100=0 miscoded) — solver error or degenerate dist.
Forgetting to reset a gate knob between phases — stale lock_size pins fields test-wide (rand_mode's cousin).
Making knobs rand inside the txn at item cadence — knob thrash, no coherent phase character.
Knobs without their own sanity constraints — config randomization produces illegal combinations.
Editing constraints per test instead of adding a knob — the Nth test forks the class permanently.