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.

systemverilog
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 };
  }
endclass

Two 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

systemverilog
// --- 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 random

The 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

systemverilog
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
endmodule
diagram
KNOB 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 edits

Measure 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.