The UVM driver as the bridge between the sequence world and the DUT — the driver’s role, build_phase setup, the run_phase forever loop, the full driver-sequencer API, and all three drive models: unidirectional non-pipelined, bidirectional non-pipelined, and pipelined.
The driver is the only component in the testbench that drives the DUT’s input signals. It sits at the boundary between the class-based UVM world and the signal-based DUT world. Its job is to take sequence items (abstract transaction objects) from the sequencer and translate them into precise, cycle-accurate signal activity on the virtual interface.
The driver is a consumer — it pulls items from the sequencer and signals when each is done. It never pushes items to the sequencer, never decides what to send next (that is the sequence’s job), and never looks at DUT output signals directly for analysis (that is the monitor’s job). The driver’s focus is exclusively on driving.
A driver extends uvm_driver #(REQ, RSP) which is parameterised on the request item type (and optionally a separate response type). The base class provides the seq_item_port TLM port and the complete driver-sequencer API.
class apb_driver extends uvm_driver #(apb_seq_item); `uvm_component_utils(apb_driver) virtual apb_if vif; // virtual interface — assigned in build_phase function new(string name, uvm_component parent); super.new(name, parent); endfunction // ── Build phase: retrieve virtual interface ─────────── function void build_phase(uvm_phase phase); super.build_phase(phase); if (!uvm_config_db #(virtual apb_if)::get( this, "", "vif", vif)) `uvm_fatal("DRV", "No virtual interface!") endfunction // ── Run phase: drive items forever ──────────────────── task run_phase(uvm_phase phase); apb_seq_item req; // ① Set idle / reset conditions before first transfer vif.PSELx <= 0; vif.PENABLE <= 0; vif.PWRITE <= 0; // ② Wait for reset to deassert before doing anything @(posedge vif.PRESETn); forever begin // ③ Get next item — blocks until sequence sends one seq_item_port.get_next_item(req); // ④ Execute the bus transfer drive_transfer(req); // ⑤ Signal completion — unblocks sequence's finish_item seq_item_port.item_done(); end endtask task drive_transfer(apb_seq_item req); // SETUP phase (1 cycle) @(posedge vif.PCLK); vif.PADDR <= req.addr; vif.PWDATA <= req.write_data; vif.PWRITE <= ~req.read_not_write; vif.PSTRB <= req.byte_en; vif.PSELx <= 1; vif.PENABLE <= 0; // ACCESS phase @(posedge vif.PCLK); vif.PENABLE <= 1; do @(posedge vif.PCLK); while (!vif.PREADY); // Populate response fields req.error = vif.PSLVERR; if (req.read_not_write) req.read_data = vif.PRDATA; // Return to idle vif.PSELx <= 0; vif.PENABLE <= 0; endtask endclass
The seq_item_port in uvm_driver provides the full driver-sequencer communication API. The choice of which methods to use determines the driver model.
| Method | Blocks until | Use in | Effect |
|---|---|---|---|
get_next_item(req) | Sequence calls finish_item() | Unidirectional, Bidirectional | Gets next item. Paired with item_done(). Sequence stays blocked in finish_item(). |
item_done() | — | Unidirectional, Bidirectional | Signals item consumed. Unblocks sequence’s finish_item(). Call after driving completes. |
item_done(rsp) | — | Bidirectional | item_done() with an explicit response object — for get_response() patterns. |
get(req) | Sequence calls finish_item() | Pipelined | Gets item AND immediately unblocks finish_item(). Driver can accept next item while processing current. |
put(rsp) | — | Pipelined | Returns response item to sequence. Sequence uses get_response() to receive. |
try_next_item(req) | — | Specialised | Non-blocking get. Returns null if no item ready — for bus protocols that need to drive idle cycles. |
The simplest drive model. The driver sends stimulus to the DUT but receives no response back. The sequence only checks that the item was accepted — it does not care about the DUT’s output. One item is in flight at a time.
// ── Unidirectional driver run_phase ────────────────────── task run_phase(uvm_phase phase); apb_seq_item req; vif.PSELx <= 0; @(posedge vif.PRESETn); forever begin seq_item_port.get_next_item(req); // block until item ready drive_signals(req); // drive all signals seq_item_port.item_done(); // signal complete — no response end endtask // ── Matching sequence (no response reading) ─────────────── task body(); apb_seq_item item; repeat(10) begin item = apb_seq_item::type_id::create("item"); start_item(item); item.randomize() with { read_not_write == 0; }; // writes only finish_item(item); // No response to read — unidirectional end endtask
The most common model for register bus protocols (APB, AHB, AXI-Lite). The driver sends a request AND waits for the DUT to respond, then populates the response fields in the item before calling item_done(). The sequence can read the response after finish_item() returns.
// ── Bidirectional driver — recommended pattern ──────────── task run_phase(uvm_phase phase); apb_seq_item req; vif.PSELx <= 0; vif.PENABLE <= 0; @(posedge vif.PRESETn); forever begin seq_item_port.get_next_item(req); // SETUP phase @(posedge vif.PCLK); vif.PADDR <= req.addr; vif.PWDATA <= req.write_data; vif.PWRITE <= ~req.read_not_write; vif.PSTRB <= req.byte_en; vif.PSELx <= 1; vif.PENABLE <= 0; // ACCESS phase — wait for DUT @(posedge vif.PCLK); vif.PENABLE <= 1; do @(posedge vif.PCLK); while (!vif.PREADY); // Populate response fields BEFORE item_done() req.error = vif.PSLVERR; if (req.read_not_write) req.read_data = vif.PRDATA; // Return to idle vif.PSELx <= 0; vif.PENABLE <= 0; seq_item_port.item_done(); // unblocks sequence's finish_item() end endtask
Pipelined protocols (AHB, OCP, AXI full) allow the address/command phase of the next transfer to overlap with the data/response phase of the current one. This requires the driver to accept a new item from the sequencer before the previous item’s response has arrived.
The key difference from bidirectional: use get() instead of get_next_item(). get() immediately unblocks the sequence’s finish_item() — the sequencer can already be sending the next item while the driver is still driving the first. Responses are returned separately via put().
// ── Pipelined driver using get() + put() ───────────────── task run_phase(uvm_phase phase); apb_seq_item req, rsp; @(posedge vif.PRESETn); forever begin // get() unblocks finish_item IMMEDIATELY — pipeline fills seq_item_port.get(req); // Drive address phase (pipeline stage 1) @(posedge vif.PCLK); vif.PADDR <= req.addr; vif.PWRITE <= ~req.read_not_write; vif.PSELx <= 1; vif.PENABLE <= 0; // Drive data phase (pipeline stage 2) @(posedge vif.PCLK); vif.PENABLE <= 1; do @(posedge vif.PCLK); while (!vif.PREADY); // Build response object — clone request, add response data $cast(rsp, req.clone()); rsp.set_id_info(req); // copy sequence ID for routing rsp.read_data = vif.PRDATA; rsp.error = vif.PSLVERR; vif.PSELx <= 0; vif.PENABLE <= 0; // Return response — sequence receives via get_response() seq_item_port.put(rsp); end endtask // ── Pipelined sequence — stimulus and response threads ──── task body(); apb_seq_item req, rsp; fork // Stimulus thread: send items begin repeat(10) begin req = apb_seq_item::type_id::create("req"); start_item(req); req.randomize(); finish_item(req); // unblocks immediately with get() end end // Response thread: collect responses begin repeat(10) begin get_response(rsp); // waits for driver put() `uvm_info("SEQ", rsp.convert2string(), UVM_MEDIUM) end end join endtask
rsp.set_id_info(req), responses are delivered to the wrong sequence or lost entirely. Always clone the request and call set_id_info before calling put().The driver must handle DUT reset correctly. During reset all driven signals should return to their default (idle) state. The recommended pattern uses a fork/join to detect reset and abort any in-progress transfer:
task run_phase(uvm_phase phase); apb_seq_item req; forever begin // Wait for reset to deassert before starting vif.PSELx <= 0; vif.PENABLE <= 0; @(posedge vif.PRESETn); // wait for reset release // Main drive loop — exits on reset reassertion fork: drive_loop begin forever begin seq_item_port.get_next_item(req); drive_transfer(req); seq_item_port.item_done(); end end begin @(negedge vif.PRESETn); // detect reset assertion end join_any disable drive_loop; // kill drive thread safely // Return signals to idle state vif.PSELx <= 0; vif.PENABLE <= 0; end // outer loop: wait for next deassertion endtask
The driver is responsible for the signal state at all times — not just during active transfers. There are three situations where the driver controls the idle state:
For protocols that require explicit idle driving (not just tri-stating), use try_next_item() which returns null if no item is ready, allowing the driver to drive idle state while waiting:
task run_phase(uvm_phase phase); apb_seq_item req; @(posedge vif.PRESETn); forever begin seq_item_port.try_next_item(req); if (req != null) begin drive_transfer(req); seq_item_port.item_done(); end else begin // No item ready — drive protocol-specific idle @(posedge vif.PCLK); vif.PSELx <= 0; vif.PENABLE <= 0; end end endtask
| Model | API methods | Items in flight | Response | Typical protocols |
|---|---|---|---|---|
| Unidirectional | get_next_item + item_done | 1 | None — driver does not return data | Write-only interfaces, stimulus-only |
| Bidirectional | get_next_item + item_done (with response fields) | 1 | In-place — response fields updated in req object before item_done | APB, AXI-Lite, SPI, I2C |
| Pipelined | get + put | N (pipeline depth) | Separate object — rsp cloned from req, set_id_info, put() | AHB, OCP, AXI full |
| Item | Key fact |
|---|---|
| Base class | uvm_driver #(REQ, RSP=REQ) — extends uvm_component |
| seq_item_port | TLM port inherited from uvm_driver — connects to sequencer’s seq_item_export in connect_phase |
| run_phase structure | Set idle state → wait reset → forever { get_next_item → drive → item_done } |
| get_next_item() | Blocks until sequence calls finish_item(). Paired with item_done(). |
| item_done() | Non-blocking. Signals item consumed. Must be called after every get_next_item(). |
| Response in bidirectional | Populate response fields in req BEFORE item_done() — sequence reads after finish_item() returns |
| get() vs get_next_item() | get() immediately unblocks sequence’s finish_item — allows pipeline to fill |
| put(rsp) | Returns response to sequence. Sequence receives with get_response(). |
| set_id_info(req) | Mandatory on response when using put() — enables correct sequence routing |
| try_next_item() | Non-blocking — returns null if no item ready. Use for idle-driving protocols. |
| Reset pattern | Set idle → wait PRESETn posedge → drive loop → detect negedge PRESETn → disable → repeat |
| Response before item_done | Populate all response fields before calling item_done() — not after |
| Unidirectional use case | Write-only or no-response protocols. Simple. get_next_item + item_done. |
| Bidirectional use case | Request-response in lockstep. APB, AXI-Lite. get_next_item + drive + populate + item_done. |
| Pipelined use case | Overlapping address/data phases. AHB, AXI. get + drive + clone + set_id_info + put. |