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.
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
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
endclassDesign 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.
// 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.