Skip to content

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 vs Constrained Random — Side by Side
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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);
end

The 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.

The CRV Loop — All Five Steps in Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
endmodule

rand 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:

KeywordFull NameBehaviourUse For
randRandomNew value every randomize() call, picked independently from the full range (within constraints)Address, data, burst length, direction
randcRandom CyclicCycles through every possible value exactly once before repeating — guarantees no repeats until the full set is exhaustedTransaction IDs, scenario selectors, round-robin patterns
(plain)Not randomisedrandomize() ignores this property entirely — you set it manuallyFlags set by the driver, response fields filled by the monitor
rand vs randc vs plain — Behaviour Difference
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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=0

Constraints — 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.

Constraint Blocks — Basic Syntax
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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);
    }
endclass

CRV 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 CoverageWith 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 — undetectedCoverage 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

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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<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.

✅ Solver-friendly constraint
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<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>
❌ Tangled, slow-to-solve constraint
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<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

1

"randomize() returns 0 and the test silently passes"

DEBUG
Symptom

A test runs to completion with no transactions reaching the DUT. Coverage is at 0% on every bin. No errors were logged.

Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<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>
Root Cause

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.

Fix

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.

2

"Same transaction values every time the test runs"

DEBUG
Symptom

A "random" test produces identical transactions every run. The first transaction's paddr is always 0x4000_1234; the second is always 0x4000_5678.

Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<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=...

3

"Coverage is stuck at 60% no matter how many random tests we run"

DEBUG
Symptom

The CRV regression has run 100,000 transactions over a weekend. Coverage is flat at 60% — the missing 40% of bins never light up.

Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<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>
Root Cause

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.

Fix

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.

4

"randomize() is taking forever — solver dominates run-time"

DEBUG
Symptom

Simulator profiling shows randomize() is consuming 42% of total CPU. A test that should run in 5 minutes takes 25 minutes.

Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<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>
Root Cause

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.

Fix

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.

5

"Constraint A and constraint B work fine alone but conflict when both active"

DEBUG
Symptom

A test passes when run with only c_addr active, and again when only c_align is active. The combined run fails every randomize() call.

Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<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>
Root Cause

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.

Fix

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

  1. Always assert the return value of randomize(). A silent failure that leaves stale fields is the most common CRV bug. Use assert(tx.randomize()) or UVM's ``uvm_do_with` macro.
  2. 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.
  3. Decompose constraints into named blocks. c_addr, c_data, c_protocol_legality, c_error_injection — so individual tests can enable or disable subsets via constraint_mode().
  4. 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.
  5. 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 into post_randomize().
  6. Use rand for general stimulus, randc for small exhaustive cycles. randc on a 32-bit field is wasteful; on a 4-bit error code it is precisely the right tool.
  7. 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.
  8. 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.
  9. 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.
  10. 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.