UVM Driver Use Models – Part 2

Hi! As we already know that UVM Driver plays a very significant role in interface protocol implementation since it deals with class based transaction or sequence items on one side and on the other side works at clock based signal/pin level activities. So how a UVM Driver is implemented & its synchronization with the Sequences is crucial for smooth interaction between DUT and UVM environment and avoiding any dead lock situation between UVM Driver and Sequence.

In Part 1, UVM Driver Use Models – Part 1, we came across about different types of UVM Driver use models & covered in detail about Unidirectional Non-pipelined and Bidirectional Non-pipelined. In Part 2, we’ll talk about Pipelined use model which is bidirectional in nature.

Pipelined Use Model:

In a pipelined bus protocol, a data transfer is broken down into two or more different phase and these phases are executed one after another. Often these phase involves different set of signals on the bus. In pipelined operations, several data transfers gets triggered and each data transfer used to be active or operating in some or other pipeline processing stage.

AMBA AHB is a good example of pipelined protocol which is having two different phases called as Address Phase and Data Phase. In the address phase, address and control signals are set up by the Master and in the data phase, data of the transfer is exchanged between Master & Slave based on the type of transfer i.e. Read or Write. This is shown in the waveform diagram 1 below:

Diagram 1: Pipelined Bus Protocol (source: cookbook)

As shown in the diagram above, data phase of the first write transaction i.e. W0 and address phase of the first read transaction i.e. R0 are executing during the same time. It means these two phases are pipelined in nature. It is seen in the first drawn circle in the above diagram.

In the second drawn circle in above diagram, address phase of the second write transaction i.e. W1 is pipelined with the data phase (burst) of the second read transaction i.e. R3 (R1-R4).

Implementing a pipelined protocol brings in certain requirements to fulfill –

  • The UVM Driver needs to run multiple threads and each thread to receive a sequence item from sequence and process it through each stage or phases of the transfer.
  • The UVM Driver needs to unblock the Sequence to receive the next sequence item to keep the pipeline funnel full.
  • The Sequence needs to have separate stimulus generation and response handling threads. Stimulus generating thread needs to continuously sending the sequence items without getting blocked by response thread. Hence to kept these two threads separate.

The most straight forward way to implement a pipelined protocol is to use the put() and get() methods with Driver-Sequencer API.

Pipelined Driver Implementation:

For a pipelined transfer, multiple Sequence items needs to be processed through each phase of the transfer. To achieve this – UVM Driver executes multiple parallel threads inside its run_phase method & each of these parallel threads process the sequence item to the completion taking them through each phase of the pipeline. Number of parallel threads depends on the number of stages/phases in the pipeline. Each thread uses get() method to receive the new sequence item, this unblocks the sequencer and finish_item() in sequence to send the next sequence item to the UVM Driver to keep the pipeline funnel full.

In order to ensure that only one thread gets the next sequence item using the get() method, and also to ensure that the first phase of the transfer is being driven on the bus by one thread only at a time, semaphore is being utilized for this task.  The semaphore is grabbed by one of the parallel threads at the beginning of the loop and it releases the semaphore at the end of the first phase of the pipeline which then allows another waiting thread to grab the semaphore and take ownership.

Each thread at the end of last phase in the pipeline sends the response back to the sequence using the put() method. Sequence process this response provided by the thread of the UVM Driver.

Related UVM Driver code with the above mentioned mechanism is shown in the later part of this article.

Pipelined Sequence Implementation:

Before getting into the details of pipelined sequence implementation, lets understand first the response handing behavior of the Sequence because the blocking or unblocking nature matters a lot in pipelined implementation.

By default, Sequence retrieves responses from the UVM Driver by calling get_response() method inside it. Alternatively, the sequence behavior can be set to use an automatic report handler to fetch response instead. The report handler is an overridden response_handler() function which is inherited from the uvm_sequence_base. The response_handler() is enabled using use_response_handler() method, and then response_handler() is called everytime a response is available.

Now, lets see the application of both the response handling approaches. Since get_response() is a blocking method and it blocks until it receives the response from the UVM Driver. So if we implement using get_response() method and place start_item(), finish_item() & finally get_response() in this order inside the body() method of the Sequence, which results into an un-pipelined Sequence implementation because next sequence item can only be provided to the UVM Driver once the response item of the previous request sequence item is received by the get_response() method.

Alternative response handling approach i.e. response_handler() can be used to implement pipelined sequences. A pipeline sequence needs to have two separate threads to handle the request sequence items generation and for response sequence items handling.

Inside the Sequence, the request sequence item generation blocks at finish_item() until one of threads completes the get() method call inside UVM Driver. Once the sequence loop is unblocked, new sequence item is generated & ready to serve another parallel thread inside UVM Driver.

Please note that with the get() and put() UVM Driver implementation, there is a response FIFO in the Sequence which must be managed. By enabling the response_handler() using use_response_handler() method whenever there is a response from UVM Driver, response_handler() is called and it makes response FIFO empty.

Lets see now the UVM Sequence code for the pipeline implementation which works well with the pipelined UVM Driver.


////////// Sequence Item //////////
class ahb_seq_item extends uvm_sequence_item;
 `uvm_object_utils(ahb_seq_item)
 
 
 /// Master to Slave Data Flow
 rand logic [31:0] HADDR;
 rand logic [31:0] HWDATA;
 rand logic HWRITE;
 ahb_burst_e HBURST;
 
 /// Slave to Master Data Flow
 logic [31:0] HRDATA;
 ahb_resp_e HRESP;
 
 /// Constructor
 function new (string name);
 super.new(name);
 endfunction: new
 
 /// Constraints
 constraint addr_for_32bit {HADDR[1:0] == 0;}
 
endclass: ahb_seq_item


////////// AHB Interface //////////
interface ahb_interface;
 
 /// TO both Master & Slave
 logic HCLK;
 logic RESETn;
 
 /// Master to Slave
 logic [31:0] HADDR;
 logic [31:0] HWDATA;
 logic HWRITE;
 ahb_burst_e HBURST;
 
 /// Slave to Master
 logic [31:0] HRDATA;
 ahb_resp_e HRESP;
 logic HREADY;
 
endinterface: ahb_interface


////////// Pipelined UVM Driver //////////
class ahb_pipelined_driver extends uvm_driver #(ahb_seq_item);
 `uvm_component_utils(ahb_pipelined_driver)
 
 /// Virtual Interface
 virtual ahb_interface ahb_if;
 
 /// Constructor
 function new (string name, uvm_component parent);
 super.new(name, parent);
 endfunction: new
 
 /// Semaphore Declaration
 semaphore pipeline_lock = new(1);
 
 /// Run Phase Task
 task run_phase (uvm_phase phase);
 
 @(posedge ahb_if.HRESETn);
 @(posedge ahb_if.HCLK);
 
 fork
   do_pipelined_transfer;
   do_pipelined_transfer;
 join
 
 endtask: run_phase
 
 /// do_pipelined_transfer task
 task automatic do_pipelined_transfer;
 
 ahb_seq_item req;
 
 forever begin
   pipeline_lock.get();
   seq_item_port.get(req);
   accept_tr(req, $time);
   void'(begin_tr(req, "pipelined_driver");
   ahb_if.HADDR <= req.HADDR;
   ahb_if.HWRITE <= req.HWRITE;
   ahb_if.HBURST <= req.HBURST;
   @(posedge ahb_if.HCLK);
   while(!ahb_if.HREADY == 1) begin
   @(posedge ahb_if.HCLK);
 end
 // Command phase ends here
 // Unlock semaphore
 pipeline_lock.put();
 // Data phase starts here
 if (req.HWRITE == 0) begin
   @(posedge ahb_if.HCLK);
 while(ahb_if.HREADY != 1) begin
   @(posedge ahb_if.HCLK);
 end
   req.HRDATA = ahb_if.HRDATA;
   req.HRESP = ahb_if.HRESP;
 end
 else begin
   ahb_if.HWDATA <= req.HWDATA;
   @(posedge ahb_if.HCLK);
     while(ahb_if.HREADY != 1) begin
   @(posedge ahb_if.HCLK);
   end
   req.HRESP = ahb_if.HRESP;
 end
 // Return the Request as Response
   seq_item_port.put(req);
   end_tr(req);
 end
 endtask: do_pipelined_transfer
 
 endclass: ahb_pipelined_driver


////////// Pipelined Sequence ////////// 
 class ahb_pipelined_seq extends uvm_sequence  #(ahb_seq_item);
 `uvm_object_utils(ahb_pipelined_seq)
 
   logic [31:0] addr[10]; // To save addresses
   int count; // To ensure that the seq does'nt complete too early
 
 // Constructor
 function new (string name);
   super.new(name);
 endfunction: new
 
 // Task body()
 task body;
 
   ahb_seq_item req;
   req = ahb_seq_item::type_id::create("req", this);
   use_response_handler(1); // Enable Response Handler
   count = 0;
 
   for(int i=0; i<10; i++) begin
     start_item(req);
     assert(req.randomize() with {MWRITE == 1; HBURST == SINGLE; HADDR inside {[32'h0010_1000:32'h0010_1FFC]};});
     addr[i] = req.HADDR;
     finish_item(req);
   end
 
   foreach (addr[i]) begin
     start_item(req);
     req.HADDR = addr[i];
     req.HWRITE = 0;
     finish_item(req);
   end
 
 // Wait till last seq item is over
   wait(count == 20);
 endtask: body
 
 /// This response_handler function is enabled to keep the sequence response FIFO empty
   function void response_handler(uvm_sequence_item response);
     count++;
   endfunction: response_handler
 
 endclass: ahb_pipelined_seq

Lets analyze the code snippets being shared here a little bit, though most of it is self explanatory. First of all is the defined sequence item i.e. ahb_seq_item which contains the data members driven from Master to Slave and from Slave to Master plus default constraints can be defined inside this sequence item.

Next is the interface definition i.e. ahb_interface which defines signals Master to Slave and Slave to Master. This interface is used to declare a virtual interface inside the driver.

Next is the definition of pipelined UVM Driver i.e. ahb_pipelined_driver. Here important piece of code which needs attention is the fork..join statement which starts two parallel threads. These two threads are made to compete for the semaphore access which is declared inside the UVM Driver. At the start, one thread capture the semaphore and fetches the sequence item from the Sequence. This thread completes the address phase of the transfer and release the semaphore for the another waiting thread. The first thread’s next moves is to complete the data phase of the first transfer. At the same time, in parallel, second thread can process the address phase of the second sequence item received from the Sequence. As final stage of each thread, after the data phase completion, request sequence items is updated and sent back as the response item to the Sequence using put() method.

At last, definition of the pipelined Sequence i.e. ahb_pipelined_seq is shown in the code. Here important thing to note is that finish_item(req) gets unblocked by the seq_item_port.get() method inside UVM Driver & Sequence is ready with another sequence item to supply to the Driver but it has to wait till the second threads get the semaphore access. Another thing to note is that response handler is enabled using use_response_handler(1) and response from the UVM Driver is handled as a separate thread.

With all that, as defined above, the requirements for a pipelined protocol implementation can be fulfilled & a pipelined behavior can be achieved with a smooth interaction between UVM Driver and Sequence without any deadlock condition.


That’s all I want to cover with respect to UVM Driver use models in Part 2. I hope you found it useful and helpful. Please feel free to share your suggestion & inputs regarding this topic OR any another future topic. I would love to hear that.

Thank you for your time in going through this post. Hope you enjoyed it! I wish to see you again visiting this portal for more upcoming topics!


amazon

3 thoughts on “UVM Driver Use Models – Part 2

  1. Thanks for wonderful Explanation. Can you please add also Out of Order driver implementation? It would be grat.

Leave a Reply

Your email address will not be published. Required fields are marked *