Skip to content

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 over uvm_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_id is 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_tr stamp 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_item is a transaction plus sequencer machinery; the part that makes it a transaction — that it has a lifetime and an identity — is uvm_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, and end_tr for free by extending uvm_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_transaction is a uvm_object that 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.

uvm_transaction layers transaction_id (identity) and begin_tr/end_tr timing/recording over the uvm_object data toolkitWhat uvm_transaction adds over uvm_objectWhat uvm_transaction adds over uvm_objectTiming & recordingbegin_tr / end_tr stamp begin/end times and log to the recording database — shown as a span in waveform viewersbegin_tr / end_tr stamp begin/end times and log to the recording database — shown as a span in waveform viewersIdentitytransaction_id — correlates related transactions (a request with its response, even out of order)transaction_id — correlates related transactions (a request with its response, even out of order)uvm_object (inherited)the full data toolkit — copy, compare, print, pack — unchangedthe full data toolkit — copy, compare, print, pack — unchanged
Figure 1 — uvm_transaction = uvm_object + identity + timing/recording. The inherited base is the uvm_object data toolkit (copy/compare/print/pack). On top, uvm_transaction adds a transaction_id (identity, for correlating related transactions) and begin_tr/end_tr with begin/end times (timing and recording to the database, for transaction-level waveform display). These two additions are what make a data object a transaction.

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.

uvm_transaction — identity and timed recording
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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")
endfunction

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

Data lineage: uvm_object base, uvm_transaction adds identity and timing, uvm_sequence_item adds sequencer machinery, uvm_sequence is the generatoruvm_object → uvm_transaction → uvm_sequence_item → uvm_sequenceuvm_object → uvm_transaction → uvm_sequence_item → uvm_sequence1uvm_objectthe data toolkit: copy, compare, print, pack — any data.2uvm_transactionadds transaction identity (transaction_id) and timing/recording(begin_tr/end_tr).3uvm_sequence_itemadds the machinery to be generated by a sequence on a sequencer(what you usually extend).4uvm_sequencea generator of sequence items — itself a transient object that runson a sequencer.
Figure 2 — the data-class lineage and what each level adds. uvm_object is the base data toolkit (copy/compare/print/pack). uvm_transaction adds transaction identity (transaction_id) and timing/recording (begin_tr/end_tr). uvm_sequence_item adds the machinery to be generated by a sequence on a sequencer. uvm_sequence (a generator) extends sequence_item. You normally extend uvm_sequence_item for transactions — inheriting uvm_transaction's identity and timing — and rarely extend uvm_transaction directly.

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.

Recording lifecycle: begin_tr opens and stamps begin time, transaction active over cycles, end_tr stamps end time and closes the recordingbegin_tr → active → end_trbegin_tr → active → end_tr1begin_tr — open + stamp beginthe begin time is recorded and a transaction recording is opened inthe database.2Active — driven/observedthe transaction occupies several cycles on the bus; the recordingis open through this span.3end_tr — stamp end + closethe end time is recorded and the recording is closed and filed withits full span.4Displayed as a spana transaction-aware waveform viewer shows the closed recording as alabelled span over its cycles.
Figure 3 — the transaction recording lifecycle. A transaction is begun with begin_tr (its begin time is stamped and a recording is opened in the database), remains active while it is driven/observed over several cycles, and is ended with end_tr (its end time is stamped and the recording is closed). The closed recording, with its begin and end times, is what a transaction-aware waveform viewer displays as a span. Every begin_tr must be matched by an end_tr, or the recording stays open.

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 cycles
begin_tr and end_tr bracket a transaction's recorded lifetime — shown as a spanbegin_tr(): the transaction's begin_time is stamped and recording opensbegin_tr(): the transa…transaction active; its transaction_id correlates it with its responsetransaction active; it…end_tr(): the end_time is stamped and the recording closes — a span in the viewerend_tr(): the end_time…clktrvaliddata00A0A1A2A30000000000t0t1t2t3t4t5t6t7t8t9
Figure 4 — begin_tr at the first cycle stamps the transaction's begin time and opens a recording; the transaction is then driven over several cycles (valid/data); end_tr stamps the end time and closes the recording. The tr span (begin_tr to end_tr) is what a transaction-aware waveform viewer displays as a labelled transaction above the pin wiggles. Its transaction_id correlates it with a response. This recorded span — not the raw pins — is how you debug at the transaction level.

The 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

Symptom

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.

Root cause

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:

why order-based matching fails out of order
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 id

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

Diagnosis

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:

  1. 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 by transaction_id).
  2. 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.
  3. 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.
Prevention

Correlate by identity, never by order, for anything that can reorder:

  1. Match requests and responses by transaction_id. Index pending requests by id and look up the response's id — the transaction_id is what uvm_transaction provides for exactly this. Don't assume in-order completion.
  2. 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.
  3. 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 by transaction_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. Every begin_tr must be matched by an end_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_transaction directly for stimulus. A transaction you generate from a sequence should extend uvm_sequence_item (which extends uvm_transaction), so it has the sequencer machinery. Extend uvm_transaction only when you genuinely don't need sequence generation.
  • Confusing identity with the object name. transaction_id is a correlation number for matching related transactions; it is not the object's get_name(). Use transaction_id for request/response pairing.
  • Forgetting recording is opt-in work. begin_tr/end_tr must 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

  1. Name the additions. State the two capabilities uvm_transaction adds over uvm_object, and for each, the method(s) you use and the problem it solves.
  2. 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.
  3. 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.
  4. Place it in the lineage. Order uvm_object, uvm_sequence_item, uvm_transaction, uvm_sequence by inheritance, state what each adds, and explain why you typically extend uvm_sequence_item yet rely on uvm_transaction's features.

Summary

  • uvm_transaction is the uvm_object subtype that makes data a timed, identifiable transaction — it inherits the full data toolkit and adds two things.
  • Identity: a transaction_id to 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_tr stamp 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. Every begin_tr must be matched by an end_tr.
  • In the data lineage (uvm_objectuvm_transactionuvm_sequence_itemuvm_sequence), you normally extend uvm_sequence_item (for sequencer machinery) and inherit uvm_transaction's identity and timing — so uvm_transaction is 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.