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 aseq_item_porttyped specifically to pullfifo_txninstances, rather than generic objects. This is the same parameterized-class mechanic Utility & Field Macros covers for`uvm_component_utils—uvm_driveritself 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 realfifo_ifinstance that testbench-top will create later in this section.build_phase'suvm_config_db#(virtual fifo_if)::get(...)— retrieves that handle. The full mechanics ofset()/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_fatalhere (rather than silently continuing with a nullvif) turns a wiring mistake into an immediate, readable error instead of a confusing crash the first timerun_phasetries 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_portis a TLM port built into everyuvm_driver, connecting (inconnect_phase, one level up, not shown here) to a matchingseq_item_exportbuilt into everyuvm_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, sowr_en/rd_enbehave 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 nextget_next_item()— or, on the sequence side,finish_item().- Initializing
wr_en/rd_ento0before 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 ofget_next_item(). If the sequencer has nothing ready, it returns immediately withtxnset tonullinstead 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 whentry_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()withrsp_port— instead ofget_next_item()+item_done(), a driver can callseq_item_port.get(txn)to fetch an item and later callput()(or write to a separatersp_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_driverabove 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 subsequentpeek()call keeps returning the exact same item until it's actually consumed viaget_next_item()(orget()). 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.