Logo

UVM Driver

29 Jun 2025
6 mins

In the last article, we learnt about UVM sequencer which acts a bridge between sequence and driver. But sequencer just manages the flow of transaction. How these transactions are further sent to the DUT? UVM provides UVM Driver which is responsible for converting these transactions into actual signals which can be sent to DUT. Let's deep dive more into UVM Driver in this article.

What is UVM Driver

UVM Driver is a UVM component which receives transactions (uvm_sequence_item) and translates them into pin-level activity that follows the DUT's protocol.

For example, imagine that we have a sequence which generates apb_transaction item. Driver is responsible to:

  1. Request for a new transaction item from the sequencer and receives it.
  2. Drive the corresponding signals on the APB interface with correct timing and protocol-specific behaviour.

Defining a UVM Driver

We can't use the base uvm_driver class directly like we did for sequencer. We need to define our own driver which is a sub-class of uvm_driver class. Let's see how we Drivers are defined.

1. Declaring driver class

UVM driver is declared by extending uvm_driver parameterized class. Similar to UVM sequencer, drivers also has to 2 class parameter (REQ and RSP).

class my_driver extends uvm_driver#(my_seq_item);

2. Factory registration and constructor definition

We need to register the driver with the UVM factory and define constructor as below.

`uvm_component_utils(my_driver)

function new(string name, uvm_component parent)
	super.new(name, parent);
endfunction

3. Virtual interface handle

Interface is not a new UVM concept but comes from System Verilog. We have seen earlier how interface provides a seamless way for the test bench to interact with the DUT avoiding any race conditions and driving and sampling signals at correct edges.

virtual my_interface vif_handle; 
As driver interacts with DUT directly, it needs interface and is a must for any driver.
As Interface is static in nature we can not directly instantiate it in the class. Rather we will create a instance in the tb_top and pass it through uvm_config_db which we will learn in detail in later articles.

Sequencer Driver connection

We saw in the last article that sequencer provides seq_item_export for connecting with driver. uvm_driver provides seq_item_port which connects with seq_item_export.

This port provides with different methods through which driver can fetch transactions from sequencer. Lets deep dive into this:

get_next_item()

This is a blocking task. When the driver calls get_next_item(output REQ req), the driver execution is suspended till the driver receives a transaction from sequencer. Once the driver receives the transaction req, driver proceeds further and drives corresponding signal towards DUT.

item_done()

This is a non_blocking method and is called by the driver once the driver has finished driving all the signals. This is an indication to the sequencer that the driver is ready to receive next transaction. We can optionally pass a response transaction rsp of type RSP. This response can be used by the sequence to generate a next packet.

get()

This is also a blocking method. When get(output REQ req) is called by driver, item_done() is implicitly called and thus need not to be called manually. While using get() we can't send response packet back to sequence.

peek()

This method helps check if there is any new transaction available without consuming it. peek() is also a blocking method. Subsequent call to peek() will fetch the same transaction until the transaction is consumed by calling get_next_item() or get().

Handshake between Sequence, Sequencer and Driver

Understanding the handshake between the Sequence, Sequencer and Driver is important. Any wrong implementation can lead to hang in the test bench or lead to Sequencer sending wrong transaction to the Driver.

Below is the flow:

  1. When sequence is ready to send a new transaction, it sends a request to sequencer by calling start_item() method.
  2. Sequencer waits for the request from driver. When driver calls get_next_item() or get(), the sequencer receives a request from driver for a new packet.
  3. Sequencer will now check if there is any sequence trying to send a transaction. If there is none, it will wait for the request from sequence. If there are more than one sequence trying to send packet, sequencer will grant permission to any one of the sequence based on arbitration method.
  4. Once the sequence receives a grant, it will generate a packet. When finish_item() is called the transaction is actually sent to the sequencer.
  5. Sequencer will forward the transaction as is to the driver.
  6. Driver receives the transaction and drives the signal to the DUT. Once it has finished driving signals, driver will call item_done() to indicate the same.
In case driver had called get() then there is no need to call item_done() in step 6.

This handshake mechanism ensures that Sequence, Sequencer and Driver remains synchronised and neither of them is overwhelmed by excessive requests. This means that if Driver is not able to process any new transaction, transaction will be blocked until Driver gets freed. Similarly, if Sequence is not ready with a new transaction it would block the entire flow.

Wrong implementation of the handshake can lead to test bench hang or wrong transaction being sent to driver.

Example

import uvm_pkg::*;

class driver extends uvm_driver#(transaction);
    `uvm_component_utils(driver)
    
    virtual alu_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 alu_if)::get(this, "", "vif", vif))
            `uvm_fatal("NOVIF", "Virtual interface not found")
    endfunction
    
    task run_phase(uvm_phase phase);
        super.run_phase(phase);
        forever begin
            seq_item_port.get_next_item(req);
            drive_item(req);
            seq_item_port.item_done();
        end
    endtask
    
    task drive_item(transaction req);
        vif.OPA <= req.OPA;
        vif.OPB <= req.OPB;
        vif.cin <= req.cin;
        vif.cmd <= req.cmd;
        vif.ce <= req.ce;
        vif.mode <= req.mode;
    endtask
endclass

Interface

As Interface is already covered in our SV article, we won't go over the details again. For this example we will create a very basic interface as below.

interface alu_if(input logic clk);
    logic [7:0] OPA, OPB;
    logic cin, mode, ce;
    logic [3:0] cmd;
    logic [2:0] egl;
    logic [8:0] res; 
    logic cout, err, oflow;
endinterface

TB Top

Let's modify the test bench top to add driver in the TB and see how the TB hierarchy changes. We will also be able to appreciate the different arbitration method in the sequencer.

`include "package.svh"
`include "uvm_macros.svh"
`include "interface.sv"

class env extends uvm_component;
    `uvm_component_utils(env)

    sequencer custom_seqr;
    uvm_sequencer#(transaction) base_seqr;
    random_seq rnd_seq1, rnd_seq2, rnd_seq3;
    int use_arbitratoin_method;
    driver drv;

    // Constructor:new
    function new(string name, uvm_component parent);
        super.new(name, parent);
    endfunction: new

    // function: build
    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        custom_seqr = sequencer::type_id::create("custom_seqr", this);
        base_seqr = uvm_sequencer#(transaction)::type_id::create("base_seqr", this);
        drv = driver::type_id::create("drv", this);
        rnd_seq1 = random_seq::type_id::create("rnd_seq1");
        rnd_seq2 = random_seq::type_id::create("rnd_seq2");
        rnd_seq3 = random_seq::type_id::create("rnd_seq3");
    endfunction: build_phase

    // function: connect_phase
    function void connect_phase(uvm_phase phase);
        super.connect_phase(phase);
        drv.seq_item_port.connect(custom_seqr.seq_item_export);
    endfunction: connect_phase

    // function: end_of_elaboration_phase
    function void end_of_elaboration_phase(uvm_phase phase);
        super.end_of_elaboration_phase(phase);
        uvm_top.print_topology();
    endfunction: end_of_elaboration_phase

    // task: run_phase
    task run_phase(uvm_phase phase);
        super.run_phase(phase);
        phase.raise_objection(this);
        fork
            rnd_seq1.start(custom_seqr, null, 20);
            rnd_seq2.start(custom_seqr, null, 10);
            rnd_seq3.start(custom_seqr, null, 20);
        join
        phase.drop_objection(this);
    endtask: run_phase

    // function: start_of_simulation_phase
    function void start_of_simulation_phase(uvm_phase phase);
        super.start_of_simulation_phase(phase);

        // set the below variable (1-6) to try different arbitration methods
        use_arbitratoin_method = 6;
        // ---------------------------------------
        
        case(use_arbitratoin_method)
            1: custom_seqr.set_arbitration(UVM_SEQ_ARB_FIFO);
            2: custom_seqr.set_arbitration(UVM_SEQ_ARB_RANDOM);
            3: custom_seqr.set_arbitration(UVM_SEQ_ARB_WEIGHTED);
            4: custom_seqr.set_arbitration(UVM_SEQ_ARB_STRICT_FIFO);
            5: custom_seqr.set_arbitration(UVM_SEQ_ARB_STRICT_RANDOM);
            6: custom_seqr.set_arbitration(UVM_SEQ_ARB_USER);
            default: custom_seqr.set_arbitration(UVM_SEQ_ARB_FIFO);
         endcase
    endfunction: start_of_simulation_phase
endclass: env

// temp tb_top
module temp_top();
    bit clk;
    alu_if alu_if_inst(clk);

    always #5 clk = ~clk;
    
    initial begin
        run_test("env");
    end

    // Set the virtual interface for driver
    initial begin
        uvm_config_db#(virtual alu_if)::set(null, "*", "vif", alu_if_inst);
    end
endmodule: temp_top
Try this code in EDA Playground
Output
# KERNEL: UVM_INFO /home/build/vlib1/vlib/uvm-1.2/src/base/uvm_root.svh(583) @ 0: reporter [UVMTOP] UVM testbench topology:
# KERNEL: ----------------------------------------------------------
# KERNEL: Name                   Type                    Size  Value
# KERNEL: ----------------------------------------------------------
# KERNEL: uvm_test_top           env                     -     @336 
# KERNEL:   base_seqr            uvm_sequencer           -     @490 
# KERNEL:     rsp_export         uvm_analysis_export     -     @499 
# KERNEL:     seq_item_export    uvm_seq_item_pull_imp   -     @617 
# KERNEL:     arbitration_queue  array                   0     -    
# KERNEL:     lock_queue         array                   0     -    
# KERNEL:     num_last_reqs      integral                32    'd1  
# KERNEL:     num_last_rsps      integral                32    'd1  
# KERNEL:   custom_seqr          sequencer               -     @353 
# KERNEL:     rsp_export         uvm_analysis_export     -     @362 
# KERNEL:     seq_item_export    uvm_seq_item_pull_imp   -     @480 
# KERNEL:     arbitration_queue  array                   0     -    
# KERNEL:     lock_queue         array                   0     -    
# KERNEL:     num_last_reqs      integral                32    'd1  
# KERNEL:     num_last_rsps      integral                32    'd1  
# KERNEL:   drv                  driver                  -     @627 
# KERNEL:     rsp_port           uvm_analysis_port       -     @646 
# KERNEL:     seq_item_port      uvm_seq_item_pull_port  -     @636 
# KERNEL: ----------------------------------------------------------
# KERNEL: 
# KERNEL: UVM_INFO /home/runner/sequencer.sv(20) @ 0: uvm_test_top.custom_seqr [custom_seqr] running custom sequencer...
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(15) @ 0: uvm_test_top.custom_seqr@@rnd_seq1 [random_seq] Pre-body called (Seuqence started)
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(15) @ 0: uvm_test_top.custom_seqr@@rnd_seq2 [random_seq] Pre-body called (Seuqence started)
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(15) @ 0: uvm_test_top.custom_seqr@@rnd_seq3 [random_seq] Pre-body called (Seuqence started)
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(21) @ 0: uvm_test_top.custom_seqr@@rnd_seq3 [random_seq] Sending packet
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(27) @ 0: uvm_test_top.custom_seqr@@rnd_seq3 [RND_SEQ] Sent packet
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(21) @ 0: uvm_test_top.custom_seqr@@rnd_seq3 [random_seq] Sending packet
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(27) @ 0: uvm_test_top.custom_seqr@@rnd_seq3 [RND_SEQ] Sent packet
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(21) @ 0: uvm_test_top.custom_seqr@@rnd_seq2 [random_seq] Sending packet
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(27) @ 0: uvm_test_top.custom_seqr@@rnd_seq2 [RND_SEQ] Sent packet
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(21) @ 0: uvm_test_top.custom_seqr@@rnd_seq2 [random_seq] Sending packet
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(27) @ 0: uvm_test_top.custom_seqr@@rnd_seq2 [RND_SEQ] Sent packet
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(21) @ 0: uvm_test_top.custom_seqr@@rnd_seq1 [random_seq] Sending packet
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(27) @ 0: uvm_test_top.custom_seqr@@rnd_seq1 [RND_SEQ] Sent packet
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(21) @ 0: uvm_test_top.custom_seqr@@rnd_seq1 [random_seq] Sending packet
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(27) @ 0: uvm_test_top.custom_seqr@@rnd_seq1 [RND_SEQ] Sent packet

Let's decode the output:

  1. In the testbench hierarchy we can see a new component driver with name drv.
  2. When the run_phase starts, all 3 sequences requests for a grant.
  3. At 0ns, driver requests for a transaction. Due to our custom arbitration method, sequencer grants access to rnd_seq3.
  4. rnd_seq3 sends transaction to driver and driver drives the signal to the DUT @5ns.
  5. At 5ns, driver calls item_done() and again on next iteration calls get_next_item() requesting a new transaction.
  6. At 5ns, sequencer has grant request from rnd_seq2 and rnd_seq1. Sequencer gives grant to rnd_seq2.
  7. rnd_seq2 sends a transaction which is driven by the driver and driver request for a new transaction @10ns.
  8. At 7ns, rnd_seq3 requests for a grant again.
  9. At 10ns, sequencer has grant request from rnd_seq3 and rnd_seq1. Sequencer grants access to rnd_seq3.
  10. This goes on until all transaction has been sent by all the sequences.

Conclusion

In this article we went through how to declare UVM Driver using uvm_driver base class. We also saw various methods through which we can pull transaction from the sequencer and how the handshake happens between Sequence, Sequencer and Driver. In the next article we will see how we monitors the actual activity happening in the DUT interface.