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.
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
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.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:
| Method | Behavior | When 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 pending | When 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 view | After get_next_item, before next iteration |
item_done(rsp) | Returns response item to the sequence | When 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
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
#1). This ensures the signal settles before the next clock edge and avoids race conditions in the simulator's active region.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
| Feature | analysis_port | Regular TLM port |
|---|---|---|
| Blocking | No — write() returns immediately | Can block if FIFO full |
| Subscribers | Any number (broadcast) | One-to-one |
| Direction | Output only (producer) | Bidirectional |
| Typical use | Monitor → scoreboard/coverage | Sequencer ↔ driver |
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
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
"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").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