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.
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
|
transaction item as given below, it is working