Verification

UVM driver-sequencer handshake

By CourseTron Team ·  8 min read ·  Updated Oct 2026 💬 𝕏 in f
Table of Contents
    ? Print / Save as PDF

    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.

    Sequence, sequencer and driver handshakeSequencestart_item()finish_item()Sequencerarbitration FIFODriverget_next_item()item_done()Item travels forward to the driveritem_done() releases finish_item() back in the sequenceCourseTron · coursetron.com
    The pull port: the driver pulls, the sequence blocks on 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.

    Atomic driver loop: one item at a time1. Fetchget_next_item(req)2. Drivedrive(req)3. Acknowledgeitem_done()forever: fetch the next item only after item_done()finish_item() stays blocked in the sequence for the whole spanCourseTron · coursetron.com
    The atomic loop: fetch, drive, acknowledge, then fetch again.

    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, then item_done() immediately.
    • Worker thread: forked per item, drives the remaining data beats and collects the response.
    • A semaphore seeded 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.

    Atomic vs pipelined bus occupancyAtomicPipelinedItem 1Item 2Item 3idleidleItem 1Item 2Item 3Item 4Item 5Token = a request flowing down the bus; early item_done() keeps it fullCourseTron · coursetron.com
    Atomic leaves gaps between transactions; pipelined packs them. The moving token shows a request flowing down the bus continuously.

    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().

    AspectAtomicPipelined
    item_done() timingAfter full driveAfter accept (early)
    Outstanding items1N (DUT depth)
    Bus utilisationLow on overlapping protocolsHigh
    ComplexityTrivialThreads + semaphore
    Typical useRegister writes, simple IOAXI, 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.

    Advertisement