Sequences UVM

UVM Sequence, Sequencer, and Driver Communication

Cover image for UVM Sequence, Sequencer, and Driver Communication

A sequence is blocked in finish_item(). The driver is waiting for reset deassertion. Nothing moves on the interface, and the test objection remains raised.

The useful question is not “where did the transaction go?” It is “which side of the handshake failed to progress?” If finish_item() is blocked, determine whether the request was submitted, whether the driver retrieved it, and whether the driver completed the outstanding item.

Four separate mechanisms are involved:

  1. The sequence-item handshake coordinates grants, submission, retrieval, and item completion.
  2. The DUT protocol determines when a hardware transfer is accepted or completed.
  3. Response routing returns optional data or status to the originating sequence.
  4. Phase objections determine whether the test is allowed to end.

Adding an objection cannot repair a missing item_done(). Increasing a timeout cannot release a held grab. Calling item_done() does not prove that a monitor observed a valid transfer. Each mechanism has its own synchronization points.

The practical model is straightforward: the sequence decides what to request, the sequencer decides which sequence may submit next, and the driver decides how to execute the request on the interface. The sequencer arbitrates among competing sequences and preserves the identity needed for completion and response routing. It does not drive pins, automatically clone every item, or behave as an application-visible unlimited FIFO.

This article uses the public behavior of Accellera UVM 2020.3.1 as its reference. Private arbitration queues, request storage, and bookkeeping in that implementation are useful for library debugging, but they are not portable application contracts.

Connecting the Driver and Sequencer

A conventional active agent connects the driver’s sequence-item port to the sequencer’s export:

function void connect_phase(uvm_phase phase);
  super.connect_phase(phase);

  if (is_active == UVM_ACTIVE)
    driver.seq_item_port.connect(sequencer.seq_item_export);
endfunction

uvm_driver#(REQ, RSP) provides seq_item_port, and uvm_sequencer#(REQ, RSP) provides the matching seq_item_export. This specialized connection supports request retrieval, item completion, and response operations.

It is unrelated to the monitor’s analysis port. The monitor independently reconstructs transactions from DUT signals and broadcasts observations to scoreboards and coverage collectors. When the driver calls item_done(), it reports satisfaction of its request-side contract. The monitor remains the independent witness of what actually happened on the interface.

The sequence does not call a driver method directly. It submits through the sequencer, while the driver creates demand by calling a pull-interface method such as:

seq_item_port.get_next_item(req);

Calling the whole arrangement either “push” or “pull” is incomplete. Sequence code submits a selected request; across the sequencer-driver boundary, the driver pulls it. The sequencer coordinates both sides.

A passive agent, a missing connection, or a sequence started on the wrong sequencer can stall the flow before any DUT activity occurs. Check topology and active/passive configuration early.


The Explicit Sequence Flow

Explicit sequence code exposes the two most important blocking boundaries:

class write_sequence extends uvm_sequence #(bus_item);
  `uvm_object_utils(write_sequence)

  function new(string name = "write_sequence");
    super.new(name);
  endfunction

  task body();
    bus_item req;

    req = bus_item::type_id::create("req");

    start_item(req);

    if (!req.randomize() with {
          op == WRITE;
          addr inside {[16'h1000:16'h10ff]};
        })
      `uvm_fatal("RAND", "Failed to randomize bus request")

    finish_item(req);
  endtask
endclass

This flow has four stages: create the object, acquire a grant, prepare the granted request, and submit it while waiting for item completion. Directed assignment is just as valid as randomization; randomization is a stimulus choice, not a handshake requirement.

What start_item() waits for

start_item(req) establishes the item’s sequence and sequencer context, participates in arbitration, and blocks until the sequence receives permission to prepare and submit that item.

It does not randomize the request, drive the DUT, or mean that the driver has started pin activity. A grant only identifies the sequence allowed to supply the next selected item.

start_item() can remain blocked because:

  • another sequence keeps winning under the configured arbitration policy;
  • a lock or grab excludes the sequence;
  • custom relevance makes it ineligible;
  • the sequence targets the wrong sequencer; or
  • driver demand is not causing arbitration to progress.

In the normal explicit flow, preparation occurs after the grant:

start_item() → grant → randomize or assign → finish_item()

Keep this granted interval short. Waiting on clocks, reset, DUT state, or scoreboard events between start_item() and finish_item() can prevent competing sequences from making progress.

What finish_item() waits for

finish_item(req) runs the applicable send callbacks, submits the prepared request, and waits for matching UVM item completion.

With a driver using get_next_item(), the corresponding item_done() normally releases that wait. Therefore, finish_item() is not a nonblocking send operation.

The lower-level relationship is best treated as pseudocode rather than interchangeable application APIs:

Conceptual sequence flow only:

start_item(item):
    establish sequence/sequencer context
    request and wait for a grant
    run applicable pre-send callback behavior

finish_item(item):
    run applicable send callback behavior
    submit the granted request
    wait for matching item completion
    run applicable post-send callback behavior

Accellera UVM exposes lower-level operations through sequence and sequencer classes, but their declarations, calling context, ordering requirements, callbacks, and identity handling differ. Application sequences should not casually replace start_item() and finish_item() with sequencer-side calls.


Driver Retrieval Pattern 1: get_next_item()

The standard explicit-completion driver pattern pairs every successful get_next_item() with exactly one item_done():

task run_phase(uvm_phase phase);
  bus_item req;

  forever begin
    seq_item_port.get_next_item(req);

    drive_request(req);
    wait_for_protocol_completion();

    seq_item_port.item_done();
  end
endtask

get_next_item(req) blocks until a selected request is available. After it returns successfully, the driver owns an outstanding-item obligation.

The central invariant is:

Every successful get_next_item() or successful try_next_item() retrieval must eventually have exactly one matching item_done().

Calling item_done() zero times leaves the originating sequence waiting. Calling it twice violates the pull protocol because the second call has no matching outstanding item. The driver must also avoid another get_next_item() while the current item remains outstanding.

Audit every path after retrieval: normal completion, reset, timeout, continue, return, error recovery, and process termination. SystemVerilog has no automatic finally block to close the handshake.

Calling item_done() too early is another ownership bug. Once completion is reported, the sequence may reuse or mutate the request object. The driver must not continue reading the shared request handle unless the project documents a stronger ownership guarantee.


An APB-Style Blocking Driver

APB provides a natural completion point: the active transfer completes on a clock edge where PSEL, PENABLE, and PREADY are asserted. The following compact example returns a distinct response and treats reset during a retrieved transfer as cancellation.

interface apb_if(input logic pclk);
  logic        preset_n;
  logic        psel;
  logic        penable;
  logic        pwrite;
  logic [31:0] paddr;
  logic [31:0] pwdata;
  logic [31:0] prdata;
  logic        pready;
  logic        pslverr;
endinterface

typedef enum logic [1:0] {
  APB_OK,
  APB_SLVERR,
  APB_ABORTED
} apb_status_e;

class apb_req extends uvm_sequence_item;
  `uvm_object_utils(apb_req)

  rand bit        write;
  rand bit [31:0] addr;
  rand bit [31:0] write_data;

  function new(string name = "apb_req");
    super.new(name);
  endfunction
endclass

class apb_rsp extends uvm_sequence_item;
  `uvm_object_utils(apb_rsp)

  apb_status_e status;
  bit [31:0]   read_data;

  function new(string name = "apb_rsp");
    super.new(name);
  endfunction
endclass

class apb_driver extends uvm_driver #(apb_req, apb_rsp);
  `uvm_component_utils(apb_driver)

  virtual apb_if vif;

  function new(string name = "apb_driver",
               uvm_component parent = null);
    super.new(name, parent);
  endfunction

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);

    if (!uvm_config_db#(virtual apb_if)::get(
          this, "", "vif", vif))
      `uvm_fatal("NOVIF", "APB virtual interface was not configured")
  endfunction

  task run_phase(uvm_phase phase);
    apb_req req;
    apb_rsp rsp;

    vif.psel    <= 1'b0;
    vif.penable <= 1'b0;

    forever begin
      wait (vif.preset_n === 1'b1);

      seq_item_port.get_next_item(req);

      rsp = apb_rsp::type_id::create("rsp");
      rsp.set_id_info(req);
      rsp.status    = APB_ABORTED;
      rsp.read_data = '0;

      if (vif.preset_n === 1'b1) begin
        vif.psel    <= 1'b1;
        vif.penable <= 1'b0;
        vif.pwrite  <= req.write;
        vif.paddr   <= req.addr;
        vif.pwdata  <= req.write_data;

        @(posedge vif.pclk);
        vif.penable <= 1'b1;

        forever begin
          @(posedge vif.pclk);

          if (vif.preset_n !== 1'b1) begin
            rsp.status = APB_ABORTED;
            break;
          end

          if (vif.pready === 1'b1) begin
            rsp.status = vif.pslverr ? APB_SLVERR : APB_OK;
            if (!req.write)
              rsp.read_data = vif.prdata;
            break;
          end
        end
      end

      vif.psel    <= 1'b0;
      vif.penable <= 1'b0;

      seq_item_port.item_done(rsp);
    end
  endtask
endclass

There is exactly one item_done(rsp) after each successful retrieval, including the reset-cancellation path. On an ordinary transfer, the driver withholds item completion until it samples PREADY and captures the result. Consequently, finish_item() returns at the agent’s chosen APB completion point.

That alignment is a driver policy, not a universal UVM guarantee. The monitor should still sample the interface independently and verify that a legal APB transfer occurred.

External process kills remain a special hazard. If testbench code kills the driver thread after retrieval, normal control flow never reaches item_done(). Shutdown code must either avoid such termination or explicitly define sequencer and request cleanup.


Driver Retrieval Pattern 2: get()

A driver can instead retrieve a request with get():

task run_phase(uvm_phase phase);
  bus_item req;
  bus_item local_req;

  forever begin
    seq_item_port.get(req);

    local_req = bus_item::type_id::create("local_req");
    local_req.copy(req);

    drive_request(local_req);
  end
endtask

get() completes the sequencer-side request operation as part of retrieval. The driver must not call item_done() afterward.

The sequence may therefore return from finish_item() before drive_request() completes. The copy creates a stable driver-owned object because the sequence may legally reuse the original request after its request operation completes.

These patterns are alternatives:

seq_item_port.get(req);
drive_request(req);
seq_item_port.item_done(); // Wrong: get() already completed the request

The opposite mismatch is equally serious:

seq_item_port.get_next_item(req);
drive_request(req);
// Wrong: the outstanding item was never completed

Choose the retrieval model deliberately and document what its completion point means.


Responses and Routing Identity

Item completion and response delivery are separate operations. A driver using get_next_item() may complete without a response:

seq_item_port.item_done();

Or it may complete and submit a response together:

rsp = bus_rsp::type_id::create("rsp");
rsp.status = status;
rsp.data   = captured_data;
rsp.set_id_info(req);

seq_item_port.item_done(rsp);

A sequence that expects a response performs a separate wait:

class read_sequence extends uvm_sequence #(bus_req, bus_rsp);
  `uvm_object_utils(read_sequence)

  function new(string name = "read_sequence");
    super.new(name);
  endfunction

  task body();
    bus_req req;
    bus_rsp rsp;
    int     transaction_id;

    req = bus_req::type_id::create("req");

    start_item(req);
    if (!req.randomize() with { kind == READ; })
      `uvm_fatal("RAND", "Failed to randomize read request")
    finish_item(req);

    transaction_id = req.get_transaction_id();
    get_response(rsp, transaction_id);
  endtask
endclass

finish_item() waits for item completion. get_response() waits for a response matching the requested transaction ID. Calling both is correct only when the driver contract guarantees a response.

Two identities matter:

  • The sequence ID associates an item with the originating sequence instance.
  • The transaction ID distinguishes requests, especially when a sequence has multiple responses in flight or requests a response by ID.

For a newly created response, rsp.set_id_info(req) is the portable defensive practice. It preserves both identities and is essential when the new object otherwise has no copied routing metadata. It is especially important on a separate delayed put_response() path, where response submission is no longer coupled to the outstanding request completion.

Do not overgeneralize this rule into “every item_done(rsp) is unroutable without set_id_info().” A response may already carry valid identity—for example, if the request object itself is reused as the response or metadata was copied earlier. In Accellera UVM 2020.3.1, response routing uses the response object’s sequence ID; item_done(rsp) does not provide a portable promise that a newly created response’s missing identity will be inferred from the outstanding request. Explicit set_id_info(req) avoids depending on such assumptions.

The public pull interface also supports separating completion from response submission:

seq_item_port.item_done();

// Later, after constructing an identified response:
seq_item_port.put_response(rsp);

put_response() does not replace the item_done() owed after get_next_item(). Before completing the request, delayed-response logic must copy the routing metadata and every request field it will need later. It must not retain req after completion without an explicit ownership guarantee.

A genuinely pipelined implementation also needs outstanding-ID tracking, stable request copies, backpressure, reset cancellation, delayed response submission, and a policy for responses that never arrive. Without those pieces, “complete now, respond later” is not a complete design.

Responses require a consumption policy as well. UVM sequences provide response-queue configuration and optional response-handler behavior. If the driver produces responses that no sequence consumes, queue-overflow diagnostics or discarded responses can follow.


Shared Handles and Ownership

Sequence items are class objects. The normal handshake transfers handles, not automatic deep copies.

With get_next_item(), the following reuse is safe only if item_done() was delayed until every driver-side consumer finished using req:

finish_item(req);
req.addr = next_addr;

With get(), finish_item() may return while the driver is still operating on the same handle. A local copy or clone is then required unless the project has another documented lifetime guarantee.

The same rule applies to asynchronous helpers, protocol trackers, recorders, and delayed response threads. Copy the required fields and routing metadata before releasing the original request. Cloning is useful only when do_copy() or field automation correctly copies every relevant field.


Arbitration, Priority, Exclusivity, and Relevance

When two sequences call start_item() on one sequencer, both may become arbitration contenders. The sequencer selects an eligible sequence according to its configured policy. The winner receives a grant, prepares its item, and calls finish_item(). The other sequence remains blocked until another arbitration opportunity.

Priority affects selection only through arbitration modes that use priority. It does not preempt an item already granted or being driven, and it has no hardware QoS meaning unless the driver explicitly maps it into DUT behavior.

A lock requests exclusive sequencer access through normal lock arbitration. A grab requests more urgent exclusive access according to the sequencer’s grab rules. Neither preempts a transfer already active in the driver, and neither physically locks the DUT.

Keep exclusive regions short and always release them on ordinary application paths. Standard UVM sequence-kill processing performs sequencer cleanup. The remaining hazards are custom process termination, overrides that omit required super behavior, and application paths that acquire exclusivity but never release it.

Custom relevance controls whether a sequence is currently eligible for arbitration. If is_relevant() returns false, wait_for_relevant() must wait for a real condition that can eventually change. If all contenders remain irrelevant, the sequencer has nothing eligible to grant.


What Sequence Macros Hide

The compact form:

`uvm_do_with(req, {
  op == WRITE;
  addr inside {[16'h1000:16'h10ff]};
})

conceptually hides item creation when needed, sequencer selection, grant waiting, randomization, callbacks, request submission, and item completion. It also has different handling for items and subsequences, so it should not be treated as a literal textual expansion.

Macros are valid UVM usage, but they hide the blocking boundaries needed during handshake debugging. Explicit start_item() and finish_item() code gives clean locations for logs, breakpoints, timing measurements, and randomization checks.


Five Events Commonly Called “Done”

Use one precise completion vocabulary:

Event Meaning Typical mechanism
Submission The prepared request enters the sequencer-driver transfer path finish_item()
UVM item completion The request-side pull-protocol obligation is closed item_done() or completion within get()
DUT completion The agent’s defined hardware milestone occurs Interface signals such as APB PREADY
Response delivery Result data or status reaches the originating sequence get_response() or response handler
Phase completion The run phase may end Objection count reaches zero

These events may be deliberately aligned, as in the APB example, but UVM does not align them automatically.


Debugging in Protocol Order

Instrument transitions, not just transaction contents:

`uvm_info("SEQ", "Before start_item", UVM_MEDIUM)
start_item(req);
`uvm_info("SEQ", "Grant received", UVM_MEDIUM)

`uvm_info("SEQ", "Before finish_item", UVM_MEDIUM)
finish_item(req);
`uvm_info("SEQ", "Item completion observed", UVM_MEDIUM)

Add matching driver logs before and after retrieval, before DUT activity, at the selected DUT completion point, and immediately before item_done() or response submission.

Protocol stage Symptom First checks
Grant Blocked in start_item() Target sequencer, driver pull activity, arbitration mode, priority, lock/grab owner, relevance
Submission Driver waits while sequence never reaches finish_item() Randomization failure, delay after grant, early return, macro expansion
Driver retrieval Sequence submitted but driver receives nothing Active-agent configuration, port/export connection, driver thread, reset wait
Item completion Blocked in finish_item() after retrieval Exactly one item_done(), timeout/reset branches, process termination, accidental get()/item_done() mixing
DUT event Driver holds the item indefinitely Impossible edge wait, PREADY/ready-valid conditions, reset semantics, interface clocking
Response Blocked in get_response() Whether a response exists, set_id_info(req), transaction ID, response consumption and overflow policy
Phase lifetime Test ends early or never ends Objection owner, drain policy, outstanding driver work; diagnose independently of item completion

The last “before” log without its matching “after” identifies the failed stage. Once that stage is known, the waveform and UVM source become focused tools rather than places to search blindly.


References

Public behavior and release-specific implementation observations above are pinned to Accellera UVM 2020.3.1:

Treat the documented public APIs as normative for application code. Treat private queue structure, internal scheduler ordering, and source-level bookkeeping as reference-implementation details tied to this release.


Discussion

0 comments

No comments yet — start the conversation.

Leave a comment

Comments are moderated and appear here after review.

Name
Email
Won't be published.
Comment