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

+-------------------------+
|     UVM Environment     |
|                         |
|  +--------------------+ |
|  |        Agent       | |
|  | +---------------+  | |
|  | |    Driver     |  | |
|  | +---------------+  | |
|  +--------------------+ |
|                         |
|  +--------------------+ |
|  |     Sequencer      | |
|  +--------------------+ |
+-------------------------+
          |
          v
+------------------------+
|        DUT             |
+------------------------+

Key Concepts of UVM Driver

  1. Driver Class (uvm_driver): A base class for creating driver components.
  2. Sequence Items: Transactions that the driver receives and drives to the DUT.
  3. Interface: Defines the signal connections between the driver and the DUT.
  4. 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:

  1. `uvm_component_utils` Macro: This macro is essential for registering the driver with the UVM factory and enabling other UVM features.   
  2. seq_item_port: A port of type uvm_seq_item_pull_port used to receive sequence items from the sequencer.
  3. Interface Handle: A handle to the interface connected to the DUV.
  4. new() Constructor: The constructor is used to create an instance of the driver.
  5. run_phase(): This phase is the main execution phase of the driver. It’s where the driver receives sequence items and drives the interface.
  6. 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:

  1. The driver declares a seq_item_port to receive transactions of type my_transaction.
  2. It also declares a virtual interface handle vif.
  3. In the run_phase, the driver continuously gets new sequence items from the sequencer using seq_item_port.get_next_item().
  4. 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.
  5. 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.
  6. The driver signals the completion of the item using seq_item_port.item_done().