The Driver
The generator has transactions waiting in a mailbox; The Virtual Interface Bridge gave a class a way to reach real pins. This page connects the two: a driver that turns each transaction into the pin wiggles the FIFO actually needs.
The driver class
class driver;
virtual fifo_if vif;
mailbox #(fifo_txn) gen2drv;
function new(virtual fifo_if vif, mailbox #(fifo_txn) gen2drv);
this.vif = vif;
this.gen2drv = gen2drv;
endfunction
task run();
vif.wr_en <= 0;
vif.rd_en <= 0;
forever begin
fifo_txn txn;
gen2drv.get(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;
end
endtask
endclass
run() is a forever loop: block on gen2drv.get(txn) until the generator has something ready, wait for the next clock edge, drive either wr_en/wr_data or rd_en for exactly one cycle depending on txn.op, then de-assert both before looping back to wait for the next transaction. This is the entire driver — nothing here does anything other than turn a transaction into pin activity, one clock at a time.
This class is a concrete example of what the verification industry generically calls a bus functional model (BFM): a non-synthesizable component whose only job is translating higher-level transactions into the exact pin-level waveforms/protocol a specific bus or interface expects. "Driver" and "BFM" name the identical role here — this topic simply calls it "driver" throughout, matching UVM's own naming for the equivalent component.
Why <=, not =
Every assignment to vif in run() uses the non-blocking form (<=), never a blocking assignment (=). This isn't a style preference — it's what keeps the driver from racing the DUT.
Both the driver and the DUT's registers react to the same posedge vif.clk. If the driver used a blocking assignment (vif.wr_en = 1;), the new value could become visible to DUT logic evaluating in the same simulation time step, depending purely on process-scheduling order — a classic testbench/RTL race with no guaranteed outcome. A non-blocking assignment schedules its update for the NBA (non-blocking-assignment) update region, which always runs after every process's Active-region reads for that time step have completed. That guarantees the DUT sees the old value of wr_en/rd_en/wr_data during the current edge, and the new value only from the next edge onward — the same ordering a real synchronous register would produce, regardless of scheduling order. This is also exactly why the FIFO DUT's own registers are written with <= rather than =: on both sides of the interface, non-blocking assignment is what makes "driven this edge, visible next edge" a guarantee instead of a coincidence.
This also explains why the two-clock-edge assert/de-assert pattern works reliably: since every vif write in the loop is non-blocking, the DUT always samples a clean, race-free value on both the asserting edge and the de-asserting edge, one full cycle apart.
Comparing directly against UVM's driver
UVM's own driver, built for this identical FIFO, does the same job with two differences worth naming precisely, not just gesturing at:
gen2drv.get(txn)here vs.seq_item_port.get_next_item(txn)/item_done()there. A plain mailboxget()is a one-way handoff — the generator has no idea when the driver actually finishes with a transaction. UVM's handshake is bidirectional:item_done()explicitly tells the sequencer this transaction is complete, which is what lets a sequence do something meaningful after a transaction has actually been driven (react to it, chain a follow-up transaction), not just fire-and-forget into a queue.vifarrives via constructor argument here, vs.uvm_config_db#(virtual fifo_if)::get(...)there. Exactly the gap The Virtual Interface Bridge flagged — this driver's constructor has to be handed the interface directly by whoever builds it.
Everything else — the clock-edge timing, the pin-level logic for WRITE vs. READ — is identical. This is the part of a testbench that UVM standardizes least, because there's nothing to standardize: driving specific pins in a specific protocol is inherently DUT-specific work, and a UVM driver's run_phase body looks exactly like this one.
What's next
The driver only ever writes to vif. The next page builds the component that only ever reads from it: the monitor, watching independently, with no connection to the driver at all.