When verifying a digital design, one of the core challenges is generating meaningful stimulus — inputs that exercise the design the way it will be used in the real world. SystemVerilog constraints let you define the boundaries of legal behavior and let the simulator's built-in solver figure out what values to generate within those boundaries.
14 min read | Intermediate Level
What You'll Learn
- The difference between direct and randomized tests, and why constrained-random became the dominant verification methodology
- How to declare a constraint, and the difference between
randandrandc - The core constraint expression types: relational,
inside,dist, and implication - How to enable and disable constraints at runtime, and add a one-off constraint without changing the class
Direct Tests vs Randomized Tests
What Are Direct Tests?
A direct test is fully hand-crafted. You explicitly set every signal to every value, in the exact sequence you want:
// Direct test: manually drive a specific transaction
pkt.addr = 8'h10;
pkt.data = 8'hAB;
pkt.cmd = WRITE;
Direct tests are fast to write for well-understood scenarios and are great for checking a specific known bug. However, they don't scale — a moderately complex design might have hundreds of valid configurations, and writing a direct test for every combination is impractical.
What Are Randomized Tests?
A randomized test relies on the simulator to generate values automatically. The test runs repeatedly with different seeds, so each run exercises a different slice of the design's behavior:
// Randomized test: let the solver pick the values
assert(pkt.randomize());
Over many runs, randomized tests naturally exercise corner cases a human engineer simply wouldn't think to write explicitly — exactly why constrained-random verification is the dominant methodology in modern chip verification, including industry frameworks like UVM. The catch is the word constrained: purely unconstrained randomization is noisy, spending most of your simulation budget on invalid or uninteresting inputs. Constraints are how you guide the randomizer to stay in bounds.
What Is a Constraint?
A constraint narrows the space of valid random values for one or more variables. You define constraints inside a class, alongside the variables they govern. The solver reads every active constraint and picks values that satisfy all of them simultaneously:
class Packet;
rand bit [7:0] addr;
rand bit [7:0] data;
constraint addr_range { addr inside {[8'h00 : 8'h7F]}; }
endclass
randkeyword: marking a variablerandtells the solver it's a candidate for randomization. Without it,randomize()ignores the variable entirely.constraintblock: named with a label (addr_range), the body is an expression that must evaluate to true.insideoperator: restrictsaddrto the lower half of the 8-bit range.
8'h00 : 8'h7F spans exactly 128 values (0 through 127) out of the 256 possible values an 8-bit field can hold — precisely half the full range, which is why it's described as "the lower half."
The rand and randc Keywords
rand — Uniform Random
Variables declared with rand are assigned uniformly random values within the constraint space on each call to randomize(). Each call is independent — you might get the same value twice in a row:
rand bit [3:0] opcode; // any value 0-15, independently each time
randc — Random Cyclic
Variables declared with randc cycle through all possible values before repeating any — like drawing cards from a shuffled deck without replacement:
randc bit [3:0] opcode; // cycles through 0-15 before repeating
randc is particularly useful when you want guaranteed coverage of all values, not just statistically likely coverage — verifying a register address space where every address must be hit is a classic case.
Quick Check
Q: A randc bit [3:0] opcode has already produced 0, 5, 12, and 3 across its first four calls to randomize(). Could the fifth call produce 5 again?
Show Answer
No. randc guarantees no value repeats until every one of its 16 possible values (0 through 15) has been produced exactly once — with only 4 of those 16 used so far, 5 can't come up again yet.
Writing a Constraint Block
Multiple expressions inside one constraint block are treated as a logical AND — all must be satisfied:
class BusTransaction;
rand bit [7:0] addr;
rand bit [31:0] data;
rand bit [1:0] burst_len;
// addr must be 4-byte aligned
constraint align_addr { addr[1:0] == 2'b00; }
// burst_len between 1 and 3
constraint valid_burst { burst_len >= 1; burst_len <= 4; }
endclass
burst_len is only 2 bits wide, so it can never actually hold a value of 4 — the type itself already caps it at 3. The burst_len <= 4 clause is therefore redundant and never actually restricts anything beyond what the type already guarantees, even though the comment describes the intended range as "1 and 3." The constraint that actually does the work here is just burst_len >= 1.
You can also write multiple separate constraint blocks. Keeping constraints logically separated makes them easier to enable, disable, and override in subclasses — which matters as your testbench grows.
Calling randomize()
Constraints only come into play when you call randomize() on a class object. This method invokes the solver, generates new values, and returns 1 (success) or 0 (failure — meaning no valid combination existed):
Packet pkt = new();
// Correct: assert will trigger a fatal if randomize fails
assert(pkt.randomize()) else $fatal(1, "Randomization failed!");
Always check the return value. A failing randomize() is a silent killer in testbenches — ignore the return value, and your variables hold stale data from the previous call with no indication anything went wrong. If the solver can't find values satisfying every constraint simultaneously, it returns 0, typically meaning the constraints are over-specified or outright contradictory:
constraint impossible { addr > 8'hFF; } // addr is 8-bit, can never exceed 0xFF
The solver fails immediately on this — addr can never legally exceed 0xFF since that's already the maximum an 8-bit field can hold.
Constraint Expressions: A Quick Tour
Relational Constraints
constraint c1 { data < 32'h1000; }
constraint c2 { addr >= 8'h10; addr <= 8'hF0; }
The inside Operator
Restricts a variable to a set or range of values, and can be negated with !:
constraint c3 { opcode inside {READ, WRITE, IDLE}; }
constraint c4 { size inside {[4:64]}; }
constraint c5 { !(opcode inside {RESERVED_1, RESERVED_2}); }
Weighted Distributions With dist
Controls the probability weighting of different values — useful when you want a biased distribution:
constraint cmd_dist {
cmd dist { READ := 70, WRITE := 30 };
}
Use := to assign a weight to each listed item individually, or :/ to split a weight evenly across a range:
// Values 0-7 SHARE a total weight of 10; value 8 gets 50 all to itself
constraint priority_dist {
priority dist { [0:7] :/ 10, 8 := 50 };
}
The difference between := and :/ here has a much bigger practical effect than the numbers alone suggest. :/ divides its weight across the whole range, so each of the 8 individual values in [0:7] only gets 10/8 = 1.25 weight units. := gives 8 the full 50 on its own. Out of a total weight of 60, that works out to roughly 83% for value 8 and just 2% for any single value between 0 and 7 — value 8 is about 40 times more likely than any one of the others, not just "5 times" as the raw 50-vs-10 comparison might suggest.
Quick Check
Q: If priority_dist were rewritten as priority dist { [0:7] := 10, 8 := 50 }; (changing :/ to :=), would value 8 still be roughly 40 times more likely than any single value in 0-7?
Show Answer
No. With :=, every value in [0:7] individually gets the full weight of 10 — the same as value 8 gets 50. The ratio becomes 50:10, or 5 times more likely, not 40 — a completely different distribution from swapping just that one operator.
Implication Constraints
Sometimes a constraint only applies when another condition is met, using ->:
constraint read_no_data {
(cmd == READ) -> (data == 0);
}
This reads: if cmd is READ, then data must be 0. If cmd is anything else, the constraint on data simply doesn't apply.
Where Is This Used?
Constrained-random combined with coverage-driven verification is the dominant methodology for complex SoC and IP verification — UVM is built almost entirely around this pattern. A single well-constrained testbench, run across enough random seeds, routinely finds bugs that no hand-written directed test list would ever have anticipated.
Enabling and Disabling Constraints
Every named constraint block can be turned on or off at runtime with constraint_mode():
pkt.addr_range.constraint_mode(0); // disable
pkt.addr_range.constraint_mode(1); // re-enable
This is extremely useful when the same transaction class is reused across many test scenarios — write every possible constraint into the class once, and selectively activate the ones each specific test needs, rather than creating a new subclass for every variation.
Inline Constraints With randomize() with
For a one-off constraint on a single randomize() call, without modifying the class:
assert(pkt.randomize() with { addr == 8'hFF; });
The inline constraint applies on top of every existing class constraint, for that one call only — a clean way to specialize a specific test scenario without polluting the base class.
Common Beginner Mistakes
Mistake #1: Forgetting rand or randc on a variable
Without one of these, randomize() silently ignores the variable entirely. Why? The variable keeps whatever value it already held — no error, no warning, just a value that quietly never changes across calls.
Mistake #2: Writing a constraint that's redundant with the variable's own type
As the burst_len example shows, a bound that the type already enforces (like <= 4 on a 2-bit field) doesn't cause an error, but it also doesn't do what a comment or the author might have intended. Why? This kind of mismatch between stated intent and actual effect is easy to miss in review, since the constraint compiles and runs without complaint.
Mistake #3: Ignoring randomize()'s return value
A failed randomize() leaves variables holding stale data from the previous call, with nothing to indicate anything went wrong. Why? Always wrap it in an assert (or explicit check) so a contradictory or over-constrained solve fails loudly instead of silently corrupting your stimulus.
Mistake #4: Confusing := and :/ in a dist constraint
As shown above, swapping one for the other can change a distribution dramatically — not just by a small margin. Why? := applies a weight to each listed value individually; :/ splits a weight across an entire range — mixing them up produces a distribution that looks superficially similar but behaves very differently in practice.
Practice Exercise
Challenge: Combine dist and Implication
Write a class Frame with rand bit [2:0] kind (0=DATA, 1=CTRL, 2=IDLE) and rand bit [7:0] len. Add a dist constraint making DATA occur 60% of the time, CTRL 30%, and IDLE 10%. Add a second constraint requiring len to be exactly 0 whenever kind is IDLE.
Hint
The dist weights don't need to be split across a range here — each of the three kind values is a distinct, individual outcome, so := is the right operator throughout.
Solution
class Frame;
rand bit [2:0] kind;
rand bit [7:0] len;
constraint c_kind_dist {
kind dist { 0 := 60, 1 := 30, 2 := 10 };
}
constraint c_idle_len {
(kind == 2) -> (len == 0);
}
endclass
Since all three weights (60, 30, 10) apply to individual values rather than ranges, they translate directly into percentages: DATA 60% of the time, CTRL 30%, IDLE 10%. The implication constraint only takes effect when kind actually lands on IDLE (2) — for DATA or CTRL, len is left completely unconstrained by this rule.
Summary
In this tutorial, you learned:
- Constraints define the legal value space for random variables, keeping stimulus realistic instead of arbitrary
randassigns independently each call;randccycles through every value before repeating any- Relational,
inside,dist, and implication (->) constraints cover the vast majority of everyday needs, and the solver satisfies all active constraints simultaneously constraint_mode()enables and disables a named constraint block at runtime, and inlinerandomize() with {}adds a one-off constraint without touching the class- Always check
randomize()'s return value — a failed solve leaves variables silently unchanged, not reset to some obviously-wrong value
The subsequent articles in this chapter dive deeper into each of these features: constraint blocks in depth, solve...before ordering hints, foreach constraints for arrays, soft constraints, and more.