Skip to content

UVM

Random Testing

Constrained-random verification — writing constraints instead of cases, the seed that makes randomness reproducible, over-constraint, and why breadth needs coverage.

Verification Foundations · Module 1 · Page 1.7

The Engineering Problem

Directed testing hit a wall: its effort grows by addition while the design's corner space grows by multiplication, so the escaping bugs hide in the corners nobody thought to hand-write. The obvious fix is to stop enumerating scenarios one by one and instead generate them — let a machine produce stimulus you never imagined.

But naïve randomness fails immediately. If you randomize every input bit independently, almost everything you generate is illegal — misaligned addresses, impossible opcodes, protocol-violating handshakes — and the DUT rejects it before any interesting behaviour happens. Pure random is mostly noise.

The methodology that actually works is constrained-random (CRV): you don't pick the values, and you don't let chaos pick them either — you describe the legal space with constraints and let a solver draw legal-but-unpredictable samples from it. That buys enormous breadth, but it introduces two new problems directed testing never had:

If a machine chose the stimulus, (1) how do you reproduce the exact run that found a bug, and (2) how do you know what the random flow actually exercised?

Motivation — why generate stimulus instead of writing it

The case for constrained-random is an asymmetry of effort against reach:

  • One generator, millions of scenarios. A single constrained transaction class, written once, produces a different legal stimulus on every call to randomize(). Ten thousand iterations is ten thousand scenarios you did not type.
  • It finds the corners you'd never enumerate. The bugs that escape directed suites live in combinations — feature A in mode X while feature B back-pressures during a wrap burst. Randomisation stumbles into these interactions naturally; a human almost never writes them.
  • The surprise is the point. A directed test can only confirm what you already suspected. A constrained-random run can surprise you — fail in a way you didn't predict — which is exactly the bug class that matters most, because it is the one you would otherwise ship.
  • It scales with compute, not headcount. Adding verification means adding cycles and seeds on a regression farm, not adding engineers to write more cases. Effort moves from enumerating scenarios to describing the legal space and measuring the result.

The motivation is not that random replaces directed — the previous lesson showed directed owns the specific, known, high-value scenarios and regression — but that for breadth, generating legal stimulus beats hand-writing it by orders of magnitude.

Mental Model

Hold this picture:

Directed testing is writing the script; constrained-random is writing the rules of the game and letting a solver play it a million times. You declare which fields are random (rand), you write constraints that fence off the legal region of the value space, and on each randomize() a constraint solver picks one legal-but-unpredictable point inside that fence. You never name the values — you name the boundaries, and the solver explores within them. A single integer, the seed, determines the entire sequence of draws, so "random" is fully replayable: same seed, same run, every time.

The skill shifts accordingly. In directed testing you craft cases; in constrained-random you craft the space — tight enough that every sample is legal, loose enough that the interesting corners remain reachable. Over-constrain and the solver can never reach the bug; under-constrain and it wastes cycles generating illegal noise.

Visual Explanation — the constrained-random generator

A constrained-random environment is a small machine: constraints plus a seed feed a solver, the solver produces one legal transaction per call, and that stream of transactions drives the DUT. The seed is the single knob that makes the whole stream reproducible.

Constrained-random generator — constraints and seed feed a solver, randomize produces legal transactions that drive the DUTlegal spacereproducible drawrandomize()legalstimulusConstraintsfence off the legal regionSeedone integer = wholesequenceConstraint solverpicks one legal pointRandom transactionlegal-but-unpredictableDUT12
Figure 1 — the constrained-random generator. Constraints describe the legal space and a seed initialises the solver's pseudo-random sequence; each randomize() call draws one legal-but-unpredictable transaction, which drives the DUT. Same seed plus same constraints reproduces the exact transaction stream — this is what turns 'random' into a repeatable experiment rather than a gamble.

The two inputs to the solver carry the whole idea. Constraints are how you keep every sample legal without naming any value — they replace ten thousand hand-written cases with one description of the space. The seed is how you make a pseudo-random process deterministic: the same seed reproduces the same draws, so a failure found at run-time can be replayed bit-for-bit at debug-time. Lose either and the methodology breaks — no constraints means illegal noise, no recorded seed means an unreproducible failure.

RTL / Simulation Perspective — write the rules, let the solver play

Take a bus transaction. Instead of hand-writing addresses and lengths, declare the random fields and fence the legal space with constraints; then loop randomize() to get a fresh legal scenario every iteration.

constrained-random stimulus — describe the space, not the cases
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class bus_txn;
  rand bit [31:0] addr;
  rand bit [7:0]  len;        // burst length, in beats
  rand bit [1:0]  burst;      // 00=FIXED, 01=INCR, 10=WRAP
 
  // Constraints define the LEGAL space — never specific values.
  constraint c_addr { addr % 4 == 0; }                      // word-aligned
  constraint c_len  { len inside {[1:16]}; }                // 1..16 beats
  constraint c_wrap { burst == 2'b10 -> len inside {2,4,8,16}; } // WRAP ⇒ power-of-two
endclass
 
initial begin
  bus_txn t = new();
  repeat (10_000) begin
    if (!t.randomize())                  // solver draws one legal-but-unpredictable txn
      $fatal(1, "constraints unsatisfiable — the legal space is empty");
    drive(t);                            // each iteration: a scenario no one hand-wrote
  end
end

Read what changed from directed testing. There is no list of cases — there is a description of legality (c_addr, c_len, c_wrap) and a loop. The solver guarantees every one of the ten thousand transactions is legal (word-aligned, in-range, WRAP bursts power-of-two), yet no two runs of different seed produce the same stream. The engineer's effort moved from typing scenarios to bounding the space — and that single c_wrap implication is doing the work of excluding a whole class of illegal bursts that a directed author would have had to remember to avoid in every case.

Verification Perspective — breadth's two blind spots

Constrained-random buys breadth, but breadth alone is not verification. Two blind spots come with it, and each is closed by a different companion methodology:

  • You don't know what it hit. Ten million legal transactions is an unknown amount of verification until you measure it. Without functional coverage, you cannot tell a thorough run from a lucky one, and the solver may pound the same easy states forever while a hard corner goes untouched. The fix is coverage — the entire subject of the next lesson, and the reason coverage and constrained-random are always taught together.
  • It judges nothing. A random generator produces stimulus; it does not know the right answer. Something else has to check — assertions for contracts, a scoreboard for end-to-end intent. Random stimulus without checking just exercises the design quietly.

There is also a tuning hazard between the two: the legal space can be mis-fenced. Constrain too loosely and the solver wastes cycles on illegal or uninteresting stimulus; constrain too tightly and it can never reach the corner where the bug lives — and with no coverage, nobody notices the corner was excluded. This is why constrained-random is never run "open-loop": it is steered by coverage and judged by checkers. On its own it is a powerful engine with no map and no referee.

Runtime / Execution Flow — the seed makes a random failure reproducible

The operational fact that makes constrained-random engineering is reproducibility. A regression runs thousands of seeds; when one fails, the recorded seed lets you replay that exact run deterministically and debug it like any directed test.

Constrained-random regression loop — run with recorded seed, on failure replay the exact seed, debug, fix, rerunyesnoreplay same seedRun regression —each seedrecordedAll seedspass?Bank coverage, runmore seedsReplay the failingseed (deterministic)Debug + fix —exactly like adirected testBug fixed; seedkept as aregression
Figure 2 — the seed-driven regression loop. Each run uses a recorded seed; a pass moves on, a failure is reproduced by replaying that exact seed, debugged deterministically, and fixed. The dashed 'replay same seed' edge is the whole point: because the seed reproduces the run bit-for-bit, a random failure becomes as debuggable as a directed one. A failure whose seed was not recorded is the cardinal sin — it may never be seen again.

This loop is why "random" is not a synonym for "irreproducible." A pseudo-random generator seeded with a known integer is fully deterministic: same seed, same constraints, same RTL → identical run. So the discipline is simple and absolute — always log the seed. A failing seed is a perfect, replayable bug report; an unlogged failing seed is a bug you may never reproduce. The best practice is to keep the seed that found each bug as a permanent directed-style regression, marrying random's discovery to directed's permanence.

Waveform Perspective — an unplanned corner the solver found for free

Here is breadth made concrete. A constrained-random run drives a multi-beat burst, and because ready back-pressure is also randomised, the solver produces a mid-burst stallvalid held high while ready drops in the middle of the burst — a corner a directed author rarely writes by hand.

Constrained-random stimulus produces a mid-burst back-pressure stall no one scripted

10 cycles
Constrained-random stimulus produces a mid-burst back-pressure stall no one scriptedsolver-chosen 4-beat burst begins — legal, but a length you didn't enumeratesolver-chosen 4-beat b…mid-burst stall: valid high, ready low — a corner directed tests skipmid-burst stall: valid…burst completes correctly through the stall (last beat)burst completes correc…clkvalidreadybeat0012223000lastt0t1t2t3t4t5t6t7t8t9
Figure 3 — a solver-chosen 4-beat burst (beats 0–3) meets randomised back-pressure: ready drops at cycles 3–4 while valid stays high, stalling on beat 2 mid-burst, then completes through to the last beat. A directed test usually scripts the polite no-stall case; the stall-in-the-middle overlap emerges for free from randomising both valid and ready. This is exactly the interaction-corner that escapes hand-written suites.

Nothing on this waveform was hand-placed. The burst length, the moment ready drops, the stall landing on beat 2 rather than at the start — all fell out of the solver sampling a legal space. That is the breadth directed testing cannot reach by addition. The two things still missing from the picture are exactly the two blind spots: a coverage bin to record that "mid-burst stall" was finally hit, and an assertion to judge that the handshake stayed legal through the stall. Random created the corner; measurement and checking give it meaning.

DebugLab — 50 million cycles, and the bug was never given a chance

A green constrained-random regression that silently excluded the bug

Symptom

A DMA engine ran a constrained-random regression for weeks — 50 million cycles across thousands of seeds, all passing. The block taped out. In silicon, a zero-length descriptor (a legal, spec-permitted transfer of 0 beats) hung the engine. Not one of the 50 million transactions had ever been a zero-length transfer. The most thorough-looking campaign on the project had never tested the failing case.

Root cause

Over-constraint. The transaction's length constraint was written to model "useful" transfers and accidentally fenced the bug out of the legal space entirely:

what the constraint allowed vs what the spec allowed
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
constraint c_len { len inside {[1:16]}; }   // author's "legal" space
spec-legal lengths:  0, 1, 2, ... 16        // ← 0 is legal and must be handled
the solver could draw:  1..16 only          // len == 0 was UNREACHABLE
zero-length transfers actually generated:  0  (in 50,000,000 cycles)

The solver did its job perfectly — it never produced an illegal transaction, and it never produced len == 0, because the constraint declared len == 0 illegal. The breadth was real but bounded by a fence the author drew in the wrong place, and the corner that mattered lived on the far side of it.

Diagnosis

The tell is a high-cycle, all-green campaign with no coverage evidence that the important corners were reached. Diagnose a mis-fenced space by measuring it:

  1. Add a functional-coverage bin on len, including a bin for len == 0. It would have read zero hits from day one — a screaming signal that the regression, however large, never exercised the zero-length case.
  2. Cross length against descriptor type. The empty cross {len == 0} × {any type} exposes that an entire legal category was excluded, independent of how many cycles ran.
  3. Re-read every constraint against the spec's legal range, not the typical range. inside {[1:16]} encodes an assumption ("lengths start at 1") that the spec never made. Constraints are claims about legality and must be audited as such.
Prevention

Constrained-random is only as good as the space you fenced and the coverage that audits it:

  1. Never run random open-loop. Pair every constrained-random campaign with functional coverage; cycle count is not coverage, and a green run with empty bins is unmeasured risk — this is precisely why the next lesson exists.
  2. Constrain to spec-legal, not author-convenient. Include the awkward legal extremes (zero, max, boundary) explicitly; the bug usually lives at exactly the value a "reasonable" constraint quietly excludes.
  3. Audit constraints as legality claims. Each constraint asserts "outside this is illegal." Review every one against the specification's true legal range, and add a directed test for known boundary cases so they are never solely at the mercy of the fence.
  4. Always log the seed. Orthogonal but non-negotiable: a failure you cannot replay is a failure you cannot fix.

The one-sentence lesson: an over-constrained random test runs millions of cycles to confidently verify everything except the bug — and without coverage, its green result looks identical to success.

Common Mistakes

  • Equating cycle count with coverage. "We ran 50 million cycles" measures effort, not reach. Without functional coverage you have no idea what those cycles exercised — they may have pounded the same easy states forever.
  • Over-constraining the legal space. A too-tight constraint silently excludes the corner where the bug lives, and with no coverage nobody notices. Constrain to what the spec calls legal, not to what feels typical — and include the boundary values explicitly.
  • Under-constraining into illegal noise. The opposite failure: randomise too freely and the solver burns cycles generating stimulus the DUT legally rejects, exercising nothing. Legality is the floor; the solver should never produce an illegal transaction.
  • Not logging the seed. A random failure whose seed wasn't recorded may never be reproduced. The seed is the entire bridge from "random" to "debuggable" — log it on every run.
  • Running random without a checker. A generator produces stimulus, not verdicts. Constrained-random must be paired with assertions and/or a scoreboard, or it exercises the design without ever judging it.
  • Treating random as a replacement for directed. They are complementary: random for breadth, directed for the specific corners and for locking found bugs against regression. Drop directed entirely and you lose the deterministic regression net.

Senior Design Review Notes

Interview Insights

Constrained-random verification generates stimulus automatically by declaring certain fields random (rand) and writing constraints that describe the legal space, then letting a constraint solver draw legal-but-unpredictable values on each randomize() call. It is "constrained" because pure, unconstrained randomisation produces mostly illegal stimulus — misaligned addresses, impossible opcodes, protocol violations — that the DUT rejects before anything interesting happens. Constraints keep every generated transaction legal while still leaving the values unpredictable, so the solver explores the legal space broadly without you naming a single case. The engineer's effort shifts from writing scenarios to describing the boundaries of legality.

Exercises

  1. Constraint as a legality claim. For the bus_txn class in this lesson, state in plain English what each of c_addr, c_len, and c_wrap declares to be illegal. Then identify one spec-legal value of len that c_len excludes, and the bug class that exclusion would hide.
  2. Diagnose the green campaign. A team reports "8 million cycles, all passing, zero failures" on a packet router. Write the single question that exposes whether this is thorough or merely lucky, and name the artifact you would ask to see.
  3. Find vs lock. A constrained-random run fails on seed 0xC0FFEE. Describe exactly how you reproduce it, and then explain what you would add to the suite so this corner is protected even after the constraints later change.
  4. Fence the space correctly. You must verify a FIFO whose legal depths are 0–16 (zero is legal and special). Write the len constraint two ways — one that over-constrains (and hides the zero bug) and one that is correct — and add the one coverage bin that would have caught the difference.

Summary

  • Random testing climbs directed testing's wall by generating stimulus instead of enumerating it. Its usable form is constrained-random: declare rand fields, fence the legal space with constraints, and let a solver draw legal-but-unpredictable transactions — reaching interaction corners no one would hand-write.
  • The effort shifts from writing cases to describing the legal space: one generator yields millions of scenarios, scaling with compute rather than headcount.
  • Two mechanisms make it engineering rather than gambling. The seed makes any run fully reproducible — same seed, same run — so a random failure is as debuggable as a directed one; always log the seed.
  • Breadth has two blind spots: random doesn't know what it hit (closed by coverage — the next lesson) and doesn't judge anything (closed by assertions and a scoreboard). And the legal space can be mis-fenced — over-constraint silently excludes the bug, invisible without coverage.
  • The durable rule of thumb: constrain to the spec's legal space, log every seed, and never trust cycle count over coverage — random discovers the corner, but only measurement proves you reached it.

Next — Coverage-Driven Verification: constrained-random's breadth is unaccountable until you can measure it. The next lesson takes up functional coverage — how you define "done," reveal the corners random never hit, and steer stimulus to close them.