Skip to main content

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 mailbox get() 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.
  • vif arrives 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.