Part 3 · Constraint Randomization · Intermediate
Dependent-Field Puzzles
Drills: kind-dependent address ranges, size*count capacity products, start+len wraparound (and deliberate wraparound), and legal FSM transition pairs.
Problem 1 — READ from ROM, otherwise RAM
Problem
“If kind == READ, the address must fall in ROM space [0x0000:0x3FFF]; otherwise it must fall in RAM space [0x8000:0xFFFF]. Write it two ways.”
Think it through
Conditional structure inside constraints comes in two spellings: implication (->) and if/else. For a two-sided condition, if/else is one construct; with implications you need BOTH directions explicitly — a lone implication says nothing when its antecedent is false, which is the trap.
typedef enum {READ, WRITE} kind_e;
class addr_txn;
rand kind_e kind;
rand bit [15:0] addr;
// Way 1: if/else — both sides in one construct
constraint c_map {
if (kind == READ)
addr inside {[16'h0000:16'h3FFF]};
else
addr inside {[16'h8000:16'hFFFF]};
}
// Way 2: paired implications — equivalent, must cover both arms
// constraint c_map2 {
// (kind == READ) -> addr inside {[16'h0000:16'h3FFF]};
// (kind != READ) -> addr inside {[16'h8000:16'hFFFF]};
// }
endclassWhy this works
Both spellings define the same legal set. The classic incomplete answer writes only the first implication — then for WRITE the address is unconstrained and can land in ROM, a bug that no compile step catches. Also volunteer the distribution skew: kind is NOT 50/50 here in general — the solver picks uniformly over legal (kind, addr) pairs , and both arms have 16K and 32K addresses respectively, so WRITE outweighs READ 2:1 unless you add solve kind before addr.
Variation — restore 50/50 on kind
constraint c_order { solve kind before addr; }
// kind now picked first, uniformly; addr then solved within the chosen arm.
// solve...before changes PROBABILITY only — never which solutions are legal.Problem 2 — size × count must fit the buffer
Problem
“rand size (bytes per beat) and rand count (beats). Their product must not exceed a 4096-byte buffer. Both are 32-bit. What goes wrong with the obvious answer?”
Think it through
The obvious size * count <= 4096 multiplies in 32-bit context: large size and count overflow the product, wrap, and can satisfy the comparison — admitting wildly illegal pairs. Widen the arithmetic, or bound the factors so overflow is impossible.
class buf_txn;
rand bit [31:0] size, count;
// TRAP: 32-bit product wraps — e.g. size=65536, count=65536 → product
// wraps to 0, passes <= 4096, and the "burst" is 4 GB.
// constraint c_fit_bad { size * count <= 4096; }
// Fix 1: widen the arithmetic before multiplying
constraint c_fit { longint'(size) * longint'(count) <= 4096; }
// Fix 2 (also state it): bound the factors so the product can't wrap
constraint c_sane { size inside {[1:4096]};
count inside {[1:4096]}; }
endclassWhy this works
Casting either operand to longint promotes the whole multiplication to 64 bits — the product of two 32-bit values always fits, so the comparison is exact. Fix 2 is complementary: with both factors capped at 4096 the true product caps at 16M, far below the 32-bit wrap point, so even unwidened arithmetic would be safe. Production code does both: sane bounds for realism, widened arithmetic for safety.
Common wrong answers
size * count <= 4096 with no widening or factor bounds — the wraparound admits illegal giants.
Casting the RESULT: longint'(size * count) — too late; the multiply already wrapped in 32 bits before the cast.
solve size before count to “fix” it — ordering changes probability, not arithmetic width; the wrap remains.
Problem 3 — start + len must not wrap the address space
Problem
“16-bit address space. rand start and rand len (at least 1). The transfer [start : start+len-1] must not wrap past 0xFFFF. Then: write the variation that deliberately ALWAYS wraps.”
Think it through
“No wraparound” means start + len <= 65536 — but computed in 16 bits, start + len itself wraps and the comparison lies. Widen to 32-bit arithmetic for the guard. The deliberate-wrap variation just flips the inequality.
class xfer;
rand bit [15:0] start;
rand bit [15:0] len;
constraint c_len { len >= 1; }
// No wraparound: end address fits. Widen BEFORE adding.
constraint c_fit { int'(start) + int'(len) <= 65536; }
// Variation: transfer must ALWAYS wrap past 0xFFFF
// constraint c_wrap { int'(start) + int'(len) > 65536; }
endclassWhy this works
In 32-bit arithmetic the sum is exact, so the comparison against 65536 is meaningful. Note the boundary convention: start + len == 65536 means the last byte is exactly 0xFFFF — legal, no wrap; that is why the guard is <= 65536, not < 65536. Interviewers probe that off-by-one deliberately. The wrap variation is used to TEST the DUT’s wraparound handling — flipping one inequality turns a legality constraint into targeted stimulus, which is the entire philosophy of constrained random in one line.
Common wrong answers
start + len <= 16'hFFFF in 16-bit arithmetic — the sum wraps first; large pairs pass the check.
Using <= 65535 — off-by-one that forbids the legal transfer ending exactly at 0xFFFF.
start <= 16'hFFFF - len — actually a correct rearrangement (no overflow since len <= 0xFFFF), worth offering as the no-cast alternative, but only if you can defend why the subtraction cannot underflow (len >= 1 keeps it in range).
Problem 4 — Legal FSM transition pairs
Problem
“An FSM has states IDLE, SETUP, ACCESS. Legal transitions: IDLE→SETUP, SETUP→ACCESS, ACCESS→IDLE, ACCESS→SETUP. Randomize a (cur, next) pair that is always a legal transition. Encode the table.”
Think it through
Two encodings: per-state implications (readable, mirrors the spec table row by row) or packing the pair into one vector and using inside on the concatenations (compact, scales to big tables generated from a spec). Both are worth showing.
typedef enum bit [1:0] {IDLE, SETUP, ACCESS} state_e;
class fsm_pair;
rand state_e cur, next;
// Encoding 1: one implication per spec-table row group
constraint c_table {
(cur == IDLE) -> next == SETUP;
(cur == SETUP) -> next == ACCESS;
(cur == ACCESS) -> next inside {IDLE, SETUP};
}
// Encoding 2: pack (cur,next) into 4 bits, match against legal codes
// constraint c_packed {
// {cur, next} inside { {IDLE, SETUP },
// {SETUP, ACCESS},
// {ACCESS, IDLE },
// {ACCESS, SETUP } };
// }
endclassWhy this works
Encoding 1 reads exactly like the transition table and is self-documenting; because every state appears as an antecedent, the table is total and no (cur, next) pair escapes unconstrained. Encoding 2 concatenates the 2-bit enums into a 4-bit code and whitelists the four legal codes — the form you would script-generate for a 30-state FSM. Distribution note: pairs are uniform over the four legal transitions, so cur == ACCESS appears twice as often as the others; add solve cur before next if you need uniform cur instead.
Common wrong answers
Constraining only next without reference to cur — generates next-states legal for SOME state, not for THIS one.
Leaving a state out of the implication table — pairs from that state become fully unconstrained, silently.
if/else-if chain missing the final else — same hole as a missing implication; totality is the requirement either way.
Key takeaways
Conditional constraints must cover BOTH arms — a lone implication frees the other side entirely.
Widen arithmetic BEFORE multiply/add when products or sums can exceed the operand width.
Implication skews distribution toward the bigger arm; solve...before reshapes probability, never legality.
FSM tables: implications for readability, packed-pair inside for generated scale.
Common pitfalls
Single-direction implication leaving the else-case unconstrained — the most common dependent-field bug.
Casting the result instead of the operands — the wrap already happened inside the expression.
Off-by-one at the address-space boundary: end exactly at 0xFFFF is legal, <= 65536 not < 65536.
Missing FSM states in the constraint table — unconstrained pairs pass compile and corrupt stimulus.