UVM Driver

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:

  1. Receiving transactions from the sequencer.
  2. Converting transactions into signal-level activity.
  3. 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_phase task 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:

  1. get_next_item / try_next_item and item_done methods
  2. get and put methods

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_item or try_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 of get_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 get method.
  • The driver can send back responses or updates using the put method.

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:

  1. seq_item_port.get(trans) retrieves the transaction from the sequencer.
  2. The transaction is processed, and DUT signals (addr, data, start) are driven.
  3. A response (rsp) is created and sent back to the sequencer using seq_item_port.put(rsp).

Choosing Between get_next_item / try_next_item and get / put

  • Use get_next_item and item_done for simple scenarios where the driver only needs to consume transactions without sending responses. This is more common method.
  • Use get and put for advanced scenarios where the driver needs to communicate back to the sequencer, such as for handshake protocols or status updates.