SystemVerilog Randomization: rand, randc and Constraints
Directed tests only check the cases you already thought of. The bugs that reach silicon are usually the ones you did not. SystemVerilog randomization is how a verification engineer hands that problem to the tool: you describe the space of legal stimulus with a class and a few constraints, and a built-in solver draws fresh, legal values on every call. This is the engine behind constrained-random verification (CRV) and, by extension, every UVM sequence you will ever write. We will build the full picture from first principles using small programs you can compile and run yourself.
Randomization lives inside classes. You tag data members with the rand or randc keyword, optionally pin down their legal values with constraint blocks, then call the built-in randomize() method on an object handle. The solver assigns every active random member a value that satisfies every active constraint simultaneously. It returns 1 on success and 0 when the constraints cannot all be met at once, so you should always check that return value.
rand versus randc
Both keywords make a member solvable, but they differ in how values are distributed over repeated calls. A rand member is drawn independently each time, roughly uniformly across its legal values, and the same value may appear on two calls in a row. A randc member is random-cyclic: the solver builds a random permutation of the member’s entire legal range and walks through it, so no value repeats until every value has been produced once. After the full cycle is exhausted, a new permutation begins.
Use randc when you want a short, guaranteed sweep of every possibility, for example cycling through all opcodes or all small address indices so no case is accidentally starved. The cost is that the solver must enumerate the whole range, so keep randc members narrow (a few bits). In the program below the randc b column produces all eight 3-bit values exactly once across eight iterations, while rand a is free to repeat.
| Property | rand | randc |
|---|---|---|
| Repetition | Values may repeat on any call | No repeat until the full range is covered |
| Distribution | Independent, roughly uniform | Random permutation of all legal values |
| Typical width | Any | Keep narrow (enumerates whole range) |
| Use for | General data, payloads, addresses | Exhaustive sweeps of small sets |
// dice.sv - rand vs randc
module tb;
class Dice;
rand bit [2:0] a; // may repeat freely
randc bit [2:0] b; // visits all 8 values before repeating
constraint c_a { a inside {[1:6]}; }
endclass
Dice d;
int ok;
initial begin
d = new();
$display("iter | rand a | randc b");
for (int i = 0; i < 8; i++) begin
ok = d.randomize();
if (ok == 0) $display("randomize failed at iter %0d", i);
$display(" %0d | %0d | %0d", i, d.a, d.b);
end
$finish;
end
endmoduleiter | rand a | randc b 0 | 1 | 7 1 | 2 | 5 2 | 3 | 6 3 | 2 | 3 4 | 6 | 0 5 | 2 | 4 6 | 1 | 2 7 | 1 | 1
Notice the constraint c_a restricts a to the range 1 to 6, turning an 8-value member into a legal-die roll. randc b has no constraint, so its cycle covers the full 0 to 7 set.
Calling randomize() and inline constraints
You rarely want to hard-code every scenario inside the class. The randomize() method accepts an inline with { ... } block whose constraints apply only to that one call, layered on top of the class constraints. This lets a sequence steer the object, for example forcing a particular mode, without editing or sub-classing the transaction. The constraints inside with are combined with the class constraints, and all of them must hold together.
Constraints are not procedural statements; they are relations the solver satisfies. One of the most useful is the implication operator ->, which reads “if the left side is true, the right side must hold.” In the example, a wrapping burst is limited to power-of-two lengths. The inline block then demands a wrapping burst with a length greater than 2, so the only values the solver can legally pick are 4 and 8.
// burst.sv - inline constraints and implication
module tb;
class Burst;
rand bit is_wrap;
rand bit [3:0] len; // beats in the burst
// wrapping bursts are restricted to legal power-of-two lengths
constraint c_wrap { is_wrap -> len inside {1, 2, 4, 8}; }
endclass
Burst b;
int ok;
initial begin
b = new();
// Force a wrapping burst and demand len > 2 with an inline "with" block.
for (int i = 0; i < 4; i++) begin
ok = b.randomize() with { is_wrap == 1; len > 2; };
if (ok == 0) $display("randomize failed");
$display("wrap=%0d len=%0d", b.is_wrap, b.len);
end
$finish;
end
endmodulewrap=1 len=4 wrap=1 len=8 wrap=1 len=4 wrap=1 len=8
Weighted distributions with dist
Uniform randomness wastes cycles on boring stimulus. The dist operator biases the draw toward the values you care about. Each item carries a weight, and two operators divide that weight differently. The := operator gives the stated weight to each value in the item, while :/ spreads the weight across the whole range so every value in it shares a slice. Weights are relative, not percentages, so READ := 6, WRITE := 3, IDLE := 1 simply means a 6:3:1 ratio.
In the class below, operation type is biased roughly 60/30/10, and length is biased so that short bursts of 1 to 4 beats dominate (weight 80 shared across that range) while the wide 5 to 255 band is rare (weight 20 shared across it). Weighted distributions are a cornerstone of realistic traffic shaping. Simulator support for dist varies, so treat this as a reference pattern to drop into your transaction class rather than a universally runnable snippet.
// txn.sv - weighted distribution with dist
module tb;
typedef enum {READ, WRITE, IDLE} op_e;
class Txn;
rand op_e op;
rand bit [7:0] len;
// READ 60%, WRITE 30%, IDLE 10%
constraint c_op { op dist {READ := 6, WRITE := 3, IDLE := 1}; }
// small lengths very common, the wide band rare
constraint c_len { len dist {[1:4] :/ 80, [5:255] :/ 20}; }
endclass
Txn t;
int ok;
initial begin
t = new();
ok = t.randomize();
$display("op=%s len=%0d", t.op.name(), t.len);
end
endmodulepre_randomize and post_randomize
The solver calls two optional callbacks on every randomize(). pre_randomize() runs just before solving, and post_randomize() runs right after. You override them as plain void functions. Use pre_randomize() to set up guidance, such as copying in a mode flag or counting attempts, and use post_randomize() to compute fields that are derived from the random members rather than solved for, such as a CRC, a parity bit, or a formatted address.
The Frame class keeps parity and attempts outside the rand set. The pre-callback bumps the attempt counter, and the post-callback folds the payload bits with a reduction XOR to produce the parity bit. Because these run automatically, the derived values stay consistent with the randomized payload without any extra call in your test.
// frame.sv - pre_randomize and post_randomize hooks
module tb;
class Frame;
rand bit [3:0] payload;
bit parity; // derived, not randomized
int attempts; // bookkeeping
function void pre_randomize();
attempts++;
$display("pre : attempt #%0d starting", attempts);
endfunction
function void post_randomize();
parity = ^payload; // XOR reduction = parity bit
$display("post: payload=%b parity=%b", payload, parity);
endfunction
endclass
Frame f;
int ok;
initial begin
f = new();
for (int i = 0; i < 3; i++) begin
ok = f.randomize();
if (ok == 0) $display("randomize failed");
end
$display("total attempts = %0d", f.attempts);
$finish;
end
endmodulepre : attempt #1 starting post: payload=1011 parity=1 pre : attempt #2 starting post: payload=1100 parity=0 pre : attempt #3 starting post: payload=0001 parity=1 total attempts = 3
Switching things off: constraint_mode and rand_mode
A single transaction class often serves many scenarios. Two built-in controls let you reshape it at run time without rewriting it. constraint_mode(0) called on a named constraint handle disables that constraint; constraint_mode(1) re-enables it. This is the clean way to inject illegal stimulus: turn off the legality constraint, randomize, and watch the design handle out-of-range input. rand_mode(0) called on a member turns off its randomization, so the member keeps its current value and the solver treats it as a fixed input while still solving the remaining members around it.
Follow the four stages in the program. With everything default, base stays within 0 to 15. After c_low.constraint_mode(0), base can use its full 8-bit range. After offset.rand_mode(0), the offset is frozen at whatever it last held while base is still randomized. Re-enabling both restores the original behaviour. Both methods also have a function form that returns the current state when you call them with no argument.
// addr.sv - constraint_mode and rand_mode control knobs
module tb;
class Addr;
rand bit [7:0] base;
rand bit [7:0] offset;
constraint c_low { base inside {[0:15]}; } // keep base small
endclass
Addr a;
int ok;
initial begin
a = new();
// 1) default: c_low active, both fields random
ok = a.randomize();
$display("default : base=%0d offset=%0d", a.base, a.offset);
// 2) switch off the c_low constraint -> base may use full range
a.c_low.constraint_mode(0);
ok = a.randomize();
$display("c_low off : base=%0d offset=%0d", a.base, a.offset);
// 3) freeze offset with rand_mode(0): keep value, randomize the rest
a.offset.rand_mode(0);
a.base = 0;
ok = a.randomize();
$display("offset frozen: base=%0d offset=%0d (unchanged)", a.base, a.offset);
// 4) re-enable both
a.c_low.constraint_mode(1);
a.offset.rand_mode(1);
ok = a.randomize();
$display("all restored : base=%0d offset=%0d", a.base, a.offset);
if (ok == 0) $display("randomize failed");
$finish;
end
endmoduledefault : base=11 offset=16 c_low off : base=99 offset=222 offset frozen: base=61 offset=222 (unchanged) all restored : base=6 offset=4
Habits that keep you out of trouble
- Always test the return value of
randomize(). A silent0means an over-constrained class and stale values, which hides real coverage holes. - Prefer narrow
randcmembers; a wide one forces the solver to enumerate an enormous range. - Put derived values in
post_randomize(), never in therandset, so they cannot contradict the solved members. - Reach for inline
withandconstraint_modebefore you copy a class. Reusing one transaction keeps your environment small.
These five constructs, rand and randc, constraints and inline with, dist weights, the pre and post callbacks, and the two mode controls, are the complete vocabulary of stimulus generation in SystemVerilog. Everything a UVM sequence does is built on top of them.
Quick check
What is the key behavioural difference between rand and randc?
A rand member is drawn independently each call and may repeat immediately. A randc member is random-cyclic: it walks a random permutation of its entire legal range and never repeats a value until all values have been produced once.
In dist, how do the := and :/ operators differ?
With :=, the stated weight is given to each individual value in the item. With :/, the weight is divided across the whole range so the values in that range share it. For a single value the two are identical.
Where should a CRC or parity bit be computed, and why?
In post_randomize(). It is derived from the randomized members, not solved for, so computing it after randomization keeps it consistent with the payload. It should not be a rand member.
You want to inject an illegal address that your legality constraint normally forbids. What is the cleanest way?
Call constraint_mode(0) on that constraint’s handle to disable it for the next randomize(), then re-enable it with constraint_mode(1). No sub-classing or rewrite is needed.
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