Skip to main content

Randomization and Constraints

A plain Verilog testbench generates stimulus by hand — a directed sequence of specific values the test writer chose. That finds exactly the bugs the writer thought to test for, and nothing else. Constrained-random verification flips this: generate stimulus randomly, but constrained to stay legal (a packet length that's actually a valid packet length, an address that's actually in range) — covering combinations of inputs the test writer never thought to write by hand, while still never generating a value the DUT wasn't supposed to receive.

rand and randc​

class transaction;
rand bit [7:0] data;
randc bit [1:0] opcode;
endclass

rand marks a property as randomizable with an ordinary uniform random distribution — the same value can repeat across successive randomizations, exactly like rolling a die repeatedly. randc ("random-cyclic") instead guarantees every possible value is produced exactly once before any value repeats — for opcode above, with 4 possible 2-bit values, eight calls to randomize() (two full cycles) produce every one of the 4 values exactly twice, in some random order each cycle, never repeating a value early. randc is useful specifically when coverage of every possible value matters more than uniform probability — but is more expensive to compute and is practically restricted to small ranges (randc over a full 32-bit field is not realistic), so rand is the default choice unless exhaustive cycling of a small value set is specifically the goal.

randomize()​

transaction tr = new();

if (!tr.randomize())
$display("ERROR: randomization failed");

$display("data=%0h opcode=%0d", tr.data, tr.opcode);

Calling .randomize() on an object assigns new random values to every rand/randc property, honoring any constraints declared on the class (below). It returns 1 on success and 0 if no legal solution exists for the current constraints — checking that return value (as above) matters, since a testbench that ignores a randomization failure silently proceeds with stale, unrandomized data instead of catching the constraint conflict that caused the failure.

Constraint blocks​

class transaction;
rand bit [7:0] data;
rand bit [3:0] length;

constraint valid_length {
length inside {[1:15]}; // length is never 0 or above 15
}

constraint even_data {
data % 2 == 0; // only even data values
}
endclass

A constraint block restricts which random values are legal for the class's rand/randc properties — randomize() only ever produces values satisfying every active constraint simultaneously. inside {[1:15]} restricts length to a range; ordinary relational/arithmetic expressions (data % 2 == 0) work directly inside a constraint the same as anywhere else in the language. A class can declare any number of constraint blocks, and all of them apply together — valid_length and even_data above both constrain the same randomize() call.

Inline constraints: randomize() with​

tr.randomize() with {
length == 8;
data > 100;
};

randomize() with { ... } adds temporary, call-site-specific constraints on top of the class's own declared constraints, without changing the class itself — useful for a specific test that needs to force one particular randomization run into a narrower range (exactly length 8, in this example) while every other call to tr.randomize() elsewhere in the testbench still uses the class's general constraints unmodified.

More constraint operators​

Constraint blocks aren't limited to plain relational expressions — a handful of dedicated operators show up constantly in real testbenches:

class packet;
rand bit [3:0] len;
rand bit is_broadcast;
rand bit [7:0] addr[4];

constraint len_dist {
len dist { 1 := 5, [2:14] := 60, 15 := 5 }; // weighted, not uniform
}

constraint broadcast_addr {
is_broadcast -> addr[0] == 8'hFF; // implication
}

constraint addr_range {
foreach (addr[i])
addr[i] inside {[0:200]}; // constrain every array element
}

constraint order_hint {
solve is_broadcast before addr; // pick is_broadcast first
}
endclass
  • dist assigns weighted, non-uniform probability instead of rand's default uniform spread. 1 := 5 means the value 1 gets weight 5; [2:14] := 60 means that whole range shares weight 60 (not 60 each) — use :/ instead of := when a range's weight should be divided evenly across its values rather than shared as one lump. dist only applies to rand properties — it cannot be used to weight a randc property, since randc's cyclic guarantee (every value exactly once per cycle) is already a fixed, non-probabilistic scheme that a weighted distribution would directly conflict with.
  • -> (implication) encodes if-then logic inside a constraint: is_broadcast -> addr[0] == 8'hFF only forces addr[0] when is_broadcast is 1; when is_broadcast is 0, that half of the constraint drops out entirely and addr[0] is free. if (...) ... else ... blocks work inside a constraint the same way, for cases with more than two branches.
  • foreach applies a constraint expression to every element of an array property in one line, instead of writing one constraint per index — addr[i] inside {[0:200]} above applies to all four elements of addr.
  • solve ... before doesn't change which values are legal, only the order the solver picks them in — biasing which corner cases show up more often when multiple valid solutions exist for the same constraint set. It's a probability-distribution hint, not a correctness requirement: a solver ignoring it entirely would still only ever produce legal values.
  • rand_mode()/constraint_mode() toggle randomization and constraint blocks on/off at runtime, per-property or per-constraint, without editing the class: tr.addr.rand_mode(0) stops addr from being randomized on the next randomize() call (it keeps whatever value it already had), and tr.addr_range.constraint_mode(0) disables just that one constraint block while leaving every other constraint on the class active.

Randomizing only some fields, and forcing distinct values​

Two more tools come up constantly once a class has several rand properties: randomizing only a subset of them, and forcing a group of them to never collide.

class packet;
rand bit [3:0] len;
rand bit [7:0] addr[4];
rand bit [7:0] header_id, footer_id;

constraint distinct_ids {
unique { header_id, footer_id };
}
endclass

packet pkt = new();
pkt.len = 8; // held fixed — set directly, not randomized
void'(pkt.randomize(addr, header_id, footer_id)); // only these three are randomized
  • randomize(field1, field2, ...) — passing a list of property names into randomize() restricts randomization to only those properties; every other rand/randc property on the object keeps whatever value it already had, completely untouched. This is a different mechanism from rand_mode(0): rand_mode(0) is a persistent per-property toggle that affects every future randomize() call until re-enabled, while naming fields directly in one randomize(...) call only affects that single call.
  • unique { a, b, c } — a constraint guaranteeing every variable (or array element) listed never equals any other listed in the same unique block. Above, unique { header_id, footer_id } guarantees the solver never assigns them the same value — useful for anything that must never accidentally collide (two IDs meant to be distinguishable, a set of addresses that must each be unique).
  • std::randomize() — for randomizing a plain variable that isn't a class property at all (a local variable inside a task, for example), the class-based .randomize() method call doesn't apply; the free function std::randomize(var) with { ... } fills the same role outside of any class.

Why this replaces hand-written directed tests, not just supplements them​

Directed testingConstrained-random testing
Test writer manually enumerates specific input valuesrandomize() (plus constraints) generates legal values automatically
Coverage is exactly what the writer thought to writeCoverage extends to combinations the writer never explicitly considered
Adding a new legal input case means writing new test codeOften free — the existing randomized test already explores the new case eventually
Finding a corner-case bug requires guessing it existsA large enough randomized run can stumble onto it without anyone guessing first

The tradeoff (and it's real, not free) is that constrained-random testing needs a way to know, automatically, whether the DUT's response to an unpredictable input was actually correct — a hand-written directed test's expected value is trivial to compute by hand, but a scoreboard checking thousands of randomly-generated transactions needs its own automated reference model. Testbench covers building that scoreboard/checker infrastructure; this topic's job stops at generating the randomized stimulus itself.

What's next​

The next page goes deeper on constraint techniques that come up constantly in real testbenches: soft constraints (a default that can be overridden rather than a hard rule), enabling/disabling constraints at runtime, and hooks that run automatically before and after every randomize() call.