Day 09 / 25
← Day 08 Day 10 →
HomeVerificationDay 9 — UVM Driver & Monitor
Track 2 — UVM Core

UVM Driver & Monitor

The driver and monitor are the two components that touch actual DUT signals. The driver translates abstract sequence items into pin wiggling; the monitor observes DUT pins and reconstructs transactions to broadcast to scoreboards and coverage collectors.

⏱ 27 min read📖 Day 9 of 25🎯 Driver · Monitor · Virtual IF
Contents
  1. Driver Role in the UVM Testbench
  2. seq_item_port Handshake
  3. Driving via Virtual Interface
  4. Monitor Role & analysis_port
  5. Monitor Implementation
  6. Passing the Virtual Interface with config_db
  7. Complete SPI Driver & Monitor Example

1. Driver Role in the UVM Testbench

The uvm_driver is the only component that writes to DUT inputs. It sits between the sequencer and the DUT interface. Its job is simple but precise: get a sequence item, convert it to timed signal activity, confirm completion, repeat:

// Minimal uvm_driver skeleton
class my_driver extends uvm_driver #(my_seq_item);
  `uvm_component_utils(my_driver)

  virtual my_if vif;   // handle to the DUT interface

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction

  task run_phase(uvm_phase phase);
    my_seq_item req;
    forever begin
      seq_item_port.get_next_item(req);   // blocks until item available
      drive_item(req);                    // do the actual pin toggling
      seq_item_port.item_done();          // MUST call — releases sequencer
    end
  endtask

  task drive_item(my_seq_item req);
    // Override in derived classes to drive specific protocol
  endtask
endclass
Never forget item_done(): If you omit the call to seq_item_port.item_done(), the sequencer hangs forever waiting for the driver to become free. The test will time out with no useful error message. Always call it as the last step after driving completes.

2. seq_item_port Handshake

The sequencer-to-driver communication uses a TLM FIFO under the hood. The driver calls one of three methods depending on whether it needs a response path:

MethodBehaviorWhen to use
get_next_item(req)Blocks until item ready; item stays "in flight" until item_done()Standard — most drivers
try_next_item(req)Non-blocking; returns null if no item pendingWhen driver has idle-cycle work to do
get(req)Blocks AND automatically calls item_done (no explicit call needed)Simple drivers with no response
item_done()Releases the item from the sequencer's viewAfter get_next_item, before next iteration
item_done(rsp)Returns response item to the sequenceWhen sequence uses get_response()
// try_next_item pattern — useful for idle-bus maintenance
task run_phase(uvm_phase phase);
  my_seq_item req;
  forever begin
    seq_item_port.try_next_item(req);
    if (req != null) begin
      drive_item(req);
      seq_item_port.item_done();
    end else begin
      drive_idle();   // keep bus in idle state between transactions
      @(posedge vif.clk);
    end
  end
endtask

// Response-path pattern — driver returns rsp to sequence
task run_phase(uvm_phase phase);
  my_seq_item req, rsp;
  forever begin
    seq_item_port.get_next_item(req);
    drive_item(req);
    rsp = my_seq_item::type_id::create("rsp");
    rsp.set_id_info(req);   // copy sequence_id and transaction_id
    rsp.status = get_dut_status();
    seq_item_port.item_done(rsp);
  end
endtask

3. Driving via Virtual Interface

The driver must never reference DUT ports directly. Instead it uses a virtual interface — a SystemVerilog reference to a physical interface instance that is wired to the DUT at the top level. This keeps the driver reusable and synthesizer-independent:

// Interface definition (in a separate .sv file, not inside a module)
interface apb_if (input logic clk, rstn);
  logic [31:0] paddr;
  logic        psel, penable, pwrite;
  logic [31:0] pwdata, prdata;
  logic        pready, pslverr;

  // Clocking block for master (driver) — output skew 1ns, input sample 1ns before edge
  clocking master_cb @(posedge clk);
    output #1 paddr, psel, penable, pwrite, pwdata;
    input  #1 prdata, pready, pslverr;
  endclocking
endinterface

// Driver build_phase — get the virtual interface from config_db
function void build_phase(uvm_phase phase);
  super.build_phase(phase);
  if (!uvm_config_db#(virtual apb_if)::get(this, "", "vif", vif))
    `uvm_fatal("NO_VIF", "APB virtual interface not found in config_db")
endfunction

// Driving an APB write via clocking block
task drive_write(apb_seq_item req);
  @(vif.master_cb);
  vif.master_cb.psel    <= 1;
  vif.master_cb.paddr   <= req.addr;
  vif.master_cb.pwdata  <= req.data;
  vif.master_cb.pwrite  <= 1;
  vif.master_cb.penable <= 0;
  @(vif.master_cb);
  vif.master_cb.penable <= 1;    // setup phase → access phase
  @(vif.master_cb iff vif.master_cb.pready);   // wait for slave ready
  vif.master_cb.psel    <= 0;
  vif.master_cb.penable <= 0;
endtask
Clocking block skew: Always drive DUT inputs via a clocking block with an output skew (e.g., #1). This ensures the signal settles before the next clock edge and avoids race conditions in the simulator's active region.

4. Monitor Role & analysis_port

The uvm_monitor is a passive observer — it never drives signals. It samples DUT outputs and/or internal signals through the virtual interface and reconstructs completed transactions. It then broadcasts them via uvm_analysis_port to any subscriber:

// uvm_monitor skeleton with analysis_port
class apb_monitor extends uvm_monitor;
  `uvm_component_utils(apb_monitor)

  virtual apb_if                         vif;
  uvm_analysis_port #(apb_seq_item)      ap;   // broadcast port

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    ap = new("ap", this);
    if (!uvm_config_db#(virtual apb_if)::get(this, "", "vif", vif))
      `uvm_fatal("NO_VIF", "APB monitor: virtual interface not set")
  endfunction

  task run_phase(uvm_phase phase);
    apb_seq_item trans;
    forever begin
      collect_transaction(trans);   // blocking — waits for next transaction
      ap.write(trans);              // broadcast to scoreboard, coverage, etc.
    end
  endtask
endclass
Featureanalysis_portRegular TLM port
BlockingNo — write() returns immediatelyCan block if FIFO full
SubscribersAny number (broadcast)One-to-one
DirectionOutput only (producer)Bidirectional
Typical useMonitor → scoreboard/coverageSequencer ↔ driver

5. Monitor Implementation

The monitor samples signals using the interface's clocking block to avoid glitches. It typically waits for a valid transaction indicator, samples the relevant fields, creates a sequence item, and writes it:

// APB monitor — collect_transaction task
task collect_transaction(output apb_seq_item trans);
  // Wait for APB access phase (psel=1, penable=1)
  @(posedge vif.clk iff (vif.psel && vif.penable && vif.pready));

  trans = apb_seq_item::type_id::create("trans");
  trans.addr   = vif.paddr;
  trans.write  = vif.pwrite;
  trans.data   = vif.pwrite ? vif.pwdata : vif.prdata;
  trans.slverr = vif.pslverr;

  `uvm_info("APB_MON",
    $sformatf("Captured %s @0x%0h = 0x%0h",
              trans.write ? "WR" : "RD", trans.addr, trans.data),
    UVM_HIGH)
endtask

// The monitor NEVER drives signals — it only reads
// Using vif directly (not clocking block) for input sampling is fine
// but add #1 delay to avoid sampling before combinational logic settles
task collect_safe(output apb_seq_item trans);
  @(posedge vif.clk);
  #1;  // let combinational logic settle after the clock edge
  if (vif.psel && vif.penable && vif.pready) begin
    trans = apb_seq_item::type_id::create("t");
    trans.addr  = vif.paddr;
    trans.write = vif.pwrite;
    trans.data  = vif.pwrite ? vif.pwdata : vif.prdata;
  end
endtask

6. Passing the Virtual Interface with config_db

The virtual interface is set once at the top level and retrieved by each component that needs it. The standard pattern uses the component's full hierarchical path as the context:

// tb_top.sv — set the virtual interface into the config_db
module tb_top;
  import uvm_pkg::*;

  apb_if apb_bus(.clk(clk), .rstn(rstn));

  // Wire interface to DUT
  my_dut dut(.pclk(clk), .presetn(rstn), .paddr(apb_bus.paddr), ...);

  initial begin
    // Set ONCE at top — all components in "uvm_test_top" can get it
    uvm_config_db#(virtual apb_if)::set(null, "uvm_test_top.*", "vif", apb_bus);
    run_test("my_test");
  end
endmodule

// Inside driver (or monitor) build_phase — get the 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("CFG", {get_full_name(), ": virtual interface 'vif' not set"})
endfunction
Path wildcard: Using "uvm_test_top.*" in the set() call makes the virtual interface available to all components underneath the test — driver, monitor, and any sub-agents. You can narrow the scope to a specific component by using its exact hierarchical path (e.g., "uvm_test_top.env.agent.driver").

7. Complete SPI Driver & Monitor Example

A complete, working SPI master driver and monitor for a 4-wire SPI interface (SCLK, MOSI, MISO, CS_N):

// spi_if.sv
interface spi_if (input logic clk);
  logic sclk, mosi, miso, cs_n;
endinterface

// spi_seq_item.sv
class spi_seq_item extends uvm_sequence_item;
  `uvm_object_utils_begin(spi_seq_item)
    `uvm_field_int(data_out, UVM_ALL_ON)
    `uvm_field_int(data_in,  UVM_ALL_ON)
  `uvm_object_utils_end
  rand logic [7:0] data_out;   // sent by master (MOSI)
       logic [7:0] data_in;    // captured from slave (MISO)
  function new(string name="spi_seq_item"); super.new(name); endfunction
endclass

// spi_driver.sv
class spi_driver extends uvm_driver #(spi_seq_item);
  `uvm_component_utils(spi_driver)
  virtual spi_if vif;
  parameter HALF_PERIOD = 4;  // SPI SCLK half-period in ns

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    if (!uvm_config_db#(virtual spi_if)::get(this,"","vif",vif))
      `uvm_fatal("VIF", "No SPI vif in config_db")
  endfunction

  task run_phase(uvm_phase phase);
    spi_seq_item req;
    vif.cs_n = 1; vif.sclk = 0; vif.mosi = 0;
    forever begin
      seq_item_port.get_next_item(req);
      drive_frame(req);
      seq_item_port.item_done(req);   // return captured data_in
    end
  endtask

  task drive_frame(spi_seq_item req);
    vif.cs_n = 0;                     // assert chip select
    #HALF_PERIOD;
    for (int i = 7; i >= 0; i--) begin
      vif.mosi = req.data_out[i];    // shift out MSB first
      #HALF_PERIOD; vif.sclk = 1;   // rising edge — slave samples MOSI
      req.data_in[i] = vif.miso;    // capture MISO on rising edge
      #HALF_PERIOD; vif.sclk = 0;   // falling edge — master updates MOSI
    end
    #HALF_PERIOD; vif.cs_n = 1;     // deassert chip select
  endtask
endclass

// spi_monitor.sv
class spi_monitor extends uvm_monitor;
  `uvm_component_utils(spi_monitor)
  virtual spi_if                      vif;
  uvm_analysis_port #(spi_seq_item)  ap;

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    ap = new("ap", this);
    if (!uvm_config_db#(virtual spi_if)::get(this,"","vif",vif))
      `uvm_fatal("VIF", "No SPI vif for monitor")
  endfunction

  task run_phase(uvm_phase phase);
    spi_seq_item trans;
    forever begin
      // Wait for CS_N to assert (start of frame)
      @(negedge vif.cs_n);
      trans = spi_seq_item::type_id::create("trans");
      for (int i = 7; i >= 0; i--) begin
        @(posedge vif.sclk);          // sample on rising edge
        trans.data_out[i] = vif.mosi;
        trans.data_in[i]  = vif.miso;
      end
      @(posedge vif.cs_n);            // wait for CS_N to deassert
      ap.write(trans);                // broadcast completed frame
      `uvm_info("SPI_MON",
        $sformatf("Frame: MOSI=0x%02h MISO=0x%02h", trans.data_out, trans.data_in),
        UVM_MEDIUM)
    end
  endtask
endclass

Key Takeaways — Day 9

Next → Day 10
UVM Scoreboard & Checker
uvm_scoreboard, uvm_analysis_imp, write() callbacks, reference model, in-order vs out-of-order checking, and complete APB scoreboard.