UVM
Transaction Recording
Capturing each transaction as a time-spanned object in the simulator's database — begin_tr/end_tr mark its span, do_record logs its fields — so the waveform viewer shows labeled transaction streams above the pins.
Transaction Modeling · Module 8 · Page 8.7
The Engineering Problem
Module 8 built the transaction as the testbench's unit of meaning — a logical operation modeled as data (Module 8.1), carried through the stimulus path (8.2), over a lifecycle in time (8.3), with automatic data services (8.4–8.6). But when a test fails, the debugger by default shows only the pins: clock edges and signal transitions, with no sign of the transactions that were so carefully modeled. Reconstructing "this was a read of 0x40 that got the wrong data" from raw waveforms — by hand, cycle by cycle — throws away the entire abstraction at the moment you most need it.
Transaction recording closes that gap. It captures each transaction as a time-spanned object in the simulator's debug database: a begin time, an end time, and its field values — so the waveform viewer shows transaction-level streams (a labeled "READ addr=0x40" box spanning its cycles) above the pin signals, aligned in time. The mechanism is small: a component marks a transaction's span with begin_tr/end_tr, and the field automation's do_record logs the fields into the recorder — and because recording is opt-in, it must be enabled (a command-line switch or API call) for anything to appear. Get it right and debugging happens at the transaction level: you read the protocol story off the waveform instead of decoding wiggles. Get it wrong — recording not enabled, or a begin_tr with no matching end_tr — and you see nothing, or open-ended garbage. This chapter is recording: what it captures, the accept_tr/begin_tr/end_tr timeline, how do_record populates fields, and how to enable and debug it.
What is transaction recording — how does
begin_tr/end_trcapture a transaction as a time-spanned object, how doesdo_recordlog its fields, how is recording enabled, and how does it surface transaction-level streams in the waveform viewer?
Motivation — why record transactions
Recording exists to make the transaction abstraction usable in debug, and each of its properties serves that goal:
- It debugs at the level you modeled at. You designed transactions so the testbench reasons in protocol units (Module 8.1); recording extends that to debugging, so a failure is "this READ returned wrong data," visible as a labeled box, not forty signal edges to decode. The abstraction pays off exactly when you need it most.
- It aligns the logical and physical views. A recorded transaction spans the same time as the pin activity that realized it, so the viewer shows the "what" (the transaction box) directly above the "how" (the pins) — you can click a transaction and see the cycles it caused, or a glitch and see which transaction it belongs to.
- It shows both stimulus and observation. The driver records what it drove and the monitor records what it observed; seeing both transaction streams aligned makes a mismatch obvious — the driven READ and the observed READ side by side, with their fields, instead of inferring both from pins.
- It's nearly free, thanks to field automation. The same
uvm_field_*macros that generate copy/compare/print (Module 8.4) also generatedo_record, so a registered transaction records its fields automatically — recording is a service you already paid for by registering fields. - It's opt-in, so it costs nothing when off. Recording can be heavy (every transaction logged to a database), so it's disabled by default and enabled on demand — you turn it on for a failing test, off for bulk regression. The cost is paid only when the debug value is wanted.
The motivation, in one line: recording carries the transaction abstraction into the debugger — turning modeled units into visible, time-aligned, field-annotated streams above the pins — so failures are diagnosed in protocol terms, using automation you already have, paid for only when you enable it.
Mental Model
Hold recording as a subtitle track over raw footage:
The pins are raw footage; recorded transactions are the subtitle track that says, in plain protocol language, what's happening at each moment — anchored to the exact time span. Without subtitles, debugging is watching the footage frame by frame and decoding it yourself: these forty signal edges mean a burst read. Recording adds a subtitle track above the footage: a labeled caption — "READ addr=0x40, data=0xDE" — that appears for exactly the span the operation occupies, so you read the story instead of decoding the frames. To produce a subtitle you mark when it starts and when it ends (
begin_tr/end_tr) and what it says (do_recordwrites the fields), and the studio (the recorder) burns it into the timeline. Two things can go wrong, both familiar from subtitling: you forget to turn subtitles on (recording not enabled — the footage plays with no captions), or you start a caption and never close it (begin_trwith noend_tr— a subtitle that hangs on screen forever). When it works, the driver's track captions what was sent and the monitor's track captions what was seen, side by side — and a bug jumps out as two captions that disagree over the same span of footage.
So recording is captioning the waveform: mark each transaction's start, end, and contents, turn the track on, and read the protocol story directly off the timeline — with the driver's "sent" captions and the monitor's "seen" captions aligned for instant comparison.
Visual Explanation — transaction streams above the pins
The payoff of recording is the debug view: transaction-level streams, as labeled time-spans, sitting above the pin signals they correspond to. Seeing that layout is seeing why recording matters.
The figure is the debug view recording produces, and the layout is the value. The top tracks are recorded transaction streams: the driver's stream of what it sent and the monitor's stream of what it observed, each rendered as a row of labeled, time-spanned boxes — a box for the READ of 0x40, a box for the WRITE of 0x44, each annotated with its field values and spanning exactly the cycles it occupied. The bottom track is the raw pin activity (the "how" from Module 8.1) — the same signals you'd otherwise debug alone. The crucial property is time alignment: a transaction box sits directly above the pin transitions that realized it, so you can read the protocol story off the transaction tracks ("READ, then WRITE") and drill down into the pins only for the one transaction that looks wrong. Having both the driver's and the monitor's streams is what makes mismatches pop: a driven READ and an observed READ should agree over the same span, so a discrepancy — different data, a missing observed box, a misaligned span — is visible at a glance, where in raw pins it would take cycle-by-cycle decoding to find. Recording, in short, turns the waveform from a wall of wiggles into an annotated, navigable, transaction-level story — which is the entire reason to do it.
RTL / Simulation Perspective — begin_tr, end_tr, and do_record
Recording a transaction is two calls bracketing its span — begin_tr and end_tr — plus the automatic field logging from do_record. A component records the transactions it handles around the time they happen.
class bus_item extends uvm_sequence_item;
rand bit [31:0] addr;
rand bit [31:0] data;
rand bit is_read;
`uvm_object_utils_begin(bus_item) // field macros ALSO generate do_record
`uvm_field_int(addr, UVM_ALL_ON) // → addr, data, is_read are recorded automatically
`uvm_field_int(data, UVM_ALL_ON)
`uvm_field_int(is_read, UVM_ALL_ON)
`uvm_object_utils_end
function new(string name="bus_item"); super.new(name); endfunction
endclass
// ── DRIVER: record the transaction across the time it is driven ──
task my_driver::run_phase(uvm_phase phase);
forever begin
seq_item_port.get_next_item(req);
void'(begin_tr(req)); // STAMP begin time → opens the recorded transaction
drive_on_pins(req); // CONSUMES TIME — the span being recorded (Module 8.3)
end_tr(req); // STAMP end time → closes it; do_record logs the fields
seq_item_port.item_done();
end
endtask
// the recorded transaction spans begin_tr → end_tr; its addr/data/is_read appear in the viewer
// (recording must be ENABLED — e.g. +UVM_TR_RECORD / set_recording_enabled — or nothing is logged)The recording surface is small and bracket-shaped. begin_tr(req) stamps the begin time and opens a recorded transaction in the database — called right when the transaction's activity starts (here, just before the driver drives it). end_tr(req) stamps the end time and closes it — called when the activity finishes (after driving). Between them is the span the recorded box will occupy, which is exactly the time-consuming driving from Module 8.3. The field values are logged automatically by do_record, which — like copy/compare/print — is generated by the uvm_field_* macros (Module 8.4): because addr, data, and is_read are registered, they appear in the recorded transaction with no extra code. (There's also accept_tr, stamped when a transaction is accepted before it begins — useful when accept and begin differ in time, e.g. queuing — but begin_tr/end_tr are the core pair.) The one non-code prerequisite is in the comment: recording is opt-in, so unless it's enabled (via a simulator switch like +UVM_TR_RECORD, a set_recording_enabled/recording_detail setting, or tool-specific configuration), these calls log nothing and the viewer shows no transaction streams. Bracket the span, register the fields, enable recording — and the transaction appears.
Verification Perspective — the accept / begin / end timeline
Recording places a transaction on the timeline with up to three stamps — accept_tr, begin_tr, end_tr — and understanding what each marks (and that begin and end must pair) is what makes recorded streams correct rather than open-ended.
The three stamps place a transaction precisely on the debug timeline. accept_tr (optional) marks the moment the transaction was accepted — distinct from when it executes. This matters when acceptance and execution differ in time: an item accepted into a queue at one moment but not driven until later can record both the accept time and the begin time, so the viewer shows the wait. For many drivers accept and begin coincide and accept_tr is skipped. begin_tr marks where execution starts and opens the recorded transaction — the left edge of the box. end_tr marks where execution finishes and closes the box — the right edge — and triggers do_record to log the field values into it. The box therefore spans begin_tr to end_tr, which should match the actual activity (the driving span, Module 8.3). The non-negotiable rule is pairing: every begin_tr must be matched by an end_tr. An unmatched begin_tr leaves the transaction open — it never gets an end time, so the viewer renders it as running forever (or to end-of-sim), overlapping everything after it and making the streams unreadable. This is the recording equivalent of a resource leak, and it's the DebugLab. The discipline is bracket-shaped: each begin_tr is a promise that an end_tr will follow on every path.
Runtime / Execution Flow — from transaction to recorded stream
At run time, recording is a data path: a component brackets a transaction with begin_tr/end_tr, the recorder writes a time-spanned, field-annotated entry into the database, and the viewer renders it as a stream — all only if recording is enabled.
The data path turns a modeled transaction into a visible stream — gated, at its head, by the enable switch. Step 1 is the gate: recording is opt-in, so unless it's enabled (a command-line switch like +UVM_TR_RECORD, a set_recording_enabled call, a recording_detail setting, or tool configuration), the entire path is inert — begin_tr/end_tr become effectively no-ops and nothing is logged. This is deliberate (recording is expensive, Module's motivation), but it's also the #1 reason "I see no transactions": the path was never turned on. Step 2, with recording enabled, a component brackets the transaction with begin_tr/end_tr, and do_record supplies the registered fields. Step 3, the recorder writes a time-spanned, field-annotated entry into the simulator's debug database — the persisted artifact. Step 4, the waveform viewer reads the database and renders the transaction as a labeled box above the pins, aligned in time (Figure 1). The shape to remember is enable → bracket → record → render: the modeled transaction becomes a database entry becomes a visible stream, but only past the enable gate. Most recording problems are at one of two points — the gate (not enabled) or the bracket (an unmatched begin_tr) — and knowing the path tells you where to look.
Waveform Perspective — the recorded transaction box over the pins
Recording's whole purpose is visible in one waveform: a transaction-level box spanning the pin cycles that realized it. This is the debug artifact the chapter produces.
A recorded transaction box spans the pin cycles that realized it
11 cyclesThe waveform is the chapter's deliverable: a transaction box over the cycles it caused. The txn_box trace is the recorded transaction — a single labeled span (READ addr=0x40) that goes high at begin_tr and returns low at end_tr, so its width is the transaction's duration. Below it, the pin signals (valid, data) show the actual cycles the driver produced — the data beats D0–D3 — and the box spans exactly those cycles, because begin_tr/end_tr were placed around the driving (Module 8.3's time-consuming span). The value is the alignment: at a glance you see one operation (the box) and, directly beneath it, how it happened (the pins). When debugging, you scan the transaction track to find the operation of interest, then drop into the pins only for that one span — instead of decoding every cycle to figure out where transactions begin and end. Had end_tr been missing, txn_box would never return low — the span would run to the end of the trace, swallowing every later transaction. The picture to carry is this: recording draws the "what" as a box directly above the "how," turning the waveform into a layered, navigable view where the abstraction you modeled is finally visible at the moment you're debugging.
DebugLab — the transaction that never closed
A recorded transaction that ran to end-of-sim because end_tr was skipped on an error path
A driver recorded its transactions with begin_tr/end_tr, and most appeared correctly in the viewer. But occasionally a transaction box would open and never close — it stretched from where it began all the way to the end of simulation, drawn as one enormous span that overlapped and visually swallowed every transaction after it, making the stream unreadable from that point on. The pins for that transaction looked normal; only its recorded box was broken — open-ended, with no end time.
A begin_tr was not matched by an end_tr on an early-exit path — so the recorded transaction was opened but never closed:
driver:
void'(begin_tr(req)); // OPENS the recorded transaction (begin time stamped)
if (req.is_read && error_cond) begin
report_and_skip();
continue; // ✗ early exit — end_tr NEVER reached for this item
end
drive_on_pins(req);
end_tr(req); // only reached on the normal path
result: on the error path, begin_tr opened a transaction that end_tr never closed
→ the recorded box has no end time → renders open-ended to end-of-sim
fix — guarantee end_tr on EVERY path that took begin_tr:
void'(begin_tr(req));
if (req.is_read && error_cond) begin
report_and_skip();
end_tr(req); // ✓ close it before exiting
continue;
end
drive_on_pins(req);
end_tr(req); // ✓ normal pathThis is the recording equivalent of a leak: a begin_tr opens a recorded transaction, and the matching end_tr closes it by stamping the end time — but the early-exit path returned (via continue) before reaching end_tr, so that transaction stayed open. An open transaction has a begin time but no end time, so the viewer cannot bound its box and draws it running to the end of simulation, overlapping every later transaction and corrupting the stream's readability. The pins were fine because driving and recording are independent — the bug is purely in the recording bracket, not the stimulus. The fix is the bracket-discipline rule: every begin_tr must be matched by an end_tr on every code path that executed the begin_tr — including error paths, early returns, and exception handling. Concretely, add an end_tr before the early continue (or restructure so end_tr is unconditionally reached). The deeper habit is to treat begin_tr/end_tr like an open/close pair (an acquire/release): the moment you write begin_tr, ensure an end_tr is guaranteed to follow no matter how the code leaves.
The tell is a recorded transaction with no end — an open-ended box. Diagnose recording bracket/enable bugs:
- First confirm recording is even enabled. If you see no transaction streams at all (not just a broken one), the likely cause is recording not being enabled — check the switch/API before anything else.
- Find the unclosed
begin_tr. An open-ended box means abegin_trwhoseend_trwasn't reached. Inspect the recording component for paths — earlyreturn/continue, error branches — that skipend_tr. - Check every exit path took
end_tr. For the transaction type that broke, trace all paths between itsbegin_trand the loop's next iteration; any path missingend_tris the bug. - Confirm the span matches the activity. Even when paired, a box whose width doesn't match the driving means
begin_tr/end_trare placed at the wrong moments — align them with the actual transaction span.
Treat begin_tr/end_tr as a guaranteed pair:
- Match every
begin_trwith anend_tron all paths. Including error, early-exit, and exception paths — the moment you writebegin_tr, ensureend_tris unconditionally reachable afterward. - Place the bracket tightly around the activity.
begin_trright as the transaction starts,end_trright as it finishes, so the recorded span matches the real one (Module 8.3's driving span). - Enable recording deliberately for debug. It's opt-in and off by default; turn it on (switch/API) for the failing test, and confirm streams appear — don't assume recording is happening.
- Let field macros handle the fields. Register fields with
uvm_field_*sodo_recordlogs them automatically; don't hand-roll field recording, and check a key field is registered if it's missing from the box.
The one-sentence lesson: a begin_tr opens a recorded transaction and end_tr closes it, so a begin_tr not matched by an end_tr on every path (especially error/early-exit paths) leaves the transaction open and renders it running to end-of-sim — bracket every transaction with a guaranteed begin_tr/end_tr pair, and remember recording must be enabled at all for any of it to appear.
Common Mistakes
- Expecting recording without enabling it. Recording is opt-in and off by default; without the switch/API (
+UVM_TR_RECORD,set_recording_enabled),begin_tr/end_trlog nothing and the viewer shows no streams. - A
begin_trwith no matchingend_tr. The transaction never closes and renders open-ended to end-of-sim; guarantee anend_tron every path, including error/early-exit. - Misplaced
begin_tr/end_tr. A span that doesn't match the actual activity gives misleading boxes; bracket tightly around the real transaction timing. - Hand-rolling field recording. The
uvm_field_*macros generatedo_recordautomatically; a field missing from the box is usually an unregistered field, not a recording-code problem. - Recording too much, always on. Recording every transaction in bulk regression is expensive; enable it for debug runs, not by default.
- Confusing recording with the transaction's behavior. Recording is a debug observation layer — a recording bug (open box, no stream) doesn't change what was driven; the pins are independent.
Senior Design Review Notes
Interview Insights
Transaction recording captures each transaction as a time-spanned object in the simulator's debug database — with a begin time, an end time, and its field values — so the waveform viewer can show transaction-level streams above the pin signals. Instead of debugging a failure by decoding raw pin wiggles cycle by cycle, you see labeled boxes like "READ addr=0x40, data=0xDE" spanning exactly the cycles the operation occupied, aligned in time with the pins that realized it. It's useful because it carries the transaction abstraction — the whole reason you modeled operations as data — into the debugger, which is exactly where you most need it. You read the protocol story off the transaction track and drill into the pins only for the one operation that looks wrong. It's especially powerful when both the driver and the monitor record, because then you see the sent stream and the observed stream side by side, and a mismatch — wrong data, a missing observed transaction, a misaligned span — jumps out at a glance instead of requiring cycle-by-cycle comparison. And it's nearly free to get, because the same uvm_field_* macros that generate copy, compare, and print also generate do_record, so a registered transaction records its fields automatically. The one catch is that recording is opt-in and off by default, because logging every transaction is expensive, so you enable it for the failing test and leave it off for bulk regression.
Exercises
- Bracket a transaction. Write the driver loop that records each driven item, placing
begin_trandend_trso the recorded span matches the driving, and state what enables the recording to actually appear. - Fix the open box. A driver calls
begin_trthencontinues on an error path beforeend_tr. Describe what the viewer shows and rewrite the path to close the transaction. - Diagnose the empty viewer. You see pins but zero transaction streams. List the checks in order, starting with the most likely cause.
- Explain the field path. A recorded transaction shows
addrbut notdata. Explain the most likely cause and the fix, and which mechanism logs the fields.
Summary
- Transaction recording captures each transaction as a time-spanned object in the simulator's debug database —
begin_trstamps its start,end_trits end, anddo_recordlogs its fields — so the waveform viewer shows labeled transaction streams above the pins, aligned in time. - It debugs at the abstraction level you modeled at: a failure is a labeled "READ addr=0x40" box (the "what") directly over the pin cycles that realized it (the "how"), and the driver's sent stream beside the monitor's observed stream makes mismatches obvious.
do_recordis generated by theuvm_field_*macros (Module 8.4), so registered fields record automatically — recording is a service you already paid for by registering fields; a missing field usually means it isn't registered.- Two things make it work: recording must be enabled (it's opt-in and off by default — the #1 reason "I see no transactions"), and every
begin_trmust be matched by anend_tron all paths, or the transaction renders open-ended to end-of-sim. - The durable rule of thumb: bracket each transaction with a guaranteed
begin_tr/end_trpair (closed on every path, error paths included), let the field macros'do_recordlog the fields, record from both driver and monitor for side-by-side comparison, and enable recording deliberately for debug runs — so the transaction abstraction you modeled becomes a visible, navigable, field-annotated layer above the pins exactly when you need it.
Next — Sequence Basics: transactions are the unit; sequences are how you generate them in meaningful patterns. The next module opens with the sequence — what it is, how its body produces a stream of items, and how it runs on a sequencer to drive coordinated stimulus.