UVM Driver Use Models – Part 1

Since it is evident that Driver is a component in the UVM environment which deals with transaction or sequence item and transform it into pin level signal activities in temporal domain by following a particular protocol or use model & vice versa. Another important activity is UVM Driver’s synchronization with the Sequences to produce a verification scenario seamlessly and without any dead lock situation to happen. In case mutual understanding of implementation between UVM Driver and Sequence towards each other is not properly defined – there is a high chances of dead lock situation to occur.

We’ll look into this aspect & try to understand the possible way a UVM Driver can be implemented based on the direction of data flow and pipeline behavior. UVM Driver is capable of accepting the transaction request and responding back with the response items depending on the interface protocol requirements.

Primarily, there are 4 use models being used to implement the UVM Driver. Lets first know about these use models briefly and later we’ll go into details of these:

  1. Unidirectional Non-pipelined

    • In this use model, the data flow is in forward direction and no response data flow takes place which means requests are sent to the Driver but no response is expected from the UVM Driver. An example is PCM interface.
  2. Bidirectional Non-pipelined

    • In this use model, the data flow happens in the both direction which means the request (req) is sent from a Sequence to the UVM Driver and in result UVM Driver responds back to the Sequence with a response. Important thing to note is that the response occurs in lock-step with the request and only one transfer is active at a time. One example of this implementation is AMBA APB bus.
  3. Pipelined

    • In this use model, the data flow is bidirectional. The request phase overlaps with the response phase of the previous request phase. This kind of UVM Driver implementation provides improved performance but at the same time it adds complexity with respect to Sequence Driver synchronization. An Example of this use model is AMBA AHB bus.
  4. Out-of-Order Pipelined

    • This use model provides supports for advanced feature like out-of-order transactions, where response phases need not to be in an order with the request phases. Depending on the availability of the response data (driven from the corresponding slave) can be responded back in a pipelined fashion. Again, such features further improve the system performance with the additional complexity being introduced. Usually these features take support of queues and IDs to handle the complexities. Another variation of this use model is interleaved burst transfer where a running burst can be interleaved by an another transfer. An example of this use model is AMBA AXI bus which are used by most of the advanced SoC now a days.

Now lets dive deep into these UVM Driver use models to understand each one by one in detail along with the corresponding UVM code:

  1. Unidirectional Non-pipelined

In this use model, the data flow is uni-directional. Sequence-Driver combination sends series of transaction items to the DUT interface but don’t receive any response transaction items or sequence_item. That means, request (req) item is being generated from the Sequence to the UVM Driver but no response item is sent back/updated from the UVM Driver to the Sequence.

Here, Driver controls the flow of transaction items by using get_next_item() to fetch the next transaction item to be processed & does’nt call item_done() until it finishes the processing of this transaction item. The Sequence is blocked at its finish_item() call until the item_done() call is made by the Driver.

An Unidirectional Example:

Below is shown a timing diagram for ADPCM packets transmission using the PCM framing protocol.

Diagram 1: PCM Unidirectional Timing Diagram

In the above waveform, first signal called “Frame” gets asserted on the positive edge of the clock i.e. “Clk“. On the next & all upcoming positive edge of the Clk, Data[] bus is driven with the new values till the signal “Frame” gets de-asserted.

Now lets see this use model implementation using the UVM Driver & Sequence Code:


///// Interface Declaration
interface adpcm_int;
 
 logic clk;
 logic frame;
 logic [3:0] data;
 
endinterface: adpcm_int


///// Transaction item class adpcm_txn
class adpcm_txn extends uvm_sequence_item;
 `uvm_object_util(adpcm_txn)
 
 rand logic[3:0] data;
 rand int delay;
 
 function new (string name);
 super.new(name);
 endfunction: new
 
 constraint del_bet_pac {delay inside {[1:20]};}
 
endclass: adpcm_txn


///// Driver class adpcm_drv
class adpcm_drv extends uvm_driver #(adpcm_txn);
 `uvm_component_utils (adpcm_drv)
 
 adpcm_txn req;
 
 virtual adpcm_int adpcm_if;
 
 function new(string name, uvm_components parent)
 super.new(name, parent);
 endfunction: new
 
 task run_phase (uvm_phase phase);
 
 // Default conditions
 adpcm_if.frame <= 0;
 adpcm_if.data <= 0;
 forever 
 begin
 seq_item_port.get_next_item(req);
 repeat(req.delay) begin
 @(posedge adpcm_if.clk);
 end
 
 adpcm_if.frame <= 1;
 for (int i = 0; i < 8; i++) begin
 @(posedge adpcm_if.clk);
 adpcm_if.data <= req.data[3:0];
 req.data = req.data >> 4;
 end
 adpcm_if.frame <= 0;
 seq_item_port.item_done();
 end
 endtask: run_phase
 
endclass: adpcm_drv

                                                                                                                                                                   
///// Sequence Class adpcm_seq
class adpcm_seq extends uvm_sequence #(adpcm_txn);
 `uvm_component_utils (adpcm_seq)
 
 adpcm_txn req;
 
 rand int no_of_reps = 10;
 
 function new (string name);
 super.new(name);
 endfunction: new
 
 task body;
 req = adpcm_txn::type_id::create("req", this);
 for (int i = 0; i < no_of_reps; i++) begin
 start_item(req);
 if (!req.randomize()) begin
 `uvm_error("BODY", "req randomization failed")
 end
 finish_item(req);
 `uvm_info("ADPCM_SEQ BODY", $sformatf("Transmitted frame %0d", i), UVM_LOW)
 end
 endtask: body
 
endclass: adpcm_seq

Here in the above UVM code, we can see that inside the Driver i.e. adpcm_drv class code, virtual interface i.e. adpcm_if is being driven by the transaction item i.e. req request data member i.e. frame & data[] and no response data member is updated as part of Driver’s run_phase() task. At last once the whole of req transaction item is consumed by the Driver, item_done() call is made to unblock the Sequence’s finish_item() which completes the loop.

2. Bidirectional Non-pipelined

In this use model as the name indicates “Bidirectional”, data flow happens in both paths i.e. request path and response path. When the sequence item is sent to the Driver from the Sequence, the request phase of pin level protocol is executed first by the UVM Driver and once its over, in the response phase the UVM Driver sends back the response to the Sequencer/Sequence. The main point here is that – next request phase can not take place unless the previous response phase is completed.

A peripheral bus called AMBA APB is a good example for this kind of protocol. This protocol timing diagram is shown below:

Diagram 2: Peripheral bus APB timing waveform

Out of the above signals Valid, RNW (Read Not Write), Address, Write Data participates in the request phase which is initiated with the assertion of the Valid signal, while Address and RNW defines the type of transfer.

Signals Ready, Read Data & Error participates in the response phase and response phase is completed when the Ready signal got asserted.

Lets see this implementation using UVM code below:


///// APB Bus Interface
interface apb_interface;
 
 logic clk;
 logic rstn;
 logic [31:0] Addr;
 logic [31:0] Write_Data;
 logic RNW;
 logic Valid;
 logic Ready;
 logic [31:0] Read_Data;
 logic Error;
 
endinterface: apb_interface

///// APB Bus Sequence Item Class
class apb_seq_item extends uvm_sequence_item;
 `uvm_object_utils(apb_seq_item)
 
 /// Request fields
 rand logic [31:0] addr;
 rand logic [31:0] write_data;
 rand bit read_not_write;
 rand int delay;
 
 /// Response fields
 bit error;
 logic [31:0] read_data;
 
endclass: apb_seq_item

///// APB Bus Driver Class
class apb_bus_driver extends uvm_driver #(apb_seq_item);
 `uvm_component_utils(apb_bus_driver)
 
 apb_seq_item req;
 
 virtual apb_interface apb_if;
 
 function new (string name, uvm_component parent);
 super.new(name, parent);
 endfunction: new
 
 task run_phase (uvm_phase phase);
 
 apb_if.Valid <= 0;
 apb_if.RNW <= 0;
 
 /// Out of Reset
 @(posedge apb_if.rstn);
 /// Pin Level Transfer
 forever
 begin
 seq_item_port.get_next_item(req);
 repeat(req.delay) begin
 @(posedge apb_if.clk);
 end
 apb_if.Valid <= 1;
 apb_if.Addr <= req.addr;
 apb_if.RNW <= req.read_not_write;
 if (req.read_not_write == 0) begin
 apb_if.Write_Data <= req.write_data;
 end
 while(apb_if.Ready != 1) begin
 @(posedge apb_if.clk);
 end
 // At the end of pin level bus transaction
 // Copy response data into the req related fields
 if (req.read_not_write == 1) begin
 req.read_data <= apb_if.Read_Data;
 end
 req.error <= apb_if.Error;
 apb_if.Valid <= 0;
 seq_item_port.item_done();
 end
 endtask: run_phase
endclass: apb_bus_driver

///// APB Bus Sequence
class apb_bus_seq extends uvm_sequence #(apb_seq_item)
 `uvm_object_utils(apb_bus_seq)
 
 function new(string name);
 super.new(name);
 endfunction: new
 
 // No. of iterations
 rand int iti = 30
 
 apb_seq_item req;
 
task body();
 req = apb_seq_item::type_id::create("req", this);
 repeat(iti)
 begin
 start_item(req);
 assert (!req.randomize() with {addr inside {[32`h1000_0000:32`h1000_001C]};}) begin
 `uvm_error("Body", "Randomization Error");
 end 
 finish_item(req);
`uvm_info ("Response Fields", $sformatf("Read Data = %h", req.read_data), UVM_LOW)
 end
 endtask: body
 
endclass: apb_bus_seq

If we’ll analyze the Driver UVM code above, inside the body() task, first the request phase is completed then Ready signal is waited to be asserted and then in the response phase, all the request fields of the sequence items are updated with the latest values.

The key point here to note is that UVM Driver is sending back the response to the Sequence/Sequencer by updating the fields within the “req” transaction item before making the item_done() call. At the Sequence side of the transfer, the Sequence is blocked in the finish_item() call until the item_done() occurs. When Sequence is unblocked, its req handle is still pointing to the req object which has had its response items/fields/data members updated by the UVM Driver. This means that before starting the next loop (if its applicable) the Sequence can access the response items of the transaction item i.e. req. This we can see in the above code of Sequence i.e. apb_bus_seq. Just next line after the finish_item() call i.e. `uvm_info where we can access the req.read_data response field for the latest updated values by the Driver.

Hence above explanation demonstrates the bidirectional but non-pipelined behavior Driver use model implementation & how it works in tandem with the Sequence.


With that, I would prefer to stop here for Part 1 and we’ll go into rest of the topic in Part 2 which I’ll try to publish as soon as possible in near time. I believe the topic helps in developing understanding towards interface protocols implementations since UVM Driver is the component which drives the pin level activity for the DUT so Driver use models are key to implement interface protocols. Thank you for your time to go through this article, see you soon with Part 2 of Driver use models. Till then, take care bye!


amazon