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.

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

Why 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

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

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

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

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

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

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

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