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.
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
endclassWhy 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
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.
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; }
endclassWhy 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.
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
endclassWhy 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.
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]};
}
endclassWhy 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
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.