Day 16 / 25
← Day 15 Day 17 →
HomeVerificationDay 16 — Constrained Random Verification
Track 4 — Verification Techniques

Constrained Random Verification

By EcrioniX · Updated Jun 22, 2026

Directed tests are slow to write and miss edge cases. Constrained random verification (CRV) lets the solver generate thousands of legal stimuli automatically — you write the rules once, the solver does the rest. Master rand, randc, constraint blocks, distributions, implications, soft constraints, solve‑before, and inline overrides in one deep session.

⏱ 22 min read 📖 Day 16 of 25 🎯 CRV · SystemVerilog
DIRECTED TESTING Test A: addr=0x00, len=4 Test B: addr=0xFF, len=1 Test C: addr=0x80, len=8 each case hand-coded misses 4B-address × 256-len corner cases manual effort DUT CONSTRAINED RANDOM TESTING Constraint Block addr inside {[0:32'hFFFF_FFFF]} Constraint Solver addr=0x3A7C len=63 addr=0x0001 len=255 addr=0xDEAD len=1 1000s of legal stimuli from one constraint block
Contents
  1. Directed Tests vs Constrained Random
  2. rand vs randc — The Key Difference
  3. Constraint Blocks — Syntax & AND-ing
  4. Distribution Constraints — dist
  5. The inside Operator
  6. Implication Constraints (a -> b)
  7. Soft Constraints
  8. solve-before Ordering
  9. Inline Constraints — randomize() with {}
  10. Complete Transaction Class
  11. Debugging Over-Constrained Failures
  12. FAQ

1. Directed Tests vs Constrained Random

A directed test hard-codes every stimulus field. An engineer who knows the bug can write the perfect reproducer in minutes. But for exploring a 64-bit address space combined with a 256-value burst-length field, the number of combinations is astronomically large — writing one directed test per corner case is not feasible.

Constrained random verification (CRV) inverts the process: you describe the legal space of stimuli as a set of constraints, and the simulator's built-in constraint solver generates a new legal stimulus every time you call randomize(). Run the same test 10 000 times with different seeds and you cover the entire legal space probabilistically.

AspectDirected TestingConstrained Random
Setup costLow per bug, high for full coverageHigher upfront (constraint authoring)
Coverage growthLinear — one test, one scenarioExponential — one run, many scenarios
Corner-case reachOnly what engineer anticipatesSolver finds unexpected combinations
ReproducibilityDeterministic by definitionSeed-controlled: same seed = same run
MaintenanceFragile — changes in DUT require new testsConstraints adapt naturally to spec changes
Typical useBug regression, protocol smokeRegression campaigns, coverage closure
Rule of thumb: Use directed tests to verify specific bug fixes and protocol corner cases that the random solver is unlikely to stumble upon. Use CRV for everything else — it scales.

2. rand vs randc — The Key Difference

SystemVerilog provides two randomisation qualifiers for class members. Both require the rand or randc keyword in the class declaration and become active when randomize() is called on the object.

QualifierDistribution behaviourRepetitionTypical use
randUniform random over legal domain, independent each callAny value may repeat immediatelyMost stimulus fields
randcCyclic — cycles through all legal values before repeatingNo repeats within one cycleOpcodes, transaction IDs, small enums where full coverage is needed
class Packet;
  // rand — each randomize() picks independently from the legal domain
  rand  logic [31:0] addr;
  rand  logic [7:0]  burst_len;

  // randc — cycles through all 4 values before repeating
  randc bit   [1:0]  cmd;   // values 0,1,2,3 appear once per cycle
endclass

module tb;
  Packet pkt;
  initial begin
    pkt = new();
    repeat(8) begin
      void'(pkt.randomize());
      // cmd will visit 0,1,2,3 then 0,1,2,3 — never 0,0,1,...
      $display("addr=%0h cmd=%0d len=%0d", pkt.addr, pkt.cmd, pkt.burst_len);
    end
  end
endmodule
randc limitation: randc only works on integral types with a small domain. The solver must enumerate all legal values to build the cycle. Large bit-widths (e.g., 32-bit randc) are illegal and will cause a compile error. Keep randc to enums or small integer fields.

3. Constraint Blocks — Syntax & AND-ing

A constraint block is a named block inside a class containing one or more constraint expressions. The solver AND's every active constraint in the object together before solving. All constraints must be simultaneously satisfiable.

class BusTxn;
  rand logic [31:0] addr;
  rand logic [7:0]  len;
  rand bit          write;

  // Constraint block 1 — address alignment
  constraint c_addr_align {
    addr[1:0] == 2'b00;          // always 4-byte aligned
    addr < 32'h1000_0000;        // within mapped region
  }

  // Constraint block 2 — burst length
  constraint c_len {
    len inside {1, 2, 4, 8, 16}; // only valid AXI burst lengths
  }

  // Constraint block 3 — write transactions are short
  constraint c_write_short {
    write -> len <= 8;           // implication: if write then len ≤ 8
  }
endclass

All three blocks are active simultaneously. The solver must find addr, len, and write that satisfy every expression at once. Individual constraints within a block are separated by semicolons; you may also use multiple expressions in a single block — they are treated identically to expressions across multiple named blocks.

Enabling / Disabling Constraint Blocks

Call constraint_mode(0) to deactivate a block at runtime. This is useful in tests that need a special stimulus outside the normal constraint envelope, without permanently modifying the class.

BusTxn txn = new();

// Temporarily disable the length constraint
txn.c_len.constraint_mode(0);
void'(txn.randomize());          // len is now fully unconstrained (0–255)
txn.c_len.constraint_mode(1);   // re-enable

4. Distribution Constraints — dist

By default the solver assigns uniform probability across the legal domain. dist lets you bias that distribution by attaching weights to individual values or ranges. Two operators control how weights apply:

OperatorMeaningExample weight behaviour
:=Each item in range gets the stated weight individually[0:3] := 10 — values 0,1,2,3 each have weight 10 (total 40)
:/Weight is divided equally among all items in range[0:3] :/ 10 — values 0,1,2,3 each have weight 2.5 (total 10)
class PktType;
  rand logic [7:0] opcode;
  rand logic [7:0] priority_lvl;

  // opcode distribution: READ 70%, WRITE 20%, NOP 10%
  constraint c_opcode_dist {
    opcode dist {
      8'h01 := 70,    // READ
      8'h02 := 20,    // WRITE
      8'h00 := 10     // NOP
    };
  }

  // priority: low (0–63) gets half the weight, high (192–255) gets other half
  constraint c_priority_dist {
    priority_lvl dist {
      [0:63]   :/ 50,   // 50 total weight shared among 64 values
      [192:255] :/ 50    // 50 total weight shared among 64 values
    };
  }
endclass
Coverage tip: Model your dist weights to mirror real traffic profiles — e.g., reads are typically 3x more frequent than writes on most buses. This ensures coverage points accumulate at the same rate as real use.

5. The inside Operator

The inside operator is the cleanest way to constrain a variable to a set or range of values. It expands to a chain of equality checks under the hood, but reads much more naturally than a long chain of || conditions.

class MemAccess;
  rand logic [31:0] addr;
  rand logic [3:0]  be;       // byte-enable
  rand bit           secure;

  // addr must fall within one of the two valid memory windows
  constraint c_addr_range {
    addr inside {
      [32'h0000_0000 : 32'h0FFF_FFFF],   // ROM window
      [32'h2000_0000 : 32'h3FFF_FFFF]    // DRAM window
    };
  }

  // byte enables: only single-byte or full-word valid
  constraint c_be {
    be inside {4'b0001, 4'b0010, 4'b0100, 4'b1000, 4'b1111};
  }

  // secure access only allowed in ROM region
  constraint c_secure_range {
    secure -> addr inside {[32'h0000_0000 : 32'h0FFF_FFFF]};
  }
endclass

You can negate inside with the ! operator: !(addr inside {forbidden_range}) excludes that range entirely from the solution space.

6. Implication Constraints — the -> Operator

Implication (a -> b) means "if a is true, then b must also be true." When a is false, the constraint places no restriction on b. This is the go-to tool for modelling conditional protocol rules without hard-coding separate constraint blocks for each mode.

class AXITxn;
  rand bit          write;
  rand logic [7:0]  burst_len;
  rand bit   [1:0]  burst_type;  // 0=FIXED 1=INCR 2=WRAP
  rand bit   [3:0]  id;
  rand bit          lock;

  // WRAP bursts require power-of-2 length: 2,4,8,16
  constraint c_wrap_len {
    (burst_type == 2'b10) -> burst_len inside {1, 3, 7, 15};
    // AWLEN is len-1 encoding so beat counts 2,4,8,16 → values 1,3,7,15
  }

  // Locked transactions must be INCR and single beat
  constraint c_lock {
    lock -> (burst_type == 2'b01 && burst_len == 0);
  }

  // Write-only IDs are in upper nibble range
  constraint c_id {
    write -> id inside {[8:15]};
  }
endclass

Under the hood, a -> b is equivalent to (!a || b). The solver satisfies this by either making a false (no restriction on b) or making both a and b true simultaneously. Both paths are legal and will appear in the random solution space.

7. Soft Constraints

A soft constraint expresses a preference, not a requirement. If a hard constraint or a higher-priority soft constraint contradicts it, the soft constraint is silently dropped rather than causing an infeasibility. This makes base-class constraints much more reusable because sub-classes can override them without causing solve failures.

class BaseTxn;
  rand logic [7:0] len;
  rand bit          write;

  // Default preference: short bursts, reads
  constraint c_soft_defaults {
    soft len    inside {[1:16]};
    soft write == 1'b0;
  }
endclass

// Sub-class scenario: long write bursts — overrides both soft defaults
class LongWriteTxn extends BaseTxn;
  constraint c_long_write {
    len   inside {[64:255]};   // hard — overrides soft len
    write == 1'b1;             // hard — overrides soft write
  }
endclass

// Inline override via randomize() with {} — also overrides soft defaults
BaseTxn txn = new();
void'(txn.randomize() with { write == 1'b1; len inside {[32:64]}; });
Priority: hard constraints in inline with{} blocks > hard constraints in the class > soft constraints in with{} blocks > soft constraints in the class. When two soft constraints at the same priority level conflict, the solver picks one arbitrarily.

8. solve-before Ordering

The constraint solver is free to pick the order in which it assigns values to variables. In most cases this makes no difference, but it becomes critical when one variable controls the legal domain of another and you want a specific statistical distribution rather than letting the solver collapse probabilities.

The classic solve-before problem

Suppose mode is a 1-bit variable (50% chance of 0 or 1) and data is an 8-bit variable that is unconstrained when mode == 1 but must be 0 when mode == 0. Without solve-before, the solver sees 257 total solutions — 1 for mode==0 and 256 for mode==1. It picks uniformly, giving mode==0 only a 1/257 ≈ 0.4% probability instead of 50%.

class BiasedPkt;
  rand bit          mode;
  rand logic [7:0]  data;

  // mode=0 → data must be 0; mode=1 → data can be anything
  constraint c_data {
    !mode -> data == 8'h00;
  }

  // Without this: mode==0 has probability 1/257 ≈ 0.4%
  // With this: mode first (50/50), then data conditioned on mode
  constraint c_order {
    solve mode before data;
  }
endclass

With solve mode before data, the solver first picks mode with a fair 50/50 distribution, then independently solves for data given the chosen mode value. The result is the expected 50% probability on each mode.

Performance note: solve-before forces the solver into a two-phase approach and can be slower on complex constraint graphs. Use it only when you observe statistical skew and need to correct it.

9. Inline Constraints — randomize() with {}

Inline constraints let a test scenario impose additional restrictions on a single randomize() call without modifying the class. They are hard constraints, they are AND'd with all class constraints, and they are discarded after the call returns.

BusTxn txn = new();

// Scenario A: force a write transaction with a specific address window
if (!txn.randomize() with {
    write == 1'b1;
    addr inside {[32'h8000_0000 : 32'h8FFF_FFFF]};
    len inside {4, 8};
}) `uvm_fatal("RAND", "Randomize failed")

// Scenario B: read from page-boundary addresses only
if (!txn.randomize() with {
    write == 1'b0;
    addr[11:0] == 12'h000;    // 4 KB page boundary
}) `uvm_fatal("RAND", "Randomize failed")

// Note: inline constraints use the calling scope for variable references
logic [31:0] base_addr = 32'hC000_0000;
void'(txn.randomize() with { addr == base_addr; });

The body of the with{} block runs in a mixed scope — names refer first to the randomized object's variables, then fall back to the calling scope. This lets you pass parameters from the test environment directly into the inline constraint.

10. Complete Transaction Class

The following class integrates all the features covered above into a realistic AXI4 transaction. It demonstrates how constraint blocks compose, how soft defaults interact with sub-class overrides, and how dist weights model real traffic.

class AXI4Transaction;

  // ── Randomizable fields ─────────────────────────────────────
  rand  logic [31:0]  addr;
  rand  logic [7:0]   burst_len;   // AWLEN/ARLEN encoding (beats - 1)
  rand  bit   [2:0]   burst_size;  // bytes per beat: 2^burst_size
  rand  bit   [1:0]   burst_type;  // FIXED=0 INCR=1 WRAP=2
  rand  bit           write;
  rand  bit   [3:0]   id;
  rand  bit           lock;        // exclusive access
  rand  bit   [3:0]   prot;
  randc bit   [1:0]   cache;       // cyclic: covers all 4 values evenly

  // ── 1. Address: aligned, within mapped windows ───────────────
  constraint c_addr {
    addr inside {
      [32'h0000_0000 : 32'h0FFF_FFFF],  // ROM
      [32'h2000_0000 : 32'h3FFF_FFFF],  // SRAM
      [32'h8000_0000 : 32'hBFFF_FFFF]   // DRAM
    };
    addr[1:0] == 2'b00;  // always 4-byte aligned
  }

  // ── 2. Traffic distribution — reads are 3× more common ──────
  constraint c_rw_dist {
    write dist { 1'b1 := 25, 1'b0 := 75 };
  }

  // ── 3. Burst length: weighted toward short transfers ─────────
  constraint c_len {
    burst_len dist {
      8'd0             := 30,   // 1 beat
      [8'd1:8'd3]      := 40,   // 2–4 beats
      [8'd4:8'd15]     := 20,   // 5–16 beats
      [8'd16:8'd255]   := 10    // 17–256 beats
    };
  }

  // ── 4. Burst size: 4B or 8B transfers most common ───────────
  constraint c_size {
    burst_size inside {3'b010, 3'b011};  // 4B or 8B
  }

  // ── 5. WRAP burst requires power-of-2 beat count ────────────
  constraint c_wrap {
    (burst_type == 2'b10) ->
        burst_len inside {8'd1, 8'd3, 8'd7, 8'd15};
  }

  // ── 6. Lock: only INCR single beat allowed ──────────────────
  constraint c_lock {
    lock -> (burst_type == 2'b01 && burst_len == 8'd0);
    lock dist { 1'b1 := 5, 1'b0 := 95 }; // rare but present
  }

  // ── 7. ROM window is read-only ───────────────────────────────
  constraint c_rom_readonly {
    (addr inside {[32'h0000_0000 : 32'h0FFF_FFFF]}) -> !write;
  }

  // ── 8. Solve write before burst_len for correct probability ──
  constraint c_order {
    solve write before burst_len;
    solve burst_type before burst_len;
  }

  // ── 9. Soft defaults (overridable in tests) ──────────────────
  constraint c_defaults {
    soft prot == 4'b0010;  // non-secure, non-privileged data
  }

  // ── Helper: print summary ────────────────────────────────────
  function void print();
    $display("[AXI4] %s addr=%0h len=%0d size=%0dB type=%0d id=%0d lock=%0d",
      write ? "WR" : "RD", addr, burst_len+1,
      (1 << burst_size), burst_type, id, lock);
  endfunction
endclass

11. Debugging Over-Constrained Failures

randomize() returns 0 (logic false) when the solver cannot find any solution. This is called an over-constrained condition. It does NOT throw a fatal error by default — which means silent failures if you do not check the return value.

Always check the return value. Never call txn.randomize() and ignore the result. Use if (!txn.randomize()) `uvm_fatal(...) or assert(txn.randomize()) at minimum.

Step-by-step debug workflow

  1. Check the return value — add `uvm_fatal or $fatal on failure so it is immediately visible.
  2. Print active constraints — call txn.print_constraints() (VCS/Xcelium extension) to list all active constraint expressions and their current bindings.
  3. Bisect with constraint_mode(0) — disable half the constraint blocks, re-run. If it now solves, the problem is in the disabled half. Repeat until the offending block is isolated.
  4. Check implication directions — a common bug is reversing an implication: b -> a instead of a -> b, which constrains the wrong variable.
  5. Check dist weights — if other constraints filter the domain so that a dist block references values that can no longer be reached, the solver fails.
  6. Use randomize() with {} to narrow — add inline constraints that pin suspected variables to specific values to confirm which combination is infeasible.
AXI4Transaction txn = new();

// WRONG — silent failure if over-constrained
txn.randomize();

// CORRECT — fatal on failure
if (!txn.randomize())
  `uvm_fatal("RAND_FAIL", "randomize() returned 0 — check constraints")

// Debug: disable blocks one by one
txn.c_wrap.constraint_mode(0);
if (txn.randomize()) begin
  $display("c_wrap was the problem — check WRAP burst length rules");
end
txn.c_wrap.constraint_mode(1);

// Debug: pin a variable to check one slice
if (!txn.randomize() with { burst_type == 2'b10; burst_len == 8'd5; })
  $display("WRAP with len=5 is over-constrained (must be 1,3,7,15)");

Common over-constrained patterns

Root causeExampleFix
Contradictory rangesa > 100; a < 50Reconcile range bounds
dist references unreachable valuesx dist {255 := 10}; x < 10Align dist set with range
Implication chain deadlocksa -> b; b -> !aBreak circular dependency
randc exhausted domainAll values of randc variable filtered outWiden constraints on randc variable
solve-before creates empty domainLeading variable has only one value that makes second infeasibleBroaden leading variable domain

Key Takeaways — Day 16

FAQ

What is the difference between rand and randc in SystemVerilog?

rand variables are randomized independently each time randomize() is called, with the solver free to repeat any value. randc (random cyclic) guarantees that every legal value is generated exactly once before any value repeats — similar to shuffling a deck of cards. randc is useful for ensuring complete coverage without explicit bins, but it only applies to integer variables and the cycle resets whenever constraints change.

Why does randomize() return 0 even though my constraints look correct?

A return value of 0 means the solver could not find a valid solution — the constraints are over-constrained. Common causes include: contradictory range constraints (e.g., a > 100 and a < 50 together), an implication constraint that creates an unsatisfiable dependency, a dist block whose weights sum to zero after other constraints filter the domain, or a solve-before ordering that makes the leading variable's range empty. Use constraint_mode(0) to disable constraints one by one until randomize() succeeds, then re-enable them to isolate the conflict.

When should I use soft constraints instead of regular constraints?

Use soft constraints when you want a preferred default value but need sub-classes or inline with{} blocks to be able to override it without causing a conflict. Hard constraints always participate in the AND of all active constraints and cannot be overridden — they simply make the problem infeasible if contradicted. Soft constraints are discarded silently when they conflict with a hard constraint or a higher-priority soft constraint. A typical pattern is to put soft constraints in a base transaction class as sensible defaults and override them in specific test scenarios using inline constraints.

What does solve-before do and when is it needed?

solve a before b tells the constraint solver to pick a value for variable a first, then solve for b given that value. Without solve-before, the solver is free to optimise the joint distribution, which often collapses biased distributions. The classic example: if mode is 1-bit (50% chance of 0 or 1) and data is constrained to {0} when mode==0 and {0..255} when mode==1, you want a true 50/50 split on mode. Without solve-before, the solver might pick values that statistically favour mode==1 because there are 256 solutions in that branch vs 1 in the mode==0 branch. Adding solve mode before data forces the 50/50 split before the solver enumerates data's range.