UVM
uvm_transaction
The uvm_object subtype that adds transaction identity (transaction_id) and timing/recording (begin_tr/end_tr), correlation of out-of-order responses, and the base sequence items extend.
UVM Base Classes · Module 4 · Page 4.4
The Engineering Problem
uvm_object (Module 4.2) gives a data class its toolkit — copy, compare, print, pack. But a transaction is a special kind of data: it represents an operation that happens over time and that often must be correlated with another (a request with its response). A plain uvm_object has no notion of when it started and ended, and no identity to tie a response back to the request that caused it. Those two ideas — timing and identity — are exactly what uvm_transaction adds.
uvm_transaction extends uvm_object and contributes two things: a transaction identity (transaction_id) used to correlate related transactions, and timing/recording (begin_tr/end_tr, which stamp the transaction's begin and end times and log it to the recording database so it appears as a transaction in waveform tools). In modern practice you rarely extend uvm_transaction directly — you extend uvm_sequence_item, which extends uvm_transaction — but the identity and timing you rely on come from here, and not understanding them produces real bugs: a scoreboard that can't match out-of-order responses to their requests, or a transaction recording that never closes. This chapter is the layer that turns a data object into a timed, identifiable transaction.
What is
uvm_transaction, what does it add overuvm_object(the transaction_id for identity and begin_tr/end_tr for timing and recording), and how do those features support correlation and transaction-level debugging?
Motivation — why a transaction needs identity and timing
The two additions of uvm_transaction solve problems a plain data object cannot:
- Identity enables correlation. Many protocols decouple a request from its response — pipelined, split, or out-of-order (AXI is the classic case). To check a response, a scoreboard must know which request it answers, and the
transaction_idis the key that links them. Without it, you can only match by arrival order, which is wrong the moment responses come back out of order. - Timing/recording enables transaction-level debug.
begin_tr/end_trstamp when a transaction started and ended and record it to a database, so waveform viewers can display it as a labelled span above the pin wiggles. Debugging at the transaction level — "this transfer took 40 cycles and overlapped that one" — is far faster than reading raw signals, and that view exists because transactions are recorded. - It's the conceptual home of "transaction-ness." A
uvm_sequence_itemis a transaction plus sequencer machinery; the part that makes it a transaction — that it has a lifetime and an identity — isuvm_transaction. Knowing the split clarifies what each layer of the data lineage contributes. - The features are inherited, so misusing them is common. Because you get
transaction_id,begin_tr, andend_trfor free by extendinguvm_sequence_item, engineers use them without understanding them — leading to uncorrelated responses and unbalanced recordings. Understanding the source prevents the misuse.
The motivation, in one line: a transaction is data that happens over time and often must be correlated, and uvm_transaction is the layer that adds exactly those two capabilities — identity and timed recording — to a plain object.
Mental Model
Hold uvm_transaction as a data object that carries a ticket and a timestamp:
A
uvm_transactionis auvm_objectthat has been issued a ticket number and a clock-in/clock-out stamp. The data toolkit (copy/compare/print) is still there, but now the object also carries an identity ticket (transaction_id) — a number that lets you match it to a related transaction later, the way a coat-check ticket matches you to your coat even if others are claimed first. And it carries a timecard: when its operation begins you clock it in (begin_tr, stamping the begin time) and when it ends you clock it out (end_tr, stamping the end time), and that timecard is filed in a database the waveform viewer reads, so the transaction appears as a labelled span over the cycles it occupied. The ticket is for correlation (which request does this response answer?); the timecard is for recording (when did this transaction live, shown on the wave?).
So when you work with a transaction, two questions beyond its data become available: what is its identity, and when did it begin and end? You set/read the ticket (transaction_id) to correlate, and you clock it in and out (begin_tr/end_tr) to record — and both must be used in balanced, deliberate ways, or correlation fails and recordings don't close.
Visual Explanation — what uvm_transaction adds over uvm_object
uvm_transaction is a thin but meaningful layer: it keeps the entire uvm_object data toolkit and adds identity and timing/recording on top.
The layering shows uvm_transaction for what it is: a focused extension. The base (greyed) is the unchanged uvm_object toolkit — a transaction is still copied, compared, and printed like any data. The two additions are narrow and specific. Identity (transaction_id) is a single number whose job is correlation: it lets a scoreboard say "this response belongs to that request." Timing/recording (begin_tr/end_tr and the begin/end times) is the lifetime: clock-in and clock-out, filed to a database so the transaction is visible as a span in a transaction-aware waveform viewer. That's the whole of uvm_transaction — two capabilities that a timed, correlatable operation needs and a plain object lacks.
RTL / Simulation Perspective — recording a transaction's lifetime
In practice, identity is set/read on the transaction, and a component (driver or monitor) records the transaction's lifetime with begin_tr/end_tr. The code below shows both — noting that you normally extend uvm_sequence_item, inheriting these from uvm_transaction.
class bus_txn extends uvm_transaction; // normally extend uvm_sequence_item (which extends this)
rand bit [31:0] addr, data;
`uvm_object_utils(bus_txn)
function new(string name = "bus_txn"); super.new(name); endfunction
endclass
// In a driver (a component), record each transaction's lifetime as it drives it:
task run_phase(uvm_phase phase);
bus_txn req;
forever begin
seq_item_port.get_next_item(req);
void'(begin_tr(req)); // stamp begin_time; open a recording for this transaction
drive_on_pins(req); // takes several cycles on the bus
end_tr(req); // stamp end_time; close the recording → a span in the waveform DB
seq_item_port.item_done();
end
endtask
// Identity, for correlating a response with its request (e.g., out-of-order protocols):
function void check_response(bus_txn rsp);
// rsp.set_transaction_id(req.get_transaction_id()); // propagate the id from request to response
bus_txn req = pending[rsp.get_transaction_id()]; // match by ID, not by arrival order
if (!rsp.compare(req)) `uvm_error("SB", "mismatch")
endfunctionTwo mechanisms are at work. Recording: the driver wraps its per-transaction work in begin_tr(req) … end_tr(req), which stamp the transaction's begin and end times and log it to the recording database — so in a transaction-aware waveform viewer, req appears as a labelled span covering the cycles it was driven. Identity: get_transaction_id() / set_transaction_id() provide the correlation key; a scoreboard for an out-of-order protocol indexes its pending requests by transaction_id and matches a response to its request by id, not by arrival order. Both come from uvm_transaction, inherited even when you extend uvm_sequence_item — which is why the next section places uvm_transaction precisely in the data lineage.
Verification Perspective — uvm_transaction in the data lineage
uvm_transaction sits in the middle of the data-class lineage: above plain uvm_object, below uvm_sequence_item. Knowing what each level adds tells you why you extend uvm_sequence_item in practice yet rely on uvm_transaction's features.
This lineage answers a common point of confusion. You almost always write class my_item extends uvm_sequence_item;, not extends uvm_transaction — because a real transaction needs the sequencer machinery that uvm_sequence_item adds (so it can be generated by a sequence). But the transaction-ness — the transaction_id and the begin_tr/end_tr timing — lives in uvm_transaction, one level down, and is inherited. So uvm_transaction is rarely the class you name, yet it is always the source of the identity and timing your transactions carry. Understanding the lineage tells you where each capability comes from: data from uvm_object, identity and timing from uvm_transaction, generatability from uvm_sequence_item. (The next chapter covers uvm_sequence_item itself.)
Runtime / Execution Flow — the recording lifecycle
begin_tr/end_tr define a transaction's recorded lifecycle: it is accepted, begun (begin time stamped, recording opened), active for some duration, then ended (end time stamped, recording closed and filed). This open-then-close discipline is what makes the recording valid.
The lifecycle is a balanced open/close, and the balance matters. begin_tr opens a recording and stamps the begin time; end_tr closes it and stamps the end time; the duration between them is the transaction's recorded lifetime. The key discipline is that every begin_tr must be matched by an end_tr — an unmatched begin_tr leaves a recording open (a transaction that "never ends" in the database), and the displayed span is wrong or missing. This is the same shape as raise_objection/drop_objection from end-of-test: a bracketed open/close that must balance. Used correctly, the lifecycle gives you the transaction-level view of a run — labelled spans you can read and correlate — which is one of the most powerful debugging aids UVM provides, and it exists because uvm_transaction records timing.
Waveform Perspective — the transaction as a recorded span
begin_tr/end_tr are most tangible on the waveform: they bracket a transaction's lifetime, and a transaction-aware viewer draws that lifetime as a span above the pins — exactly what transaction recording produces.
begin_tr and end_tr bracket a transaction's recorded lifetime — shown as a span
10 cyclesThe tr span from begin_tr to end_tr is the transaction's recorded lifetime, and it is the whole point of uvm_transaction's timing. On a raw waveform you'd see only the valid/data wiggles and have to mentally group them into operations; with recording, the viewer draws the operation for you — a labelled span covering exactly the cycles between begin and end, annotated with the transaction's fields and its transaction_id. This is transaction-level debugging: you read the spans (and correlate requests to responses by id) instead of decoding cycles. The span exists because the driver called begin_tr and end_tr, stamping the times uvm_transaction provides — which is why a balanced begin/end and a correctly-set id are what make the transaction view trustworthy.
DebugLab — the scoreboard that matched responses by order on an out-of-order bus
Spurious mismatches because responses were matched by arrival order, not transaction_id
A scoreboard for a pipelined bus checked each response against the next pending request, in arrival order. It passed on simple in-order traffic, but the moment the DUT returned responses out of order (legal for the protocol), it reported a flood of mismatches — responses compared against the wrong requests. The data was actually correct; the pairing was wrong.
Responses were correlated to requests by arrival order instead of by transaction_id. The protocol allowed out-of-order completion, so the Nth response was not necessarily the answer to the Nth request — but the scoreboard assumed it was:
requests sent: id=1 (addr X), id=2 (addr Y), id=3 (addr Z)
responses back: id=2, id=1, id=3 (out of order — legal)
scoreboard did: match responses to requests by ARRIVAL ORDER
→ response#1 (id=2) checked against request#1 (id=1) → MISMATCH (wrong pair)
correct: match by transaction_id → response id=2 ↔ request id=2 → correct pair
fix: index pending requests by transaction_id; match the response by its idThe transaction_id exists precisely to correlate a response with its request independent of order, and the scoreboard ignored it. On in-order traffic, arrival order happened to coincide with id order, hiding the bug; out-of-order traffic exposed it. The mismatch was in the correlation, not the design.
The tell is mismatches that appear only under out-of-order or pipelined traffic and vanish in-order — an ordering-correlation bug. Diagnose by checking how requests and responses are paired:
- Confirm matching is by
transaction_id, not arrival order. If the scoreboard pairs the Nth response with the Nth request, it assumes in-order completion. For any protocol that can reorder, match by id (index pending requests bytransaction_id). - Verify the id is propagated. The response must carry the request's
transaction_id(set it when the response is created/returned), or there's nothing to match on. A missing/zero id breaks correlation just as order-based matching does. - Test with out-of-order stimulus. If checks pass in-order but fail when responses are reordered, the correlation is order-based — exactly the signature of this bug.
Correlate by identity, never by order, for anything that can reorder:
- Match requests and responses by
transaction_id. Index pending requests by id and look up the response's id — thetransaction_idis whatuvm_transactionprovides for exactly this. Don't assume in-order completion. - Propagate the id end to end. Ensure the response carries the request's
transaction_id(set it explicitly when the response is formed), so the correlation key is present. - Stress with reordering. Verify the scoreboard with out-of-order and pipelined responses, so an order-based assumption is caught immediately rather than only on hard traffic.
The one-sentence lesson: transaction_id exists to correlate a response with its request regardless of order — match by id, not by arrival order, or any out-of-order protocol will produce spurious mismatches that hide on in-order traffic.
Common Mistakes
- Matching responses to requests by order instead of
transaction_id. Order-based pairing assumes in-order completion and breaks on any reordering protocol. Correlate bytransaction_id, which is exactly what it's for. - Not propagating the
transaction_id. If a response doesn't carry its request's id, there's no key to correlate on. Set the id end to end so requests and responses can be matched. - Unbalanced
begin_tr/end_tr. Everybegin_trmust be matched by anend_tr; an unmatched begin leaves a recording open (a transaction that never ends in the database) and a wrong or missing span. Bracket them like objections. - Extending
uvm_transactiondirectly for stimulus. A transaction you generate from a sequence should extenduvm_sequence_item(which extendsuvm_transaction), so it has the sequencer machinery. Extenduvm_transactiononly when you genuinely don't need sequence generation. - Confusing identity with the object name.
transaction_idis a correlation number for matching related transactions; it is not the object'sget_name(). Usetransaction_idfor request/response pairing. - Forgetting recording is opt-in work.
begin_tr/end_trmust be called (typically by the driver/monitor) for a transaction to appear in the recording database; transactions aren't recorded automatically just by existing.
Senior Design Review Notes
Interview Insights
uvm_transaction is the uvm_object subtype that turns a plain data object into a timed, identifiable transaction. Over uvm_object — whose toolkit (copy, compare, print, pack) it inherits unchanged — it adds two things. First, identity: a transaction_id, a correlation number used to tie related transactions together, most importantly to match a response with the request that caused it, even when responses come back out of order. Second, timing and recording: begin_tr and end_tr, which stamp the transaction's begin and end times and log it to the recording database, so a transaction-aware waveform viewer can display it as a labelled span above the pin wiggles. In modern UVM you rarely extend uvm_transaction directly — you extend uvm_sequence_item, which extends uvm_transaction — but the transaction_id and the begin_tr/end_tr timing you use come from uvm_transaction. So it's the layer that contributes "transaction-ness" — a lifetime and an identity — to a data object.
Exercises
- Name the additions. State the two capabilities
uvm_transactionadds overuvm_object, and for each, the method(s) you use and the problem it solves. - Fix the correlation. A scoreboard for an out-of-order bus matches responses to requests by arrival order and reports spurious mismatches. Describe the fix in terms of
transaction_id, and what must be true of the response for it to work. - Balance the recording. A transaction view shows transactions that "never end." Explain the likely cause in terms of
begin_tr/end_tr, and the rule that prevents it. - Place it in the lineage. Order
uvm_object,uvm_sequence_item,uvm_transaction,uvm_sequenceby inheritance, state what each adds, and explain why you typically extenduvm_sequence_itemyet rely onuvm_transaction's features.
Summary
uvm_transactionis theuvm_objectsubtype that makes data a timed, identifiable transaction — it inherits the full data toolkit and adds two things.- Identity: a
transaction_idto correlate related transactions — crucially a response with its request — independent of arrival order, which is essential for pipelined and out-of-order protocols. - Timing and recording:
begin_tr/end_trstamp the transaction's begin and end times and log it to the recording database, so a transaction-aware waveform viewer shows it as a labelled span — enabling transaction-level debugging. Everybegin_trmust be matched by anend_tr. - In the data lineage (
uvm_object→uvm_transaction→uvm_sequence_item→uvm_sequence), you normally extenduvm_sequence_item(for sequencer machinery) and inherituvm_transaction's identity and timing — souvm_transactionis rarely named yet always the source of those features. - The durable rule of thumb: a transaction has a ticket (
transaction_id) and a timecard (begin_tr/end_tr) — correlate responses to requests by id, not by order; bracket every begin with an end; and record transactions so you can debug in spans, not pins.
Next — uvm_sequence_item: uvm_transaction makes data a timed, identifiable transaction; the next chapter adds the final layer — uvm_sequence_item, the class you actually extend, which gives a transaction the machinery to be generated by a sequence on a sequencer.