UVM
start_item()
The first half of the item handshake — start_item places the sequence in the sequencer's arbitration and blocks until granted, then opens the window to randomize the item before finish_item sends it.
Sequences · Module 9 · Page 9.3
The Engineering Problem
The previous chapter showed the sequencer arbitrating among sequences and deciding, at each grant, whose item goes next (Module 9.2). This chapter is the call on the sequence side that requests that grant: start_item. It looks like a simple "begin sending an item," but it does two load-bearing things — it blocks the sequence until the sequencer grants it, and it opens the window in which you finalize (randomize) the item before sending. Misunderstand either and you get a sequence stuck forever, or items that don't reflect the state they should.
start_item(item) is the first half of the two-phase item handshake (the second is finish_item, Module 9.4). It places the sequence into the sequencer's arbitration and blocks until this sequence wins the grant — so what start_item waits on is exactly the arbitration decision from Module 9.2. When it returns, the sequence has the floor: the item is not yet sent, and this is the one correct moment to randomize it (Module 8.3's "randomize between start and finish"). That two-phase split — request the grant, then finalize the item, then send it — is not an accident; it exists precisely to give a randomization point after the grant, so the item reflects the latest state at the moment it's actually about to be driven. This chapter is start_item: what it does, what it blocks on, the window it opens, and why a start_item that never returns means the grant isn't coming.
What does
start_itemdo — how does it place the sequence in arbitration and block until granted, what window does it open beforefinish_item, and why does the two-phase handshake exist to give a randomization point after the grant?
Motivation — why a separate "start" before sending
start_item is a separate call (rather than part of one send) because requesting the grant and finalizing the item are distinct steps that must happen in that order:
- Arbitration must happen before the item is finalized, so the grant is a distinct step. When several sequences compete, only one is granted at a time (Module 9.2).
start_itemis where this sequence enters that competition and waits to win — it has to complete (the grant must be obtained) before the item is sent, so it's a call of its own that blocks. - The grant gives "the floor," and finalizing should happen on the floor. Once granted, the sequence is the one the sequencer will take an item from next. Finalizing the item then — after winning, before sending — means the item's values reflect the moment it's actually about to be driven, including any state that settled during the wait for the grant.
- The two-phase split is the whole reason late randomization works. If sending were a single call, there'd be no point after the grant but before the send to randomize. The split (
start_item…finish_item) creates that point. Randomizing in that window is what makes constrained-random stimulus reflect up-to-date context (Module 8.3). start_itemblocking is the back-pressure entry point. Becausestart_itemwaits for the grant, and the grant depends on the driver pulling and the sequencer arbitrating, this is where the sequence first synchronizes with the rest of the stimulus path — the sequence can't even prepare the next item until it's granted.- It's a hook point. Around the grant, the sequencer invokes
pre_do— a callback for last-moment adjustments — sostart_itemis also where per-item customization can attach, between requesting and finalizing.
The motivation, in one line: requesting the grant and finalizing the item are ordered, distinct steps — you must win arbitration before you finalize, and finalizing after winning is what makes the item current — so start_item is a separate blocking call that obtains the grant and opens the window where the item is randomized.
Mental Model
Hold start_item as reserving your slot, then filling in the form:
start_itemis taking a number and waiting for it to be called — and only once it's called do you fill in your form's details, so the form reflects the moment you actually hand it over. You walk up to the counter and take a number (request the grant): you're now in line, and you wait —start_itemblocks — until the dispatcher (the sequencer) calls your number (grants you), which may be immediate or may wait behind others (arbitration, Module 9.2). The instant your number is called, you have the floor, but you haven't handed anything over yet: now you fill in the form (randomize the item) with the latest information — because you waited until your turn, the form reflects now, not whenever you got in line. Then you hand it over (finish_item, Module 9.4). The reason the process has two steps — take a number, then fill in the form — is exactly so the form is filled after you're called: if you filled it out while still waiting in line, it might be stale by the time you're served. And if your number is never called — because someone reserved the counter exclusively and won't leave (an unreleasedgrab/lock), or you're at a counter with no clerk (no sequencer) — you wait atstart_itemforever.
So start_item is get in line and wait to be called, and the moment it returns you finalize the item before handing it over. The two-phase shape is what guarantees the item is finalized on your turn, not before it.
Visual Explanation — the two-phase handshake and the window it opens
The defining picture is the gap start_item creates: it ends when the grant arrives, and finish_item begins later — and the gap between them is the randomization window.
The figure shows what start_item is really for: it opens a window. Step 1, start_item places the sequence into arbitration and blocks — the sequence is suspended in the queue until the sequencer grants it (the arbitration decision of Module 9.2). Step 2, the grant arrives and start_item returns: the sequence now has the floor, meaning the sequencer will take this sequence's item next — but the item has not been sent yet. This is the pivotal moment: the window is now open. Step 3, in that window, the item is randomized/finalized — its values are set now, after the grant, so they reflect the most current state (constraints can reference up-to-date context, late config, the result of the wait). Step 4, finish_item sends the finalized item to the driver (Module 9.4), closing the window. The entire reason the handshake is two calls rather than one is to create step 3 — a place to finalize the item after winning arbitration and before sending. Collapse it to one call and there's no such place; the item would have to be finalized before the grant, when the state might still change. So start_item's job is not just "begin" — it's "win the floor and open the window," and the window is where the item becomes what it will actually be.
RTL / Simulation Perspective — start_item in code
start_item is one call with an item argument (plus optional priority and sequencer), and the idiom is always the same: start_item, then randomize in the window, then finish_item.
task my_seq::body();
bus_item req = bus_item::type_id::create("req");
// signature: start_item(item, priority = -1, sequencer = null)
start_item(req); // ① request the grant — BLOCKS until this sequence is granted
// (the sequencer arbitrates; pre_do is called around here)
// ── the WINDOW: the sequence has the floor; the item is not sent yet ──
assert(req.randomize() with { // ② finalize the item NOW — reflects the latest state
addr inside {[0:'hFF]};
is_read == 1;
});
finish_item(req); // ③ send the finalized item to the driver (Module 9.4)
endtask
// optional: override priority / sequencer for this one item
// start_item(req, 500); // this item at priority 500
// start_item(req, -1, other_sequencer); // route this item to a different sequencerThe idiom is fixed and worth memorizing: start_item → randomize → finish_item. The call start_item(req) requests the grant and blocks until the sequencer grants this sequence — so the line does not return immediately; it suspends the sequence until it wins arbitration (Module 9.2). Once it returns, the code is in the window: the item has the floor but isn't sent, so the randomize() here finalizes it with current state — this placement (after start_item, before finish_item) is the correct one (Module 8.3), and it's what the two-phase split exists to enable. Then finish_item(req) sends the finalized item to the driver (next chapter). The optional arguments let you override behavior for this item: a priority (biasing this item's arbitration, Module 9.2) or a sequencer (routing this one item elsewhere) — both default to "use the sequence's." And note pre_do: around the grant, the sequencer calls the pre_do callback, a hook for last-moment per-item logic, which is why start_item is the attachment point for customization. The shape to carry: start_item is the blocking "get the floor" call, and the line right after it is where the item is made final.
Verification Perspective — why two phases, not one
The single most important thing to understand about start_item is why it's separate from finish_item. The answer is the randomization window — and seeing the one-phase alternative makes the value concrete.
The comparison is the justification for the whole design. In a hypothetical one-phase send (top), the item would have to be finalized before it's sent — and since the send obtains the grant, the item is finalized before the grant. That means it cannot reflect anything that settles during arbitration: if the sequence waited (because another sequence was granted first), or if config/context updated in that interval, the item's values are as of when it was prepared, potentially stale by the time it's actually driven. In the two-phase handshake (bottom), start_item obtains the grant first, then the window lets the item be finalized after the grant, so finish_item sends current values. The difference matters whenever an item's values depend on when it's driven — which, in a system with arbitration delays, concurrent sequences, and late configuration, is common. This is why UVM splits the handshake: not for ceremony, but to relocate randomization to after the grant, making constrained-random stimulus reflect the state at drive time rather than at prepare time. The practical corollary is the rule you already know — randomize between start_item and finish_item — and now you know why: that window is the only place where the item is both finalizable and post-grant.
Runtime / Execution Flow — what start_item does internally
At run time, start_item runs a small protocol with the sequencer: register the request, wait for the grant, invoke the pre-hook, and return. Knowing these steps explains both its blocking and its timing.
The internal protocol explains start_item's behavior. Step 1, start_item registers the sequence's request with the sequencer — the sequence joins the arbitration pool for this one item (this is the act that makes it a candidate in Module 9.2's arbitration). Step 2, it blocks: the sequence is suspended in the pool, and the wait lasts until this sequence is selected — which depends on the arbitration policy, the priorities of competing sequences, any held lock/grab, and the driver actually pulling (get_next_item). All of start_item's blocking is this wait; there's nothing else slow about it. Step 3, when the driver pulls and arbitration selects this sequence, the grant is issued. Step 4, the sequencer calls the pre_do hook (a per-item callback for last-moment logic) and start_item returns control to the sequence — which now has the floor and is in the window. The key runtime insight is that start_item's duration is entirely the grant wait: a sequence with no competition on an idle, pulling driver gets granted quickly; a sequence behind a held grab, or one whose priority keeps losing, waits longer; and a sequence whose grant never comes (an unreleased lock, no driver, no sequencer) blocks forever. So when you see a start_item taking a long time — or never returning — you're looking at the grant not arriving, and the fix is in the arbitration/exclusivity/connection world of Module 9.2, not in start_item itself.
Waveform Perspective — start_item blocks, then the window opens
start_item's two behaviors — blocking for the grant and opening the window — are visible on a timeline: the sequence suspends until the grant, then a brief window, then the send.
start_item blocks until the grant, then the randomization window opens
12 cyclesThe waveform separates start_item's two roles in time. From the call, si_wait is high: the sequence is blocked inside start_item, suspended in the arbitration pool waiting for the grant — and this is the long part, lasting as long as arbitration makes it (here a few cycles; in contention or behind a lock, longer). When the grant pulse arrives, si_wait falls: start_item returns, and the window goes high — the brief interval where the item, now holding the floor, is randomized (finalized after the grant). Then finish_item (fi pulse) sends the finalized item and the driver drives it (drv). The shape to read off this is the asymmetry: start_item is mostly waiting (the grant), and the window it opens is short but essential — it's where randomization happens. The diagnostic reading is just as important: if si_wait never falls, start_item is stuck waiting for a grant that isn't coming — the sequence is starved (an unreleased lock/grab, Module 9.2) or not actually on a pulling sequencer. The timeline makes start_item's nature concrete: a blocking request for the floor, followed by the small but critical window where the item becomes final.
DebugLab — the start_item that never returned
A sequence stuck forever at start_item because the grant never came
A sequence ran concurrently with others on a shared sequencer, and it simply stopped making progress: it produced no items, and a message placed right after its start_item(req) never printed. There was no error — the sequence was alive but suspended, hung inside start_item. Other sequences on the same sequencer ran fine; only this one was stuck, indefinitely, waiting at the start of its handshake.
Another sequence had grabbed the sequencer and never released it, so arbitration granted only the holder — and this sequence's start_item waited for a grant that could never come:
other_seq::body():
grab(m_sequencer); // takes EXCLUSIVE access
`uvm_do(item)
if (err) return; // ✗ early return — ungrab NEVER reached → exclusivity held forever
stuck_seq::body():
start_item(req); // requests a grant... but the sequencer grants ONLY the grab-holder
// → start_item blocks forever (grant never arrives)
result: start_item never returns; the sequence is suspended in the arbitration pool indefinitely
(no error — it's a starvation hang, not a crash)
fix — the grabbing sequence must release on every path:
grab(m_sequencer);
`uvm_do(item)
if (err) begin ungrab(m_sequencer); return; end // ✓ release before exiting
...
ungrab(m_sequencer); // ✓ normal releaseThis is a start_item grant-starvation hang, and the bug is not in start_item — it's in the grant not arriving. start_item blocks until the sequencer grants this sequence (Figure 3), and while another sequence holds an exclusive grab/lock, the sequencer grants only that holder (Module 9.2's exclusivity). The grabbing sequence took exclusivity but hit an early return before ungrab, so it never released — leaving the sequencer permanently reserved. The stuck sequence's start_item therefore waits for a grant that will never come, suspending it forever with no error (it's a starvation hang, not a crash). The fix is in the holder, not the waiter: the grabbing sequence must ungrab on every path, including error/early-exit, so exclusivity is released and arbitration can grant others. The general lesson is diagnostic: a start_item that never returns is always a grant-not-arriving problem — look for a held lock/grab that wasn't released, a sequencer with no driver pulling (so no grants are ever issued), or a sequence that isn't actually started on a real sequencer (no arbitration to win). The wait is in start_item; the cause is in the grant path.
The tell is a start_item that never returns. Diagnose grant-starvation hangs:
- Confirm it's stuck at
start_item. A message afterstart_itemthat never prints (while the sequence is otherwise alive) localizes the hang to the grant wait. - Look for an unreleased
lock/grab. Check other sequences on the same sequencer for agrab/locknot paired withungrab/unlockon every path — the most common cause of a withheld grant. - Confirm the driver is pulling. No grants are issued unless the driver calls
get_next_item; a driver that isn't pulling (or isn't connected, Module 9.2) means no sequence is ever granted. - Confirm the sequence is on a real sequencer. A sequence whose
body()was called directly (not viastart()on a sequencer) has no arbitration to win, sostart_itemcan't be granted.
Keep grants flowing:
- Release every
lock/grabon all paths. Pair everygrab/lockwithungrab/unlock, including error and early-exit paths, so exclusivity never leaks and starves waiters. - Keep exclusive regions short. While a sequence holds the sequencer, every other sequence's
start_itemis blocked; hold it only as long as atomicity requires (Module 9.2). - Ensure the driver pulls continuously. The driver's
get_next_item/item_doneloop must run, or no grants are issued and everystart_itemstalls. - Run sequences via
start()on a sequencer. Don't callbody()directly; a sequence needs to be started on a real sequencer to participate in arbitration and be granted.
The one-sentence lesson: start_item blocks until the sequencer grants this sequence, so a start_item that never returns means the grant isn't coming — most often because another sequence holds a lock/grab it never released (starving all waiters), or the driver isn't pulling, or the sequence isn't on a real sequencer; the fix is in the grant path, not in start_item.
Common Mistakes
- Randomizing before
start_item. Randomize in the window afterstart_item(and beforefinish_item), so the item reflects post-grant state — that window is the reason the handshake is two-phase. - Expecting
start_itemto return immediately. It blocks until the grant; under contention or behind alock/grabit can wait a long time, or forever if the grant never comes. - Calling
body()directly instead ofstart(). A sequence not started on a real sequencer has no arbitration to win, sostart_itemcan't be granted. - Forgetting
finish_itemafterstart_item.start_itemopens the handshake; withoutfinish_itemthe item is never sent and the sequencer is left expecting it. - Diagnosing a
start_itemhang insidestart_item. The wait is the grant; the cause is in the arbitration/exclusivity/connection path (Module 9.2), not instart_itemitself. - Misusing the priority/sequencer arguments.
start_item(item, priority, sequencer)overrides apply to this item only; use them deliberately, not as a substitute for sequence-level configuration.
Senior Design Review Notes
Interview Insights
start_item is the first half of the two-phase item handshake on the sequence side. It places the sequence into the sequencer's arbitration for one item and blocks until this sequence is granted — so what it blocks on is exactly the arbitration decision: the sequencer choosing, at a driver pull, which competing sequence's item goes next. When start_item returns, the sequence has won the grant and now has the floor: the item is not yet sent, and this is the window where you randomize or otherwise finalize the item before finish_item sends it. Internally, start_item registers the sequence's request with the sequencer, suspends until the grant is issued, and around the grant the sequencer calls the pre_do hook before start_item returns. The important consequence is that all of start_item's blocking is the grant wait — a sequence with no competition on a pulling driver is granted quickly, while a sequence behind a held lock or grab, or one whose priority keeps losing, waits longer, and a sequence whose grant never comes blocks forever. So start_item has two roles: it obtains the grant by participating in arbitration, and it opens the randomization window. Both are why it's a separate blocking call rather than part of a single send: you must win arbitration before finalizing, and finalizing after the grant is what keeps the item current. When debugging a hung start_item, you look at the grant path — exclusivity, the driver pulling, the connection — not at start_item itself.
To create a randomization window after the grant. The split exists so the item can be finalized at the right moment: after the sequence wins arbitration but before the item is sent. If sending were a single call, the item would have to be finalized before it's sent, and since the send obtains the grant, the item would be finalized before the grant — meaning it couldn't reflect anything that settles during arbitration. In a system with arbitration delays, concurrent sequences, and late configuration, that matters: if the sequence waited because another sequence was granted first, or if config or context updated during the wait, a pre-grant-finalized item would carry stale values by the time it's actually driven. The two-phase handshake fixes this by having start_item obtain the grant first, then opening a window in which the item is randomized with current state, then finish_item sends those current values. So the practical rule everyone learns — randomize between start_item and finish_item — is a direct consequence of this design: that window is the only place where the item is both finalizable and post-grant. The split isn't ceremony; it relocates randomization to after the grant so constrained-random stimulus reflects the state at drive time rather than at prepare time. This is also why the uvm_do macros expand to create, start_item, randomize, finish_item in that order — they encode the correct placement so you can't accidentally finalize before the grant.
Exercises
- Place the randomize. Write the
body()idiom that creates an item, requests the grant, randomizes it with a constraint, and sends it — and label which line opens the window. - Explain the split. In two sentences, explain why the handshake is
start_item+finish_itemrather than a single send, in terms of the randomization window. - Diagnose the hang. A sequence's
start_itemnever returns while others run fine. List the grant-path causes to check, in order. - Reason about timing. Explain why two sequences calling
start_itemat the same time can return at very different times, referencing whatstart_itemblocks on.
Summary
start_item(item)is the first half of the two-phase item handshake: it places the sequence in the sequencer's arbitration and blocks until granted — so it waits on exactly the arbitration decision of Module 9.2 — then returns, giving the sequence the floor.- When
start_itemreturns, the item is not yet sent: this opens the randomization window, the correct place to finalize the item (Module 8.3), andfinish_item(Module 9.4) then sends the finalized item. - The two-phase split exists to create that window — a randomization point after the grant — so the item reflects the latest state at drive time, not at prepare time; a single-call send couldn't.
- All of
start_item's blocking is the grant wait: quick with no contention, longer behind alock/grabor losing priority, and forever if the grant never comes (unreleased exclusivity, no driver pulling, or not on a real sequencer). - The durable rule of thumb: call
start_item, randomize in the window it opens, thenfinish_item— and whenstart_itemnever returns, look in the grant path (a heldlock/grab, a non-pulling driver, a sequence not started on a sequencer), because the wait is instart_itembut the cause is the grant not arriving.
Next — finish_item(): start_item won the grant and opened the window; finish_item is what closes it — sending the finalized item to the driver and blocking until the driver completes it. The next chapter drills into finish_item: what it sends, what it blocks on, and how it completes the handshake.