SystemVerilog Constraints and Randomization Explained
Directed tests only check the cases you already thought of. Constrained random verification turns that around: you describe the legal space of inputs once, and the simulator generates thousands of distinct, valid stimuli for you, corner cases included. SystemVerilog bakes this into the language through rand variables, constraint blocks, and the randomize() method. This guide walks through every core piece with code you can actually run.
rand vs randc: two kinds of randomness
A class variable marked rand gets a fresh random value every time you call randomize(). Values can repeat from one call to the next, which is what you usually want for data fields. A variable marked randc is cyclic: the solver walks through every value in its range once, in a random order, before any value repeats. Use randc for things like a small opcode or a priority field where you want guaranteed coverage of all states before duplicates appear.
The example below declares a packet with one randc field (priority_id, 2 bits, so four values) and two rand fields bounded by inside ranges. Notice how priority_id never repeats across the four draws.
module tb;
class Packet;
rand bit [7:0] length;
randc bit [1:0] priority_id;
rand bit [3:0] src_port;
constraint c_len { length inside {[8:64]}; }
constraint c_port { src_port inside {[1:14]}; }
endclass
Packet p;
initial begin
p = new();
for (int i = 0; i < 4; i++) begin
if (p.randomize() == 0)
$fatal(1, "randomize failed");
$display("run=%0d length=%0d priority_id=%0d src_port=%0d",
i, p.length, p.priority_id, p.src_port);
end
$finish;
end
endmodulerun=0 length=57 priority_id=0 src_port=7 run=1 length=63 priority_id=2 src_port=3 run=2 length=57 priority_id=1 src_port=10 run=3 length=16 priority_id=3 src_port=6
Constraint blocks and the inside operator
A constraint block is a named set of relational rules the solver must satisfy on every call. It is declarative, not procedural: the order of lines does not matter, and the solver finds a value that makes all active constraints true simultaneously. Above, c_len and c_port both apply at once.
The inside operator is the cleanest way to express set and range membership. length inside {[8:64]} means “any value from 8 to 64 inclusive.” You can also list discrete values and ranges together, for example x inside {0, 2, [10:20]}. Prefix it with ! to invert it. Always remember to size your field wide enough for the range; an 8-bit field cannot hold the value 300, and the solver will simply fail.
dist: weighting the odds
Plain constraints give you a roughly uniform spread. When you want some values more often than others, use the dist operator. Each item carries a weight. The := operator assigns that weight to each listed value, while :/ divides the weight evenly across a range. Weights are relative, not percentages, so {0 := 1, 1 := 4} simply means value 1 is four times as likely as value 0.
This snippet is shown for syntax reference. Weighted distribution is a solver feature that not every open simulator implements, so treat it as a pattern to carry into a full tool such as a commercial simulator.
class Bus;
rand bit [1:0] mode; // 0=IDLE 1=READ 2=WRITE 3=BURST
// ':=' gives each listed value that weight.
// ':/' spreads one weight evenly across a whole range.
constraint c_mode {
mode dist { 0 := 1, 1 := 4, 2 := 4, 3 := 1 };
}
endclassImplication: conditional constraints
Real protocols are full of “if this, then that” rules. The implication operator -> expresses exactly that. The constraint is_jumbo -> payload > 200 reads as: whenever is_jumbo is true, payload must exceed 200. When is_jumbo is false, the constraint imposes nothing and payload is free. Implication is one-directional, which is the usual source of confusion: a large payload does not force is_jumbo to be set. If you need the rule to work both ways, use if...else or a biconditional.
The testbench draws twelve frames and self-checks the implication on every single one, which is a habit worth keeping: a constraint plus an assertion documents your intent twice.
module tb;
class Frame;
rand bit [7:0] payload;
rand bit is_jumbo;
// Implication: a jumbo frame forces a large payload.
// When is_jumbo is 0, payload is left free.
constraint c_jumbo { is_jumbo -> payload > 200; }
constraint c_pl { payload > 0; }
endclass
Frame f;
int jumbo_cnt, normal_cnt;
initial begin
f = new();
for (int i = 0; i < 12; i++) begin
if (f.randomize() == 0) $fatal(1, "randomize failed");
if (f.is_jumbo) jumbo_cnt++; else normal_cnt++;
// Prove the implication on every single draw
if (f.is_jumbo && !(f.payload > 200))
$fatal(1, "implication broken: jumbo with payload %0d", f.payload);
$display("draw=%0d is_jumbo=%0d payload=%0d", i, f.is_jumbo, f.payload);
end
$display("jumbo=%0d normal=%0d implication held every time",
jumbo_cnt, normal_cnt);
$finish;
end
endmoduledraw=0 is_jumbo=0 payload=110 draw=1 is_jumbo=0 payload=14 draw=2 is_jumbo=0 payload=66 draw=3 is_jumbo=0 payload=157 draw=4 is_jumbo=0 payload=30 draw=5 is_jumbo=0 payload=60 draw=6 is_jumbo=0 payload=14 draw=7 is_jumbo=0 payload=109 draw=8 is_jumbo=0 payload=190 draw=9 is_jumbo=1 payload=241 draw=10 is_jumbo=1 payload=221 draw=11 is_jumbo=0 payload=45 jumbo=2 normal=10 implication held every time
solve…before and inline randomize() with
Two features often go together when you need finer control. The first is solve...before. The solver normally picks values for all random variables at once, which can skew the distribution of a flag that gates a range. Consider a flag use_high and an address that lands in a low window or a high window depending on it. Treated as one combined space, one window may dominate. Writing solve use_high before addr tells the solver to choose use_high first, then addr, so both halves of the map are exercised fairly. Crucially, solve...before changes the probability distribution only; it never changes which solutions are legal.
The second is the inline constraint, randomize() with { ... }. It adds extra rules for a single call without editing the class. Class constraints and the inline block both apply, so the result must satisfy everything. This is perfect for a test that wants one specific scenario out of an otherwise general-purpose class. The second loop below pins use_high to 1 and caps addr below 220.
module tb;
class Addr;
rand bit use_high;
rand bit [7:0] addr;
// Without a hint, the solver treats {use_high, addr} as one space,
// making use_high==1 rare. solve...before makes the tool pick
// use_high first, so both halves of the map are exercised evenly.
constraint c_map {
if (use_high) addr inside {[200:255]};
else addr inside {[0:63]};
}
constraint c_order { solve use_high before addr; }
endclass
Addr a;
int high_cnt;
initial begin
a = new();
for (int i = 0; i < 10; i++) begin
if (a.randomize() == 0) $fatal(1, "randomize failed");
if (a.use_high) high_cnt++;
end
$display("use_high chosen %0d/10 times (solve-before balances it)", high_cnt);
for (int i = 0; i < 4; i++) begin
if (a.randomize() with { use_high == 1; addr < 220; } == 0)
$fatal(1, "inline randomize failed");
$display("inline draw %0d: use_high=%0d addr=%0d", i, a.use_high, a.addr);
end
$finish;
end
endmoduleuse_high chosen 6/10 times (solve-before balances it) inline draw 0: use_high=1 addr=201 inline draw 1: use_high=1 addr=206 inline draw 2: use_high=1 addr=211 inline draw 3: use_high=1 addr=217
Common pitfalls to watch
| Symptom | Likely cause |
|---|---|
randomize() returns 0 | Constraints contradict each other, or a value cannot fit the field width. Check ranges and bit sizes. |
| A field never changes | You forgot the rand keyword, so it keeps its default value. |
| Implication seems ignored | Remember -> is one-way; the consequent does not force the antecedent. |
| Skewed distribution | Add solve...before or dist to steer the solver’s choices. |
| Values repeat too soon | Use randc instead of rand when you need cyclic coverage. |
Checking the return value
Every randomize() call returns 1 on success and 0 on failure. Never ignore it. A failed randomize usually means over-constrained logic, and silently continuing lets a broken test “pass.” The examples above all branch on the return value and call $fatal if it fails, which is the minimum discipline for any real environment.
Quick check
What is the difference between rand and randc?
A rand variable can take any value in its range on each call, and values may repeat. A randc variable is cyclic: it visits every value in its range once, in random order, before any value repeats.
Does the constraint a -> b > 10 force a to be true when b is 50?
No. Implication is one-directional. It only requires that when a is true, b > 10. A large b places no requirement on a; a can be true or false.
Does solve…before change which values are legal?
No. It only changes the order in which the solver assigns variables, which affects the probability distribution of the results. The set of legal solutions is identical with or without it.
If a class already constrains length to [8:64], what does randomize() with { length == 100; } do?
It fails and returns 0. The inline constraint is added to, not substituted for, the class constraints, so 100 violates the existing [8:64] range and no legal solution exists.
Share your question in comments or talk to our mentor team for batch guidance.
Compile and simulate this in the browser — no install. Edit the code and re-run instantly.
Learn Verilog with mentors, labs and placement support
This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.
Explore VLSI Courses