Skip to content

UVM

Transaction Lifecycle

The temporal journey of one transaction — created and randomized in zero time, sent through a blocking handshake while the driver consumes time on the pins, then completed and answered — and what blocks when.

Transaction Modeling · Module 8 · Page 8.3

The Engineering Problem

The previous chapter covered the interface by which an item is generated and consumed — the start_item/finish_item and get_next_item/item_done calls (Module 8.2). This chapter follows one transaction through its lifecycle in time: the ordered states it passes through from creation to completion, which of those happen in zero time and which consume simulation time, and — most importantly — what blocks when. Getting the lifecycle wrong is how you randomize too late and drive default values, or reuse one object and corrupt an item still in flight.

A transaction's life has a precise shape. It is created and randomized in zero time (object construction and constraint solving are function-land operations). It is then sent through a blocking handshake: start_item blocks until the sequencer grants this sequence arbitration, then finish_item blocks until the driver signals item_done. During that block, the driver drives the item on the pins — which consumes simulation time (clocks, handshakes). When the driver calls item_done, the item is complete, finish_item unblocks, and an optional response routes back. So a single transaction spans both worlds: a zero-time front (build, randomize) and a time-consuming middle (the handshake and driving), with the sequence suspended through the middle. That suspension is not incidental — it's what paces the sequence to the DUT, so stimulus can never outrun the design. This chapter is that timeline: the states, the zero-time/time-consuming split, the blocking points, and the lifecycle bugs that come from misordering them.

What is the lifecycle of one transaction in time — the ordered states from creation to completion, which happen in zero time versus consume simulation time, what blocks when, and how does the blocking pace the sequence to the DUT?

Motivation — why the lifecycle has this shape

The lifecycle's structure — zero-time front, blocking time-consuming middle — is not arbitrary; each part exists for a reason:

  • Creation and randomization are zero-time because they're decisions, not actions. Building the object and solving its constraints decide what the transaction will be; nothing has happened on the wires yet. Decisions belong in function-land (zero time), so the sequence can prepare an item without advancing the clock.
  • Driving consumes time because it's physical. Translating the item into pin activity — asserting signals, waiting for handshakes, clocking data beats — is the one part that interacts with the DUT, and that interaction unfolds over real clock cycles. The time cost lives entirely in the driver (Module 8.1's "how").
  • The blocking is what synchronizes the sequence with the driver. If the sequence didn't block at finish_item, it would race ahead generating items the driver hasn't consumed. Blocking until item_done means the sequence advances in lockstep with the driver — one item fully handled before the next begins (for a simple sequence).
  • That lockstep is back-pressure: the DUT paces the stimulus. Because the sequence is suspended until the driver finishes (and the driver finishes only as fast as the DUT accepts), the stimulus rate is governed by the design. A slow DUT naturally slows the sequence — no manual flow control needed.
  • Each transaction needs its own lifetime because lifetimes overlap with references. An item being driven, recorded, or analyzed is still referenced by other components; if the sequence reuses that object for the next transaction, it mutates something still in use. Distinct objects keep each transaction's lifetime independent.

The motivation, in one line: the lifecycle separates deciding the transaction (zero-time create/randomize) from performing it (time-consuming driving), and blocks the sequence across the performance — which synchronizes sequence and driver, paces stimulus to the DUT, and demands that each transaction be its own object so overlapping references don't collide.

Mental Model

Hold the lifecycle as placing an order at a kitchen counter and waiting for it:

A transaction's life is an order at a counter: you decide and write it instantly, then you wait — blocked — while the kitchen cooks it, and the waiting is what keeps you in step with the kitchen. Writing the order — choosing the dish, filling in the details — is instant (create and randomize, zero time): you've decided what, but nothing is cooking yet. Then you hand it over and wait at the counter (finish_item blocks). First you wait for the counter to take your order (start_item — the grant, especially if others are queued). Then, once accepted, you wait while the kitchen actually cooks it — which takes real time (the driver driving the pins). You can't walk away and place ten more orders; you're held at the counter until this one is served (item_done). That waiting isn't a nuisance — it's exactly what stops you from flooding a kitchen that can only cook so fast: the kitchen's pace becomes your pace. When it's served, you might also get a receipt confirming what was made (the response). And crucially, you write each order on a fresh ticket — reusing one ticket and scribbling the next order over it would corrupt the one the kitchen is still cooking.

So a transaction's life is decide-instantly-then-wait-while-it-cooks: the zero-time front is your decision, the blocking middle is the kitchen's work, and the wait is the pacing. Write each on its own ticket, and randomize before you hand it over — scribbling the details after it's in the kitchen is too late.

Visual Explanation — the lifecycle as an ordered journey

The lifecycle is a fixed sequence of states. Seeing them in order — and noting which are zero-time and which consume time — is the whole picture.

The ordered transaction lifecycle from zero-time creation and randomization through the time-consuming handshake and driving to completion and responsecreate → randomize → start_item → finish_item ↔ drive → item_done → responsecreate → randomize → start_item → finish_item ↔ drive → item_done → response1Create (zero time)type_id::create — object exists, fields at defaults. A decision,not an action.2Randomize (zero time)randomize() solves constraints — what the transaction will be.Still no clock advance.3start_item / finish_item (block)start_item waits for grant; finish_item sends and blocks untilitem_done.4Driver drives (consumes time)translates to pin activity over clock cycles — the onetime-consuming part.5item_done → complete (+ response)completion unblocks finish_item; an optional response routes backvia get_response.
Figure 1 — the ordered lifecycle of one transaction. Create and randomize happen in zero time (deciding what the transaction is). start_item blocks for the sequencer's grant; finish_item sends the item and blocks. During that block the driver drives the item on the pins, consuming simulation time. item_done completes the item and unblocks finish_item; an optional response routes back. The journey runs left to right: a zero-time front (decide), a time-consuming middle (perform), and completion.

The journey reads left to right as decide, then perform. Create (step 1) constructs the object — it exists, fields at defaults — a zero-time operation because nothing has touched the wires. Randomize (step 2) solves the constraints to decide the transaction's values — also zero-time, because choosing what an item is doesn't advance the clock. These two steps are the decision half: the sequence has fully determined the transaction without any simulation time passing. Then send (step 3) enters the blocking handshake: start_item suspends the sequence until the sequencer grants it arbitration, and finish_item hands the item to the driver and suspends until the driver is done. Drive (step 4) is where simulation time actually passes: the driver translates the item into pin activity over real clock cycles — the only time-consuming part of the lifecycle, and it happens while the sequence is blocked. Finally complete (step 5): item_done signals the driver is finished, which unblocks finish_item, and an optional response routes back for get_response. The shape to carry is the asymmetry: steps 1–2 are instantaneous decisions, steps 3–5 are a time-consuming performance during which the sequence waits. The boundary between "deciding" and "performing" — between randomize and finish_item — is exactly where the two lifecycle rules live.

RTL / Simulation Perspective — the lifecycle in code, annotated by time

The lifecycle maps directly onto the sequence and driver code, and annotating each line with zero-time versus consumes-time makes the structure explicit.

one transaction's lifecycle, annotated by time and blocking
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── SEQUENCE side: decide (zero time), then send (blocks) ──
task my_seq::body();
  bus_item req = bus_item::type_id::create("req");   // CREATE      — zero time
  start_item(req);                                    // SEND/grant  — BLOCKS until granted
  assert(req.randomize() with { addr inside {[0:'hF]}; });  // RANDOMIZE — zero time (between start & finish)
  finish_item(req);                                   // SEND/drive  — BLOCKS until item_done
  // ── (sequence is SUSPENDED here while the driver drives over clock cycles) ──
  get_response(rsp);                                  // optional    — BLOCKS until response
endtask
 
// ── DRIVER side: the time-consuming middle ──
task my_driver::run_phase(uvm_phase phase);
  forever begin
    seq_item_port.get_next_item(req);   // BLOCKS until an item is available
    drive_on_pins(req);                 // CONSUMES TIME — clocks, handshakes, beats
    seq_item_port.item_done();          // completes the item → unblocks finish_item
  end
endtask

Read the lifecycle off the annotations. On the sequence side, create and randomize are zero-time — they decide the transaction without advancing the clock. Note the placement of randomize: it sits between start_item and finish_item, which is the correct lifecycle position (after the grant, before the send) — and the reason is mechanical: finish_item is what hands the item to the driver, so the item's values must be final before finish_item, not after. The two start_item/finish_item calls are the blocking points: start_item blocks until the sequencer grants, and finish_item blocks until the driver's item_done. The comment marks the crucial fact — while finish_item is blocked, the sequence is suspended, doing nothing, as the driver works. On the driver side, get_next_item blocks until an item arrives, drive_on_pins is the one time-consuming step (the pin-level "how" from Module 8.1, unfolding over clock cycles), and item_done completes the item — which is what unblocks the sequence's finish_item. So the code is the lifecycle: zero-time decisions on the sequence side, a time-consuming performance on the driver side, joined by the blocking handshake that suspends the sequence across the driver's work.

Verification Perspective — what blocks when

The defining feature of the lifecycle is its blocking structure: four calls block, each waiting on a specific event. Knowing exactly what each waits for is what lets you reason about hangs, pacing, and ordering.

The blocking points: start_item waits for grant, finish_item waits for item_done, get_next_item waits for an item, get_response waits for a responseblocks untilblocks untilblocks untilblocks untilstart_item (seq)request to sendsequencer grantarbitration wonfinish_item (seq)send + waitdriver item_donedriving completeget_next_item (drv)await an itemitem availablefinish_item sent oneget_response (seq)await answermatching responserouted back by ID12
Figure 2 — the four blocking points and what each waits for. start_item blocks until the sequencer grants arbitration. finish_item blocks until the driver calls item_done. On the driver side, get_next_item blocks until an item is available. get_response blocks until a matching response arrives. Between the blocks, work happens: randomization (zero time) between start and finish on the sequence side, driving (consumes time) between get_next_item and item_done on the driver side. The blocks interlock the two ends so they advance in lockstep.

The blocking structure is the lifecycle's logic. start_item blocks until the sequencer grants this sequence arbitration — so when many sequences share a sequencer, this is where a sequence waits its turn. finish_item blocks until the driver calls item_done — so the sequence is suspended for the entire duration the driver spends driving the item; this is the longest and most important block, because it's what synchronizes the sequence to the driver. On the driver side, get_next_item blocks until an item is available (i.e., until some sequence's finish_item has sent one) — so a driver with no stimulus simply waits. And get_response blocks until a matching response arrives (Module 8.2) — which is why a missing set_id_info hangs it forever. The pattern is an interlock: finish_item (sequence) and get_next_item/item_done (driver) wait on each other, so neither end can run ahead — the sequence can't send a second item until the driver finishes the first, and the driver can't get a second item until the sequence sends it. Reasoning about almost any sequence/driver timing question — Why is my sequence stuck? Why isn't the driver getting items? Why does get_response hang? — comes down to identifying which of these four blocks is waiting and what event it needs. The blocks are the lifecycle's joints.

Runtime / Execution Flow — zero-time decisions, timed performance, and pacing

At run time the lifecycle alternates between zero-time bursts (decide) and time-consuming spans (perform), and the blocking across the performance is what paces the sequence to the DUT.

The lifecycle loop: zero-time decide, time-consuming driving while blocked, unblock when the DUT accepts, repeatdecide (0t) → block while driving (Δt, DUT-paced) → unblock → decide next (0t)decide (0t) → block while driving (Δt, DUT-paced) → unblock → decide next (0t)1Decide next item (zero time)create + randomize — instant; the sequence prepares one item.2Block in finish_item (Δt)suspended while the driver drives over clock cycles — the DUT setsthe pace.3DUT accepts → item_donedriving completes only as fast as the DUT allows; finish_itemunblocks.4Loop: decide the nextthe sequence advances one item per DUT-paced cycle — automaticback-pressure.
Figure 3 — the lifecycle paces the sequence to the DUT. The sequence decides the next item in zero time (create + randomize), then blocks in finish_item while the driver spends real clock cycles driving it. Only when the DUT accepts and the driver completes (item_done) does the sequence unblock and decide the next item. So the sequence's rate is governed by the driver's rate, which is governed by the DUT — a slow DUT naturally slows the stimulus. The blocking is automatic back-pressure: no manual flow control is needed.

The run-time loop reveals the pacing that the blocking creates. Decide (step 1) is a zero-time burst: the sequence creates and randomizes the next item instantly. Block (step 2) is the time-consuming span: finish_item suspends the sequence while the driver drives the item over real clock cycles — and the driver proceeds only as fast as the DUT accepts (waiting on ready signals, handshakes, backpressure from the design). Accept (step 3): when the DUT has taken the item and the driver completes, item_done fires and finish_item unblocks. Loop (step 4): the sequence decides the next item and the cycle repeats. The consequence is automatic flow control: the sequence advances exactly one item per DUT-paced driving cycle, so it cannot outrun the design — a slow DUT (lots of wait states) naturally slows the stimulus, and a fast DUT lets it run quickly, with no explicit rate management anywhere. This is why UVM stimulus is robust by default: the lifecycle's blocking makes the DUT the metronome. (Advanced patterns — pipelined drivers that accept multiple outstanding items — deliberately relax this lockstep for throughput, but they're an explicit departure from the default one-at-a-time pacing, and they're where the "give each transaction its own object" rule becomes critical, since multiple items are then in flight at once.)

Waveform Perspective — the lifecycle on a timeline

The lifecycle's zero-time/time-consuming split is clearest on an actual timeline: the decision happens at a single instant, then driving spans many cycles while the sequence waits.

One transaction's lifecycle: instant decision, then time-consuming driving while blocked

12 cycles
One transaction's lifecycle: instant decision, then time-consuming driving while blockedt0: create + randomize + finish_item — the decision, at one instantt0: create + randomize…seq_blocked high: sequence suspended while the driver drivesseq_blocked high: sequ…item_done: driving complete → finish_item unblocksitem_done: driving com…next item decided instantly, then blocked again — the loopnext item decided inst…clkdecideseq_blockeddrv_activeitem_donet0t1t2t3t4t5t6t7t8t9t10t11
Figure 4 — the lifecycle over simulation time. At t0 the sequence creates and randomizes the item and calls finish_item — all at one instant (decide). seq_blocked then goes high: the sequence is suspended. drv_active shows the driver driving the item over several clock cycles (the time-consuming performance). When driving completes, item_done pulses, seq_blocked falls, and the sequence decides the next item. The decision is a point; the driving is a span; the sequence is blocked across the span — that gap is the pacing.

The waveform shows the lifecycle's defining asymmetry: a point of decision and a span of performance. At t0 the decide pulse marks the sequence creating, randomizing, and calling finish_item — all collapsed into one instant, because deciding the transaction is zero-time. Immediately seq_blocked rises: the sequence is suspended, and stays suspended for the whole span that drv_active is high — the several clock cycles the driver spends realizing the item on the pins. When driving completes, item_done pulses, seq_blocked falls, and the sequence instantly decides the next item (the second decide pulse) before blocking again. So each transaction is a narrow decision followed by a wide blocked span, and the sequence's progress is the staircase of these spans — one item per driving cycle. The blocked spans are the pacing made visible: the sequence advances only as the driver (and the DUT behind it) allows. This is the timeline to carry: transactions are decided in instants and performed over spans, and the sequence lives mostly in the blocked spans, waiting — which is exactly why stimulus stays synchronized with the design.

DebugLab — the item randomized after it was already sent

A driver that always drove default values because randomize ran after finish_item

Symptom

A sequence built items with random addresses and data, but the driver drove addr = 0, data = 0 every single time — as if randomization never happened. The constraints were correct, the randomize() call returned success (the assertion passed), and the item clearly had random values after the sequence finished with it. Yet on the pins, every transaction was the default. The randomization was happening; it just wasn't reaching the driver.

Root cause

randomize() was called after finish_item() — so the item was already handed to the driver (with default values) before it was randomized:

why randomized values never reached the pins
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
sequence body:
  req = bus_item::type_id::create("req");   // fields at DEFAULT (addr=0, data=0)
  start_item(req);
  finish_item(req);                          // ✗ SENT to driver NOW — with defaults
  assert(req.randomize());                   // randomized AFTER it was already driven (too late)
 
lifecycle:  create → start_item → finish_item(driver drives addr=0) → item_done → randomize(too late)
result:     driver drove the default item; the randomize mutated an object already driven
fix — randomize BETWEEN start_item and finish_item:
  start_item(req);
  assert(req.randomize());                   // ✓ randomize while the item is still in the sequence
  finish_item(req);                          // ✓ send the FINAL (randomized) item to the driver

This is a lifecycle-ordering error. In the transaction lifecycle, finish_item is the step that hands the item to the driver — and finish_item blocks until the driver has driven it. So by the time finish_item returns, the driver has already consumed the item's values. Calling randomize() after finish_item therefore mutates the object after it was already driven — far too late to affect the pins, which saw the default values the item had at finish_item. The randomize "worked" (the object does end up with random values), which is what makes it confusing: the values are right when you inspect the item afterward, but they were applied after the lifecycle moment that mattered. The fix is to place randomize() in its correct lifecycle position — between start_item and finish_item — so the item holds its final, randomized values before finish_item sends it. (This is also why the uvm_do macros randomize internally at exactly that point: they encode the correct lifecycle order so you can't get it wrong.) The general rule: the item must be fully decided before finish_item, because finish_item is where deciding ends and performing begins.

Diagnosis

The tell is driven values that are always defaults despite a successful randomize. Diagnose lifecycle-ordering bugs:

  1. Check randomize placement relative to finish_item. Confirm randomize() is called before finish_item. A randomize after finish_item is the signature — the item was sent before it was randomized.
  2. Confirm the driven values match the item's values at finish_item. Print the item just before finish_item; if it's at defaults there but random afterward, the randomization is mis-ordered.
  3. Suspect manual handshakes over uvm_do. Hand-written start_item/finish_item sequences are where ordering errors creep in; the uvm_do macros enforce the order. A bug here often means a hand-rolled handshake.
  4. Distinguish from a missing randomize. A randomize() that's absent (not just mis-ordered) also drives defaults — check whether it exists at all versus exists-but-too-late.
Prevention

Decide the item fully before sending it:

  1. Randomize between start_item and finish_item. Make the order create → start_item → randomize → finish_item a fixed reflex, so the item is final before it's sent.
  2. Prefer the uvm_do macros for the common case. They encode the correct create/start/randomize/finish order, so you can't randomize too late; drop to manual handshakes only when you need control they don't give.
  3. Treat finish_item as the point of no return. After finish_item, the item is the driver's — don't mutate it expecting the change to reach the pins. Any value the driver needs must be set before finish_item.
  4. Give each transaction its own object. Create a fresh item per iteration; don't reuse one handle, or an item still being driven (or recorded) can be mutated by the next iteration — the related lifetime bug.

The one-sentence lesson: finish_item is where deciding ends and performing begins — it hands the item to the driver and blocks until it's driven — so the item must be fully randomized before finish_item; randomizing after sends the driver the default item and mutates the values too late to reach the pins.

Common Mistakes

  • Randomizing after finish_item. The item was already sent with defaults; randomize between start_item and finish_item so the driver gets final values.
  • Reusing one transaction object across iterations. An item still being driven, recorded, or analyzed gets mutated by the next iteration; create a fresh object per transaction (or clone before handoff).
  • Expecting finish_item not to block. It blocks until item_done — the sequence is suspended for the driver's entire driving time. Treat it as a synchronization point, not a fire-and-forget send.
  • Mutating an item after finish_item. Past finish_item the item belongs to the driver; later changes don't reach the pins. Set everything the driver needs before finish_item.
  • Assuming create/randomize consume time. They're zero-time decisions; only driving consumes simulation time. Mis-modeling this leads to wrong timing expectations.
  • Calling get_response with no responding driver. It blocks forever waiting for a response that never comes; only retrieve responses for items the driver answers.

Senior Design Review Notes

Interview Insights

A transaction's lifecycle is an ordered journey that spans zero-time decisions and time-consuming performance. First it's created — type_id::create constructs the object with its fields at defaults; this is zero-time, because nothing has touched the pins. Then it's randomized — randomize() solves the constraints to decide the values; also zero-time, since choosing what the item is doesn't advance the clock. Then it enters the blocking handshake: start_item blocks until the sequencer grants this sequence arbitration, and — importantly — randomization happens between start_item and finish_item, so the item is final before it's sent. finish_item then hands the item to the driver and blocks until the driver signals item_done. During that block, the driver drives the item on the pins, which consumes real simulation time over clock cycles — this is the one time-consuming part, and the sequence is suspended throughout it. When the driver finishes, item_done completes the item, finish_item unblocks, and optionally a response routes back for get_response to retrieve. So the shape is a zero-time front (create, randomize — deciding what the transaction is), a time-consuming middle (the handshake and driving — performing it), and completion. The boundary between deciding and performing is finish_item, and the two rules that follow are: randomize before finish_item (after is too late, the driver already got the defaults), and give each transaction its own object (so an in-flight item isn't corrupted by reuse).

Exercises

  1. Annotate the timeline. For create, randomize, start_item, finish_item, drive, item_done, label each as zero-time or time-consuming, and mark which two calls block and on what.
  2. Fix the ordering. A sequence calls finish_item(req) then req.randomize(). State what the driver drives, why, and the corrected order.
  3. Explain the pacing. A DUT inserts many wait states and the sequence slows down without any code change. Explain, using the blocking lifecycle, why the stimulus paces itself.
  4. Reason about a hang. A sequence is stuck at finish_item. List the lifecycle reasons it might not be unblocking, and how you'd confirm which.

Summary

  • A transaction's lifecycle is an ordered journey: create and randomize (both zero-time decisions), then start_item (blocks for the grant) and finish_item (blocks until item_done), during which the driver drives it on the pins (consumes simulation time), then complete and an optional response.
  • The lifecycle separates deciding the transaction (zero-time) from performing it (time-consuming), with finish_item as the boundary — and the sequence is suspended across the performance.
  • Four calls block: start_item (until grant), finish_item (until item_done), get_next_item (until an item), get_response (until a matching response) — and finish_item/get_next_item interlock the sequence and driver so they advance in lockstep.
  • That lockstep is back-pressure: the sequence advances one item per DUT-paced driving cycle, so stimulus can't outrun the design — automatic flow control with no manual pacing.
  • The durable rule of thumb: randomize between start_item and finish_item (after is too late — the driver already got the defaults), give each transaction its own object (so an in-flight item isn't corrupted by reuse), and treat finish_item as the point of no return — because the lifecycle's blocking is what keeps stimulus synchronized with, and paced by, the DUT.

Next — Field Macros: a transaction needs its data services — copy, compare, print, pack — to be checked and logged through its lifecycle; the next chapter covers the uvm_field_* macros that generate those automatically from a field declaration, and the trade-offs of the automation.