Why directed tests run out of road
In rung 2 you built a testbench and fed it directed vectors — hand-chosen inputs like "add 3 and 5, expect 8." Directed tests are wonderful: they are readable, they target a specific feature, and the very act of writing one forces you to think. But they share a fatal weakness. They only test the cases you thought of. The bug that ships is almost always the one nobody imagined — the back-to-back write that collides with a refresh, the packet that arrives one cycle after reset deasserts, the carry that ripples exactly when the FIFO is one slot from full.
The deeper problem is arithmetic. A block with two 32-bit data inputs has 2⁶⁴ ≈ 1.8×10¹⁹ input combinations on a single cycle. Add internal state and a sequence of cycles and the legal-stimulus space explodes past anything you could enumerate before the heat death of the universe. If each directed test takes an engineer an hour to write and review, you are bringing a teaspoon to drain an ocean.
Let the machine invent the stimulus
The idea behind constrained-random verification is to flip who does the work. Instead of you specifying *the value*, you specify *the rules a legal value must obey*, and a constraint solver inside the simulator picks an actual value at random on every transaction. Run the same simulation a million times with a million different random seeds, and you sweep across a million legal scenarios you never had to type out by hand.
In SystemVerilog you bundle the stimulus into a transaction class: a struct of `rand` fields plus `constraint` blocks. Calling `randomize()` asks the solver to assign every `rand` field a value that satisfies all active constraints simultaneously. A pure `$random` would happily generate an illegal opcode or a misaligned address that the DUT was never built to handle; constraints keep every randomized transaction inside the legal envelope so a failure means a real bug, not a self-inflicted one.
class BusTxn;
rand bit [31:0] addr;
rand bit [31:0] data;
rand bit [3:0] len; // burst length
rand bit is_write;
// --- legality constraints ---
constraint c_align { addr[1:0] == 2'b00; } // word-aligned
constraint c_range { addr inside {[32'h0000_0000 : 32'h0000_FFFF]}; }
constraint c_len { len inside {1, 2, 4, 8}; } // legal burst sizes only
// --- shaping the DISTRIBUTION (not legality) ---
constraint c_mix { is_write dist { 1 := 70, 0 := 30 }; } // 70% writes
constraint c_hotcold{ addr dist { [0:'hFF] := 50, // hammer page 0
['h100:'hFFFF] := 50 }; }
endclass
// In the driver / sequence body:
BusTxn t = new();
assert ( t.randomize() ) // solver fills legal values
else $fatal("constraints unsatisfiable");
drive(t); // send it to the DUTWeighted distributions: where you spend your randomness
Uniform randomness is rarely what you want. If `addr` is uniform over 64 K addresses, the chance any single byte gets written twice in a short run is tiny — yet write-after-write to the same location is exactly where coherency and forwarding bugs hide. The `dist` operator lets you bias the dice: `:=` gives every value in a range that fixed weight, while `:/` spreads one weight *across* the range. By concentrating fire on a small hot page, you make collisions likely instead of astronomically rare.
Distributions are also how you re-create realistic traffic. A network switch sees mostly small packets with an occasional jumbo frame; a memory controller sees bursts that cluster in time. Encode that shape with weights and your random testbench stresses the design the way the real world will — but still wanders into the rare combinations a hand-written test would never reach.
Coverage: did random actually hit anything?
Throwing dice forever proves nothing on its own. A million seeds might pound the same three scenarios and leave a critical mode untouched. So constrained-random only becomes a *method* when you pair it with coverage — a measurement of what the simulation actually exercised. Coverage answers the question every project manager eventually asks: *are we done yet, and how do we know?* The answer lives in your verification plan, which lists the behaviours that must be hit before tape-out.
There are two different rulers, and confusing them is a classic trap. [[functional-coverage|Functional coverage]] asks *did we test the right things?* — it is hand-written intent. You declare `covergroup`s and `coverpoint`s describing scenarios that matter: every burst length, every read/write mix, the cross of "FIFO full" with "incoming write." [[ic-code-coverage|Code coverage]] asks *did we exercise the code?* — it is automatic, extracted by the simulator: which lines ran, which branches took both directions, which states and arcs of an FSM were visited, which expression toggled.
100% code coverage, 60% functional coverage
----------------------------------------------
Every LINE of RTL ran... but you never drove
a write that lands while the FIFO is FULL.
The line for that branch executed (read path),
so code coverage is happy --- yet the bug in
the full+write CORNER is still in there, unseen.
Lesson: code coverage = necessary, not sufficient.
functional coverage encodes the SCENARIOS
your verification plan actually cares about.Closing the loop: coverage steers the seeds
Here is the engine that makes the whole approach converge. You run a regression of many seeds, merge the coverage databases from every run into one picture, and read off the holes — the bins your random stimulus never filled. Those holes are a to-do list. Maybe burst length 8 never co-occurred with a back-pressured bus; you tweak a distribution weight, add a constraint, or write a targeted sequence to force the corner. Re-run, re-merge, watch the curve climb. This feedback loop — random fans out, coverage measures, you steer toward the gaps — is the day-to-day rhythm of constrained-random verification.
- Plan — translate the verification plan into concrete covergroups and coverpoints; this defines "done."
- Generate — run the regression with many random seeds; each seed explores a fresh legal path through the simulation.
- Measure — sample functional coverage and collect code coverage automatically; merge all seeds into one database.
- Analyze — rank the holes by importance; a stubborn empty bin usually means an over-tight constraint or a scenario randomness can't reach by luck.
- Steer — adjust weights, relax/add constraints, or write a directed-random sequence to drive the hole; then loop back to Generate.
- Close — when every must-hit bin is filled and the regression is green, you have reached coverage closure — the defensible answer to "are we done?"
Rebuilding rung 2's testbench, the random way
Return to the testbench from rung 2. Its skeleton — generator, driver, monitor, scoreboard — does not change. What changes is the *generator*: where it used to read a fixed array of directed vectors, it now constructs a transaction, calls `randomize()`, and ships it. The scoreboard still predicts the expected result and compares, exactly as before — randomness on the input side, determinism on the checking side. Add a covergroup sampled by the monitor, and the same testbench you already understand becomes a self-driving exploration machine.
// rung 2: directed --------------------------------
bit [31:0] vecs[] = '{32'h0003, 32'h0005, 32'h00FF};
foreach (vecs[i]) drive(vecs[i]); // 3 hand-picked cases
// rung 3: constrained-random ----------------------
covergroup cg @(posedge clk);
cp_len : coverpoint t.len { bins b[] = {1,2,4,8}; }
cp_dir : coverpoint t.is_write;
x_full : cross cp_dir, fifo_full; // the dangerous corner
endgroup
cg cov = new();
repeat (100_000) begin // 100k random cases
BusTxn t = new();
assert ( t.randomize() ); // legal by construction
drive(t);
cov.sample(); // record what we hit
end
$display("functional coverage = %0.1f%%", cov.get_coverage());That is the whole shift in mindset. You stop describing *examples* and start describing *the space of legal behaviour plus the scenarios worth measuring*. The simulator does the tireless wandering; coverage keeps the score; and you spend your scarce human attention not on typing vectors but on the far higher-value question of what "interesting" even means for this design.