The UVM driver is a critical component in the Universal Verification Methodology (UVM) testbench architecture. It acts as a bridge between the sequencer and the DUT (Device Under Test). By converting transaction-level stimulus generated by the sequencer into pin-level signals, the driver ensures accurate communication between the testbench and the DUT.
The UVM driver extends from the uvm_driver base class and is inherited by uvm_component. It should be parameterized with request(REQ) and response(RSP) sequence_item types. Response is optional.
class <driver_name> extends uvm_driver #(type REQ = uvm_sequence_item);
uvm_driver is responsible for:
- Receiving transactions from the sequencer.
- Converting transactions into signal-level activity.
- Driving the DUT’s interface based on the received transaction data.
In short, the driver works at the signal level, translating abstract data structures (transactions) into protocol-specific signals on the DUT interface.
Steps for writing UVM_Driver:
- Define the Driver Class and register it with UVM Macros.
- Include handles for the transaction and the DUT interface.
- Create a constructor to initialize the component and the virtual interface.
- In build_phase, get the virtual interface from the configuration database using config_db.
- Implement the
run_phasetask to fetch transactions and drive signals. - Implement a task for driving DUT signals.
Key UVM Driver Methods for Handling Transactions
UVM provides two primary mechanisms for drivers to handle transactions from the sequencer:
get_next_item/try_next_itemanditem_donemethodsgetandputmethods
Each method serves specific needs in different test scenarios.
1. Using get_next_item / try_next_item and item_done
Workflow:
- The driver pulls the transaction from the sequencer using
get_next_itemortry_next_item. - Once the transaction is processed, the driver notifies the sequencer that the transaction is complete using
item_done.
Key Methods:
get_next_item: Blocks the driver until a transaction is available.try_next_item: Non-blocking version ofget_next_item. Returns a null if no transaction is available.item_done: Marks the transaction as completed.
Example Code:
Let’s consider a simple UVM driver for an APB interface.
class p_driver extends uvm_driver#(p_transaction);
`uvm_component_utils(p_driver)
p_transaction req;
virtual p_interface vif;
//constructor
function new(string name="p_driver",uvm_component parent);
super.new(name,parent);
endfunction: new
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if(!uvm_config_db#(virtual p_interface)::get(this, "","vif",vif))
`uvm_fatal("ERROR","ERROR IN DRIVER")
endfunction
virtual task run_phase(uvm_phase phase);
super.run_phase(phase);
this.vif.master_cb.psel <=0;
this.vif.master_cb.penbl <=0;
forever begin
seq_item_port.get_next_item(req);
@ (this.vif.master_cb)
uvm_report_info("p_driver",$psprintf("Got Transaction %s",req.convert2string()));
//Decode the APB command
case(req.pwrite)
1'b0: drive_read(req.paddr,req.prdata);
1'b1: drive_write(req.paddr,req.pwdata);
endcase
seq_item_port.item_done();
end
endtask
virtual task drive_read (input bit[31:0] addr,output logic[31:0] prdata);
this.vif.master_cb.paddr <= addr;
this.vif.master_cb.pwrite <=0;
this.vif.master_cb.psel <= 1;
@ (this.vif.master_cb);
this.vif.master_cb.penbl <=1;
@ (this.vif.master_cb);
prdata = this.vif.master_cb.prdata;
this.vif.master_cb.psel <= 0;
this.vif.master_cb.penbl <=0;
endtask
virtual task drive_write (input bit[31:0] addr, input bit[31:0] pwdata);
this.vif.master_cb.psel <= 1;
this.vif.master_cb.paddr <= addr;
this.vif.master_cb.pwrite <=1;
this.vif.master_cb.pwdata <= pwdata;
this.vif.master_cb.penbl<=0;
@ (this.vif.master_cb);
this.vif.master_cb.penbl <=1;
@ (this.vif.master_cb);
wait(vif.master_cb.pready)
this.vif.master_cb.psel <= 0;
this.vif.master_cb.penbl <=0;
endtask
endclass
2. Using get and put
Workflow:
- The driver gets transactions from the sequencer using the
getmethod. - The driver can send back responses or updates using the
putmethod.
This approach is useful in scenarios where a feedback loop is required between the driver and the sequencer.
Key Methods:
get: Blocks the driver until a transaction is available.put: Sends a response back to the sequencer. The RSP sequence item as an argument is required while calling put() method but it is optional for item_done() method.
Example Code:
Here’s an example for an SPI driver that sends responses to the sequencer:
class spi_driver extends uvm_driver #(spi_transaction);
`uvm_component_utils(spi_driver)
// DUT interface handle
virtual spi_if spi_vif;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db#(virtual spi_if)::get(this, "", "vif", spi_vif))
`uvm_fatal("NOVIF", "SPI virtual interface not found")
endfunction
virtual task run_phase(uvm_phase phase);
spi_transaction trans, rsp;
forever begin
// Get the next transaction
`uvm_info(get_type_name(), "Waiting for next transaction...", UVM_LOW)
seq_item_port.get(trans); // Blocking call
`uvm_info(get_type_name(), $sformatf("Received transaction: %s", trans.convert2string()), UVM_MEDIUM)
// Drive DUT signals
spi_vif.addr <= trans.addr;
spi_vif.data <= trans.data;
spi_vif.start <= 1'b1;
@(posedge spi_vif.clk);
spi_vif.start <= 1'b0;
// Simulate DUT response (e.g., echo data)
rsp = spi_transaction::type_id::create("rsp");
rsp.addr = trans.addr;
rsp.data = trans.data; // Echo back the data
// Send the response back to the sequencer
seq_item_port.put(rsp);
end
endtask
endclass
Explanation:
seq_item_port.get(trans)retrieves the transaction from the sequencer.- The transaction is processed, and DUT signals (
addr,data,start) are driven. - A response (
rsp) is created and sent back to the sequencer usingseq_item_port.put(rsp).
Choosing Between get_next_item / try_next_item and get / put
- Use
get_next_itemanditem_donefor simple scenarios where the driver only needs to consume transactions without sending responses. This is more common method. - Use
getandputfor advanced scenarios where the driver needs to communicate back to the sequencer, such as for handshake protocols or status updates.