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.

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

systemverilog
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 volume

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

systemverilog
// 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 coverage

The coverage-feedback loop

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