get_next_item(), item_done(), start_item() and finish_item() block makes these problems easy to find.A uvm_driver is a uvm_component with a TLM port, seq_item_port, that it uses to talk to the sequencer. The driver class is parameterized with a request type (REQ) and a response type (RSP). The response type can differ from the request type, but most drivers use the same type for both.
The driver gets request items from the sequencer through a handshake, and can optionally send a response item back. There are two common styles for this handshake: get_next_item() with item_done(), and get() with put(). This article covers the first style.
8 min read | Intermediate Level
What You'll Learn
- How a driver gets an item with
get_next_item()and completes it withitem_done() - How
start_item()andfinish_item()in a sequence pair up with the driver calls - When each of these calls blocks, and what unblocks it
- How to read the simulation log of a complete driver-sequencer example
The Driver-Sequencer Handshake
The sequence, the sequencer and the driver each play a part in moving one item to the DUT:
- The driver calls
get_next_item()and waits for an item. - The sequence calls
start_item(), which waits until the sequencer grants it access to the driver. - The sequence fills in the item, usually by randomizing it, and calls
finish_item(). This sends the item to the driver. get_next_item()returns with the item, and the driver drives it to the DUT.- The driver calls
item_done(), which letsfinish_item()return in the sequence.
If the driver passes a response to item_done(), the response is placed in the sequencer's response FIFO. The sequence can then collect it with get_response(). Without a response argument, nothing goes into that FIFO.
Using get_next_item() in a Driver
The driver calls get_next_item() through seq_item_port. This port is connected to the sequencer's seq_item_export, which implements the call. The task blocks until the sequencer has an item for the driver, and then returns the item in its argument, usually the built-in handle req.
class my_driver extends uvm_driver #(my_data);
`uvm_component_utils (my_driver)
function new (string name = "my_driver", uvm_component parent = null);
super.new (name, parent);
endfunction
virtual task run_phase(uvm_phase phase);
super.run_phase(phase);
// 1. This task will get an item from the sequencer using get_next_item()
`uvm_info ("DRIVER", $sformatf ("Waiting for data from sequencer"), UVM_MEDIUM)
seq_item_port.get_next_item(req);
// 2. For simplicity, lets just assume the driver drives the received packet
// during this time and consumes 20ns to complete driving the transaction
`uvm_info ("DRIVER", $sformatf ("Start driving tx addr=0x%0h data=0x%0h", req.addr, req.data), UVM_MEDIUM)
#20;
// 3. After driver has finished the transaction, it has to let the sequencer know
// by calling item_done()
`uvm_info ("DRIVER", $sformatf ("Finish driving tx addr=0x%0h data=0x%0h", req.addr, req.data), UVM_MEDIUM)
seq_item_port.item_done();
endtask
endclass
Once the driver has the item, it drives the data to the DUT through a virtual interface. When it has finished, it must tell the sequencer with item_done().
Syntax
seq_item_port.get_next_item (req); // Blocks until an item is available
// ... drive req to the DUT ...
seq_item_port.item_done (); // Item finished, no response
seq_item_port.item_done (rsp); // Item finished, with a response
seq_item_port.try_next_item (req); // Returns null in req if no item is ready
try_next_item() does not wait for a sequence to provide an item. It is useful when the driver has to do something else, such as drive idle cycles, when no item is ready. If it returns an item, the driver must still call item_done().
How Does the Sequencer Get These Items?
A sequence is started on a sequencer, and its body() task creates items and sends them with start_item() and finish_item().
class my_sequence extends uvm_sequence;
`uvm_object_utils (my_sequence)
function new (string name = "my_sequence");
super.new (name);
endfunction
virtual task body();
// 1. Create an item the connected sequencer can accept
my_data tx = my_data::type_id::create("tx");
`uvm_info ("SEQ", $sformatf("About to call start_item"), UVM_MEDIUM)
// 2. Call the start_item() task which will send this object to the driver
start_item(tx);
`uvm_info ("SEQ", $sformatf("start_item() fn call done"), UVM_MEDIUM)
// 3. Because the class handle passed to the driver points to the same object, we
// can do late randomization
tx.randomize();
`uvm_info ("SEQ", $sformatf("tx randomized with addr=0x%0h data=0x%0h", tx.addr, tx.data), UVM_MEDIUM)
// 4. Call finish_item method so that the sequence waits until the driver lets the
// sequencer know that this item has finished
finish_item(tx);
`uvm_info ("SEQ", $sformatf("finish_item() fn call done"), UVM_MEDIUM)
endtask
endclass
| Call | Called by | Blocks until |
|---|---|---|
get_next_item() | Driver | An item has been sent by a sequence |
start_item() | Sequence | The sequencer grants the sequence access to the driver |
finish_item() | Sequence | The driver calls item_done() |
item_done() | Driver | Does not block |
The sequencer only grants access when the driver is asking for an item. So start_item() returns only after the driver has called get_next_item(). Randomizing the item between start_item() and finish_item() is called late randomization. The item gets its values just before it is sent, when the driver is ready for it.
Example
This example connects a driver and a sequencer, and starts one sequence on the sequencer. The structure is shown below.
Define a sequence item
The sequence item my_data carries two random variables, addr and data.
// Note that this class is derived from "uvm_sequence_item"
class my_data extends uvm_sequence_item;
rand bit [7:0] data;
rand bit [7:0] addr;
// Rest of the class contents come here ...
endclass
Define the driver
The driver is parameterized with my_data, so req has that type. In a real driver, the received item is used to drive the DUT signals through a virtual interface. Here, a #20 delay stands in for that work.
class my_driver extends uvm_driver #(my_data);
`uvm_component_utils (my_driver)
// Other parts of the driver code if they exist
virtual task run_phase(uvm_phase phase);
super.run_phase(phase);
// 1. Get the next available item from the sequencer. If none exists, then wait until
// next item is available -> this is blocking in nature
`uvm_info ("DRIVER", $sformatf ("Waiting for data from sequencer"), UVM_MEDIUM)
seq_item_port.get_next_item(req);
// 2. Let us assume that the driver actively does the pin wiggling of the DUT during this time and
// consider it takes 20ns
`uvm_info ("DRIVER", $sformatf ("Start driving tx addr=0x%0h data=0x%0h", req.addr, req.data), UVM_MEDIUM)
#20;
// 3. After the driver has driven all data to the DUT, it should let the sequencer know that it finished
// driving the transaction by calling "item_done". Optionally the response packet can be passed along with
// the item_done method call and it will be placed in the sequencer's response FIFO
`uvm_info ("DRIVER", $sformatf ("Finish driving tx addr=0x%0h data=0x%0h", req.addr, req.data), UVM_MEDIUM)
seq_item_port.item_done();
endtask
endclass
Define the sequence
The sequence sends a single item with start_item() and finish_item(), and prints a message after each step.
class my_sequence extends uvm_sequence #(my_data);
// Rest of the sequence code
virtual task body();
// 1. Create a sequence item of the given type
my_data tx = my_data::type_id::create("tx");
`uvm_info ("SEQ", $sformatf("About to call start_item"), UVM_MEDIUM)
// 2. Start the item on the sequencer which will send this to the driver
start_item(tx);
`uvm_info ("SEQ", $sformatf("start_item() fn call done"), UVM_MEDIUM)
// 3. Do some late randomization to create a different content in this transaction object
tx.randomize();
`uvm_info ("SEQ", $sformatf("tx randomized with addr=0x%0h data=0x%0h", tx.addr, tx.data), UVM_MEDIUM)
// 4. Call finish_item to let driver continue driving the transaction object or sequence item
finish_item(tx);
`uvm_info ("SEQ", $sformatf("finish_item() fn call done"), UVM_MEDIUM)
endtask
endclass
Define the test class
To keep the code short, the test creates the driver and the sequencer directly, connects them, and starts the sequence in run_phase().
In a real testbench, the driver and sequencer are placed in an agent, the agent in an environment, and the environment in the test.
class base_test extends uvm_test;
// Rest of the test code is here
// The sequencer is parameterized to accept objects of type "my_data" only
my_driver m_drv0;
uvm_sequencer #(my_data) m_seqr0;
my_sequence m_seq;
// Build the sequencer and driver components
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
m_drv0 = my_driver::type_id::create ("m_drv0", this);
m_seqr0 = uvm_sequencer#(my_data)::type_id::create ("m_seqr0", this);
endfunction
// Connect the sequencer "export" to the driver's "port"
virtual function void connect_phase (uvm_phase phase);
super.connect_phase (phase);
m_drv0.seq_item_port.connect (m_seqr0.seq_item_export);
endfunction
// Start the sequence on the given sequencer
virtual task run_phase(uvm_phase phase);
m_seq = my_sequence::type_id::create("m_seq");
phase.raise_objection(this);
m_seq.start(m_seqr0);
phase.drop_objection(this);
endtask
endclass
UVM_INFO @ 0: reporter [RNTST] Running test base_test... UVM_INFO testbench.sv(33) @ 0: uvm_test_top.m_top_env.m_drv0 [DRIVER] Waiting for data from sequencer UVM_INFO testbench.sv(71) @ 0: uvm_test_top.m_top_env.m_seqr0@@m_seq [SEQ] About to call start_item UVM_INFO testbench.sv(73) @ 0: uvm_test_top.m_top_env.m_seqr0@@m_seq [SEQ] start_item() fn call done UVM_INFO testbench.sv(75) @ 0: uvm_test_top.m_top_env.m_seqr0@@m_seq [SEQ] tx randomized with addr=0x8f data=0x1d UVM_INFO testbench.sv(35) @ 0: uvm_test_top.m_top_env.m_drv0 [DRIVER] Start driving tx addr=0x8f data=0x1d UVM_INFO testbench.sv(37) @ 20: uvm_test_top.m_top_env.m_drv0 [DRIVER] Finish driving tx addr=0x8f data=0x1d UVM_INFO testbench.sv(77) @ 20: uvm_test_top.m_top_env.m_seqr0@@m_seq [SEQ] finish_item() fn call done UVM_INFO /playground_lib/uvm-1.2/src/base/uvm_objection.svh(1271) @ 20: reporter [TEST_DONE] 'run' phase is ready to proceed to the 'extract' phase UVM_INFO /playground_lib/uvm-1.2/src/base/uvm_report_server.svh(847) @ 20: reporter [UVM/REPORT/SERVER] --- UVM Report Summary ---
The log was produced with the driver and sequencer inside an environment called m_top_env, as in the figure, which is why the paths contain uvm_test_top.m_top_env. The order of the messages shows the handshake:
- At time 0: the driver waits in
get_next_item(). Because the driver is already waiting,start_item()is granted at once. - Still at time 0: the item is randomized,
finish_item()sends it, and the driver starts drivingaddr=0x8f. - At time 20: the driver calls
item_done(), and only then doesfinish_item()return in the sequence. The test drops its objection and the run phase ends.
Quick Check
Q: The driver in this example takes 50ns per item instead of 20ns. At what time does the message finish_item() fn call done appear?
Show Answer
At time 50. finish_item() returns only when the driver calls item_done(), which happens after the driver has finished driving the item.
Where is This Used?
Most protocol drivers, such as APB, AXI-Lite or SPI drivers, use get_next_item() and item_done() inside a forever loop in run_phase(). Because finish_item() blocks until item_done(), a sequence can rely on a transfer being complete on the bus before it sends the next one.
Common Mistakes
Mistake #1: Calling get_next_item() twice without item_done()
Every get_next_item() must be followed by item_done() before the next get_next_item(). Otherwise the sequencer reports an error that get_next_item() was called twice without item_done() in between.
Mistake #2: Forgetting item_done()
If the driver never calls item_done(), finish_item() never returns. The sequence hangs, the objection is never dropped, and the test runs until a timeout.
Mistake #3: Changing the item after finish_item()
Wrong: calling tx.randomize() after finish_item(tx)
Right: randomize between start_item() and finish_item()
Why? finish_item() returns only after the driver has finished with the item. Any change made after that is never driven.
Practice Exercise
Challenge: Drive Several Items
The driver in this example handles only one item, because run_phase() has no loop. Change the driver so it can handle any number of items, and change the sequence to send three randomized items. At what times do the three items finish?
Hint
Put the driver's get, drive and done steps in a forever loop. In the sequence, create, start, randomize and finish each item inside a repeat loop.
Solution
// Driver
virtual task run_phase (uvm_phase phase);
forever begin
seq_item_port.get_next_item (req);
`uvm_info ("DRIVER", $sformatf ("Driving addr=0x%0h data=0x%0h", req.addr, req.data), UVM_MEDIUM)
#20;
seq_item_port.item_done ();
end
endtask
// Sequence
virtual task body ();
repeat (3) begin
my_data tx = my_data::type_id::create ("tx");
start_item (tx);
if (!tx.randomize ())
`uvm_error ("SEQ", "Randomization failed")
finish_item (tx);
end
endtask
The items finish at times 20, 40 and 60. The driver is back in get_next_item() as soon as it calls item_done(), so the next item is granted in the same time step.
Summary
In this tutorial, you learned:
- The driver gets items with
seq_item_port.get_next_item(), which blocks until an item is available - The driver must call
item_done()after each item, optionally with a response start_item()waits for a grant from the sequencer, andfinish_item()waits foritem_done()- Items are randomized between
start_item()andfinish_item()(late randomization) - Real drivers wrap the handshake in a
foreverloop so that they can process many items