Constrained-Random Stimulus

Let's do a small, terrifying calculation. A 32-bit adder takes two 32-bit inputs, so its input space has 2^{32} \times 2^{32} = 2^{64} pairs — about 1.8 \times 10^{19}. Suppose your simulator checks one pair per nanosecond, a billion per second (wildly optimistic). Then exhaustively testing one adder takes 1.8 \times 10^{10} seconds — roughly 585 years. For a 64-bit adder, the same sum says 10^{22} years: comfortably past the heat death of the stars. Directed tests — hand-written "try 3+5, try the carry chain, try overflow" — cover the cases you thought of, and the bugs, with great reliability, live in the cases you didn't. The industry's answer is one of verification's best ideas: throw dice — but load them.

Random finds what you didn't think to test

Pure random stimulus explores without prejudice: it wanders into corners no test plan lists — the carry that ripples all 32 bits, the back-to-back operations with a stale flag, the address that aliases a cache line. Paired with the golden model from the testbench lesson, it becomes a bug-finding machine: generate a random input, feed it to DUT and model, compare, repeat a million times overnight. No human imagination required — which is precisely the point, since human imagination is what failed.

But unconstrained randomness has a fatal flaw: most random bit patterns are illegal inputs. A random 32-bit word is almost never a valid one-hot select (32 valid patterns out of 4,294,967,296); a random address ignores alignment rules; a random opcode byte is mostly gibberish the decoder may rightly reject. Feed a DUT garbage and it fails for boring, spec-permitted reasons — noise that buries real bugs. The fix is constrained random: declare the rules of legality, and let a solver pick freely within them.

class mem_txn; rand logic [31:0] addr; rand logic [1:0] op; // 0 = READ, 1 = WRITE, 2 = FLUSH rand logic [31:0] data; constraint aligned { addr % 4 == 0; } // word-aligned only constraint in_range { addr inside {[32'h1000 : 32'h1FFC]}; } constraint op_mix { op dist { 0 := 6, 1 := 3, 2 := 1 }; } // weighted: 60/30/10 endclass initial begin mem_txn t = new(); repeat (100000) begin assert (t.randomize()); // the SOLVER picks values satisfying ALL constraints drive(t); // apply to the DUT... check(t); // ...and compare against the golden model end end

The rand variables are the dice; each constraint block is a loading rule; and randomize() invokes a genuine constraint solver that finds a value assignment satisfying every rule at once (it can handle constraints that interlock, like addr ranges that depend on op). The dist weighting tilts the dice — here 60% reads — because realistic traffic mixes find different bugs than uniform ones. Constraints are your machine-readable spec of legal inputs: writing them forces the team to say precisely what the design must tolerate.

Seeds: randomness you can replay

The dice are not truly random — they are a pseudo-random sequence unrolled from a starting seed. Same seed, same sequence, bit for bit, forever. This turns randomness from a liability into an instrument: a nightly regression runs thousands of seeds; when seed 8,675,309 fails at 3 a.m., the log records it, and in the morning you rerun exactly that seed and watch the identical failure under a debugger. A failing seed is a treasure — it is a bug, gift-wrapped, reproducible on demand. Losing the seed (or letting the bench draw entropy the seed doesn't control) means a bug you saw once and may never see again; verification teams treat that as a process failure on par with losing the RTL itself.

Watch the whole machinery run — a seeded generator, constraints, weighted ops, and perfect reproducibility:

// ── A seeded PRNG (mulberry32): same seed → same stream, forever. ── function mulberry32(seed: number): () => number { let s = seed >>> 0; return () => { s = (s + 0x6D2B79F5) >>> 0; let t = s; t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } // ── The constraints: word-aligned addresses in [0x1000, 0x1FFC], weighted ops. ── function drawTxn(rnd: () => number): { op: string; addr: number } { const r = rnd(); const op = r < 0.6 ? "READ " : r < 0.9 ? "WRITE" : "FLUSH"; // dist 6:3:1 const addr = 0x1000 + Math.floor(rnd() * 1024) * 4; // % 4 == 0, in range return { op, addr }; } for (const seed of [42, 42, 99]) { // 42 twice: watch it repeat! const rnd = mulberry32(seed); const txns = Array.from({ length: 5 }, () => drawTxn(rnd)); const line = txns.map((t) => `${t.op} @0x${t.addr.toString(16)}`).join(" "); console.log(`seed ${seed}: ${line}`); } // ── And the weights really hold, in the long run: ── const rnd = mulberry32(7); const count: Record<string, number> = { "READ ": 0, "WRITE": 0, "FLUSH": 0 }; for (let i = 0; i < 10000; i++) count[drawTxn(rnd).op]++; console.log("\n10,000 draws with seed 7:", JSON.stringify(count), "(expect ~6000/3000/1000)");

The two seed-42 lines are identical — that is reproducibility, and every address is aligned and in range — that is legality. Randomness with an audit trail.

A subtle profession-within-the-profession: the constraint solver itself can bias your dice. Solvers must pick uniformly-ish among the legal solutions, but a carelessly-written constraint set can make some legal values astronomically rarer than others — for instance, constraining a + b == c with all three random may make tiny values of c vastly over-represented depending on solve order. SystemVerilog even lets you control it (solve a before b). Verification teams therefore measure their randomness: histogram the generated values, eyeball the distribution, and — in the next lesson's language — collect coverage on the stimulus as well as the design. Trust, but verify, applies to your own dice too. And when the histogram looks wrong, the fix is almost always to simplify the constraints rather than to fight the solver.

Two ditches, one road. Drive with no constraints and you mostly generate illegal stimulus: random one-hot selects that aren't one-hot, misaligned addresses, opcodes from outer space. The DUT "fails" in ways the spec never promised to handle, your triage queue fills with non-bugs, and the team learns to ignore red — the worst possible culture. But the opposite ditch is quieter and deadlier: over-constraining. Every constraint you add is a region of input space you have promised never to explore. Constrain addresses to "nice" ranges and you will never test the boundary; force operations to alternate read/write and the back-to-back-write bug ships. The discipline: constraints must encode exactly the spec's definition of legal — no tighter, no looser. When the spec says "any address", your constraints must eventually try the ugly ones: zero, all-ones, the last word before wraparound. If a constraint exists only to make failures stop, you haven't fixed a bug — you've stopped looking for it.