SystemVerilog · Module 10
Introduction to Constrained Random Verification
Why CRV exists, where it sits between directed and pure random, the five-step generate→drive→observe→score→cover loop, and how it pairs with functional coverage.
Module 10 · Page 10.1
Module 10 · Page 10.1 Why writing tests by hand stops scaling after a hundred scenarios, what constrained randomisation actually solves, and how the whole CRV loop — randomise, constrain, drive, check, cover — fits together.
The Problem with Directed Testing
When you start verifying a new block, directed testing feels natural. You think of a scenario — write-to-address-zero, back-to-back-reads, maximum-burst-length — and write a test for each one. The first ten tests go fast. The first fifty are manageable.
Then the spec grows. The DUT has 200 registers, 15 bus commands, 4 operating modes, and 8 error conditions. The number of meaningful combinations explodes to millions. You cannot write a test for each one. Even if you could, you would finish the project years after the chip shipped — or did not ship because the bug you missed killed it.
This is not a hypothetical. Directed testing is how most verification projects start, and it is why most tape-out escapes — bugs found after the chip is manufactured — come from corner cases nobody thought to test.
Three Approaches — Directed, Random, Constrained Random
Directed TestingPro Tests exactly what you think of. Easy to debug. Con Does not scale. Misses what you did not think of. Every new scenario needs a new test file. Pure RandomPro Finds things you never imagined. Scales to millions of tests. Con Most random stimuli are illegal — wrong address ranges, invalid opcodes. The DUT just errors out. Low coverage efficiency. Constrained RandomBest of Both Random enough to find the unexpected. Constrained enough to stay legal. Millions of meaningful test cases from one class.
Constrained random verification is not a replacement for directed tests. You still write a small set of directed tests for the most critical scenarios — reset, register defaults, known error conditions. CRV handles everything else: the vast space of legal-but-unexpected combinations that no one would ever write by hand.
What Constrained Random Verification Actually Is
CRV means you describe what is legal rather than what to send. You tell the solver: "addr must be between 0x4000_0000 and 0x4FFF_FFFF, aligned to 4 bytes, and not equal to the reserved address 0x4000_0200." The solver picks a different legal value every time randomize() is called.
You write one transaction class. You run it a million times. Each run hits a different part of the address space, a different burst length, a different direction, a different combination of features — all legal, all meaningful, none of them you had to think of individually.
// ── DIRECTED: You specify every single value ──────────────────
task test_write_0x4000();
ApbTxn t = new();
t.addr = 32'h4000_0000; // hand-written
t.data = 32'hA5A5_A5A5; // hand-written
t.write = 1;
drv.drive(t);
endtask
// Need 1 task per scenario. 1000 scenarios = 1000 tasks.
// ── CONSTRAINED RANDOM: You describe what is legal ────────────
class ApbTxn;
rand bit [31:0] addr;
rand bit [31:0] data;
rand bit write;
constraint c_addr {
addr inside {[32'h4000_0000 : 32'h4FFF_FFFF]};
addr[1:0] == 2'b00; // word-aligned
addr != 32'h4000_0200; // reserved — avoid
}
endclass
// Run 1 000 000 legal, varied tests — no extra code
repeat(1_000_000) begin
ApbTxn t = new();
void'(t.randomize());
drv.drive(t);
endThe CRV Loop — Five Steps
Every constrained random testbench runs the same loop. Understanding this loop is understanding the entire methodology. 1RandomiseCall randomize() on the transaction object2ConstrainSolver picks legal values per constraint blocks3DriveDriver sends transaction to DUT via interface4CheckScoreboard compares expected vs actual DUT response5CoverCoverage model tracks which scenarios were hit You run this loop until the coverage model says you have hit all meaningful scenarios. That is your exit criterion — not "I ran N tests" but "I hit 100% of the coverage goals." Coverage-driven closure is what separates CRV from just running random stimuli and hoping for the best.
class ApbTxn;
rand bit [31:0] addr;
rand bit [31:0] data;
rand bit write;
// Step 2 — Constraints define the legal space
constraint c_addr { addr inside {[32'h4000_0000 : 32'h4FFF_FFFF]}; }
constraint c_align { addr[1:0] == 2'b00; }
endclass
module tb;
ApbTxn t;
initial begin
repeat(10_000) begin
t = new();
// Step 1 — Randomise the transaction
if (!t.randomize())
$fatal(1, "Solver failed — constraints may be over-constrained");
// Step 3 — Drive to DUT (simplified here)
$display("Driving: %s addr=0x%08h data=0x%08h",
t.write ? "WR" : "RD", t.addr, t.data);
// Step 4 — Check DUT response (scoreboard does this)
// scoreboard.check(expected, actual);
// Step 5 — Sample coverage (covergroup does this)
// apb_cov.sample(t);
end
// Run until coverage target is met
$display("Test complete — check coverage report");
end
endmodulerand and randc — First Look
Two keywords mark a class property for randomisation. Full coverage is in Page 10.2 — here is enough to understand what they do:
| Keyword | Full Name | Behaviour | Use For |
|---|---|---|---|
rand | Random | New value every randomize() call, picked independently from the full range (within constraints) | Address, data, burst length, direction |
randc | Random Cyclic | Cycles through every possible value exactly once before repeating — guarantees no repeats until the full set is exhausted | Transaction IDs, scenario selectors, round-robin patterns |
| (plain) | Not randomised | randomize() ignores this property entirely — you set it manually | Flags set by the driver, response fields filled by the monitor |
class Txn;
rand bit [7:0] data; // new random value each call
randc bit [3:0] seq_id; // cycles 0→1→...→15, then repeats
bit valid; // ignored by randomize() — set manually
endclass
module tb;
Txn t = new();
initial begin
repeat(5) begin
void'(t.randomize());
$display("data=%3d seq_id=%2d valid=%0b",
t.data, t.seq_id, t.valid);
end
end
endmodule
// Sample output:
// data=173 seq_id= 7 valid=0 ← valid unchanged (0)
// data= 44 seq_id=11 valid=0 ← seq_id no repeat yet
// data=211 seq_id= 2 valid=0
// data= 88 seq_id=14 valid=0
// data=167 seq_id= 5 valid=0Constraints — First Look
A constraint block is a named block inside a class that tells the solver which values are legal. Write it once — it applies to every randomize() call on that class.
class ApbTxn;
rand bit [31:0] addr;
rand bit [7:0] data;
rand bit write;
rand bit [2:0] burst_len;
// Each constraint has a name — use it to enable/disable later
constraint c_addr {
addr inside {[32'h4000_0000 : 32'h4FFF_FFFF]};
addr[1:0] == 2'b00; // must be word-aligned
}
constraint c_burst {
burst_len inside {[1:8]}; // valid lengths only
}
// Conditional constraint — writes need full burst, reads can be short
constraint c_write_burst {
(write == 1) -> (burst_len >= 4);
}
endclassCRV Only Works When Paired with Functional Coverage
Running a million random transactions means nothing if you cannot measure what you hit. Functional coverage is the feedback mechanism that tells CRV when to stop.
The loop is: randomise → drive → check → measure coverage → repeat until coverage closes. Without coverage, random testing is just noise. With coverage, it becomes a systematic, measurable process.
| Without Coverage | With Coverage |
|---|---|
| "I ran 100,000 tests" — no idea what was hit | "I hit all burst lengths 1–16, all address ranges, both read and write" — provable |
| No exit criterion — when do you stop? | Stop when coverage model reaches 100% of defined goals |
| Miss in corner case — undetected | Coverage hole visible — target it with additional constraints |
Functional coverage is covered in full in Module 11. For now, just know that CRV and coverage are inseparable — one without the other is incomplete.
Module 10 Roadmap — What Is Coming
- 10.1 Introduction to CRV (this page) Why CRV exists, the five-step loop, first look at rand and constraints.
- 10.2 rand & randc Keywords How rand and randc work internally, distribution behaviour, cyclic guarantee.
- 10.3 The randomize() Method & Return Value How the constraint solver works, why you must always check the return value, solver failure diagnosis.
- 10.4 Constraint Blocks Syntax, equality, range, set membership, implication, and best practices for naming.
- 10.5 Inline Constraints — the with Clause Add per-call constraints without changing the class — the most powerful everyday tool in CRV.
- 10.6 Constraint Modes Enable and disable named constraints at runtime for error injection and directed override.
- 10.7 – 10.14 Soft Constraints, Distributions, Conditional, Arrays, Callbacks, randcase, randsequence The complete constraint toolkit — each topic on its own page with full examples.
Verification Usage — Where CRV Lives in a Real UVM Environment
CRV is not a feature you sprinkle on top of a testbench — it is the architectural backbone of every modern verification environment. Four patterns recur across every UVM-based VIP.
The constrained transaction class (every protocol VIP)
Every UVM sequence item declares its protocol-shaped fields as rand and encodes the protocol's legality contract as inline constraints. A reader of the class understands the protocol's legal stimulus space by reading the constraint blocks; the solver enforces them automatically.
<code>class apb_xact extends uvm_sequence_item;
`uvm_object_utils(apb_xact)
rand bit [31:0] paddr;
rand bit [31:0] pwdata;
rand bit [3:0] pstrb;
rand bit pwrite;
// Protocol legality: address must be within the register map and
// 4-byte aligned; strobe must be all-ones for word writes.
constraint c_addr { paddr inside {[32'h4000_0000 : 32'h4000_FFFF]};
paddr[1:0] == 2'b00; }
constraint c_strb { pwrite -> pstrb == 4'b1111; }
endclass</code>The sequence library (per-test stimulus shaping)
Sequences extend a base sequence and override the constraint set by adding extends-time inline constraints (``uvm_do_with) or per-call inline with` clauses. The transaction class stays generic; the sequence narrows the stimulus to the scenario this specific test wants to exercise.
The reactive driver (request/response loop)
Drivers pull constrained transactions from the sequencer, drive them onto the bus, and may randomise inter-transaction delay locally via a per-transaction rand field. The constraint set scales naturally across drivers because every driver hits the same randomize() seam.
The coverage closure feedback loop
Coverage groups sample the same fields the constraint set governs. Gaps in coverage point directly at constraint weaknesses — bins that never fire are bins the constraints exclude. This makes the CRV ↔ functional coverage relationship a feedback loop: coverage tells you what your constraints missed; you tighten constraints; coverage convergence accelerates.
Simulation Behavior — How the Solver, the Scheduler, and Your Test Interact
randomize() is a synchronous call into the solver
When you write assert(tx.randomize());, the simulator suspends procedural execution and hands control to its constraint solver. The solver enumerates the active constraints, builds a satisfiability problem, and searches for a satisfying assignment. On success it writes every rand field and returns 1; on failure it returns 0 and leaves the fields unchanged. The call is fully synchronous — no events fire while the solver runs.
Seed determinism
Every UVM environment threads a seed through the simulator's PRNG. The same seed plus the same constraint set produces the same sequence of randomize() results — bit-for-bit reproducible. This is what makes "rerun the failing seed under waveform" a legitimate debug workflow rather than a hope-and-pray exercise.
rand vs randc — two different solver modes
rand draws each randomisation independently from the satisfying set — successive calls can repeat values. randc cycles through every value in the satisfying set without repeats before recycling — useful when you need exhaustive coverage of a small enum without coordinating from the test layer.
Solver cost scales with constraint complexity, not field width
A single 32-bit rand field with one range constraint solves in microseconds. Twenty interconnected fields with implication, distribution, and array-foreach constraints can take milliseconds per randomize. Over-constrained classes show up as "constraint solver is 40% of run-time" in simulator profiling — refactoring opportunity, not a bug.
<code>class read_xact;
rand bit [31:0] addr;
rand bit [3:0] len;
// Independent ranges — solver finishes in microseconds
constraint c_addr { addr inside {[0:'h0000_FFFF]}; }
constraint c_len { len inside {[0:15]}; }
endclass</code><code>class read_xact;
rand bit [31:0] addr;
rand bit [3:0] len;
rand bit [3:0] inflight[16];
// Highly coupled: every solve enumerates 16 inflights
// and reconciles them against addr/len simultaneously.
constraint c_a { foreach (inflight[i])
inflight[i] inside {[len:'hF]}; }
endclass</code>Waveform Analysis — Making Randomised Stimulus Visible in the Dump
CRV produces a flood of transactions — without instrumentation, the waveform viewer becomes a wall of identical-looking bus activity. Two disciplines turn that into a navigable record.
Stamp every transaction with an ID and a seed-derived tag
<code>class apb_xact extends uvm_sequence_item;
static int next_id = 0;
int id;
function new(string name = "apb_xact");
super.new(name);
id = next_id++;
endfunction
endclass
// In the driver, log the constrained values at drive time:
task drive(apb_xact t);
`uvm_info("DRV",
$sformatf("[%0t] xact #%0d addr=0x%08h data=0x%08h pwrite=%0d",
$time, t.id, t.paddr, t.pwdata, t.pwrite),
UVM_HIGH)
// ... drive the bus ...
endtask</code>ASCII view — randomised stimulus stream
<code> 100ns DRV xact #1 addr=0x4000_0010 data=0xDEAD_BEEF pwrite=1
300ns DRV xact #2 addr=0x4000_0044 data=0x0000_0000 pwrite=0
500ns DRV xact #3 addr=0x4000_0028 data=0xCAFE_BABE pwrite=1
700ns DRV xact #4 addr=0x4000_0014 data=0x0000_0000 pwrite=0
↑ ↑ ↑ ↑
sequential always inside wdata only alternating
IDs proves the legal map valid on writes read/write —
no duplicates range (=0 on reads) constraint
is firing</code>Surface the seed for failing tests
Every failing regression should include the simulator's PRNG seed in its log header. Replaying with the same seed reproduces the exact same constrained values — turning sporadic failures into deterministic ones. UVM's default report header prints the seed automatically; verify your test prints it explicitly if you have a custom logger.
Industry Insights — Hard-Won Lessons About CRV
Debugging Academy — Five Real Bugs From Early-CRV Adoption
"randomize() returns 0 and the test silently passes"
DEBUGA test runs to completion with no transactions reaching the DUT. Coverage is at 0% on every bin. No errors were logged.
<code>class apb_xact;
rand bit [31:0] paddr;
constraint c_addr { paddr inside {[32'h4000:32'h7FFF]}; }
endclass
task body();
apb_xact tx = apb_xact::type_id::create("tx");
tx.randomize(); // BUG: return value ignored
drive(tx); // tx fields are whatever was set last
endtask</code>When randomize() returns 0 (constraint unsatisfiable), it leaves the object's fields unchanged from their previous values — often zero. The driver happily sends a malformed transaction to the DUT, which the protocol may ignore silently. The test "passes" because nothing checked the randomisation outcome.
Always wrap randomize() in an assertion: if (!tx.randomize()) uvm_fatal("RAND", "constraints unsatisfiable"). The fatal aborts the test immediately with a useful error message instead of silently running with stale data. UVM's ``uvm_do_with macro does this automatically.
"Same transaction values every time the test runs"
DEBUGA "random" test produces identical transactions every run. The first transaction's paddr is always 0x4000_1234; the second is always 0x4000_5678.
<code>// In the test infrastructure:
initial begin
// Forgot to seed the PRNG — defaults to seed 0 every run.
run_test();
end</code>Root cause:Fix:+UVM_TESTNAME=...
"Coverage is stuck at 60% no matter how many random tests we run"
DEBUGThe CRV regression has run 100,000 transactions over a weekend. Coverage is flat at 60% — the missing 40% of bins never light up.
<code>class apb_xact;
rand bit [31:0] paddr;
rand bit [31:0] pwdata;
// Constraint excludes the very corner cases coverage cares about
constraint c_addr { paddr inside {[32'h4000_0000:32'h4000_FFFF]}; }
constraint c_data { pwdata != 32'h0;
pwdata != 32'hFFFF_FFFF; }
endclass
// Covergroup wants to see zero, all-ones, and the alternating pattern
covergroup cg_data;
cp_data : coverpoint pwdata {
bins zero = {0};
bins all_one = {32'hFFFF_FFFF};
bins alt = {32'hAAAA_AAAA, 32'h5555_5555};
}
endgroup</code>The constraint explicitly excludes 0 and 0xFFFF_FFFF — the same values coverage requires. The constraint set and the coverage model contradict each other; no amount of random sampling can close the gap.
Reconcile constraints with coverage. Either (a) remove the exclusion if those corner cases are legal for the protocol, or (b) split into two test scenarios — one with the exclusion (for nominal protocol legality) and one without (for corner-case sweeping). Coverage drives constraints; never the other way around.
"randomize() is taking forever — solver dominates run-time"
DEBUGSimulator profiling shows randomize() is consuming 42% of total CPU. A test that should run in 5 minutes takes 25 minutes.
<code>class burst_xact;
rand bit [31:0] addr;
rand bit [3:0] len;
rand bit [31:0] data[];
constraint c_size { data.size() == len + 1; }
constraint c_align { addr % (len * 4) == 0; }
constraint c_data { foreach (data[i])
data[i] inside {[32'h0:32'hFFFF_FFFF]}
dist {0 := 10, [1:32'hFFFE]:= 80, 32'hFFFF_FFFF := 10}; }
endclass</code>The foreach constraint with a dist per element forces the solver to compute the joint distribution across up to 16 elements simultaneously. Each randomize() call enumerates a combinatoric search space.
Decompose. Generate addr and len first via pre_randomize(), then resize data and assign each element independently in post_randomize() with a simple $urandom_range call. The simple per-element randomisation is 100× faster than the joint distribution solve and produces the same statistical mix.
"Constraint A and constraint B work fine alone but conflict when both active"
DEBUGA test passes when run with only c_addr active, and again when only c_align is active. The combined run fails every randomize() call.
<code>class apb_xact;
rand bit [31:0] paddr;
// Constraint A: address in low 256 bytes
constraint c_addr { paddr inside {[32'h0000:32'h00FF]}; }
// Constraint B: addresses must be 1KB-aligned
constraint c_align { paddr[9:0] == 10'h000; }
endclass</code>The two constraints' intersection has only one legal value — 0x0000 — and even that may be filtered out by some other constraint. Either constraint alone is satisfiable; together they over-specify the solution space.
When two constraints are individually fine but jointly infeasible, the design has a contradiction in its specification. Either widen one (e.g., relax the address range to [0:0xFFFF_FFFF] if 1KB alignment is the real legality rule) or document why they cannot coexist and disable one in the test that needs the other. The cure for solver failure is almost never "make the constraints fancier"; it is "decide which is wrong."
Interview Q&A — Twelve Questions You Will Be Asked
A verification methodology in which the testbench declares random stimulus fields (rand / randc) plus a set of declarative constraints describing the legal stimulus space; the simulator's constraint solver then generates a satisfying assignment on every randomize() call. CRV scales to verification problems where the input state space is too large for hand-written directed tests.
Best Practices — Ten Rules for Disciplined CRV Adoption
- Always assert the return value of
randomize(). A silent failure that leaves stale fields is the most common CRV bug. Useassert(tx.randomize())or UVM's ``uvm_do_with` macro. - Pass a fresh seed on every regression run. Without seed variation, a "random" regression is just a slow directed test. Record the seed per test for reproducibility.
- Decompose constraints into named blocks.
c_addr,c_data,c_protocol_legality,c_error_injection— so individual tests can enable or disable subsets viaconstraint_mode(). - Reconcile constraints with the coverage model continuously. Constraints that exclude values the coverage model requires produce an unreachable closure target. Treat coverage as the truth source for constraint correctness.
- Profile
randomize()cost when run-time grows. Solver time is wall-clock time; over-constrained classes dominate. Decompose tangled constraints into independent solves or move intopost_randomize(). - Use
randfor general stimulus,randcfor small exhaustive cycles.randcon a 32-bit field is wasteful; on a 4-bit error code it is precisely the right tool. - Track coverage closure weekly from week one. Coverage gaps near tape-out require constraint changes, which trigger full regression resignoffs. Early visibility prevents late surprises.
- Treat constraint changes like source-code changes. Code review, regression comment explaining why, and a coverage point that proves the constraint fires. Casual constraint tweaks erode test value silently.
- Log seed and constrained values at every transaction. Replay-friendly logs turn sporadic failures into deterministic ones. The cost is a few log lines per transaction; the benefit is hours saved per debugging session.
- Never randomise inside the DUT. CRV is simulation-only; classes do not synthesise. The closest hardware analogue is an LFSR, but that is a different design problem altogether.