In short: A UVM driver and sequencer trade transactions through a blocking get_next_item() / item_done() rendezvous on the sequence-item port, and the shape of that handshake — one item fully driven before the next is fetched (atomic) versus a new item pulled every clock while an earlier one is still on the bus (pipelined) — decides whether your testbench can keep a protocol like AXI or DDR saturated.
Every class-based SystemVerilog testbench eventually hinges on one quiet method call: the moment the driver asks the sequencer for the next stimulus. Beginners treat get_next_item() as boilerplate copied from a template, but the handshake it anchors is where throughput, back-pressure and bugs actually live. This article walks through the exact TLM contract, the two driving styles, and the mistakes that cost verification engineers days of debug on Synopsys VCS and Cadence Xcelium regressions.
The TLM port underneath the handshake
The driver owns a uvm_seq_item_pull_port (its field name is seq_item_port); the sequencer exposes the matching uvm_seq_item_pull_export. In the agent's connect_phase you write one line:
driver.seq_item_port.connect(sequencer.seq_item_export);
That single connection gives the driver five methods, but only a handful matter day to day: get_next_item(req), try_next_item(req), item_done(), peek(req) and get(req). The key behavioural fact: get_next_item() is blocking. If the sequence has nothing ready, the driver's run_phase thread simply suspends inside that call — no clocks are wasted, no polling loop is needed — until a sequence's start_item/finish_item pushes an item across.
finish_item() until the driver signals item_done().Atomic driving: one item, start to finish
The textbook loop is atomic. The driver fetches an item, drives every bus cycle the transaction needs, then acknowledges completion before fetching again:
forever begin seq_item_port.get_next_item(req); drive(req); seq_item_port.item_done(); end
Here item_done() is called only after the DUT has fully consumed the request. This is correct, easy to reason about, and perfectly adequate for register-model writes or any protocol where a transaction is a single, non-overlapping event. Its limitation is latency: the sequence's finish_item() stays blocked for the entire duration of drive(), so the next item cannot even be generated until the current one retires. On a protocol that allows multiple outstanding transactions, atomic driving leaves the bus idle between requests.
Key fact: item_done() takes an optional response argument — item_done(rsp) — which is equivalent to calling put_response(). Use it to return read data or status so the sequence can make decisions on the fly, instead of bolting on a separate response path.
Pipelined driving: keeping the bus full
Modern interfaces — AMBA AXI with its separate address and data channels, or a DDR controller command queue — are designed for overlap. The address phase of transaction N+1 can begin while the data phase of N is still in flight. To exploit that, you split the handshake: call item_done() early (as soon as the item is accepted, not completed) and hand the request off to a second thread that finishes driving it.
A common pipelined pattern uses fork...join_none plus a semaphore that bounds the number of outstanding items to the DUT's real depth:
- Main thread:
get_next_item(req), drive the address/command beat, thenitem_done()immediately. - Worker thread: forked per item, drives the remaining data beats and collects the response.
- A
semaphoreseeded with, say, 16 keys throttles issue so you never exceed the DUT's 16-entry queue.
The payoff is dramatic. A 64-beat burst on a 4-outstanding AXI slave driven atomically might see the bus busy ~40% of the time; the same scenario pipelined can approach 95% utilisation because the driver is always presenting the next address while data drains. That difference is exactly what a read-DMA or cache-fill stress test is meant to exercise.
get_next_item vs try_next_item
Use get_next_item() when the driver has nothing better to do than wait. Use try_next_item() — which returns null immediately if no item is available — when the driver must keep the clock advancing regardless, for example to drive idle cycles, X-injection, or protocol keep-alives. A classic bug is mixing them up and then forgetting that try_next_item() returning non-null still requires a matching item_done().
| Aspect | Atomic | Pipelined |
|---|---|---|
| item_done() timing | After full drive | After accept (early) |
| Outstanding items | 1 | N (DUT depth) |
| Bus utilisation | Low on overlapping protocols | High |
| Complexity | Trivial | Threads + semaphore |
| Typical use | Register writes, simple IO | AXI, DDR, PCIe, cache |
Where engineers get burned
Key fact: Calling get_next_item() twice without an intervening item_done() is a protocol violation. UVM will issue a SEQ error ("Driver has already fetched an item") — the sequencer refuses to hand out a second item until the first is retired.
Other recurring traps: forgetting to randomise or copy the response object before item_done(rsp) so every response aliases the same handle; starving a pipelined driver because the semaphore key count is larger than the DUT queue; and relying on response ordering when your DUT reorders completions (AXI IDs) — in that case the worker thread must match responses by ID, not by issue order. Tools like Siemens Questa and Synopsys Verdi let you trace the seq_item_port transaction stream in the wave viewer, which is the fastest way to see a stalled handshake.
For the broader mechanics of how the driver connects to the virtual interface and how the monitor mirrors this flow, see our companion piece on the UVM driver and monitor, and for arbitration behaviour read UVM sequencer arbitration and priority. If you are building a full agent, our verification course catalogue walks the whole UVC bottom-up. Teams needing a production AXI or DDR VIP built and integrated often engage a specialist like Avecas design services rather than reinventing the pipelined driver each project.
Ready to build a pipelined AXI driver you can defend in an interview? Join the CourseTron UVM Verification track — hands-on labs on Synopsys VCS and Cadence Xcelium, from first get_next_item() to a fully overlapped master VIP.
Frequently asked questions
Does item_done() consume simulation time?
No. item_done() is a zero-time bookkeeping call that unblocks the sequence's finish_item(). Any time consumed is inside your drive() task, from clocking-block or virtual-interface events — never from the handshake itself.
Can I pipeline without fork/join_none?
You can, using a request/response queue and a second persistent thread started in run_phase, but fork...join_none per item is the idiomatic UVM approach. The essential rule is unchanged: call item_done() as soon as the item is accepted, and bound outstanding issue to the DUT's real depth.
What is the difference between get() and get_next_item()?
get() blocks and completes the handshake in one step (it implies item_done()), so you cannot return a response afterward. get_next_item() plus an explicit item_done(rsp) is preferred whenever the sequence needs read data or status back.
Why does my sequence hang forever?
Almost always a missing item_done() in the driver, or a driver that never reaches get_next_item() because an earlier drive task is stuck waiting on a DUT signal. The sequence's finish_item() stays blocked until the driver acknowledges, so an un-acked item freezes the whole stimulus thread.