Site icon Hardware Design and Verification

UVM Questions and Answers part2

Welcome to section uvm question and answer part2 ,

I will try to put around 20 to 30 questions and answer related to UVM

Lets Start

What are the different arbitration mechanisms available for a

Sequencer?

Multiple sequences can interact concurrently with a driver connected to a single interface. The sequencer supports an arbitration mechanism to ensure that at any point of time only one sequence has access to the driver. The choice of which sequence can send a sequence_item is dependent on a user selectable sequencer arbitration algorithm. There are five built-in sequencer arbitration mechanisms that are implemented in UVM. There is also an additional hook to implement a user defined algorithm. The sequencer has a method called set_arbitration() that can be called to select which algorithm the sequencer should use for arbitration. The six algorithms that can be selected are following:

1) SEQ_ARB_FIFO (Default if none specified). If this arbitration mode is specified, then the sequencer picks sequence items in a FIFO order from all sequences running on the sequencer. For Example: if seq1, seq2 and seq3 are running on a sequencer, it will pick an item from seq1 first, followed by seq2, and then seq3 if available, and continue.

2) SEQ_ARB_WEIGHTED: If this arbitration mode is selected, sequence items from the highest priority sequence are always picked first until none available, then the sequence items from next priority sequence, and so on. If two sequences have equal priority, then the items from them are picked in a random order. 3) SEQ_ARB_RANDOM: If this arbitration mode is selected, sequence items from different sequences are picked in a random order by ignoring all priorities.

4) SEQ_ARB_STRICT_FIFO: This is similar to SEQ_ARB_WEIGHTED except that if two sequences have same priority, then the items from those sequences are picked in a FIFO order rather than in a random order.

5) SEQ_ARB_STRICT_RANDOM: This is similar to SEQ_ARB_RANDOM except that the priorities are NOT ignored. The items are picked randomly from sequences with highest priority first followed by next and in that order.

6) SEQ_ARB_USER: This algorithm allows a user to define a custom algorithm for arbitration between sequences. This is done by extending the uvm_sequencer class and overriding the user_priority_arbitration() method

How do we specify the priority of a sequence when it is started on a sequencer?

The priority is specified by passing an argument to the start() method of the sequence. The priority is decided based on relative values specified for difference sequences. For Example: If two sequences are started as follows, the third argument specifies the priority

of the sequence.

seq_1.start(m_sequencer, this, 500); //Highest priority

seq_2.start(m_sequencer, this, 300); //Next Highest priority

seq_3.start(m_sequencer, this, 100); //Lowest priority among three sequences

How can a sequence get exclusive access to a sequencer?

When multiple sequences are run on a sequencer, the sequencer arbitrates and grants access to each sequence on a sequence item boundary. Sometimes a sequence might want exclusive access to sequencer until all the sequence items part of it are driven to driver (for example: if you want to stimulate a deterministic pattern without any interruption).

There are two mechanisms that allow a sequence to get exclusive access to sequencer.

1) Using lock() and unlock( ): A sequence can call the lock method of the

sequencer on which it runs. The calling sequence will be granted exclusive access

to the driver when it gets the next slot via the sequencer arbitration mechanism. If

there are other sequences marked as higher priority, this sequence needs to wait

until it gets it slot. Once the lock is granted, no other sequences will be able to

access the driver until the sequence issues an unlock() call on the sequencer which

will then release the lock. The lock method is blocking and does not return until

lock has been granted.

2) Using grab() and ungrab(): The grab method is similar to the lock method and can be called by the sequence when it needs exclusive access. The difference between grab and lock is that when grab() is called, it takes immediate effect and the sequence will grab the next sequencer arbitration slot, overriding any sequence priorities in place. The only thing that can stop a sequence from grabbing a sequencer is an already existing lock() or grab() condition on the sequencer.

What is the difference between a grab() and a lock() on sequencer?

Both grab() and lock() methods of a sequencer are used by a sequence running on that sequencer to get exclusive access to the sequencer until the corresponding unlock() or ungrab() is called. The difference between grab and lock is that when a grab() on sequencer is called, it takes immediate effect and the sequence will grab the next sequencer arbitration slot overriding any sequence priorities in place. However, a call to lock() sequencer will need to wait until the calling sequence gets its next available slot based on priorities and arbitration mechanisms that are set. In terms of usage, one example usage will be to use a lock to model a prioritized interrupt and a grab to model a non-maskable interrupt, but there are several other modeling scenarios where this will be useful as well.

What is the difference between a pipelined and a non-pipelined sequence-driver model?

Based on how a design interface needs to be stimulated, there can be two modes implemented in an UVM driver class.

1) Non-pipelined model: If the driver models only one active transaction at a time, then it is called a non-pipelined model. In this case, sequence can send one transaction to the driver and driver might take several cycles (based on the interface protocol) to finish driving that transaction. Only after that the driver will accept a new transaction from sequencer

class nonpipe_driver extends uvm_driver #(req_c);

task run_phase(uvm_phase phase);

req_c req;

forever begin

get_next_item(req); // Item from sequence via sequencer

// drive request to DUT which can take more clocks

// Sequence is blocked to send new items til then

item_done(); // ** Unblocks finish_item() in sequence

end

endtask: run_phase

endclass: nonpipe_driver

2) Pipelined model: If the driver models more than one active transaction at a time, then it is called a pipelined model. In this case sequence can keep sending new transactions to driver without waiting for driver to complete a previous transaction. In this case, on every transaction send from the sequence, driver will fork a separate process to drive the interface signals based on that transaction, but will not wait until it is completed before accepting a new transaction. This modelling is useful if we want to drive back to back requests on an interface without waiting for responses from design.

class pipeline_driver extends uvm_driver #(req_c);

task run_phase(uvm_phase phase);

req_c req;

forever begin

get_next_item(req); // Item from sequence via sequencer

fork

begin

//drive request to DUT which can take more clocks

//separate thread that doesn’t block sequence

//driver can accept more items without waiting

end

join_none

item_done(); // ** Unblocks finish_item() in sequence

end

endtask: run_phase

endclass: pipeline_driver

How do we make sure that if multiple sequences are running on a sequencer-driver, responses are send back from driver to the correct sequence?

If responses are returned from the driver for one of several sequences, the sequence id field in the sequence is used by the sequencer to route the response back to the right sequence. The response handling code in the driver should use the set_id_info() call to ensure that any response items have the same sequence id as their originating request. Here is an example code in driver that gets a sequence item and sends a response back (Note that this is a reference pseudo code for illustration and some functions are assumed to be coded somewhere else)

class my_driver extends uvm_driver;

//function that gets item from sequence port and

//drives response back

function drive_and_send_response();

forever begin

seq_item_port.get(req_item);

//function that takes req_item and drives pins

drive_req(req_item);

//create a new response item

rsp_item = new();

//some function that monitors response signals from dut

rsp_item.data = m_vif.get_data();

//copy id from req back to response

rsp.set_id_info(req_item);

//write response on rsp port

rsp_port.write(rsp_item);

end

endfunction

endclass

What is m_sequencer handle?

When a sequence is started, it is always associated with a sequencer on which it is started. The m_sequencer handle contains the reference to the sequencer on which sequence is running. Using this handle, the sequence can access any information and other resource handles in the UVM component hierarchy.

What is a p_sequencer handle and how is it different in m_sequencer?

A UVM sequence is an object with limited life time unlike a sequencer or a driver or a monitor which are UVM components and are present throughout simulation time. So if you need to access any members or handles from the testbench hierarchy (component hierarchy), the sequence would need a handle to the sequencer on which it is running. m_sequencer is a handle of type uvm_sequencer_base which is available by-default in a uvm_sequence. However, to access the real sequencer on which sequence is running, we need to typecast the m_sequencer to the real sequencers, which is generally called p_sequencer (though you could really use any name and not just p_sequencer). Here is a simple example where a sequence wants to access a handle to a clock monitor component which is available as a handle in the sequencer.

class test_sequence_c extends uvm_sequence;

test_sequencer_c p_sequencer;

clock_monitor_c my_clock_monitor;

task pre_body();

if(!$cast(p_sequencer, m_sequencer)) begin

`uvm_fatal(“Sequencer Type Mismatch:”, ” Wrong Sequencer”);

end

my_clock_monitor = p_sequencer.clk_monitor;

endtask

endclass

class test_Sequencer_c extends uvm_sequencer;

clock_monitor_c clk_monitor;

endclass

What is the difference between early randomization and late randomization while generating a sequence?

In Early randomization, a sequence object is first randomized using randomize() call and then the sequence calls start_item() to request access to sequencer, which is a blocking call and can take time based upon how busy the sequencer is. Following example shows

an object (req) randomized first and then sequence waits for arbitration

task body()

assert(req.randomize());

start_item(req); //Can consume time based on sequencer arbitration

finish_item(req);

endtask

In Late randomization, a sequence first calls start_item() , waits until arbitration is granted from the sequencer, and then just before sending the transaction to sequencer/driver, randomize is called. This has the advantage that items are randomized just in time and can use any feedback from design or other components just before sending an item to driver. Following code shows late randomization of a request (req)

task body()

start_item(req); //Can consume time based on sequencer arbitration

assert(req.randomize());

finish_item(req);

endtask

What is a subsequence?

A subsequence is a sequence that is started from another sequence. From the body() of a sequence, if start() of another sequence is called, it is generally called a subsequence.

What is the difference between get_next_item() and try_next_item()

methods in UVM driver class? The get_next_item() is a blocking call (part of the driver-sequencer API) which blocks until a sequence item is available for driver to process, and returns a pointer to the sequence item. The try_next_item() is a nonblocking version which returns a null pointer if no sequence item is available for driver to process.

What is the difference between get_next_item() and get() methods in UVM driver class?

The get_next_item() is a blocking call to get the sequence item from the sequencer FIFO for processing by driver. Once the sequence item is processed by driver, it needs to call item_done() to complete the handshake before a new item is requested using get_next_item().

The get() is also a blocking call which gets the sequence item from sequencer FIFO for processing by driver. However, while using get(), there is no need to explicitly call item_done() as the get() method completes the handshake implicitly.

What is the difference between get() and peek() methods of UVM driver class?

The get() method part of driver class is a blocking call which gets the sequence item from sequencer FIFO for processing by driver. It unblocks once an item is available and completes handshake with sequencer.

The peek() method is similar to get() and blocks until a sequence item is available. However, it will not remove the request from the sequencer FIFO. So calling peek()

multiple times will return same sequencer item in driver.

What is the difference in item_done() method of driver-sequencer API when called with and without arguments?

The item_done() method is a nonblocking method in driver class that is used to complete handshake with the sequencer after a get_next_item() or try_next_item() is successful. If there is no need to send a response back, item_done() is called with no argument which will complete the handshake without placing anything in the sequencer response FIFO. If there is a need to send a response back, item_done() is passed with a pointer to a response sequence_item as an argument. This response pointer will be placed in the sequencer response FIFO which can be processed by the sequence as a response to the request it drove.

Which of the following driver class methods are blocking calls and

which are nonblocking?

1) get()

2) get_next_item()

3) item_done()

4) put()

5) try_next_item()

6) peek()

get(), get_next_item(), peek() are blocking calls.

try_next_item(), item_done(), and put() are nonblocking calls

Which of the following code is wrong inside a UVM driver class?

1)

function get_drive_req();

forever begin

req = get();

req = get();

end

endfunction

2)

function get_drive_req();

forever begin

req = get_next_item();

req = get_next_item();

item_done();

end

endfunction

3)

function get_drive_req();

forever begin

req = peek();

req = peek();

item_done();

req = get();

end

endfunction

2) is wrong as you cannot call get_next_item() twice before calling item_done() as it will not complete handshake with the sequencer.

How can you stop all sequences running on a sequencer?

The sequencer has a method stop_sequences() that can be used to stop all sequences. However, this method does not check if the driver is currently processing any sequence_items. Because of this, if driver calls an item_done() or put(), there can be a Fatal Error as the sequence pointer might not be valid. So a user needs to take care of making sure that once stop_sequence() is called, the sequencer thread is disabled (if started in a fork).

Which method in the sequence gets called when user calls sequence.print() method?

convert2string() : It is recommended to implement this function which returns a string representation of the object (values of its data members). This is useful to get debug information printed to simulator transcript or log file.

Identify any potential issues in following code part of a UVM sequence

task body();

seq_item_c req;

start_item(req);

#10 ns;

assert(req.randomize());

finish_item(req);

endtask

Adding a delay between start_item and finish_item should be avoided. The sequence wins arbitration and has access to sequencer/driver once start_item returns. Any delay from then till finish_item will hold up the sequencer/driver and will not be available for any other sequence. This will be more problematic if multiple sequences are run on an interface and more the delay, more idle on the design interface.

Exit mobile version