Part 3 · Constraint Randomization · Intermediate

Sequence & Timing Patterns

Drills: N requests with a minimum gap, jittered periods, burst-of-K-then-idle shaping, and arbitration grant patterns with a fairness bound.

Problem 1 — N requests with a minimum gap

Problem

“Generate start times for 8 requests such that consecutive requests are at least 5 cycles apart and the whole pattern fits in 200 cycles. Randomize the gaps, not the absolute times — why?”

Think it through

Randomizing absolute times forces ordering constraints between every pair. Randomizing the gaps makes the minimum-spacing rule a simple per-element bound, and ordering falls out by construction. The fit-in-200 budget becomes a sum constraint on the gaps — bring the sum-width lesson with you.

systemverilog
class req_train;
  rand bit [7:0] gap[8];          // gap[0] = delay before first request
  int unsigned start_time[8];     // derived, not rand

  constraint c_gap { foreach (gap[i]) gap[i] inside {[5:50]}; }
  constraint c_fit { gap.sum() with (int'(item)) <= 200; }

  function void post_randomize();
    int unsigned t = 0;
    foreach (gap[i]) begin
      t += gap[i];
      start_time[i] = t;
    end
  endfunction
endclass

Why this works

Gaps of at least 5 guarantee spacing; the widened sum caps the train length; post_randomize() integrates gaps into absolute times — a pure function of solved values, so it belongs in procedural code, not the solver. Feasibility aloud: minimum total is 8×5 = 40, well under 200, so solutions exist.

Variation — exactly fill the window

systemverilog
constraint c_exact { gap.sum() with (int'(item)) == 200; }
// Check feasibility: max total = 8*50 = 400 ≥ 200 ≥ min 40 — solvable.

Common wrong answers

  • rand absolute times with foreach time[i] > time[i-1] + 5 — workable but couples ordering, spacing, and budget into tangled constraints; the gap formulation is strictly simpler.

  • Making start_time rand AND computing it in post_randomize() — the solver assigns it, then you overwrite it; pick one owner per field.

  • Unwidened gap.sum() — 8-bit accumulation wraps at 256; a 296-cycle train “fits” in 200.


Problem 2 — Jittered period

Problem

“A heartbeat nominally fires every 100 cycles with ±10 cycles of jitter. Randomize the per-beat periods. Follow-up: keep the LONG-TERM average at exactly 100.”

Think it through

Per-beat jitter is a one-line inside on each element. The follow-up is the real drill: independent uniform jitter drifts (random walk) — the average over any finite run is near 100 but not pinned. Pinning the average is a sum constraint over the window.

systemverilog
class heartbeat;
  rand bit [7:0] period[16];

  // Per-beat jitter
  constraint c_jit { foreach (period[i]) period[i] inside {[90:110]}; }

  // Follow-up: pin the window average to exactly 100
  constraint c_avg { period.sum() with (int'(item)) == 16 * 100; }
endclass

Why this works

Each beat wanders in [90:110] but the sum constraint forces total slack to cancel within the 16-beat window — long beats are balanced by short ones. This “bounded element + pinned sum” shape is THE template for budgeted randomness: bandwidth shaping, credit returns, latency budgets all reduce to it.

Common wrong answers

  • period dist { 100 := 8, [90:110] :/ 2 } as an “average” — weighting toward 100 is not pinning the mean; the average still wanders.

  • Constraining each period == 100 when asked for average 100 — over-constraint; kills the jitter entirely.

  • Pinned sum without per-element bounds — solver may pick period[0] = 255 and starve the rest; both halves of the template are required.


Problem 3 — Burst of K, then idle

Problem

“Traffic arrives as bursts: K back-to-back transfers (K random in 4..16), then an idle stretch of 10..100 cycles, repeating. Model one burst+idle unit, then a window of several units.”

Think it through

Don’t randomize a flat per-cycle bit vector and try to constrain runs — run-length constraints on flat vectors are miserable. Randomize the run lengths themselves : a burst length and an idle length per unit. Structure first, then expand to cycles procedurally.

systemverilog
class burst_unit;
  rand bit [4:0] burst_len;    // back-to-back beats
  rand bit [6:0] idle_len;     // idle cycles after the burst

  constraint c_b { burst_len inside {[4:16]}; }
  constraint c_i { idle_len  inside {[10:100]}; }
endclass

class burst_window;
  rand burst_unit units[4];
  rand int unsigned budget;

  constraint c_budget {
    budget == units.sum() with (int'(item.burst_len) + int'(item.idle_len));
    budget <= 360;
  }

  function new();
    foreach (units[i]) units[i] = new();
  endfunction
endclass

Why this works

Each unit’s shape is locally constrained; the window’s total duration is a sum over OBJECT fields — note the with clause reaching into item.burst_len. The objects must be constructed in new() before randomize() is called: randomize solves fields of existing objects, it never allocates. Forgetting that is the most common nested-object failure.

Common wrong answers

  • rand bit active[360] with hand-rolled run constraints — technically expressible, practically unsolvable and unreadable; restructure instead.

  • Declaring rand burst_unit units[4] but never calling new() on elements — randomize() on null handles errors out (or silently skips, tool-dependent).

  • Summing item.burst_len without the int' cast — same width wrap as every other sum drill.


Problem 4 — Arbitration grants with a fairness bound

Problem

“Randomize a sequence of 16 grant decisions among 4 masters (0..3). Every master must receive at least 2 grants and no master more than 8 — a fairness window. How do you count per-master grants in a constraint?”

Think it through

Counting occurrences inside a constraint is a sum-with-predicate: grant.sum() with (int'(item == m)) counts elements equal to m, because the boolean is cast to 0/1 and accumulated. One such constraint per master gives the fairness band.

systemverilog
class arb_pattern;
  rand bit [1:0] grant[16];    // grant[i] = master granted in slot i

  constraint c_fair {
    foreach (grant[i]) grant[i] < 4;   // documents intent (2 bits already cap it)
    grant.sum() with (int'(item == 0)) inside {[2:8]};
    grant.sum() with (int'(item == 1)) inside {[2:8]};
    grant.sum() with (int'(item == 2)) inside {[2:8]};
    grant.sum() with (int'(item == 3)) inside {[2:8]};
  }
endclass

Why this works

Each sum-with-predicate is an independent occurrence count; the four bands plus the fixed total of 16 slots interact (counts must sum to 16, and 4×2=8 ≤ 16 ≤ 4×8=32, so the system is satisfiable). The predicate-count idiom generalizes to “at most 3 ERROR packets per 100”, “exactly one master idle”, and most quota-style interview questions.

Variation — no master granted twice in a row

systemverilog
constraint c_norepeat { foreach (grant[i]) if (i > 0) grant[i] != grant[i-1]; }

Adjacent-pair constraints compose cleanly with the occurrence bands — the solver satisfies all simultaneously or reports failure; no manual sequencing needed.

Common wrong answers

  • Trying to keep a count variable updated inside the constraint — constraints have no mutable state; counting must be expressed as a sum expression.

  • int'(item) == 0 instead of int'(item == 0) — casts the element, not the predicate; sums element values, not occurrences.

  • Fairness bands that cannot total 16 (e.g. each master exactly 5) — 4×5 = 20 ≠ 16; randomize() fails and the unchecked call hides it.

Key takeaways

  • Randomize structure (gaps, run lengths, counts), not flat per-cycle vectors — then expand procedurally.

  • Bounded elements + pinned/capped sum is the universal budget template.

  • Occurrence counting in constraints = sum() with (int'(item == value)).

  • Nested rand objects must be new()-ed before randomize() — the solver never allocates.

Common pitfalls

  • Derived quantities owned by both the solver and post_randomize() — one writer per field.

  • Narrow-width sums of gaps/periods wrapping silently — cast with int'(item) every time.

  • Quota constraints whose totals cannot equal the slot count — infeasible, fails at runtime only.

  • Casting the element instead of the predicate in occurrence counts — sums values, not matches.