Part 6 · Testbench Architecture · Intermediate
Mixing Directed & Random Stimulus
The project arc — directed bring-up, constrained-random volume, targeted-random closure — plus injecting directed sequences into a random stream and the coverage-feedback loop.
The three-phase stimulus arc
Real projects do not choose between directed and random — they sequence them. Directed first proves basic life (reset works, one write lands, one read returns). Constrained-random volume then explores the space at scale across regression seeds. Finally targeted-random narrows constraints around the coverage holes the volume phase could not reach. Each phase has a different goal and a different cost per bug found.
STIMULUS STRATEGY OVER A PROJECT
Phase 1: DIRECTED BRING-UP days
one write, one read, reset twice
goal: TB and DUT both alive; debug the testbench itself
│
Phase 2: CONSTRAINED-RANDOM VOLUME weeks
broad constraints × many seeds × nightly regression
goal: explore; bug rate per cycle is highest here
│
Phase 3: TARGETED RANDOM until closure
coverage report → holes → narrowed constraints
goal: close the last bins random volume missed
bugs/day: ▂▂ → ███████ → ▃▃
effort/bug: low → low → high (each hole is bespoke)Injecting directed sequences into a random stream
The cleanest mechanism: the generator draws from two sources — a directed queue that tests pre-load, and the random blueprint. Directed items drain first (or are interleaved by weight), then random generation resumes. The driver never knows the difference, because both sources emit the same transaction class.
class mixed_generator;
mailbox #(bus_txn) out;
bus_txn directed_q [$]; // tests pre-load this queue
bus_txn blueprint;
int unsigned n;
event done;
function new(mailbox #(bus_txn) out, int unsigned n);
this.out = out; this.n = n; blueprint = new();
endfunction
// tests call this to script exact sequences
function void add_directed(bit write, bit [15:0] addr,
bit [31:0] wdata = '0);
bus_txn t = new();
t.kind = write ? WRITE : READ;
t.addr = addr;
t.wdata = wdata;
t.source = "directed";
t.rand_mode(0); // freeze: solver must not touch it
directed_q.push_back(t);
endfunction
task run();
int unsigned sent = 0;
// phase 1: drain directed sequence exactly as scripted
while (directed_q.size() > 0) begin
out.put(directed_q.pop_front());
sent++;
end
// phase 2: constrained-random volume
while (sent < n) begin
if (!blueprint.randomize()) $fatal(1, "randomize failed");
out.put(blueprint.clone());
sent++;
end
-> done;
endtask
endclass
// in a test, script bring-up then let randomness run:
// gen.add_directed(1, 16'h0000, 32'h1111_1111); // write
// gen.add_directed(0, 16'h0000); // read back
// gen.n = 5000; // then volumeTargeted-random for holes
When the merged coverage report shows an unhit bin — say, error responses during back-to-back bursts — write a small derived transaction or scenario whose constraints force that neighborhood, and run it as its own short test. Targeted-random keeps some randomness (everything not pinned stays rand), so it often hits adjacent holes for free, which a purely directed test would not.
// hole: cross of (burst traffic) × (high addresses) never hit
class hole_txn extends bus_txn;
constraint c_target {
addr >= 16'hF000; // pin the hole's neighborhood
gap == 0; // back-to-back
} // everything else stays random
endclass
// test: e.gen.blueprint = hole_txn_handle; short run, re-merge coverageThe coverage-feedback loop
COVERAGE-FEEDBACK LOOP
┌──────────────────────────────────────────────┐
│ │
▼ │
run regression (random volume, many seeds) │
│ │
▼ │
merge coverage across seeds │
│ │
▼ │
holes? ──no──► CLOSURE → sign-off │
│ yes │
▼ │
classify each hole: │
reachable? → write targeted-random constraint ────┘
unreachable? → waive with documented justification
Loop currency: constraints in, coverage out.Interview angle
“Directed or random?” is a trap if you pick one. The senior answer is the arc — directed to bring up, random for volume, targeted-random to close — plus the mechanism for mixing them (a directed queue ahead of a random blueprint in one generator) and the loop that drives phase 3 (merge, find holes, constrain, rerun). Bonus points for noting that targeted-random beats pure directed at closure because the unpinned fields keep exploring.
Key takeaways
Directed proves life, random explores at scale, targeted-random closes — in that order.
A directed queue draining ahead of a random blueprint mixes both in one generator; the driver never knows.
rand_mode(0) on directed items guarantees the solver cannot perturb a scripted sequence.
The coverage-feedback loop is the engine of phase 3: merge, classify holes, constrain, rerun.
Common pitfalls
Staying directed-only past bring-up — bug discovery flatlines at what the author imagined.
Going random on day one — you spend the bring-up week debugging the TB and DUT simultaneously.
Chasing every hole with fully-directed tests — slow, brittle, and misses adjacent holes targeted-random gets free.
Forgetting rand_mode(0) on injected directed transactions — a later randomize() call silently rewrites the script.