UVM Driver
The UVM driver is a crucial component in a UVM testbench. Its primary responsibility is to translate transaction-level data (from sequence items) into pin-level activity on the interface connected to the Design Under Verification (DUV). It acts as the interface between the abstract world of sequences and the concrete world of hardware signals.
Diagram of UVM Driver
Your Roadmap
+-------------------------+
| UVM Environment |
| |
| +--------------------+ |
| | Agent | |
| | +---------------+ | |
| | | Driver | | |
| | +---------------+ | |
| +--------------------+ |
| |
| +--------------------+ |
| | Sequencer | |
| +--------------------+ |
+-------------------------+
|
v
+------------------------+
| DUT |
+------------------------+
Key Concepts of UVM Driver
- Driver Class (
uvm_driver
): A base class for creating driver components. - Sequence Items: Transactions that the driver receives and drives to the DUT.
- Interface: Defines the signal connections between the driver and the DUT.
- Run Phase: The phase where the driver operates, continuously fetching and driving sequence items.
Why Use a Dedicated Driver?
- Abstraction: Decouples the sequence (which generates high-level transactions) from the specific details of the interface protocol. This makes sequences more reusable.
- Protocol Handling: Encapsulates the logic for handling the interface protocol, including timing, signalling, and error handling.
- Reusability: Drivers can be reused across different tests and even other projects, as long as the interface is the same.
- Maintainability: Changes to the interface protocol only require modifications to the driver, not the sequences.
Structure of a UVM Driver:
A UVM driver is a class that extends uvm_driver
. It typically contains the following key elements:
- `uvm_component_utils` Macro: This macro is essential for registering the driver with the UVM factory and enabling other UVM features.
seq_item_port
: A port of typeuvm_seq_item_pull_port
used to receive sequence items from the sequencer.- Interface Handle: A handle to the interface connected to the DUV.
new()
Constructor: The constructor is used to create an instance of the driver.run_phase()
: This phase is the main execution phase of the driver. It’s where the driver receives sequence items and drives the interface.drive_transaction()
(User-Defined): A task (or function) that implements the actual driving of the interface based on the received sequence item.
Diagram 1: Driver in the Testbench Architecture
+-----------+ +-----------+ +---------+ +-----+
| Sequencer |---->| Sequence |---->| Driver |---->| DUV |
+-----------+ +-----------+ +---------+ +-----+
| get_next_ | | create() | | drive() | | |
| item() | | randomize()| | | | |
+-----------+ +-----------+ +---------+ +-----+
^ |
| | Sequence Item
+-------------+
Defining a UVM Driver
1. Driver Class
The driver class is derived from uvm_driver
. It implements the main functionality to fetch and drive sequence items to the DUT.
class my_driver extends uvm_driver#(my_transaction);
`uvm_component_utils(my_driver)
virtual my_interface vif;
function new(string name = "my_driver", uvm_component parent = null);
super.new(name, parent);
endfunction
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db#(virtual my_interface)::get(this, "", "vif", vif))
`uvm_fatal("NOVIF", "Virtual interface not defined! Simulation aborted.")
endfunction
task run_phase(uvm_phase phase);
my_transaction tx;
forever begin
seq_item_port.get_next_item(tx); // Fetch transaction from sequencer
drive(tx); // Drive the transaction to DUT
seq_item_port.item_done(); // Indicate transaction completion
end
endtask
virtual task drive(my_transaction tx);
// Example driving logic
vif.addr <= tx.addr;
vif.data <= tx.data;
vif.we <= tx.write_enable;
@(posedge vif.clk); // Wait for a clock edge
endtask
endclass
Connecting the Driver
2. Connecting the Driver to the Sequencer
Drivers are typically instantiated and connected within an agent.
class my_agent extends uvm_agent;
`uvm_component_utils(my_agent)
my_driver drv;
my_sequencer seqr;
function new(string name = "my_agent", uvm_component parent = null);
super.new(name, parent);
endfunction
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
drv = my_driver::type_id::create("drv", this);
seqr = my_sequencer::type_id::create("seqr", this);
endfunction
virtual function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
drv.seq_item_port.connect(seqr.seq_item_export);
endfunction
endclass
Example Code:
`include "uvm_macros.svh"
class my_transaction extends uvm_sequence_item; // (From previous example)
rand bit [7:0] address;
rand bit [31:0] data;
rand bit write_enable;
`uvm_object_utils(my_transaction)
// ... (rest of the transaction class)
endclass
class my_driver extends uvm_driver#(my_transaction);
`uvm_component_utils(my_driver)
virtual my_interface vif; // Interface handle
function new(string name = "my_driver", uvm_component parent = null);
super.new(name, parent);
endfunction
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
if(!uvm_config_db#(virtual my_interface)::get(this, "", "vif", vif))
`uvm_fatal("CFG", "Failed to get vif from config_db");
endfunction
task run_phase(uvm_phase phase);
my_transaction req;
forever begin
seq_item_port.get_next_item(req); // Get the next sequence item
phase.raise_objection(this); // Raise objection before driving
drive_transaction(req); // Drive the transaction onto the interface
phase.drop_objection(this); // Drop objection after driving
seq_item_port.item_done(); // Signal transaction completion
end
endtask
task drive_transaction(my_transaction trans);
// Drive the interface signals based on the transaction data
vif.address <= trans.address;
vif.data <= trans.data;
vif.write_enable <= trans.write_enable;
@(posedge vif.clk); // Example timing
vif.write_enable <= 0;
endtask
endclass
Diagram 2: Driver Class Structure
+-----------------+
| my_driver |
+-----------------+
| `uvm_component_|
| utils(my_driver)|
| seq_item_port |
| vif |
| new() |
| build_phase() |
| run_phase() |
| drive_trans() |
+-----------------+
Explanation:
- The driver declares a
seq_item_port
to receive transactions of typemy_transaction
. - It also declares a virtual interface handle
vif
. - In the
run_phase
, the driver continuously gets new sequence items from the sequencer usingseq_item_port.get_next_item()
. - The
drive_transaction()
task is responsible for driving the interface based on the data in the received transaction. This is where the actual pin-level activity occurs. - The driver raises an objection before driving the transaction and drops it after the transaction is driven. This is very important to keep the simulation running until the transaction is driven on the interface.
- The driver signals the completion of the item using
seq_item_port.item_done()
.