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:
- The sequence-item handshake coordinates grants, submission, retrieval, and item completion.
- The DUT protocol determines when a hardware transfer is accepted or completed.
- Response routing returns optional data or status to the originating sequence.
- 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 successfultry_next_item()retrieval must eventually have exactly one matchingitem_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:
uvm_sequence_base.svh— sequence execution,start_item(),finish_item(), response APIs, and kill processinguvm_sequencer_param_base.svh— parameterized sequencer request, completion, and response handlinguvm_driver.svh—uvm_driver#(REQ, RSP)andseq_item_portuvm_sqr_connections.svh— sequence-item pull port, export, interface declarations, and response operations
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.