Part 6 · Testbench Architecture · Intermediate

Transaction Design for Stimulus

Field taxonomy — rand controls, derived values, debug metadata — plus id/timestamp discipline and convert2string-style printing.

Three kinds of fields

A well-designed transaction sorts its fields into three buckets: rand control fields (what the solver chooses), derived/response fields (filled in by the driver or monitor as the transaction executes), and metadata (ids, timestamps, source labels — never randomized, never driven, purely for debug). Mixing the buckets is the root of most messy transaction classes.

diagram
TRANSACTION FIELD TAXONOMY

  ┌────────────────────────────────────────────────────────────┐
  │ rand CONTROL fields        solver picks; constraints shape │
  │   rand kind, addr, len, wdata[], delay                     │
  ├────────────────────────────────────────────────────────────┤
  │ DERIVED / RESPONSE fields  filled during execution         │
  │   rdata[], resp_err        (monitor/driver write these)    │
  ├────────────────────────────────────────────────────────────┤
  │ METADATA                   never rand, never on pins        │
  │   id, t_created, t_driven, source                          │
  └────────────────────────────────────────────────────────────┘

  Rule: the solver only ever sees the top box.

A disciplined transaction class

systemverilog
typedef enum {READ, WRITE, RMW} kind_e;

class bus_txn;
  // ---- rand control fields ----
  rand kind_e        kind;
  rand bit [15:0]    addr;
  rand bit [31:0]    wdata;
  rand int unsigned  gap;          // idle cycles before this txn

  // shaping constraints (full treatment in Part 3: Randomization)
  constraint c_gap   { gap inside {[0:8]}; gap dist {0 :/ 6, [1:8] :/ 4}; }
  constraint c_align { addr[1:0] == 2'b00; }

  // ---- derived / response fields ----
  bit [31:0] rdata;
  bit        resp_err;

  // ---- metadata ----
  static int unsigned next_id;
  int unsigned id;
  time t_created, t_driven;
  string source = "gen";

  function new();
    id        = next_id++;
    t_created = $time;
  endfunction

  function string convert2string();
    return $sformatf("[%0d %s@%0t] %s addr=%h wdata=%h rdata=%h err=%0b gap=%0d",
                     id, source, t_driven, kind.name(), addr,
                     wdata, rdata, resp_err, gap);
  endfunction

  function bus_txn clone();
    bus_txn c = new();
    c.kind = kind; c.addr = addr; c.wdata = wdata; c.gap = gap;
    return c;
  endfunction
endclass

Design decisions worth noticing

  • gap is a rand field — inter-transaction spacing is stimulus too, and back-to-back (gap==0) is weighted heavily because that is where pipeline bugs live.

  • rdata and resp_err are not rand — randomizing response fields wastes solver effort and confuses readers about who owns them.

  • The static next_id counter plus t_driven means any scoreboard error message names exactly one transaction at one time.

  • clone() exists because mailboxes pass handles — a generator reusing one object and re-randomizing it corrupts transactions the driver has not consumed yet.


Printing discipline

Every transaction class gets a convert2string() (the name mirrors UVM's convention deliberately) and every component logs through it. The payoff is uniform, greppable logs: one line per transaction event, always carrying the id. When a regression fails at 3 a.m., the difference between minutes and hours of debug is whether you can grep one id across generator, driver, monitor, and scoreboard lines.

systemverilog
// every component logs the same way:
$display("DRV  %s", t.convert2string());
$display("MON  %s", t.convert2string());
$display("SCB-ERR exp=%h %s", expected, t.convert2string());

// log excerpt — one txn traceable end to end:
// GEN  [42 gen@0]    WRITE addr=00a0 wdata=cafe_f00d ...
// DRV  [42 gen@1150] WRITE addr=00a0 wdata=cafe_f00d ...
// MON  [42 mon@1170] WRITE addr=00a0 wdata=cafe_f00d ...

Interview angle

“What goes in a transaction?” sounds trivial; the senior answer is the taxonomy: rand controls, derived responses, debug metadata — plus why constraints belong in the transaction (reusable shaping) while test-specific tightening comes from derived transaction classes or inline constraints, which points back to Part 3.

Key takeaways

  • Sort fields into rand controls, derived responses, and metadata — and keep the buckets pure.

  • Randomize spacing (gap/delay) as a first-class field; back-to-back stimulus finds pipeline bugs.

  • Static id counter + timestamps + convert2string() = one greppable line per transaction per component.

  • clone() before reuse — mailboxes carry handles, not copies.

Common pitfalls

  • Marking response fields rand — solver churn and reader confusion over field ownership.

  • Re-randomizing one transaction object in a loop without cloning — corrupts in-flight handles downstream.

  • Ad-hoc $display formats per component — un-greppable logs that hide cross-component correlation.

  • Base-class constraints so tight (fixed addr ranges) that derived tests must fight them with soft-constraint gymnastics.