UVM
uvm_sequence_item
The transaction class you actually extend — the start_item/finish_item handshake, the sequencer link, late randomisation, and the ordered create→start→randomize→finish protocol.
UVM Base Classes · Module 4 · Page 4.5
The Engineering Problem
The data lineage is almost complete: uvm_object (data) → uvm_transaction (identity + timing) → and now uvm_sequence_item, the class you actually extend for transactions. The previous chapters explained what a transaction is; this one explains how it is generated and delivered — because a transaction that can't be produced by a sequence and handed to a driver is inert. uvm_sequence_item adds exactly that machinery.
What it adds over uvm_transaction is the sequence/sequencer handshake: a sequence creates an item, requests the sequencer's grant with start_item, randomises it, and sends it with finish_item; meanwhile a driver pulls it with get_next_item and signals completion with item_done (Module 3.4, the driver side). This handshake is a precise, blocking protocol with a specific order — and getting that order wrong (randomising before the grant, or forgetting to finish an item) produces real bugs: stimulus that hangs, or items that miss late randomisation. uvm_sequence_item is the layer that turns a timed, identifiable transaction into one a sequence can drive through a sequencer to a driver — and the class name you'll write at the top of nearly every transaction.
What is
uvm_sequence_item, what does it add overuvm_transaction(the sequencer link and the start_item/finish_item handshake), and what is the precise, ordered protocol by which a sequence generates an item and a driver consumes it?
Motivation — why a transaction needs sequence machinery
The handshake uvm_sequence_item adds is what makes a transaction drivable, with three properties that matter:
- It connects stimulus generation to the driver. A sequence produces intent (a randomised item); a driver consumes it and drives pins. The
start_item/finish_item↔get_next_item/item_donehandshake is the bridge — without it, an item a sequence creates never reaches the driver. This is the mechanism by which the scenario layer (sequences) feeds the command layer (driver). - It enables late randomisation. Because you randomise between
start_itemandfinish_item— i.e., after the sequencer grants — the item can be randomised with the most current state and through the sequence's mid-generation hooks. Randomising before the grant defeats this, producing items that ignore arbitration-time information. - It provides flow control and arbitration.
start_itemblocks until the sequencer grants, which is how the sequencer arbitrates among multiple sequences competing for one driver. The handshake is not just delivery; it's the point where the sequencer decides whose item goes next. - It supports request/response. A sequence can get a response back for an item (correlated by the
transaction_idfromuvm_transaction), enabling reactive sequences that adapt to the DUT's replies.
The motivation, in one line: a transaction is useless until a sequence can generate it and a driver can consume it, and uvm_sequence_item adds exactly that ordered, blocking, arbitrated handshake — which is why it's the class you extend for every transaction.
Mental Model
Hold uvm_sequence_item as a transaction with a delivery protocol:
A
uvm_sequence_itemis a transaction that knows how to be handed off — like a parcel that comes with a courier handshake. The sequence is the sender, the driver is the recipient, and the sequencer is the dispatch desk between them. To send a parcel, the sender first requests a slot at the dispatch desk (start_item— and waits until the desk grants one, because other senders may be queued). Once granted, the sender fills in the parcel (randomize— done now, at the last moment, so it reflects the latest information). Then the sender hands it over (finish_item) and waits until the recipient confirms delivery. On the other side, the recipient takes the next parcel (get_next_item), processes it (drives the pins), and signs for it (item_done), which is what unblocks the sender. The protocol is strictly ordered — request, fill, hand over — and every hand-over must be signed for, or the sender waits forever.
So when you write a sequence, the item is the parcel and the four calls are the courier protocol: start_item (request a slot), randomize (fill it in now), finish_item (hand it over and wait for the signature). The order and the pairing are the discipline; break either and delivery fails.
Visual Explanation — what uvm_sequence_item adds over uvm_transaction
uvm_sequence_item keeps everything from uvm_transaction (and uvm_object) and adds the sequence/sequencer handshake machinery on top — the final layer of the data lineage.
The two added layers are what make an item drivable. The sequencer/parent-sequence link is the item's connection to where it runs — it knows its sequencer (so it can be arbitrated and delivered) and its parent sequence (so responses can be routed back). The start_item/finish_item handshake is the sequence-side half of the delivery protocol — the calls a sequence makes to request a grant and hand the item to the driver. Everything below (greyed) is inherited: the data toolkit from uvm_object, the identity and timing from uvm_transaction. So uvm_sequence_item is the complete transaction — data + identity + timing + the machinery to be generated and delivered — which is precisely why it's the class you extend for a transaction rather than any of its bases.
RTL / Simulation Perspective — generating an item in a sequence
The handshake is concrete in a sequence's body(): create the item, start_item to get the grant, randomise, finish_item to send. The item class itself is a thin extension of uvm_sequence_item.
class bus_item extends uvm_sequence_item; // the transaction class you ACTUALLY extend
rand bit [31:0] addr, data;
rand bit write;
`uvm_object_utils(bus_item)
function new(string name = "bus_item"); super.new(name); endfunction
endclass
// In a SEQUENCE's body(): the ordered, blocking handshake
class bus_seq extends uvm_sequence#(bus_item);
`uvm_object_utils(bus_seq)
task body();
bus_item req = bus_item::type_id::create("req");
start_item(req); // 1. request the sequencer's grant — BLOCKS until granted
assert(req.randomize()); // 2. randomise AFTER the grant (late randomisation)
finish_item(req); // 3. send to the driver — BLOCKS until the driver's item_done()
endtask
endclass
// DRIVER side (Module 3.4): get_next_item(req) → drive on pins → item_done()The order is the protocol, and each step is deliberate. create makes a fresh item (an object). start_item(req) asks the sequencer to grant this sequence the right to send — and blocks until it does, which is where the sequencer arbitrates if several sequences compete. randomize happens after the grant, so the item is filled in with the latest information (late randomisation) and through the sequence's generation hooks. finish_item(req) delivers the item to the driver and blocks until the driver calls item_done, providing flow control (the sequence paces to the driver). On the other side, the driver's get_next_item/item_done (Module 3.4) is the matching half. The shorthand `uvm_do(req) macro wraps create-start-randomize-finish, but knowing the explicit four steps is what lets you control randomisation and debug the handshake.
Verification Perspective — the two-sided handshake
The full handshake has two halves that meet at the sequencer: the sequence side (start_item/finish_item) and the driver side (get_next_item/item_done). Seeing them together explains the blocking points and the arbitration.
The two-sided view explains why the calls block. start_item blocks because the sequencer must grant — and if several sequences are running on one sequencer, the sequencer arbitrates among their start_item requests, so blocking here is where arbitration happens. finish_item blocks until the driver's item_done, which is flow control: the sequence cannot run ahead of the driver, so stimulus is naturally paced to how fast the driver can drive. The driver's get_next_item retrieves whichever item the sequencer granted, and its item_done is the signal that releases the sequence's finish_item. This is why the handshake is a protocol, not a function call: the two sides rendezvous at the sequencer, with arbitration on the way in and flow control on the way out — which is exactly what makes one driver serve many sequences in a controlled order.
Runtime / Execution Flow — the ordered item lifecycle
A single item's lifecycle in a sequence is a strict four-step order, and the placement of randomise in that order is what enables late randomisation — getting it wrong is a common, subtle bug.
The order is not cosmetic. Randomising at step 3 — after start_item grants — is late randomisation: the item is filled in at the last moment, so it can reflect information that only became available at grant time (the state of the DUT, the responses to prior items in a reactive sequence, the sequence's mid_do/pre_do hooks). If you instead randomise before start_item, the item is locked in before the sequencer arbitrates and before those hooks run, so it ignores arbitration-time information and bypasses the generation callbacks — a subtle bug where items carry stale or unintended values, especially in reactive or arbitrated scenarios (the DebugLab). And step 4, finish_item, must always follow a start_item — every requested item must be finished, or the sequence and driver deadlock. Create, start, randomise, finish: the order encodes late randomisation and the pairing encodes flow control.
Waveform Perspective — the handshake in time
The blocking handshake has a clear timing signature: start_item requests, the sequencer grants, the item is randomised and sent, the driver drives it, and item_done completes the rendezvous — releasing the sequence.
The sequence-item handshake in time — request, grant, drive, done
10 cyclesThe sequence of si_req → gnt → drive → done is the handshake's rendezvous on a timeline. Notice the gaps: between start_item (cycle 1) and the grant (cycle 2) the sequence is blocked, waiting for the sequencer — that's the arbitration window, where the sequencer decides whose item goes next. Between finish_item (after grant) and item_done (cycle 6) the sequence is blocked again, waiting for the driver — that's flow control, pacing the sequence to the driver. The randomisation happens in the brief moment after the grant and before the drive, which is what makes it late. Reading this trace is reading the protocol: request, grant, fill, hand over, drive, sign for — with the sequence blocked at exactly the two points where the sequencer and driver, respectively, hold control.
DebugLab — items randomised before the grant
A reactive sequence whose items ignored the latest state — randomised too early
A sequence was meant to react to the DUT — each item's address was supposed to depend on the response to the previous item, and a mid_do hook was supposed to tweak fields at send time. Instead, the items came out with values that ignored both: the reactive addressing didn't track the responses, and the mid_do adjustments never appeared. The randomisation ran (items had random values), but it ignored everything that should have shaped it.
The item was randomised before start_item, not between start_item and finish_item. So it was filled in before the sequencer granted — before the latest state was available and before the sequence's generation hooks ran:
what was written: req.randomize(); start_item(req); finish_item(req); // randomise FIRST
correct order: start_item(req); req.randomize(); finish_item(req); // randomise AFTER grant
consequence: randomised before the grant → before mid_do/late hooks → before the
response to the prior item was available → values ignore current state
result: reactive addressing doesn't track responses; mid_do tweaks never apply
fix: move randomize() to AFTER start_item (late randomisation)start_item is the point at which the sequencer grants and the sequence's send-time machinery engages; randomising before it locks the item's values in too early, bypassing the late-randomisation window. The randomisation worked — it just happened at the wrong moment, so it couldn't see what it was supposed to react to.
The tell is items that are random but ignore reactive constraints or send-time hooks — a randomisation-timing bug. Diagnose by checking the order:
- Confirm
randomize()is betweenstart_itemandfinish_item. If it's beforestart_item, the item is randomised before the grant and before the sequence's hooks — late randomisation is bypassed. The order must be create → start_item → randomize → finish_item. - Check whether reactive data was available yet. If an item should depend on a prior response or current state, that information is only present after the grant; randomising earlier can't use it. The symptom is "reactive logic ignored."
- Look for missing mid_do/pre_do effects. These hooks run as part of the start_item/finish_item flow; if their effects don't appear, the item was likely randomised outside that window.
Follow the item protocol order, and randomise late:
- Always create → start_item → randomize → finish_item. Randomise after the grant so the item reflects the latest state and the sequence's send-time hooks. Never randomise before
start_item. - Use the
`uvm_dofamily when you don't need custom timing. These macros perform create-start-randomize-finish in the correct order, so the randomisation is late by construction; drop to explicit calls only when you need to control the steps. - For reactive sequences, get the response before randomising the next item. Pair request/response (by
transaction_id) and randomise the next item after the prior response is in hand, inside the start/finish window.
The one-sentence lesson: randomise an item between start_item and finish_item, not before — late randomisation after the grant is what lets the item reflect the current state and the sequence's hooks, and randomising too early silently ignores both.
Common Mistakes
- Randomising before
start_item. Late randomisation requires randomising after the grant (betweenstart_itemandfinish_item); doing it earlier bypasses reactive constraints and send-time hooks, so items ignore current state. Order: create → start_item → randomize → finish_item. - Forgetting
finish_item(orstart_item). Everystart_itemmust be followed by afinish_item; an unmatchedstart_itemleaves the item undelivered and the sequence/driver deadlocked. Pair them, or use the`uvm_domacros. - Extending
uvm_transactioninstead ofuvm_sequence_itemfor stimulus. A transaction you generate from a sequence needs the sequence-item machinery; extenduvm_sequence_item(notuvm_transactionoruvm_object) so it can be sent viastart_item/finish_item. - Modifying the item after
finish_item. Once finished, the driver is processing the item; mutating it (or reusing the same handle for the next iteration without a fresh create) corrupts what the driver and scoreboard see. Create a new item per iteration. - Confusing the sequence and driver sides.
start_item/finish_itemare the sequence side;get_next_item/item_doneare the driver side. They are two halves of one handshake that meet at the sequencer — don't call a driver method from a sequence or vice versa. - Driving the item from the sequence. A sequence produces items; it does not touch pins. The driver drives. Putting pin activity in a sequence is a layer violation (Module 3.6).
Senior Design Review Notes
Interview Insights
uvm_sequence_item is the uvm_transaction subtype that adds the machinery for a transaction to be generated by a sequence and delivered to a driver, and it's the class you actually extend for transactions in a UVM environment. Over uvm_transaction (which gives identity and timing) and uvm_object (which gives the data toolkit), it adds a link to the sequencer it runs on and its parent sequence, plus the start_item/finish_item handshake used to send it. You extend it — rather than uvm_transaction or uvm_object directly — because virtually every transaction is produced by a sequence: the sequence creates the item, calls start_item to get the sequencer's grant, randomises it, and calls finish_item to deliver it to the driver. Without the sequence-item machinery, a transaction you create couldn't participate in that handshake, so it could never be generated and driven. So uvm_sequence_item is the complete transaction class — data plus identity plus timing plus the generation/delivery protocol — which is why it's the one you name at the top of your transaction definitions.
Exercises
- Order the handshake. Write the four sequence-side steps to generate and send an item, in the correct order, and mark which two calls block and on what.
- Fix the randomisation. A sequence does
req.randomize(); start_item(req); finish_item(req);and its reactive addressing ignores prior responses. State the bug and the corrected order, and explain what "late randomisation" gives you. - Two sides. Match each call to the sequence side or the driver side:
start_item,get_next_item,finish_item,item_done— and explain where the two sides meet and what blocks at each. - Pick the base. For a transaction generated by a sequence, state which class you extend and why, naming what
uvm_sequence_itemadds overuvm_transactionthat makes the generation possible.
Summary
uvm_sequence_itemis theuvm_transactionsubtype you actually extend for transactions — it adds the sequencer/parent-sequence link and thestart_item/finish_itemhandshake on top of inherited identity, timing, and the data toolkit.- The sequence-side protocol is ordered and blocking: create →
start_item(request grant — blocks, arbitration point) →randomize(late, after the grant) →finish_item(send — blocks untilitem_done, flow control). The driver side isget_next_item→ drive →item_done. - The two sides rendezvous at the sequencer:
start_itemblocks for the grant (where the sequencer arbitrates among sequences),finish_itemblocks foritem_done(pacing the sequence to the driver). The handshake is arbitration + flow control, not a plain call. - Two disciplines: randomise between start and finish (late randomisation, so the item reflects current state and the sequence's hooks — randomising too early silently ignores them), and pair every
start_itemwith afinish_item(or the sequence and driver deadlock). - The durable rule of thumb: extend
uvm_sequence_itemfor transactions, generate them with create → start_item → randomize → finish_item, randomise after the grant, and always finish what you start — the handshake is the courier protocol that carries a transaction from a sequence to a driver.
Next — uvm_object vs uvm_component: you've now seen both halves of the class library in depth — the data lineage (uvm_object → uvm_transaction → uvm_sequence_item) and the structural base (uvm_component). The next chapter consolidates the distinction: a definitive side-by-side of object versus component, and the rules for choosing between them.