Wednesday, 27 August 2014

Methods to model UVM driver/sequence wrt pipelined or un-pipelined transactions

Below Article shows good insight in how to write a OVM/UVM driver to model pipelined transactions. good way of modeling pipelined behavior.

Courtesy: Verification Academy

original link: https://verificationacademy.com/cookbook/ovm/driver/pipelined


In a pipelined bus protocol a data transfer is broken down into two or more phases which are executed one after the other, often using different groups of signals on the bus. This type of protocol allows several transfers to be in progress at the same time with each transfer occupying one stage of the pipeline. The AMBA AHB bus is an example of a pipelined bus, it has two phases - the address phase and the data phase. During the address phase, the address and the bus control information, such as the opcode, is set up by the host, and then during the data phase the data transfer between the target and the host takes place. Whilst the data phase for one transfer is taking place on the second stage of the pipeline, the address phase for the next cycle can be taking place on the first stage of the pipeline. Other protocols such as OCP use more phases.
A pipelined protocol has the potential to increase the bandwidth of a system since, provided the pipeline is kept full, it increases the number of transfers that can take place over a given number of clock cycles. Using a pipeline also relaxes the timing requirements for target devices since it gives them extra time to decode and respond to a host access.
A pipelined protocol could be modelled with a simple bidirectional style, whereby the sequence sends a sequence item to the driver and the driver unblocks the sequence when it has completed the bus transaction. In reality, most I/O and register style accesses take place in this way. The drawback is that it lowers the bandwidth of the bus and does not stress test it. In order to implement a pipelined sequence-driver combination, there are a number of design considerations that need to be taken into account in order to support fully pipelined transfers:
  • Driver Implementation - The driver needs to have multiple threads running, each thread needs to take a sequence item and take it through each of the pipeline stages.
  • Keeping the pipeline full - The driver needs to unblock the sequencer to get the next sequence item so that the pipeline can be kept full
  • Sequence Implementation - The sequence needs to have separate stimulus generation and response threads. The stimulus generation thread needs to continually send new bus transactions to the driver to keep the pipeline full.

Contents


Recommended Implementation Pattern Using get and put

The most straight-forward way to model a pipelined protocol with a sequence and a driver is to use the get() and put() methods from the driver-sequencer API.

Driver Implementation

In order to support pipelining, a driver needs to process multiple sequence_items concurrently. In order to achieve this, the drivers run method spawns a number of parallel threads each of which takes a sequence item and executes it to completion on the bus. The number of threads required is equal to the number of stages in the pipeline. Each thread uses the get() method to acquire a new sequence item, this unblocks the sequencer and the finish_item() method in the sequence so that a new sequence item can be sent to the driver to fill the next stage of the pipeline.
In order to ensure that only one thread can call get() at a time, and also to ensure that only one thread attempts to drive the first phase of the bus cycle, a semaphore is used to lock access. The semaphore is grabbed at the start of the loop in the driver thread and is released at the end of the first phase, allowing another thread to grab the semaphore and take ownership.
At the end of the last phase in the bus cycle, the driver thread sends a response back to the sequence using the put() method. This returns the response to the originating sequence for processing.
In the code example a two stage pipeline is shown to illustrate the principles outlined.
//
// This class implements a pipelined driver
//
class mbus_pipelined_driver extends ovm_driver #(mbus_seq_item);
 
`ovm_component_utils(mbus_pipelined_driver)
 
virtual mbus_if MBUS;
 
function new(string name = "mbus_pipelined_driver", ovm_component parent = null);
  super.new(name, parent);
endfunction
 
// The two pipeline processes use a semaphore to ensure orderly execution
semaphore pipeline_lock = new(1);
//
// The run_phase(ovm_phase phase);
//
// This spawns two parallel transfer threads, only one of
// which can be active during the cmd phase, so implementing
// the pipeline
//
task run();
 
  @(posedge MBUS.MRESETN);
  @(posedge MBUS.MCLK);
 
  fork
    do_pipelined_transfer;
    do_pipelined_transfer;
  join
 
endtask
 
//
// This task has to be automatic because it is spawned
// in separate threads
//
task automatic do_pipelined_transfer;
  mbus_seq_item req;
 
  forever begin
    pipeline_lock.get();
    seq_item_port.get(req);
    accept_tr(req, $time);
    void'(begin_tr(req, "pipelined_driver"));
    MBUS.MADDR <= req.MADDR;
    MBUS.MREAD <= req.MREAD;
    MBUS.MOPCODE <= req.MOPCODE;
    @(posedge MBUS.MCLK);
    while(!MBUS.MRDY == 1) begin
      @(posedge MBUS.MCLK);
    end
    // End of command phase:
    // - unlock pipeline semaphore
    pipeline_lock.put();
    // Complete the data phase
    if(req.MREAD == 1) begin
      @(posedge MBUS.MCLK);
      while(MBUS.MRDY != 1) begin
        @(posedge MBUS.MCLK);
      end
      req.MRESP = MBUS.MRESP;
      req.MRDATA = MBUS.MRDATA;
    end
    else begin
      MBUS.MWDATA <= req.MWDATA;
      @(posedge MBUS.MCLK);
      while(MBUS.MRDY != 1) begin
        @(posedge MBUS.MCLK);
      end
      req.MRESP = MBUS.MRESP;
    end
    // Return the request as a response
    seq_item_port.put(req);
    end_tr(req);
  end
endtask: do_pipelined_transfer
 
endclass: mbus_pipelined_driver

Sequence Implementation

Unpipelined Accesses

Most of the time unpipelined transfers are required, since typical bus fabric is emulating what a software program does, which is to access single locations. For instance using the value read back from one location to determine what to do next in terms of reading or writing other locations.
In order to implement an unpipelined sequence that would work with the pipelined driver, the body() method would call start_item(), finish_item() and get_response() methods in sequence. The get_response() method blocks until the driver sends a response using its put() method at the end of the bus cycle. The following code example illustrates this:
//
// This sequence shows how a series of unpipelined accesses to
// the bus would work. The sequence waits for each item to finish
// before starting the next.
//
class mbus_unpipelined_seq extends ovm_sequence #(mbus_seq_item);
 
`ovm_object_utils(mbus_unpipelined_seq)
 
logic[31:0] addr[10]; // To save addresses
logic[31:0] data[10]; // To save data for checking
 
int error_count;
 
function new(string name = "mbus_unpipelined_seq");
  super.new(name);
endfunction
 
task body;
 
  mbus_seq_item req = mbus_seq_item::type_id::create("req");
 
  error_count = 0;
  for(int i=0; i<10; i++) begin
    start_item(req);
    assert(req.randomize() with {MREAD == 0; MOPCODE == SINGLE; MADDR inside {[32'h0010_0000:32'h001F_FFFC]};});
    addr[i] = req.MADDR;
    data[i] = req.MWDATA;
    finish_item(req);
    get_response(req);
  end
 
  foreach (addr[i]) begin
    start_item(req);
    req.MADDR = addr[i];
    req.MREAD = 1;
    finish_item(req);
    get_response(req);
    if(data[i] != req.MRDATA) begin
      error_count++;
      `ovm_error("body", $sformatf("@%0h Expected data:%0h Actual data:%0h", addr[i], data[i], req.MRDATA))
    end
  end
endtask: body
 
endclass: mbus_unpipelined_seq
Note: This example sequence has checking built-in, this is to demonstrate how a read data value can be used. The specifictype of check would normally be done using a scoreboard.

Pipelined Accesses

Pipelined accesses are primarily used to stress test the bus but they require a different approach in the sequence. A pipelined sequence needs to have a seperate threads for generating the request sequence items and for handling the response sequence items.
The generation loop will block on each finish_item() call until one of the threads in the driver completes a get() call. Once the generation loop is unblocked it needs to generate a new item to have something for the next driver thread to get(). Note that a new request sequence item needs to be generated on each iteration of the loop, if only one request item handle is used then the driver will be attempting to execute its contents whilst the sequence is changing it.
In the example sequence, there is no response handling, the assumption is that checks on the data validity will be done by a scoreboard. However, with the get() and put() driver implementation, there is a response FIFO in the sequence which must be managed. In the example, the response_handler is enabled using the use_response_handler() method, and then the response_handler function is called everytime a response is available, keeping the sequences response FIFO empty. In this case the response handler keeps cound of the number of transactions to ensure that the sequence only exist when the last transaction is complete.
//
// This is a pipelined version of the previous sequence with no blocking
// call to get_response();
// There is no attempt to check the data, this would be carried out
// by a scoreboard
//
class mbus_pipelined_seq extends ovm_sequence #(mbus_seq_item);
 
`ovm_object_utils(mbus_pipelined_seq)
 
logic[31:0] addr[10]; // To save addresses
int count; // To ensure that the sequence does not complete too early
 
function new(string name = "mbus_pipelined_seq");
  super.new(name);
endfunction
 
task body;
 
  mbus_seq_item req = mbus_seq_item::type_id::create("req");
  use_response_handler(1);
  count = 0;
 
  for(int i=0; i<10; i++) begin
    start_item(req);
    assert(req.randomize() with {MREAD == 0; MOPCODE == SINGLE; MADDR inside {[32'h0010_0000:32'h001F_FFFC]};});
    addr[i] = req.MADDR;
    finish_item(req);
  end
 
  foreach (addr[i]) begin
    start_item(req);
    req.MADDR = addr[i];
    req.MREAD = 1;
    finish_item(req);
  end
  // Do not end the sequence until the last req item is complete
  wait(count == 20);
endtask: body
 
// This response_handler function is enabled to keep the sequence response
// FIFO empty
function void response_handler(ovm_sequence_item response);
  count++;
endfunction: response_handler
 
endclass: mbus_pipelined_seq
If the sequence needs to handle responses, then the response handler function should be extended.

Alternative Implementation Pattern Using Events To Signal Completion

Adding Completion Events to sequence_items

In this implementation pattern, events are added to the sequence_item to provide a means of signalling from the driver to the sequence that the driver has completed a specific phase. In the example, a ovm_event_pool is used for the events, and two methods are provided to trigger and to wait for events in the pool:
//------------------------------------------------------------------------------
//
// The mbus_seq_item is designed to be used with a pipelined bus driver.
// It contains an event pool which is used to signal back to the
// sequence when the driver has completed different pipeline stages
//
class mbus_seq_item extends ovm_sequence_item;
 
// From the master to the slave
rand logic[31:0] MADDR;
rand logic[31:0] MWDATA;
rand logic MREAD;
rand mbus_opcode_e MOPCODE;
 
// Driven by the slave to the master
mbus_resp_e MRESP;
logic[31:0] MRDATA;
 
// Event pool:
ovm_event_pool events;
 
`ovm_object_utils(mbus_seq_item)
 
function new(string name = "mbus_seq_item");
  super.new(name);
  events = get_event_pool();
endfunction
 
constraint addr_is_32 {MADDR[1:0] == 0;}
 
// Wait for an event - called by sequence
task wait_trigger(string evnt);
  ovm_event e = events.get(evnt);
  e.wait_trigger();
endtask: wait_trigger
 
// Trigger an event - called by driver
task trigger(string evnt);
  ovm_event e = events.get(evnt);
  e.trigger();
endtask: trigger
 
// do_copy(), do_compare() etc
 
 
endclass: mbus_seq_item

Driver Signalling Completion using sequence_item Events

The driver is almost identical to the get, put implementation except that it triggers the phase completed events in the sequence item rather than using a put() method signal to the sequence that a phase has completed and that there is response information available via the sequence_item handle.
//
// This class implements a pipelined driver
//
class mbus_pipelined_driver extends ovm_driver #(mbus_seq_item);
 
`ovm_component_utils(mbus_pipelined_driver)
 
virtual mbus_if MBUS;
 
function new(string name = "mbus_pipelined_driver", ovm_component parent = null);
  super.new(name, parent);
endfunction
 
// the two pipeline processes use a semaphore to ensure orderly execution
semaphore pipeline_lock = new(1);
//
// The run();
//
// This spawns two parallel transfer threads, only one of
// which can be active during the cmd phase, so implementing
// the pipeline
//
task run();
 
  @(posedge MBUS.MRESETN);
  @(posedge MBUS.MCLK);
 
  fork
    do_pipelined_transfer;
    do_pipelined_transfer;
  join
 
endtask
 
//
// This task has to be automatic because it is spawned
// in separate threads
//
task automatic do_pipelined_transfer;
  mbus_seq_item req;
 
  forever begin
    pipeline_lock.get();
    seq_item_port.get(req);
    accept_tr(req, $time);
    void'(begin_tr(req, "pipelined_driver"));
    MBUS.MADDR <= req.MADDR;
    MBUS.MREAD <= req.MREAD;
    MBUS.MOPCODE <= req.MOPCODE;
    @(posedge MBUS.MCLK);
    while(!MBUS.MRDY == 1) begin
      @(posedge MBUS.MCLK);
    end
    // End of command phase:
    // - unlock pipeline semaphore
    // - signal CMD_DONE
    pipeline_lock.put();
    req.trigger("CMD_DONE");
    // Complete the data phase
    if(req.MREAD == 1) begin
      @(posedge MBUS.MCLK);
      while(MBUS.MRDY != 1) begin
        @(posedge MBUS.MCLK);
      end
      req.MRESP = MBUS.MRESP;
      req.MRDATA = MBUS.MRDATA;
    end
    else begin
      MBUS.MWDATA <= req.MWDATA;
      @(posedge MBUS.MCLK);
      while(MBUS.MRDY != 1) begin
        @(posedge MBUS.MCLK);
      end
      req.MRESP = MBUS.MRESP;
    end
    req.trigger("DATA_DONE");
    end_tr(req);
  end
endtask: do_pipelined_transfer
 
endclass: mbus_pipelined_driver
 

Unpipelined Access Sequences

Unpipelined accesses are made from sequences which block, after completing the finish_item() call, by waiting for the data phase completed event. This enables code in the sequence body method to react to the data read back. An alternative way of implementing this type of sequence would be to overload the finish_item method so that it does not return until the data phase completed event occurs.
//
// Task: finish_item
//
// Calls super.finish_item but then also waits for the item's data phase
// event. This is notified by the driver when it has completely finished
// processing the item. 
//
task finish_item( ovm_sequence_item item , int set_priority = -1 );
 
  // The "normal" finish_item()
  super.finish_item( item , set_priority );
  // Wait for the data phase to complete
  item.wait_trigger("DATA_DONE");
 
endtask
As in the previous example of an unpipelined sequence, the code example shown has a data integrity check, this is purely for illustrative purposes.
class mbus_unpipelined_seq extends ovm_sequence #(mbus_seq_item);
 
`ovm_object_utils(mbus_unpipelined_seq)
 
logic[31:0] addr[10]; // To save addresses
logic[31:0] data[10]; // To save addresses
 
int error_count;
 
function new(string name = "mbus_pipelined_seq");
  super.new(name);
endfunction
 
task body;
 
  mbus_seq_item req = mbus_seq_item::type_id::create("req");
  error_count = 0;
 
  for(int i=0; i<10; i++) begin
    start_item(req);
    assert(req.randomize() with {MREAD == 0; MOPCODE == SINGLE; MADDR inside {[32'h0010_0000:32'h001F_FFFC]};});
    addr[i] = req.MADDR;
    data[i] = req.MWDATA;
    finish_item(req);
    req.wait_trigger("DATA_DONE");
  end
 
  foreach(addr[i]) begin
    start_item(req);
    req.MADDR = addr[i];
    req.MREAD = 1;
    finish_item(req);
    req.wait_trigger("DATA_DONE");
    if(req.MRDATA != data[i]) begin
      error_count++;
      `ovm_error("body", $sformatf("@%0h Expected data:%0h Actual data:%0h", addr[i], data[i], req.MRDATA))
    end
  end
endtask: body
 
endclass: mbus_unpipelined_seq

Pipelined Access

The pipelined access sequence does not wait for the data phase completion event before generating the next sequence item. Unlike the get, put driver model, there is no need to manage the response FIFO, so in this respect this implementation model is more straight-forward.
class mbus_pipelined_seq extends ovm_sequence #(mbus_seq_item);
 
`ovm_object_utils(mbus_pipelined_seq)
 
logic[31:0] addr[10]; // To save addresses
 
function new(string name = "mbus_pipelined_seq");
  super.new(name);
endfunction
 
task body;
 
  mbus_seq_item req = mbus_seq_item::type_id::create("req");
 
  for(int i=0; i<10; i++) begin
    start_item(req);
    assert(req.randomize() with {MREAD == 0; MOPCODE == SINGLE; MADDR inside {[32'h0010_0000:32'h001F_FFFC]};});
    addr[i] = req.MADDR;
    finish_item(req);
  end
 
  foreach (addr[i]) begin
    start_item(req);
    req.MADDR = addr[i];
    req.MREAD = 1;
    finish_item(req);
  end
endtask: body
 
endclass: mbus_pipelined_seq

Best method to force a signal in UVM environment across packages

Add an interface class to your package
interface class abstract_forces;
// this could be parameterized and accept arguments.
  pure virtual function void apply_forces;
endclass

Create a module that has the functions that do the forces to the DUT, and inside that module, construct a concrete class that could can add via the config_db and call from your test.
module DUT_forces;
import uvm_pkg::*;
import test_pkg::*;
 
function void forceset1;
  force $root.a.b.c = 0;
  force $root.d.e.f = 0;
endfunction
 
class concrete_set1 implements abstract_forces;
  virtual function void apply_forces;
    forceset1;
  endfunction
endclass
concrete_set1 h1 = new();
initial uvm_config_db#(DUT_api)::set(null,"*","set1",h1);
 
function void forceset2;
  force $root.g.h.i = 0;
  force $root.j.k.l = 0;
endfunction
 
class concrete_set2 implements abstract_forces;
  virtual function void apply_forces;
    forceset1;
  endfunction
endclass
concrete_set2 h2 = new();
initial uvm_config_db#(abstract)::set(null,"*,"set2",h2);
endmodule

Then in your test class, you get the concrete class object and call the forces() method
class test extends uvm_test;
 
abstract_forces f_h;
 
function void build_phase(uvm_phase);
 
 if( !uvm_config_db #(abstract_forces)::get( this , "" , "set1" , f_h; ) ) begin
      `uvm_error(...)
    end
endfunction
task run_phase(uvm_phase phase);
 
  f_h.apply_forces;
endtask
 
endclass :test

Tuesday, 12 August 2014

How to stop the simulation on `UVM_ERROR

Below is a good article which shows how to control simulation flow with different types of errors.

The default behavior of `uvm_error is to continue the simulation once the message is reported. Although one can argue over Accelera’s default choice, there are ways to stop the simulation on `uvm_error. I’ve tested them with UVM 1.1d and UVM 1.2 releases.

Using Simulator Arguments

Major simulators support the +uvm_set_action command-line argument to set a custom action for report messages:
+uvm_set_action=<comp>,<id>,<severity>,<action>
For example, to stop the simulation on `uvm_error(“MY_ERROR”, “message”), use the following argument when invoking the simulator:
+uvm_set_action="uvm_test_top.*,MY_ERROR,UVM_ERROR,UVM_STOP"
You can use _ALL_ instead of MY_ERROR to set the action for all errors, regardless of their id.

Using the uvm_component API

Call uvm_component.set_report_id_action_hier (string id,uvm_action action), for example after elaboration:
class basic_test extends uvm_test;
    function void end_of_elaboration_phase(uvm_phase phase);
        super.end_of_elaboration_phase(phase);
        set_report_id_action_hier("MY_ERROR", UVM_STOP);
    endfunction
endclass

Pay Attention: objects vs. components vs. sequence items

To my surprise, the above configuration does not apply to `uvm_error calls from within an uvm_object, for example for config objects, even if created under the “uvm_test_top.*” or “basic_test” hierarchy…
For `uvm_error calls from within an uvm_sequence_item, the message is delegated to the enclosing sequencer, hence it is not behaving like an uvm_object. The component path is enhanced with the enclosing sequencer and the sequence hierarchy path.
My recommendation is to use a generous star path pattern (“*”) when working with simulator arguments:
+uvm_set_action="*,MY_ERROR,UVM_ERROR,UVM_STOP"

or use the uvm_root component when working with the uvm_component API:
class basic_test extends uvm_test;
    function void end_of_elaboration_phase(uvm_phase phase);
        uvm_root top = uvm_root::get();
        super.end_of_elaboration_phase(phase);
        top.set_report_id_action_hier("MY_ERROR", UVM_STOP);
    endfunction
endclass

Underground Details for the Curious

The `uvm_error macro is defined as:
`define uvm_error(ID,MSG) \
   begin \
     if (uvm_report_enabled(UVM_NONE,UVM_ERROR,ID)) \
       uvm_report_error (ID, MSG, UVM_NONE, `uvm_file, `uvm_line); \
   end

That is the macro call is delegated to a uvm_report_error() function call. The function that is actually called depends on the context where the macro is used.
There are three relevant uvm_report_error() function definitions in the UVM library:
  1. uvm_report_object.uvm_report_error(). An uvm_component inherits from uvm_report_object.
  2. Global uvm_report_error() which delegates the call to uvm_root.uvm_report_error()
  3. uvm_sequence_item.uvm_report_error() which delegates the call to its sequencer, if it exists or to uvm_root.