Skip to main content

The Driver

The fifo_txn class from the previous page is just data — nothing about it knows how to make wr_en or rd_en actually toggle on the DUT's pins. That translation, from transaction object to real signal activity, is the driver's entire job.

uvm_driver, parameterized on the transaction type​

class fifo_driver extends uvm_driver #(fifo_txn);
`uvm_component_utils(fifo_driver)
virtual fifo_if vif;

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

function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db#(virtual fifo_if)::get(this, "", "vif", vif))
`uvm_fatal("NOVIF", "Virtual interface not set for fifo_driver")
endfunction
  • uvm_driver #(fifo_txn) — the #(fifo_txn) parameterization is what gives this driver a seq_item_port typed specifically to pull fifo_txn instances, rather than generic objects. This is the same parameterized-class mechanic Utility & Field Macros covers for `uvm_component_utils — uvm_driver itself is just an ordinary parameterized UVM base class.
  • virtual fifo_if vif — a virtual interface handle: a reference a class can hold onto (interfaces themselves can't be instantiated inside a class), pointing at the real fifo_if instance that testbench-top will create later in this section.
  • build_phase's uvm_config_db#(virtual fifo_if)::get(...) — retrieves that handle. The full mechanics of set()/get() are covered in depth on the Resource DB & Config DB page; the pattern to recognize for now is the guard: get() returns a success bit, and failing loudly with `uvm_fatal here (rather than silently continuing with a null vif) turns a wiring mistake into an immediate, readable error instead of a confusing crash the first time run_phase tries to touch a signal.

Driving a transaction​

task run_phase(uvm_phase phase);
vif.wr_en <= 0;
vif.rd_en <= 0;
forever begin
fifo_txn txn;
seq_item_port.get_next_item(txn);
@(posedge vif.clk);
if (txn.op == fifo_txn::WRITE) begin
vif.wr_en <= 1;
vif.wr_data <= txn.wr_data;
end else begin
vif.rd_en <= 1;
end
@(posedge vif.clk);
vif.wr_en <= 0;
vif.rd_en <= 0;
seq_item_port.item_done();
end
endtask
endclass
  • seq_item_port.get_next_item(txn) blocks until a sequence (covered two pages from now) has a transaction ready. Where this handle comes from and how the handshake on the other end works is the Sequencer & Sequence page's subject — for now, treat it as "the driver's inbox." Structurally, seq_item_port is a TLM port built into every uvm_driver, connecting (in connect_phase, one level up, not shown here) to a matching seq_item_export built into every uvm_sequencer — the same port/export vocabulary TLM Basics & Analysis Ports introduces, just prewired into the driver/sequencer base classes instead of declared by hand.
  • Two separate clock waits, not one. The first @(posedge vif.clk) is where control signals actually get asserted; the second is what deasserts them again one cycle later, so wr_en/rd_en behave as one-cycle pulses rather than staying high indefinitely. Collapsing this into a single wait is one of the most common driver bugs — it either drives signals for zero cycles (if the deassert happens before the DUT samples anything) or holds them high indefinitely (if there's no second wait to bring them back down at all).
  • seq_item_port.item_done() tells whoever is waiting on the other end of the handshake that this transaction has been fully driven, unblocking the next get_next_item() — or, on the sequence side, finish_item().
  • Initializing wr_en/rd_en to 0 before the loop matters as much as clearing them after each transaction — without it, both signals start in an unknown (x) state in simulation, which a real DUT would have to explicitly guard against on its own.

Alternatives to get_next_item()/item_done()​

The handshake above isn't the only one uvm_driver supports — two variants matter enough to recognize even in a driver this simple:

  • seq_item_port.try_next_item(txn) — a non-blocking sibling of get_next_item(). If the sequencer has nothing ready, it returns immediately with txn set to null instead of blocking the driver's process; useful when a driver needs to do something every clock edge (like driving an idle value) even when no transaction is pending. Critically, item_done() should only be called when try_next_item() actually returned a non-null item — calling it after a null return corrupts the handshake, since there's nothing to complete.
  • get()/put() with rsp_port — instead of get_next_item() + item_done(), a driver can call seq_item_port.get(txn) to fetch an item and later call put() (or write to a separate rsp_port) to send an explicit response object back to the sequence, rather than just signaling "done." This suits protocols where the sequence needs real data back (e.g. read data or a status code), not just an acknowledgment that driving finished — fifo_driver above never needs this because a FIFO write/read has nothing to hand back.
  • seq_item_port.peek(txn) — looks at the next available transaction without consuming it: a subsequent peek() call keeps returning the exact same item until it's actually consumed via get_next_item() (or get()). Useful for a driver that needs to inspect an upcoming transaction — to decide how to drive an idle cycle in the meantime, say — before committing to pull it off the sequencer for real.

What's next​

The driver only ever sees the transactions it's given — it has no way to independently confirm the DUT actually did the right thing with them. That's the monitor's job, covered next: watching the same pins from the outside, without driving anything.